From 051704a7a331d6a69913eb1e2457f41c138cbbe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Fern=C3=A1ndez?= <44572727+abefernan@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:55:09 +0200 Subject: [PATCH 01/11] Add ron parser utils --- lib/parse-ron.ts | 181 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 lib/parse-ron.ts diff --git a/lib/parse-ron.ts b/lib/parse-ron.ts new file mode 100644 index 0000000..691a2f6 --- /dev/null +++ b/lib/parse-ron.ts @@ -0,0 +1,181 @@ +import { Span } from "@/types/txs"; + +type Operation = { + label: string; + 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) { + case txRon.includes("Bank(Send"): { + return parseActorFromBankSendRon(txRon, span); + } + 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; + } + + return { + label: bankSendRon.includes("Simulate(") ? "💻🏦 Send" : "🏦 Send", + sender, + recipient, + traceId, + spanId, + }; +}; + +const parseActorFromWasmStoreCodeRon = ( + wasmStoreCodeRon: string, + { traceId, spanId }: Span, +): Operation | null => { + const sender = wasmStoreCodeRon.match(/sender: (\w+)/)?.[1]; + + if (!sender) { + return null; + } + + return { + label: wasmStoreCodeRon.includes("Simulate(") + ? "💻🕸 Store code" + : "🕸 Store code", + 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; + } + + return { + label: wasmInstantiateRon.includes("Simulate(") + ? "💻🕸 Instantiate" + : "🕸 Instantiate", + 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; + } + + return { + label: wasmMigrateRon.includes("Simulate(") ? "💻🕸 Migrate" : "🕸 Migrate", + 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; + } + + return { + label: wasmExecuteRon.includes("Simulate(") ? "💻🕸 Execute" : "🕸 Execute", + 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; + } + + return { + label: wasmUpdateAdminRon.includes("Simulate(") + ? "💻🕸 Update admin" + : "🕸 Update admin", + sender, + recipient: contractAddr, + traceId, + spanId, + }; +}; From 56054407875cf4f04efcf8c3bf6ff2599fd65800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Fern=C3=A1ndez?= <44572727+abefernan@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:55:20 +0200 Subject: [PATCH 02/11] Use ron parser utils --- lib/mermaid.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/mermaid.ts b/lib/mermaid.ts index 4f6e3ed..049dda3 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,15 @@ 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)}`; - - chart += `\n${sender}->>+${recipient}: 🏦 Send`; + for (const actor of actors) { + chart += `\n${getActorBox(actor)}`; + } - break; + for (const { label, sender, recipient, traceId, spanId } of operations) { + chart += `\n${sender}->>+${recipient}: ${label}`; } return chart; From 6e99093e882cecb53e89490bf564109b42a068ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Fern=C3=A1ndez?= <44572727+abefernan@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:55:34 +0200 Subject: [PATCH 03/11] Better showcase with filters --- components/txs-table/txs-table-toolbar.tsx | 42 ++++++++++++---------- 1 file changed, 23 insertions(+), 19 deletions(-) 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 From 2469a5d64f46093ca9bd220455fb78f086a8ad9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Fern=C3=A1ndez?= <44572727+abefernan@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:55:48 +0200 Subject: [PATCH 04/11] Tweak parser for details --- lib/parse-span.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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}`); From f600fbc400f970432369ee65729ec39c480c155a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Fern=C3=A1ndez?= <44572727+abefernan@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:58:04 +0200 Subject: [PATCH 05/11] Account for simulations --- tests/e2e/happy-paths.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); }); From 7858ce34a0c021a327d8cc016b0dde9e802e1cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Fern=C3=A1ndez?= <44572727+abefernan@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:46:04 +0200 Subject: [PATCH 06/11] Avoid sm.process_msg for wasm --- lib/parse-ron.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/parse-ron.ts b/lib/parse-ron.ts index 691a2f6..3eba4ac 100644 --- a/lib/parse-ron.ts +++ b/lib/parse-ron.ts @@ -35,6 +35,11 @@ export const getOperationsFromSpans = ( 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); } From 5ee4fb2c5e9e14b9f873d1c5bcd57b040021b38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Fern=C3=A1ndez?= <44572727+abefernan@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:46:17 +0200 Subject: [PATCH 07/11] Add isQuery to Operation --- lib/parse-ron.ts | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/parse-ron.ts b/lib/parse-ron.ts index 3eba4ac..61962c5 100644 --- a/lib/parse-ron.ts +++ b/lib/parse-ron.ts @@ -2,6 +2,7 @@ import { Span } from "@/types/txs"; type Operation = { label: string; + isQuery: boolean; sender: string; recipient: string; traceId: string; @@ -73,8 +74,11 @@ const parseActorFromBankSendRon = ( return null; } + const isSimulation = bankSendRon.includes("Simulate("); + return { - label: bankSendRon.includes("Simulate(") ? "💻🏦 Send" : "🏦 Send", + label: isSimulation ? "💻🏦 Send" : "🏦 Send", + isQuery: isSimulation, sender, recipient, traceId, @@ -92,10 +96,11 @@ const parseActorFromWasmStoreCodeRon = ( return null; } + const isSimulation = wasmStoreCodeRon.includes("Simulate("); + return { - label: wasmStoreCodeRon.includes("Simulate(") - ? "💻🕸 Store code" - : "🕸 Store code", + label: isSimulation ? "💻🕸 Store code" : "🕸 Store code", + isQuery: isSimulation, sender, recipient: sender, traceId, @@ -113,10 +118,11 @@ const parseActorFromWasmInstantiateRon = ( return null; } + const isSimulation = wasmInstantiateRon.includes("Simulate("); + return { - label: wasmInstantiateRon.includes("Simulate(") - ? "💻🕸 Instantiate" - : "🕸 Instantiate", + label: isSimulation ? "💻🕸 Instantiate" : "🕸 Instantiate", + isQuery: isSimulation, sender, recipient: sender, traceId, @@ -134,8 +140,11 @@ const parseActorFromWasmMigrateRon = ( return null; } + const isSimulation = wasmMigrateRon.includes("Simulate("); + return { - label: wasmMigrateRon.includes("Simulate(") ? "💻🕸 Migrate" : "🕸 Migrate", + label: isSimulation ? "💻🕸 Migrate" : "🕸 Migrate", + isQuery: isSimulation, sender, recipient: sender, traceId, @@ -154,8 +163,11 @@ const parseActorFromWasmExecuteRon = ( return null; } + const isSimulation = wasmExecuteRon.includes("Simulate("); + return { - label: wasmExecuteRon.includes("Simulate(") ? "💻🕸 Execute" : "🕸 Execute", + label: isSimulation ? "💻🕸 Execute" : "🕸 Execute", + isQuery: isSimulation, sender, recipient: contractAddr, traceId, @@ -174,10 +186,11 @@ const parseActorFromWasmUpdateAdminRon = ( return null; } + const isSimulation = wasmUpdateAdminRon.includes("Simulate("); + return { - label: wasmUpdateAdminRon.includes("Simulate(") - ? "💻🕸 Update admin" - : "🕸 Update admin", + label: isSimulation ? "💻🕸 Update admin" : "🕸 Update admin", + isQuery: isSimulation, sender, recipient: contractAddr, traceId, From 35325d208670d14b4ff47207d9003b541ef97579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Fern=C3=A1ndez?= <44572727+abefernan@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:46:27 +0200 Subject: [PATCH 08/11] Draw dashed for queries --- lib/mermaid.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/mermaid.ts b/lib/mermaid.ts index 049dda3..2d452a5 100644 --- a/lib/mermaid.ts +++ b/lib/mermaid.ts @@ -73,8 +73,9 @@ export function sequenceDiagramFromSpans(spans: Readonly>) { chart += `\n${getActorBox(actor)}`; } - for (const { label, sender, recipient, traceId, spanId } of operations) { - chart += `\n${sender}->>+${recipient}: ${label}`; + for (const operation of operations) { + const { label, isQuery, sender, recipient, traceId, spanId } = operation; + chart += `\n${sender}${isQuery ? "-" : ""}->>+${recipient}: ${label}`; } return chart; From c020c7aedbab89cc5161f50466501e26bd61ddff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Fern=C3=A1ndez?= <44572727+abefernan@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:07:55 +0100 Subject: [PATCH 09/11] Avoid duplicate arrows for Bank --- lib/parse-ron.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/parse-ron.ts b/lib/parse-ron.ts index 61962c5..7650861 100644 --- a/lib/parse-ron.ts +++ b/lib/parse-ron.ts @@ -33,6 +33,11 @@ export const getOperationsFromSpans = ( } 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); } From c19b2642577445cc50d8f46a7eb530ce17357640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Fern=C3=A1ndez?= <44572727+abefernan@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:08:12 +0100 Subject: [PATCH 10/11] Add activation boxes for queries --- components/mermaid.tsx | 1 + lib/mermaid.ts | 6 ++++++ 2 files changed, 7 insertions(+) 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/lib/mermaid.ts b/lib/mermaid.ts index 2d452a5..d25b6ea 100644 --- a/lib/mermaid.ts +++ b/lib/mermaid.ts @@ -76,6 +76,12 @@ export function sequenceDiagramFromSpans(spans: Readonly>) { for (const operation of operations) { const { label, isQuery, sender, recipient, traceId, spanId } = operation; chart += `\n${sender}${isQuery ? "-" : ""}->>+${recipient}: ${label}`; + + if (isQuery) { + chart += `\nactivate ${recipient}`; + chart += `\n${recipient}-->>+${sender}: `; + chart += `\ndeactivate ${recipient}`; + } } return chart; From 97806ec62d3bc40dc8fc72eb94f2dcf7ef562982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Fern=C3=A1ndez?= <44572727+abefernan@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:08:38 +0100 Subject: [PATCH 11/11] Render spans for empty mermaid charts --- components/tx-data/index.tsx | 19 +++++++++++++++---- components/tx-data/spans-details.tsx | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 components/tx-data/spans-details.tsx 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} + + + )); +}