Merge branch 'feature/trace-tx' into develop

This commit is contained in:
Willian Mitsuda 2021-11-22 14:29:58 -03:00
commit 48686249f3
24 changed files with 796 additions and 108 deletions

View File

@ -15,7 +15,7 @@ import { Metadata } from "./useSourcify";
type TokenTransferItemProps = { type TokenTransferItemProps = {
t: TokenTransfer; t: TokenTransfer;
tokenMeta?: TokenMeta | undefined; tokenMeta?: TokenMeta | null | undefined;
resolvedAddresses: ResolvedAddresses | undefined; resolvedAddresses: ResolvedAddresses | undefined;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>; metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
}; };

View File

@ -30,6 +30,12 @@ const Logs = React.lazy(
/* webpackChunkName: "txlogs", webpackPrefetch: true */ "./transaction/Logs" /* webpackChunkName: "txlogs", webpackPrefetch: true */ "./transaction/Logs"
) )
); );
const Trace = React.lazy(
() =>
import(
/* webpackChunkName: "txtrace", webpackPrefetch: true */ "./transaction/Trace"
)
);
type TransactionParams = { type TransactionParams = {
txhash: string; txhash: string;
@ -98,6 +104,7 @@ const Transaction: React.FC = () => {
{txData && ` (${txData.confirmedData?.logs?.length ?? 0})`} {txData && ` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab> </NavTab>
)} )}
<NavTab href={`/tx/${txhash}/trace`}>Trace</NavTab>
</Tab.List> </Tab.List>
</Tab.Group> </Tab.Group>
<React.Suspense fallback={null}> <React.Suspense fallback={null}>
@ -121,6 +128,12 @@ const Transaction: React.FC = () => {
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
/> />
</Route> </Route>
<Route path="/tx/:txhash/trace" exact>
<Trace
txData={txData}
resolvedAddresses={resolvedAddresses}
/>
</Route>
</Switch> </Switch>
</React.Suspense> </React.Suspense>
</SelectionContext.Provider> </SelectionContext.Provider>

View File

@ -11,11 +11,17 @@ export class ERCTokenResolver implements IAddressResolver<TokenMeta> {
): Promise<TokenMeta | undefined> { ): Promise<TokenMeta | undefined> {
const erc20Contract = new Contract(address, erc20, provider); const erc20Contract = new Contract(address, erc20, provider);
try { try {
const [name, symbol, decimals] = await Promise.all([ const [name, symbol, decimals] = (await Promise.all([
erc20Contract.name(), erc20Contract.name(),
erc20Contract.symbol(), erc20Contract.symbol(),
erc20Contract.decimals(), erc20Contract.decimals(),
]); ])) as [string, string, number];
// Prevent faulty tokens with empty name/symbol
if (!name.trim() || !symbol.trim()) {
return undefined;
}
return { return {
name, name,
symbol, symbol,

View File

@ -1,40 +1,22 @@
import React, { useMemo } from "react"; import React from "react";
import { useSelectionContext } from "../useSelection"; import SelectionHighlighter, { addressSelector } from "./SelectionHighlighter";
type AddressHighlighterProps = React.PropsWithChildren<{ type AddressHighlighterProps = React.PropsWithChildren<{
address: string; address: string;
}>; }>;
// TODO: replace all occurences with SelectionHighlighter and remove this component
const AddressHighlighter: React.FC<AddressHighlighterProps> = ({ const AddressHighlighter: React.FC<AddressHighlighterProps> = ({
address, address,
children, children,
}) => { }) => (
const [selection, setSelection] = useSelectionContext(); <SelectionHighlighter
const [select, deselect] = useMemo(() => { myType="address"
const _select = () => { myContent={address}
setSelection({ type: "address", content: address }); selector={addressSelector}
}; >
const _deselect = () => { {children}
setSelection(null); </SelectionHighlighter>
}; );
return [_select, _deselect];
}, [setSelection, address]);
return ( export default AddressHighlighter;
<div
className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 truncate ${
selection !== null &&
selection.type === "address" &&
selection.content === address
? "border-orange-400 bg-yellow-100"
: "border-transparent"
}`}
onMouseEnter={select}
onMouseLeave={deselect}
>
{children}
</div>
);
};
export default React.memo(AddressHighlighter);

View File

@ -0,0 +1,27 @@
import React from "react";
import { NavLink } from "react-router-dom";
type AddressLinkProps = {
address: string;
text?: string;
dontOverrideColors?: boolean;
};
const AddressLink: React.FC<AddressLinkProps> = ({
address,
text,
dontOverrideColors,
}) => (
<NavLink
className={`${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} font-address truncate`}
to={`/address/${address}`}
>
<span className="truncate" title={text ?? address}>
{text ?? address}
</span>
</NavLink>
);
export default AddressLink;

View File

@ -0,0 +1,34 @@
import React from "react";
import { NavLink } from "react-router-dom";
import ENSLogo from "./ensLogo.svg";
type ENSNameLinkProps = {
name: string;
address: string;
dontOverrideColors?: boolean;
};
const ENSNameLink: React.FC<ENSNameLinkProps> = ({
name,
address,
dontOverrideColors,
}) => (
<NavLink
className={`flex items-baseline space-x-1 font-sans ${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} truncate`}
to={`/address/${name}`}
title={`${name}: ${address}`}
>
<img
className="self-center"
src={ENSLogo}
alt="ENS Logo"
width={12}
height={12}
/>
<span className="truncate">{name}</span>
</NavLink>
);
export default ENSNameLink;

View File

@ -1,13 +1,20 @@
import React from "react"; import React from "react";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
const ModeTab: React.FC = ({ children }) => ( type ModeTabProps = {
disabled?: boolean | undefined;
};
const ModeTab: React.FC<ModeTabProps> = ({ disabled, children }) => (
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
`border rounded-lg px-2 py-1 bg-gray-100 hover:bg-gray-200 hover:shadow text-xs text-gray-500 hover:text-gray-600 ${ `border rounded-lg px-2 py-1 bg-gray-100 ${
selected ? "border-blue-300" : "" disabled
}` ? "border-gray-100 text-gray-300 cursor-default"
: "hover:bg-gray-200 hover:shadow text-gray-500 hover:text-gray-600"
} text-xs ${selected ? "border-blue-300" : ""}`
} }
disabled={disabled}
> >
{children} {children}
</Tab> </Tab>

View File

@ -0,0 +1,78 @@
import React, { useMemo } from "react";
import {
useSelectionContext,
OptionalSelection,
SelectionType,
} from "../useSelection";
export type ContentSelector = (
selection: OptionalSelection,
content: string
) => boolean;
export const genericSelector =
(type: SelectionType) =>
(selection: OptionalSelection, content: string): boolean =>
selection !== null &&
selection.type === type &&
selection.content === content;
export const addressSelector: ContentSelector = genericSelector("address");
export const functionSigSelector: ContentSelector =
genericSelector("functionSig");
type SelectionHighlighterProps = React.PropsWithChildren<{
myType: SelectionType;
myContent: string;
selector: ContentSelector;
}>;
const SelectionHighlighter: React.FC<SelectionHighlighterProps> = ({
myType,
myContent,
selector,
children,
}) => {
const [selection, setSelection] = useSelectionContext();
const [select, deselect] = useMemo(() => {
const _select = () => {
setSelection({ type: myType, content: myContent });
};
const _deselect = () => {
setSelection(null);
};
return [_select, _deselect];
}, [setSelection, myType, myContent]);
return (
<HighlighterBox
selected={selector(selection, myContent)}
select={select}
deselect={deselect}
>
{children}
</HighlighterBox>
);
};
type HighlighterBoxProps = {
selected: boolean;
select: () => void;
deselect: () => void;
};
const HighlighterBox: React.FC<HighlighterBoxProps> = React.memo(
({ selected, select, deselect, children }) => (
<div
className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 truncate ${
selected ? "border-orange-400 bg-yellow-100" : "border-transparent"
}`}
onMouseEnter={select}
onMouseLeave={deselect}
>
{children}
</div>
)
);
export default SelectionHighlighter;

View File

@ -6,6 +6,7 @@ type ValueHighlighterProps = React.PropsWithChildren<{
value: BigNumber; value: BigNumber;
}>; }>;
// TODO: replace all occurences with SelectionHighlighter and remove this component
const ValueHighlighter: React.FC<ValueHighlighterProps> = ({ const ValueHighlighter: React.FC<ValueHighlighterProps> = ({
value, value,
children, children,

View File

@ -1,3 +1,3 @@
export const MIN_API_LEVEL = 2; export const MIN_API_LEVEL = 3;
export const PAGE_SIZE = 25; export const PAGE_SIZE = 25;

View File

@ -1,9 +1,5 @@
import React, { useContext, useMemo } from "react"; import React, { useContext, useMemo } from "react";
import { import { TransactionDescription } from "@ethersproject/abi";
TransactionDescription,
Fragment,
Interface,
} from "@ethersproject/abi";
import { BigNumber } from "@ethersproject/bignumber"; import { BigNumber } from "@ethersproject/bignumber";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
@ -36,7 +32,11 @@ import ExternalLink from "../components/ExternalLink";
import RelativePosition from "../components/RelativePosition"; import RelativePosition from "../components/RelativePosition";
import PercentagePosition from "../components/PercentagePosition"; import PercentagePosition from "../components/PercentagePosition";
import InputDecoder from "./decoder/InputDecoder"; import InputDecoder from "./decoder/InputDecoder";
import { rawInputTo4Bytes, use4Bytes } from "../use4Bytes"; import {
rawInputTo4Bytes,
use4Bytes,
useTransactionDescription,
} from "../use4Bytes";
import { DevDoc, useMultipleMetadata, UserDoc } from "../useSourcify"; import { DevDoc, useMultipleMetadata, UserDoc } from "../useSourcify";
import { ResolvedAddresses } from "../api/address-resolver"; import { ResolvedAddresses } from "../api/address-resolver";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
@ -69,18 +69,11 @@ const Details: React.FC<DetailsProps> = ({
const fourBytes = txData.to !== null ? rawInputTo4Bytes(txData.data) : "0x"; const fourBytes = txData.to !== null ? rawInputTo4Bytes(txData.data) : "0x";
const fourBytesEntry = use4Bytes(fourBytes); const fourBytesEntry = use4Bytes(fourBytes);
const fourBytesTxDesc = useMemo(() => { const fourBytesTxDesc = useTransactionDescription(
if (!fourBytesEntry) { fourBytesEntry,
return fourBytesEntry; txData.data,
} txData.value
if (!txData || !fourBytesEntry.signature) { );
return undefined;
}
const sig = fourBytesEntry?.signature;
const functionFragment = Fragment.fromString(`function ${sig}`);
const intf = new Interface([functionFragment]);
return intf.parseTransaction({ data: txData.data, value: txData.value });
}, [txData, fourBytesEntry]);
const resolvedTxDesc = txDesc ?? fourBytesTxDesc; const resolvedTxDesc = txDesc ?? fourBytesTxDesc;
const userMethod = txDesc ? userDoc?.methods[txDesc.signature] : undefined; const userMethod = txDesc ? userDoc?.methods[txDesc.signature] : undefined;

View File

@ -0,0 +1,34 @@
import React from "react";
import SelectionHighlighter, {
functionSigSelector,
} from "../components/SelectionHighlighter";
type FunctionSignatureProps = {
callType: string;
sig: string;
};
const FunctionSignature: React.FC<FunctionSignatureProps> = ({
callType,
sig,
}) => (
<SelectionHighlighter
myType="functionSig"
myContent={sig}
selector={functionSigSelector}
>
<span
className={`font-bold ${
callType === "STATICCALL"
? "text-red-700"
: callType === "DELEGATECALL" || callType === "CALLCODE"
? "text-gray-400"
: "text-blue-900"
}`}
>
{sig}
</span>
</SelectionHighlighter>
);
export default FunctionSignature;

69
src/transaction/Trace.tsx Normal file
View File

@ -0,0 +1,69 @@
import React, { useContext, useMemo } from "react";
import ContentFrame from "../ContentFrame";
import TransactionAddress from "../components/TransactionAddress";
import TraceItem from "./TraceItem";
import { TransactionData } from "../types";
import { useBatch4Bytes } from "../use4Bytes";
import { useTraceTransaction, useUniqueSignatures } from "../useErigonHooks";
import { RuntimeContext } from "../useRuntime";
import { ResolvedAddresses } from "../api/address-resolver";
import { tracesCollector, useResolvedAddresses } from "../useResolvedAddresses";
type TraceProps = {
txData: TransactionData;
resolvedAddresses: ResolvedAddresses | undefined;
};
const Trace: React.FC<TraceProps> = ({ txData, resolvedAddresses }) => {
const { provider } = useContext(RuntimeContext);
const traces = useTraceTransaction(provider, txData.transactionHash);
const uniqueSignatures = useUniqueSignatures(traces);
const sigMap = useBatch4Bytes(uniqueSignatures);
const addrCollector = useMemo(() => tracesCollector(traces), [traces]);
const traceResolvedAddresses = useResolvedAddresses(provider, addrCollector);
const mergedResolvedAddresses = useMemo(() => {
const merge = {};
if (resolvedAddresses) {
Object.assign(merge, resolvedAddresses);
}
if (traceResolvedAddresses) {
Object.assign(merge, traceResolvedAddresses);
}
return merge;
}, [resolvedAddresses, traceResolvedAddresses]);
return (
<ContentFrame tabs>
<div className="mt-4 mb-5 space-y-3 font-code text-sm flex flex-col items-start overflow-x-auto">
{traces ? (
<>
<div className="border hover:border-gray-500 rounded px-1 py-0.5">
<TransactionAddress
address={txData.from}
resolvedAddresses={resolvedAddresses}
/>
</div>
<div className="ml-5 space-y-3 self-stretch">
{traces.map((t, i, a) => (
<TraceItem
key={i}
t={t}
last={i === a.length - 1}
fourBytesMap={sigMap}
resolvedAddresses={mergedResolvedAddresses}
/>
))}
</div>
</>
) : (
<div className="border hover:border-gray-500 rounded px-1 py-1 w-96 h-7">
<div className="animate-pulse w-full h-full rounded bg-gray-200"></div>
</div>
)}
</div>
</ContentFrame>
);
};
export default React.memo(Trace);

View File

@ -0,0 +1,113 @@
import React, { useState } from "react";
import { Switch } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBomb } from "@fortawesome/free-solid-svg-icons/faBomb";
import TransactionAddress from "../components/TransactionAddress";
import FormattedBalance from "../components/FormattedBalance";
import FunctionSignature from "./FunctionSignature";
import InputDecoder from "./decoder/InputDecoder";
import { TraceEntry } from "../useErigonHooks";
import { ResolvedAddresses } from "../api/address-resolver";
import {
extract4Bytes,
FourBytesEntry,
useTransactionDescription,
} from "../use4Bytes";
type TraceInputProps = {
t: TraceEntry;
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
resolvedAddresses: ResolvedAddresses | undefined;
};
const TraceInput: React.FC<TraceInputProps> = ({
t,
fourBytesMap,
resolvedAddresses,
}) => {
const raw4Bytes = extract4Bytes(t.input);
const fourBytes = raw4Bytes !== null ? fourBytesMap[raw4Bytes] : null;
const sigText =
raw4Bytes === null ? "<fallback>" : fourBytes?.name ?? raw4Bytes;
const hasParams = t.input.length > 10;
const fourBytesTxDesc = useTransactionDescription(
fourBytes,
t.input,
t.value
);
const [expanded, setExpanded] = useState<boolean>(false);
return (
<div
className={`ml-5 border hover:border-gray-500 rounded px-1 py-0.5 ${
expanded ? "w-full" : ""
}`}
>
<div className="flex items-baseline">
<span className="text-xs text-gray-400 lowercase">{t.type}</span>
{t.type === "SELFDESTRUCT" ? (
<span className="pl-2 text-red-800" title="Self destruct">
<FontAwesomeIcon icon={faBomb} size="1x" />
</span>
) : (
<>
<span>
<TransactionAddress
address={t.to}
resolvedAddresses={resolvedAddresses}
/>
</span>
{t.type !== "CREATE" && t.type !== "CREATE2" && (
<>
<span>.</span>
<FunctionSignature callType={t.type} sig={sigText} />
{t.value && !t.value.isZero() && (
<span className="text-red-700 whitespace-nowrap">
{"{"}value: <FormattedBalance value={t.value} /> ETH{"}"}
</span>
)}
<span className="whitespace-nowrap">
(
{hasParams && (
<Switch
className="text-xs"
checked={expanded}
onChange={setExpanded}
>
{expanded ? (
<span className="text-gray-400">[-]</span>
) : (
<>[...]</>
)}
</Switch>
)}
{(!hasParams || !expanded) && <>)</>}
</span>
</>
)}
</>
)}
</div>
{hasParams && expanded && (
<>
<div className="ml-5 mr-1 my-2">
<InputDecoder
fourBytes={raw4Bytes ?? "0x"}
resolvedTxDesc={fourBytesTxDesc}
hasParamNames={false}
data={t.input}
userMethod={undefined}
devMethod={undefined}
resolvedAddresses={resolvedAddresses}
/>
</div>
<div>)</div>
</>
)}
</div>
);
};
export default TraceInput;

View File

@ -0,0 +1,92 @@
import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlusSquare } from "@fortawesome/free-regular-svg-icons/faPlusSquare";
import { faMinusSquare } from "@fortawesome/free-regular-svg-icons/faMinusSquare";
import { Switch } from "@headlessui/react";
import { FourBytesEntry } from "../use4Bytes";
import { TraceGroup } from "../useErigonHooks";
import { ResolvedAddresses } from "../api/address-resolver";
import TraceInput from "./TraceInput";
type TraceItemProps = {
t: TraceGroup;
last: boolean;
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
resolvedAddresses: ResolvedAddresses | undefined;
};
const TraceItem: React.FC<TraceItemProps> = ({
t,
last,
fourBytesMap,
resolvedAddresses,
}) => {
const [expanded, setExpanded] = useState<boolean>(true);
return (
<>
<div className="flex relative">
<div className="absolute border-l border-b w-5 h-6 transform -translate-y-3"></div>
{!last && (
<div className="absolute left-0 border-l w-5 h-full transform translate-y-3"></div>
)}
{t.children && (
<Switch
className="absolute left-0 bg-white transform -translate-x-1/2 text-gray-500"
checked={expanded}
onChange={setExpanded}
>
<FontAwesomeIcon
icon={expanded ? faMinusSquare : faPlusSquare}
size="1x"
/>
</Switch>
)}
<TraceInput
t={t}
fourBytesMap={fourBytesMap}
resolvedAddresses={resolvedAddresses}
/>
</div>
{t.children && (
<div
className={`pl-10 ${last ? "" : "border-l"} space-y-3 ${
expanded ? "" : "hidden"
}`}
>
<TraceChildren
c={t.children}
fourBytesMap={fourBytesMap}
resolvedAddresses={resolvedAddresses}
/>
</div>
)}
</>
);
};
type TraceChildrenProps = {
c: TraceGroup[];
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
resolvedAddresses: ResolvedAddresses | undefined;
};
const TraceChildren: React.FC<TraceChildrenProps> = React.memo(
({ c, fourBytesMap, resolvedAddresses }) => {
return (
<>
{c.map((tc, i, a) => (
<TraceItem
key={i}
t={tc}
last={i === a.length - 1}
fourBytesMap={fourBytesMap}
resolvedAddresses={resolvedAddresses}
/>
))}
</>
);
}
);
export default TraceItem;

View File

@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import TransactionAddress from "../../components/TransactionAddress";
import Copy from "../../components/Copy"; import Copy from "../../components/Copy";
import { ResolvedAddresses } from "../../api/address-resolver"; import { ResolvedAddresses } from "../../api/address-resolver";
import TransactionAddress from "../../components/TransactionAddress";
type AddressDecoderProps = { type AddressDecoderProps = {
r: any; r: string;
resolvedAddresses?: ResolvedAddresses | undefined; resolvedAddresses?: ResolvedAddresses | undefined;
}; };
@ -13,12 +13,9 @@ const AddressDecoder: React.FC<AddressDecoderProps> = ({
resolvedAddresses, resolvedAddresses,
}) => ( }) => (
<div className="flex items-baseline space-x-2 -ml-1 mr-3"> <div className="flex items-baseline space-x-2 -ml-1 mr-3">
<TransactionAddress <TransactionAddress address={r} resolvedAddresses={resolvedAddresses} />
address={r.toString()} <Copy value={r} />
resolvedAddresses={resolvedAddresses}
/>
<Copy value={r.toString()} />
</div> </div>
); );
export default React.memo(AddressDecoder); export default AddressDecoder;

View File

@ -72,7 +72,10 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
{paramType.baseType === "uint256" ? ( {paramType.baseType === "uint256" ? (
<Uint256Decoder r={r} /> <Uint256Decoder r={r} />
) : paramType.baseType === "address" ? ( ) : paramType.baseType === "address" ? (
<AddressDecoder r={r} resolvedAddresses={resolvedAddresses} /> <AddressDecoder
r={r.toString()}
resolvedAddresses={resolvedAddresses}
/>
) : paramType.baseType === "bool" ? ( ) : paramType.baseType === "bool" ? (
<BooleanDecoder r={r} /> <BooleanDecoder r={r} />
) : paramType.baseType === "bytes" ? ( ) : paramType.baseType === "bytes" ? (

View File

@ -30,18 +30,17 @@ const InputDecoder: React.FC<InputDecoderProps> = ({
try { try {
return toUtf8String(data); return toUtf8String(data);
} catch (err) { } catch (err) {
console.warn("Error while converting input data to string"); // Silently ignore on purpose
console.warn(err); return undefined;
return "<can't decode>";
} }
}, [data]); }, [data]);
return ( return (
<Tab.Group> <Tab.Group>
<Tab.List className="flex space-x-1 mb-1"> <Tab.List className="flex space-x-1 mb-1">
<ModeTab>Decoded</ModeTab> <ModeTab disabled={!resolvedTxDesc}>Decoded</ModeTab>
<ModeTab>Raw</ModeTab> <ModeTab>Raw</ModeTab>
<ModeTab>UTF-8</ModeTab> <ModeTab disabled={utfInput === undefined}>UTF-8</ModeTab>
</Tab.List> </Tab.List>
<Tab.Panels> <Tab.Panels>
<Tab.Panel> <Tab.Panel>

View File

@ -107,4 +107,4 @@ export type TokenMeta = {
decimals: number; decimals: number;
}; };
export type TokenMetas = Record<string, TokenMeta>; export type TokenMetas = Record<string, TokenMeta | null | undefined>;

View File

@ -1,21 +1,97 @@
import { useState, useEffect, useContext } from "react"; import { useState, useEffect, useContext, useMemo } from "react";
import {
Fragment,
Interface,
TransactionDescription,
} from "@ethersproject/abi";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { fourBytesURL } from "./url"; import { fourBytesURL } from "./url";
import { BigNumberish } from "@ethersproject/bignumber";
export type FourBytesEntry = { export type FourBytesEntry = {
name: string; name: string;
signature: string | undefined; signature: string | undefined;
}; };
export type FourBytesMap = Record<string, FourBytesEntry | null | undefined>;
const simpleTransfer: FourBytesEntry = { const simpleTransfer: FourBytesEntry = {
name: "Transfer", name: "transfer",
signature: undefined, signature: undefined,
}; };
const fullCache = new Map<string, FourBytesEntry | null>(); const fullCache = new Map<string, FourBytesEntry | null>();
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); export const rawInputTo4Bytes = (rawInput: string) => rawInput.slice(0, 10);
const fetch4Bytes = async (
assetsURLPrefix: string,
fourBytes: string
): Promise<FourBytesEntry | null> => {
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<FourBytesMap>({});
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<string, FourBytesEntry | null> = {};
for (let i = 0; i < rawFourByteSigs.length; i++) {
_fourBytesMap[rawFourByteSigs[i]] = results[i];
}
setFourBytesMap(_fourBytesMap);
};
loadSigs();
}, [assetsURLPrefix, rawFourByteSigs]);
return fourBytesMap;
};
/** /**
* Extract 4bytes DB info * Extract 4bytes DB info
* *
@ -47,35 +123,12 @@ export const use4Bytes = (
return; return;
} }
const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes); const loadSig = async () => {
fetch(signatureURL) const entry = await fetch4Bytes(assetsURLPrefix, fourBytes);
.then(async (res) => { fullCache.set(fourBytes, entry);
if (!res.ok) { setEntry(entry);
console.error(`Signature does not exist in 4bytes DB: ${fourBytes}`); };
fullCache.set(fourBytes, null); loadSig();
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("(");
let method = sig.slice(0, cut);
method = method.charAt(0).toUpperCase() + method.slice(1);
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);
});
}, [fourBytes, assetsURLPrefix]); }, [fourBytes, assetsURLPrefix]);
if (rawFourBytes === "0x") { if (rawFourBytes === "0x") {
@ -96,3 +149,29 @@ export const use4Bytes = (
fullCache.set(fourBytes, entry); fullCache.set(fourBytes, entry);
return entry; return entry;
}; };
export const useTransactionDescription = (
fourBytesEntry: FourBytesEntry | null | undefined,
data: string | undefined,
value: BigNumberish | undefined
): TransactionDescription | null | undefined => {
const txDesc = useMemo(() => {
if (!fourBytesEntry) {
return fourBytesEntry;
}
if (
!fourBytesEntry.signature ||
data === undefined ||
value === undefined
) {
return undefined;
}
const sig = fourBytesEntry?.signature;
const functionFragment = Fragment.fromString(`function ${sig}`);
const intf = new Interface([functionFragment]);
return intf.parseTransaction({ data, value });
}, [fourBytesEntry, data, value]);
return txDesc;
};

View File

@ -1,10 +1,11 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import { Block, BlockWithTransactions } from "@ethersproject/abstract-provider"; import { Block, BlockWithTransactions } from "@ethersproject/abstract-provider";
import { JsonRpcProvider } from "@ethersproject/providers"; import { JsonRpcProvider } from "@ethersproject/providers";
import { getAddress } from "@ethersproject/address"; import { getAddress } from "@ethersproject/address";
import { Contract } from "@ethersproject/contracts"; import { Contract } from "@ethersproject/contracts";
import { BigNumber } from "@ethersproject/bignumber"; import { BigNumber } from "@ethersproject/bignumber";
import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes"; import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes";
import { extract4Bytes } from "./use4Bytes";
import { getInternalOperations } from "./nodeFunctions"; import { getInternalOperations } from "./nodeFunctions";
import { import {
TokenMetas, TokenMetas,
@ -221,7 +222,7 @@ export const useTxData = (
// Extract token meta // Extract token meta
const tokenMetas: TokenMetas = {}; const tokenMetas: TokenMetas = {};
for (const t of tokenTransfers) { for (const t of tokenTransfers) {
if (tokenMetas[t.token]) { if (tokenMetas[t.token] !== undefined) {
continue; continue;
} }
const erc20Contract = new Contract(t.token, erc20, provider); const erc20Contract = new Contract(t.token, erc20, provider);
@ -237,6 +238,7 @@ export const useTxData = (
decimals, decimals,
}; };
} catch (err) { } catch (err) {
tokenMetas[t.token] = null;
console.warn(`Couldn't get token ${t.token} metadata; ignoring`, err); console.warn(`Couldn't get token ${t.token} metadata; ignoring`, err);
} }
} }
@ -308,3 +310,133 @@ export const useInternalOperations = (
return intTransfers; return intTransfers;
}; };
export type TraceEntry = {
type: string;
depth: number;
from: string;
to: string;
value: BigNumber;
input: string;
};
export type TraceGroup = TraceEntry & {
children: TraceGroup[] | null;
};
export const useTraceTransaction = (
provider: JsonRpcProvider | undefined,
txHash: string
): TraceGroup[] | undefined => {
const [traceGroups, setTraceGroups] = useState<TraceGroup[] | undefined>();
useEffect(() => {
if (!provider) {
setTraceGroups(undefined);
return;
}
const traceTx = async () => {
const results = await provider.send("ots_traceTransaction", [txHash]);
// Implement better formatter
for (let i = 0; i < results.length; i++) {
results[i].from = provider.formatter.address(results[i].from);
results[i].to = provider.formatter.address(results[i].to);
results[i].value =
results[i].value === null
? null
: provider.formatter.bigNumber(results[i].value);
}
// Build trace tree
const buildTraceTree = (
flatList: TraceEntry[],
depth: number = 0
): TraceGroup[] => {
const entries: TraceGroup[] = [];
let children: TraceEntry[] | null = null;
for (let i = 0; i < flatList.length; i++) {
if (flatList[i].depth === depth) {
if (children !== null) {
const childrenTree = buildTraceTree(children, depth + 1);
const prev = entries.pop();
if (prev) {
prev.children = childrenTree;
entries.push(prev);
}
}
entries.push({
...flatList[i],
children: null,
});
children = null;
} else {
if (children === null) {
children = [];
}
children.push(flatList[i]);
}
}
if (children !== null) {
const childrenTree = buildTraceTree(children, depth + 1);
const prev = entries.pop();
if (prev) {
prev.children = childrenTree;
entries.push(prev);
}
}
return entries;
};
const traceTree = buildTraceTree(results);
setTraceGroups(traceTree);
};
traceTx();
}, [provider, txHash]);
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<string>();
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;
};

View File

@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from "react";
import { JsonRpcProvider } from "@ethersproject/providers"; import { JsonRpcProvider } from "@ethersproject/providers";
import { ProcessedTransaction, TransactionData } from "./types"; import { ProcessedTransaction, TransactionData } from "./types";
import { batchPopulate, ResolvedAddresses } from "./api/address-resolver"; import { batchPopulate, ResolvedAddresses } from "./api/address-resolver";
import { TraceGroup } from "./useErigonHooks";
export type AddressCollector = () => string[]; export type AddressCollector = () => string[];
@ -61,6 +62,31 @@ export const transactionDataCollector =
return Array.from(uniqueAddresses); return Array.from(uniqueAddresses);
}; };
export const tracesCollector =
(traces: TraceGroup[] | undefined): AddressCollector =>
() => {
if (traces === undefined) {
return [];
}
const uniqueAddresses = new Set<string>();
let searchTraces = [...traces];
while (searchTraces.length > 0) {
const nextSearch: TraceGroup[] = [];
for (const g of searchTraces) {
uniqueAddresses.add(g.from);
uniqueAddresses.add(g.to);
if (g.children) {
nextSearch.push(...g.children);
}
}
searchTraces = nextSearch;
}
return Array.from(uniqueAddresses);
};
export const useResolvedAddresses = ( export const useResolvedAddresses = (
provider: JsonRpcProvider | undefined, provider: JsonRpcProvider | undefined,
addrCollector: AddressCollector addrCollector: AddressCollector

View File

@ -6,12 +6,14 @@ import {
useContext, useContext,
} from "react"; } from "react";
export type SelectionType = "address" | "value" | "functionSig";
export type Selection = { export type Selection = {
type: "address" | "value"; type: SelectionType;
content: string; content: string;
}; };
type OptionalSelection = Selection | null; export type OptionalSelection = Selection | null;
export const useSelection = (): [ export const useSelection = (): [
OptionalSelection, OptionalSelection,

View File

@ -14,7 +14,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx",
"downlevelIteration": true
}, },
"include": ["src"] "include": ["src"]
} }