diff --git a/components/mermaid.tsx b/components/mermaid.tsx
index 01f00ad..2071076 100644
--- a/components/mermaid.tsx
+++ b/components/mermaid.tsx
@@ -18,6 +18,7 @@ mermaid.initialize({
lineColor: "#fff",
actorTextColor: "#fff",
actorBorder: "#fff",
+ activationBkgColor: "#fff",
},
themeCSS: `
.actor {
diff --git a/components/tx-data/index.tsx b/components/tx-data/index.tsx
index f4a48ee..38e12dd 100644
--- a/components/tx-data/index.tsx
+++ b/components/tx-data/index.tsx
@@ -2,10 +2,12 @@
import { useTx } from "@/hooks/api";
import { sequenceDiagramFromSpans } from "@/lib/mermaid";
+import { Loader2 } from "lucide-react";
import { useState } from "react";
import Mermaid from "../mermaid";
import { Badge } from "../ui/badge";
import SpanDetails from "./span-details";
+import SpansDetails from "./spans-details";
type TxDataProps = {
txId: string;
@@ -21,6 +23,7 @@ export default function TxData({ txId, spanId }: TxDataProps) {
if (!tx) return "Couldn't find a Tx with id: " + txId;
const mermaidChart = sequenceDiagramFromSpans(tx.spans);
+ const canRenderMermaid = mermaidChart !== "sequenceDiagram";
const span = tx.spans.find((span) => span.spanId === spanIdToFind);
return (
@@ -30,10 +33,18 @@ export default function TxData({ txId, spanId }: TxDataProps) {
Transaction {txId}
- {!isFetching ? (
-
- ) : null}
- {span ? : null}
+ {canRenderMermaid ? (
+ <>
+ {isFetching ? (
+
+ ) : (
+
+ )}
+ {span ? : null}
+ >
+ ) : (
+
+ )}
);
}
diff --git a/components/tx-data/spans-details.tsx b/components/tx-data/spans-details.tsx
new file mode 100644
index 0000000..c6d3fda
--- /dev/null
+++ b/components/tx-data/spans-details.tsx
@@ -0,0 +1,20 @@
+import { Span } from "@/types/txs";
+import SpanDetails from "./span-details";
+
+type SpansDetailsProps = {
+ spans: readonly Span[];
+};
+
+export default function SpansDetails({ spans }: SpansDetailsProps) {
+ return spans.map((span) => (
+ <>
+ {spans.length > 1 ? (
+
+ Operation {span.operationName}{" "}
+ {span.spanId}
+
+ ) : null}
+
+ >
+ ));
+}
diff --git a/components/txs-table/txs-table-toolbar.tsx b/components/txs-table/txs-table-toolbar.tsx
index cad8fa0..bf6e492 100644
--- a/components/txs-table/txs-table-toolbar.tsx
+++ b/components/txs-table/txs-table-toolbar.tsx
@@ -15,26 +15,30 @@ import { DataTableKvFilter } from "../data-table/data-table-kv-filter";
import { DataTableViewOptions } from "../data-table/data-table-view-options";
const getSelectedValue = (table: Table) => {
- if (
- !table.getColumn("operationName")?.getFilterValue() &&
- !table.getColumn("tags")?.getFilterValue()
- ) {
- return "empty";
- }
-
- if ((table.getColumn("operationName")?.getFilterValue() as string) ?? "") {
- return "custom";
- }
-
+ const operationName = table.getColumn("operationName")?.getFilterValue() as
+ | string
+ | undefined;
const tags = table.getColumn("tags")?.getFilterValue() as
| Map
| undefined;
- if (tags?.size === 1 && tags?.get("raw_tx") === "*") {
+ if (!operationName && !tags) {
+ return "empty";
+ }
+
+ if (
+ operationName === "query" &&
+ tags?.size === 1 &&
+ tags?.get("request") === "*Bank(Send*"
+ ) {
return "simulations";
}
- if (tags?.size === 1 && tags?.get("tx_hash") === "*") {
+ if (
+ operationName === "execute_tx" &&
+ tags?.size === 1 &&
+ tags?.get("tx") === "*Wasm(*"
+ ) {
return "broadcasted";
}
@@ -56,15 +60,17 @@ export function DataTableToolbar({
onValueChange={(value) => {
switch (value) {
case "simulations": {
+ table.getColumn("operationName")?.setFilterValue("query");
table
.getColumn("tags")
- ?.setFilterValue(new Map([["raw_tx", "*"]]));
+ ?.setFilterValue(new Map([["request", "*Bank(Send*"]]));
break;
}
case "broadcasted": {
+ table.getColumn("operationName")?.setFilterValue("execute_tx");
table
.getColumn("tags")
- ?.setFilterValue(new Map([["tx_hash", "*"]]));
+ ?.setFilterValue(new Map([["tx", "*Wasm(*"]]));
break;
}
default: {
@@ -95,10 +101,8 @@ export function DataTableToolbar({
>
Custom filter
- Failed simulations
-
- Broadcasted transactions
-
+ Sim. Bank Send
+ Execute WASM tx
diff --git a/lib/mermaid.ts b/lib/mermaid.ts
index 4f6e3ed..d25b6ea 100644
--- a/lib/mermaid.ts
+++ b/lib/mermaid.ts
@@ -1,5 +1,6 @@
import { Span } from "@/types/txs";
import { getAddressType } from "./chain";
+import { getActorsFromOperations, getOperationsFromSpans } from "./parse-ron";
type TreeNode = {
id: string;
@@ -65,22 +66,22 @@ export function flowchartFromSpans(spans: Readonly>) {
export function sequenceDiagramFromSpans(spans: Readonly>) {
let chart = "sequenceDiagram";
- for (const span of spans) {
- const tx = span.tags.get("tx");
-
- if (!tx || !tx.includes("Bank(Send")) {
- continue;
- }
+ const operations = getOperationsFromSpans(spans);
+ const actors = getActorsFromOperations(operations);
- const sender = tx.match(/sender: (\w+)/)?.[1] ?? "";
- const recipient = tx.match(/recipient: (\w+)/)?.[1] ?? "";
-
- chart += `\n${getActorBox(sender)}`;
- chart += `\n${getActorBox(recipient)}`;
+ for (const actor of actors) {
+ chart += `\n${getActorBox(actor)}`;
+ }
- chart += `\n${sender}->>+${recipient}: 🏦 Send`;
+ for (const operation of operations) {
+ const { label, isQuery, sender, recipient, traceId, spanId } = operation;
+ chart += `\n${sender}${isQuery ? "-" : ""}->>+${recipient}: ${label}`;
- break;
+ if (isQuery) {
+ chart += `\nactivate ${recipient}`;
+ chart += `\n${recipient}-->>+${sender}: response placeholder`;
+ chart += `\ndeactivate ${recipient}`;
+ }
}
return chart;
diff --git a/lib/parse-ron.ts b/lib/parse-ron.ts
new file mode 100644
index 0000000..7650861
--- /dev/null
+++ b/lib/parse-ron.ts
@@ -0,0 +1,204 @@
+import { Span } from "@/types/txs";
+
+type Operation = {
+ label: string;
+ isQuery: boolean;
+ sender: string;
+ recipient: string;
+ traceId: string;
+ spanId: string;
+};
+
+export const getActorsFromOperations = (
+ operations: readonly Operation[],
+): readonly string[] =>
+ Array.from(
+ new Set(
+ operations.flatMap((operation) => [
+ operation.sender,
+ operation.recipient,
+ ]),
+ ),
+ );
+
+export const getOperationsFromSpans = (
+ spans: Readonly>,
+): readonly Operation[] =>
+ spans
+ .map((span) => {
+ const txRon =
+ span.tags.get("request") || span.tags.get("msg") || span.tags.get("tx");
+ if (!txRon) {
+ return null;
+ }
+
+ switch (true) {
+ //NOTE - Avoids showing both execute_tx and sm.process_msg for bank queries
+ case txRon.includes("Bank(") &&
+ span.operationName === "sm.process_msg": {
+ return null;
+ }
+ case txRon.includes("Bank(Send"): {
+ return parseActorFromBankSendRon(txRon, span);
+ }
+ //NOTE - Avoids showing both execute_tx and sm.process_msg for wasm queries
+ case txRon.includes("Wasm(") &&
+ span.operationName === "sm.process_msg": {
+ return null;
+ }
+ case txRon.includes("Wasm(StoreCode"): {
+ return parseActorFromWasmStoreCodeRon(txRon, span);
+ }
+ case txRon.includes("Wasm(Instantiate"): {
+ return parseActorFromWasmInstantiateRon(txRon, span);
+ }
+ case txRon.includes("Wasm(Migrate"): {
+ return parseActorFromWasmMigrateRon(txRon, span);
+ }
+ case txRon.includes("Wasm(Execute"): {
+ return parseActorFromWasmExecuteRon(txRon, span);
+ }
+ case txRon.includes("Wasm(UpdateAdmin"): {
+ return parseActorFromWasmUpdateAdminRon(txRon, span);
+ }
+ default: {
+ return null;
+ }
+ }
+ })
+ .filter((operation): operation is Operation => !!operation);
+
+const parseActorFromBankSendRon = (
+ bankSendRon: string,
+ { traceId, spanId }: Span,
+): Operation | null => {
+ const sender = bankSendRon.match(/sender: (\w+)/)?.[1];
+ const recipient = bankSendRon.match(/recipient: (\w+)/)?.[1];
+
+ if (!sender || !recipient) {
+ return null;
+ }
+
+ const isSimulation = bankSendRon.includes("Simulate(");
+
+ return {
+ label: isSimulation ? "💻🏦 Send" : "🏦 Send",
+ isQuery: isSimulation,
+ sender,
+ recipient,
+ traceId,
+ spanId,
+ };
+};
+
+const parseActorFromWasmStoreCodeRon = (
+ wasmStoreCodeRon: string,
+ { traceId, spanId }: Span,
+): Operation | null => {
+ const sender = wasmStoreCodeRon.match(/sender: (\w+)/)?.[1];
+
+ if (!sender) {
+ return null;
+ }
+
+ const isSimulation = wasmStoreCodeRon.includes("Simulate(");
+
+ return {
+ label: isSimulation ? "💻🕸 Store code" : "🕸 Store code",
+ isQuery: isSimulation,
+ sender,
+ recipient: sender,
+ traceId,
+ spanId,
+ };
+};
+
+const parseActorFromWasmInstantiateRon = (
+ wasmInstantiateRon: string,
+ { traceId, spanId }: Span,
+): Operation | null => {
+ const sender = wasmInstantiateRon.match(/sender: (\w+)/)?.[1];
+
+ if (!sender) {
+ return null;
+ }
+
+ const isSimulation = wasmInstantiateRon.includes("Simulate(");
+
+ return {
+ label: isSimulation ? "💻🕸 Instantiate" : "🕸 Instantiate",
+ isQuery: isSimulation,
+ sender,
+ recipient: sender,
+ traceId,
+ spanId,
+ };
+};
+
+const parseActorFromWasmMigrateRon = (
+ wasmMigrateRon: string,
+ { traceId, spanId }: Span,
+): Operation | null => {
+ const sender = wasmMigrateRon.match(/sender: (\w+)/)?.[1];
+
+ if (!sender) {
+ return null;
+ }
+
+ const isSimulation = wasmMigrateRon.includes("Simulate(");
+
+ return {
+ label: isSimulation ? "💻🕸 Migrate" : "🕸 Migrate",
+ isQuery: isSimulation,
+ sender,
+ recipient: sender,
+ traceId,
+ spanId,
+ };
+};
+
+const parseActorFromWasmExecuteRon = (
+ wasmExecuteRon: string,
+ { traceId, spanId }: Span,
+): Operation | null => {
+ const sender = wasmExecuteRon.match(/sender: (\w+)/)?.[1];
+ const contractAddr = wasmExecuteRon.match(/contract_addr: (\w+)/)?.[1];
+
+ if (!sender || !contractAddr) {
+ return null;
+ }
+
+ const isSimulation = wasmExecuteRon.includes("Simulate(");
+
+ return {
+ label: isSimulation ? "💻🕸 Execute" : "🕸 Execute",
+ isQuery: isSimulation,
+ sender,
+ recipient: contractAddr,
+ traceId,
+ spanId,
+ };
+};
+
+const parseActorFromWasmUpdateAdminRon = (
+ wasmUpdateAdminRon: string,
+ { traceId, spanId }: Span,
+): Operation | null => {
+ const sender = wasmUpdateAdminRon.match(/sender: (\w+)/)?.[1];
+ const contractAddr = wasmUpdateAdminRon.match(/contract_addr: (\w+)/)?.[1];
+
+ if (!sender || !contractAddr) {
+ return null;
+ }
+
+ const isSimulation = wasmUpdateAdminRon.includes("Simulate(");
+
+ return {
+ label: isSimulation ? "💻🕸 Update admin" : "🕸 Update admin",
+ isQuery: isSimulation,
+ sender,
+ recipient: contractAddr,
+ traceId,
+ spanId,
+ };
+};
diff --git a/lib/parse-span.ts b/lib/parse-span.ts
index f7ef749..c6ead4e 100644
--- a/lib/parse-span.ts
+++ b/lib/parse-span.ts
@@ -11,7 +11,7 @@ export const getParsedSpanMap = ({ operationName, startTime, tags }: Span) => {
const height = tags.get("height");
height && map.set("Height", height);
- const tx = tags.get("tx");
+ const tx = tags.get("request") || tags.get("msg") || tags.get("tx");
const signer = tx?.match(/signer: (\w+)/)?.[1];
signer && map.set("Signer", signer);
@@ -22,6 +22,9 @@ export const getParsedSpanMap = ({ operationName, startTime, tags }: Span) => {
const recipient = tx?.match(/recipient: (\w+)/)?.[1];
recipient && map.set("Recipient", recipient);
+ const contract_addr = tx?.match(/contract_addr: (\w+)/)?.[1];
+ contract_addr && map.set("Contract", contract_addr);
+
const [, amount, denom] = tx?.match(/amount: \[Coin { (\w+) \"(\w+)/) ?? [];
amount && denom && map.set("Amount", `${amount} ${denom}`);
diff --git a/tests/e2e/happy-paths.test.ts b/tests/e2e/happy-paths.test.ts
index cad46cc..f1dc84e 100644
--- a/tests/e2e/happy-paths.test.ts
+++ b/tests/e2e/happy-paths.test.ts
@@ -31,5 +31,5 @@ test("navigates to a correctly rendered tx detail", async ({ page }) => {
page.getByText("layer1y6v4dtfpu5zatqgv8u7cnfwrg9cvr3chvqkv0a"),
).toHaveCount(1);
- await expect(page.getByText("Send")).toBeVisible();
+ await expect(page.getByText("Send").first()).toBeVisible();
});