diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index 083f7f0..5a1d5cc 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -15,7 +15,7 @@ import { type TokenTransferItemProps = { t: TokenTransfer; txData: TransactionData; - tokenMeta?: TokenMeta | undefined; + tokenMeta: TokenMeta | null | undefined; }; // TODO: handle partial diff --git a/src/components/DecoratedAddressLink.tsx b/src/components/DecoratedAddressLink.tsx index 0b38fab..a39d2d8 100644 --- a/src/components/DecoratedAddressLink.tsx +++ b/src/components/DecoratedAddressLink.tsx @@ -20,7 +20,7 @@ type DecoratedAddressLinkProps = { selfDestruct?: boolean; txFrom?: boolean; txTo?: boolean; - tokenMeta?: TokenMeta; + tokenMeta?: TokenMeta | null | undefined; }; const DecoratedAddresssLink: React.FC = ({ diff --git a/src/transaction/Trace.tsx b/src/transaction/Trace.tsx index 568988c..c40cbc4 100644 --- a/src/transaction/Trace.tsx +++ b/src/transaction/Trace.tsx @@ -3,7 +3,8 @@ import AddressHighlighter from "../components/AddressHighlighter"; import DecoratedAddressLink from "../components/DecoratedAddressLink"; import ContentFrame from "../ContentFrame"; import { TransactionData } from "../types"; -import { useTraceTransaction } from "../useErigonHooks"; +import { useBatch4Bytes } from "../use4Bytes"; +import { useTraceTransaction, useUniqueSignatures } from "../useErigonHooks"; import { RuntimeContext } from "../useRuntime"; import TraceItem from "./TraceItem"; @@ -14,6 +15,8 @@ type TraceProps = { const Trace: React.FC = ({ txData }) => { const { provider } = useContext(RuntimeContext); const traces = useTraceTransaction(provider, txData.transactionHash); + const uniqueSignatures = useUniqueSignatures(traces); + const sigMap = useBatch4Bytes(uniqueSignatures); return ( @@ -37,6 +40,7 @@ const Trace: React.FC = ({ txData }) => { t={t} txData={txData} last={i === a.length - 1} + fourBytesMap={sigMap} /> ))} diff --git a/src/transaction/TraceItem.tsx b/src/transaction/TraceItem.tsx index f8e2288..6bf11dd 100644 --- a/src/transaction/TraceItem.tsx +++ b/src/transaction/TraceItem.tsx @@ -4,29 +4,36 @@ import DecoratedAddressLink from "../components/DecoratedAddressLink"; import FormattedBalance from "../components/FormattedBalance"; import FunctionSignature from "./FunctionSignature"; import { TransactionData } from "../types"; -import { rawInputTo4Bytes, use4Bytes } from "../use4Bytes"; +import { extract4Bytes, FourBytesEntry } from "../use4Bytes"; import { TraceGroup } from "../useErigonHooks"; type TraceItemProps = { t: TraceGroup; txData: TransactionData; last: boolean; + fourBytesMap: Record; }; -const TraceItem: React.FC = ({ t, txData, last }) => { - const raw4Bytes = rawInputTo4Bytes(t.input); - const fourBytesEntry = use4Bytes(raw4Bytes); +const TraceItem: React.FC = ({ + t, + txData, + last, + fourBytesMap, +}) => { + const raw4Bytes = extract4Bytes(t.input); + const sigText = + raw4Bytes === null + ? "" + : fourBytesMap[raw4Bytes]?.name ?? raw4Bytes; return ( <> -
-
-
- {!last && ( -
- )} -
-
+
+
+ {!last && ( +
+ )} +
{t.type} @@ -39,37 +46,28 @@ const TraceItem: React.FC = ({ t, txData, last }) => { . - + {t.value && !t.value.isZero() && ( {"{"}value: ETH{"}"} )} - ( - {t.input.length > 10 && ( - - input=[0x{t.input.slice(10)}] - - )} - ) + + ({t.input.length > 10 && <>input=[0x{t.input.slice(10)}]}) +
{t.children && ( -
-
-
- {t.children.map((tc, i, a) => ( - - ))} -
+
+ {t.children.map((tc, i, a) => ( + + ))}
)} diff --git a/src/types.ts b/src/types.ts index 5904b31..e71a492 100644 --- a/src/types.ts +++ b/src/types.ts @@ -108,4 +108,4 @@ export type TokenMeta = { decimals: number; }; -export type TokenMetas = Record; +export type TokenMetas = Record; diff --git a/src/use4Bytes.ts b/src/use4Bytes.ts index 6dab197..5b4a726 100644 --- a/src/use4Bytes.ts +++ b/src/use4Bytes.ts @@ -7,6 +7,8 @@ export type FourBytesEntry = { signature: string | undefined; }; +export type FourBytesMap = Record; + const simpleTransfer: FourBytesEntry = { name: "Transfer", signature: undefined, @@ -14,8 +16,76 @@ const simpleTransfer: FourBytesEntry = { const fullCache = new Map(); +export const extract4Bytes = (rawInput: string): string | null => { + if (rawInput.length < 10) { + return null; + } + return rawInput.slice(0, 10); +}; + export const rawInputTo4Bytes = (rawInput: string) => rawInput.slice(0, 10); +const fetch4Bytes = async ( + assetsURLPrefix: string, + fourBytes: string +): Promise => { + const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes); + + try { + const res = await fetch(signatureURL); + if (!res.ok) { + console.error(`Signature does not exist in 4bytes DB: ${fourBytes}`); + return null; + } + + // Get only the first occurrence, for now ignore alternative param names + const sigs = await res.text(); + const sig = sigs.split(";")[0]; + const cut = sig.indexOf("("); + const method = sig.slice(0, cut); + + const entry: FourBytesEntry = { + name: method, + signature: sig, + }; + return entry; + } catch (err) { + console.error(`Couldn't fetch signature URL ${signatureURL}`, err); + return null; + } +}; + +export const useBatch4Bytes = ( + rawFourByteSigs: string[] | undefined +): FourBytesMap => { + const runtime = useContext(RuntimeContext); + const assetsURLPrefix = runtime.config?.assetsURLPrefix; + + const [fourBytesMap, setFourBytesMap] = useState({}); + useEffect(() => { + if (!rawFourByteSigs || !assetsURLPrefix) { + setFourBytesMap({}); + return; + } + + const loadSigs = async () => { + const promises = rawFourByteSigs.map((s) => + fetch4Bytes(assetsURLPrefix, s.slice(2)) + ); + const results = await Promise.all(promises); + + const _fourBytesMap: Record = {}; + for (let i = 0; i < rawFourByteSigs.length; i++) { + _fourBytesMap[rawFourByteSigs[i]] = results[i]; + } + setFourBytesMap(_fourBytesMap); + }; + loadSigs(); + }, [assetsURLPrefix, rawFourByteSigs]); + + return fourBytesMap; +}; + /** * Extract 4bytes DB info * @@ -47,34 +117,12 @@ export const use4Bytes = ( return; } - const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes); - fetch(signatureURL) - .then(async (res) => { - if (!res.ok) { - console.error(`Signature does not exist in 4bytes DB: ${fourBytes}`); - fullCache.set(fourBytes, null); - setEntry(null); - return; - } - - // Get only the first occurrence, for now ignore alternative param names - const sigs = await res.text(); - const sig = sigs.split(";")[0]; - const cut = sig.indexOf("("); - const method = sig.slice(0, cut); - - const entry: FourBytesEntry = { - name: method, - signature: sig, - }; - setEntry(entry); - fullCache.set(fourBytes, entry); - }) - .catch((err) => { - console.error(`Couldn't fetch signature URL ${signatureURL}`, err); - setEntry(null); - fullCache.set(fourBytes, null); - }); + const loadSig = async () => { + const entry = await fetch4Bytes(assetsURLPrefix, fourBytes); + fullCache.set(fourBytes, entry); + setEntry(entry); + }; + loadSig(); }, [fourBytes, assetsURLPrefix]); if (rawFourBytes === "0x") { diff --git a/src/useErigonHooks.ts b/src/useErigonHooks.ts index dc7de4e..17d629f 100644 --- a/src/useErigonHooks.ts +++ b/src/useErigonHooks.ts @@ -1,10 +1,11 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { Block, BlockWithTransactions } from "@ethersproject/abstract-provider"; import { JsonRpcProvider } from "@ethersproject/providers"; import { getAddress } from "@ethersproject/address"; import { Contract } from "@ethersproject/contracts"; import { BigNumber } from "@ethersproject/bignumber"; import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes"; +import { extract4Bytes } from "./use4Bytes"; import { getInternalOperations } from "./nodeFunctions"; import { TokenMetas, @@ -221,7 +222,7 @@ export const useTxData = ( // Extract token meta const tokenMetas: TokenMetas = {}; for (const t of tokenTransfers) { - if (tokenMetas[t.token]) { + if (tokenMetas[t.token] !== undefined) { continue; } const erc20Contract = new Contract(t.token, erc20, provider); @@ -237,6 +238,7 @@ export const useTxData = ( decimals, }; } catch (err) { + tokenMetas[t.token] = null; console.warn(`Couldn't get token ${t.token} metadata; ignoring`, err); } } @@ -398,3 +400,43 @@ export const useTraceTransaction = ( return traceGroups; }; + +/** + * Flatten a trace tree and extract and dedup 4byte function signatures + */ +export const useUniqueSignatures = (traces: TraceGroup[] | undefined) => { + const uniqueSignatures = useMemo(() => { + if (!traces) { + return undefined; + } + + const sigs = new Set(); + let nextTraces: TraceGroup[] = [...traces]; + while (nextTraces.length > 0) { + const traces = nextTraces; + nextTraces = []; + + for (const t of traces) { + if ( + t.type === "CALL" || + t.type === "DELEGATECALL" || + t.type === "STATICCALL" || + t.type === "CALLCODE" + ) { + const fourBytes = extract4Bytes(t.input); + if (fourBytes) { + sigs.add(fourBytes); + } + } + + if (t.children) { + nextTraces.push(...t.children); + } + } + } + + return [...sigs]; + }, [traces]); + + return uniqueSignatures; +}; diff --git a/tsconfig.json b/tsconfig.json index 9d379a3..23c0098 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "downlevelIteration": true }, "include": ["src"] }