diff --git a/src/transaction/Trace.tsx b/src/transaction/Trace.tsx index 568988c..da0b24a 100644 --- a/src/transaction/Trace.tsx +++ b/src/transaction/Trace.tsx @@ -1,9 +1,14 @@ -import React, { useContext } from "react"; +import React, { useContext, useMemo } from "react"; import AddressHighlighter from "../components/AddressHighlighter"; import DecoratedAddressLink from "../components/DecoratedAddressLink"; import ContentFrame from "../ContentFrame"; import { TransactionData } from "../types"; -import { useTraceTransaction } from "../useErigonHooks"; +import { rawInputTo4Bytes, useBatch4Bytes } from "../use4Bytes"; +import { + TraceGroup, + useTraceTransaction, + useUniqueSignatures, +} from "../useErigonHooks"; import { RuntimeContext } from "../useRuntime"; import TraceItem from "./TraceItem"; @@ -14,6 +19,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 +44,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..e5390a8 100644 --- a/src/transaction/TraceItem.tsx +++ b/src/transaction/TraceItem.tsx @@ -4,18 +4,24 @@ import DecoratedAddressLink from "../components/DecoratedAddressLink"; import FormattedBalance from "../components/FormattedBalance"; import FunctionSignature from "./FunctionSignature"; import { TransactionData } from "../types"; -import { rawInputTo4Bytes, use4Bytes } from "../use4Bytes"; +import { FourBytesEntry, rawInputTo4Bytes } from "../use4Bytes"; import { TraceGroup } from "../useErigonHooks"; type TraceItemProps = { t: TraceGroup; txData: TransactionData; last: boolean; + fourBytesMap: Record; }; -const TraceItem: React.FC = ({ t, txData, last }) => { +const TraceItem: React.FC = ({ + t, + txData, + last, + fourBytesMap, +}) => { const raw4Bytes = rawInputTo4Bytes(t.input); - const fourBytesEntry = use4Bytes(raw4Bytes); + const fourBytesEntry = fourBytesMap[raw4Bytes]; return ( <> @@ -67,6 +73,7 @@ const TraceItem: React.FC = ({ t, txData, last }) => { t={tc} txData={txData} last={i === a.length - 1} + fourBytesMap={fourBytesMap} /> ))} diff --git a/src/use4Bytes.ts b/src/use4Bytes.ts index 6dab197..380c45e 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, @@ -16,6 +18,67 @@ const fullCache = new Map(); 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 * diff --git a/src/useErigonHooks.ts b/src/useErigonHooks.ts index dc7de4e..0d094ba 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 { rawInputTo4Bytes } from "./use4Bytes"; import { getInternalOperations } from "./nodeFunctions"; import { TokenMetas, @@ -398,3 +399,38 @@ 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" + ) { + sigs.add(rawInputTo4Bytes(t.input)); + } + 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"] }