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}: `; + 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(); });