Skip to content

Commit

Permalink
Support wasm msgs (#24)
Browse files Browse the repository at this point in the history
* Add ron parser utils

* Use ron parser utils

* Better showcase with filters

* Tweak parser for details

* Account for simulations

* Avoid sm.process_msg for wasm

* Add isQuery to Operation

* Draw dashed for queries

* Avoid duplicate arrows for Bank

* Add activation boxes for queries

* Render spans for empty mermaid charts
  • Loading branch information
abefernan authored Nov 18, 2024
1 parent 9d29f9e commit 782bf6e
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 38 deletions.
1 change: 1 addition & 0 deletions components/mermaid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mermaid.initialize({
lineColor: "#fff",
actorTextColor: "#fff",
actorBorder: "#fff",
activationBkgColor: "#fff",
},
themeCSS: `
.actor {
Expand Down
19 changes: 15 additions & 4 deletions components/tx-data/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 (
Expand All @@ -30,10 +33,18 @@ export default function TxData({ txId, spanId }: TxDataProps) {
Transaction {txId}
</Badge>
</a>
{!isFetching ? (
<Mermaid chart={mermaidChart} setSpanId={setSpanIdToFind} />
) : null}
{span ? <SpanDetails span={span} /> : null}
{canRenderMermaid ? (
<>
{isFetching ? (
<Loader2 className="animate-spin mx-auto" />
) : (
<Mermaid chart={mermaidChart} setSpanId={setSpanIdToFind} />
)}
{span ? <SpanDetails span={span} /> : null}
</>
) : (
<SpansDetails spans={tx.spans} />
)}
</div>
);
}
20 changes: 20 additions & 0 deletions components/tx-data/spans-details.tsx
Original file line number Diff line number Diff line change
@@ -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 ? (
<h2>
Operation <span className="font-bold">{span.operationName}</span>{" "}
<span className="font-mono">{span.spanId}</span>
</h2>
) : null}
<SpanDetails key={span.spanId} span={span} />
</>
));
}
42 changes: 23 additions & 19 deletions components/txs-table/txs-table-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,30 @@ import { DataTableKvFilter } from "../data-table/data-table-kv-filter";
import { DataTableViewOptions } from "../data-table/data-table-view-options";

const getSelectedValue = <TData,>(table: Table<TData>) => {
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<string, string>
| 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";
}

Expand All @@ -56,15 +60,17 @@ export function DataTableToolbar<TData>({
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: {
Expand Down Expand Up @@ -95,10 +101,8 @@ export function DataTableToolbar<TData>({
>
Custom filter
</SelectItem>
<SelectItem value="simulations">Failed simulations</SelectItem>
<SelectItem value="broadcasted">
Broadcasted transactions
</SelectItem>
<SelectItem value="simulations">Sim. Bank Send</SelectItem>
<SelectItem value="broadcasted">Execute WASM tx</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
Expand Down
27 changes: 14 additions & 13 deletions lib/mermaid.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Span } from "@/types/txs";
import { getAddressType } from "./chain";
import { getActorsFromOperations, getOperationsFromSpans } from "./parse-ron";

type TreeNode = {
id: string;
Expand Down Expand Up @@ -65,22 +66,22 @@ export function flowchartFromSpans(spans: Readonly<Array<Span>>) {
export function sequenceDiagramFromSpans(spans: Readonly<Array<Span>>) {
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}: <a href="/${span.traceId}/${span.spanId}">๐Ÿฆ Send</a>`;
for (const operation of operations) {
const { label, isQuery, sender, recipient, traceId, spanId } = operation;
chart += `\n${sender}${isQuery ? "-" : ""}->>+${recipient}: <a href="/${traceId}/${spanId}">${label}</a>`;

break;
if (isQuery) {
chart += `\nactivate ${recipient}`;
chart += `\n${recipient}-->>+${sender}: <a class="hidden">response placeholder</a>`;
chart += `\ndeactivate ${recipient}`;
}
}

return chart;
Expand Down
204 changes: 204 additions & 0 deletions lib/parse-ron.ts
Original file line number Diff line number Diff line change
@@ -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<Array<Span>>,
): 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,
};
};
Loading

0 comments on commit 782bf6e

Please sign in to comment.