otterscan/src/use4Bytes.ts

145 lines
4.2 KiB
TypeScript

import { useContext, useMemo } from "react";
import {
Fragment,
Interface,
TransactionDescription,
} from "@ethersproject/abi";
import { BigNumberish } from "@ethersproject/bignumber";
import { Fetcher } from "swr";
import useSWRImmutable from "swr/immutable";
import { RuntimeContext } from "./useRuntime";
import { fourBytesURL } from "./url";
export type FourBytesEntry = {
name: string;
signature: string | undefined;
};
/**
* Given a hex input data; extract the method selector
*
* @param rawInput Raw tx input including the "0x"
* @returns the first 4 bytes, including the "0x" or null if the input
* contains an invalid selector, e.g., txs with 0x00 data; simple transfers (0x)
* return null as well as it is not a method selector
*/
export const extract4Bytes = (rawInput: string): string | null => {
if (rawInput.length < 10) {
return null;
}
return rawInput.slice(0, 10);
};
type FourBytesKey = [id: "4bytes", fourBytes: string];
type FourBytesFetcher = Fetcher<
FourBytesEntry | null | undefined,
FourBytesKey
>;
const fourBytesFetcher =
(assetsURLPrefix: string): FourBytesFetcher =>
async (_, key) => {
if (key === null || key === "0x") {
return undefined;
}
// Handle simple transfers with invalid selector like tx:
// 0x8bcbdcc1589b5c34c1e55909c8269a411f0267a4fed59a73dd4348cc71addbb9,
// which contains 0x00 as data
if (key.length !== 10) {
return undefined;
}
const fourBytes = key.slice(2);
const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes);
try {
const res = await fetch(signatureURL);
if (!res.ok) {
console.warn(`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) {
// Network error or something wrong with URL config;
// silence and don't try it again
return null;
}
};
/**
* Extract 4bytes DB info
*
* @param rawFourBytes an hex string containing the 4bytes signature in the "0xXXXXXXXX" format.
*/
export const use4Bytes = (
rawFourBytes: string | null
): FourBytesEntry | null | undefined => {
if (rawFourBytes !== null && !rawFourBytes.startsWith("0x")) {
throw new Error(
`rawFourBytes must contain a bytes hex string starting with 0x; received value: "${rawFourBytes}"`
);
}
const { config } = useContext(RuntimeContext);
const assetsURLPrefix = config?.assetsURLPrefix;
const fourBytesKey = assetsURLPrefix !== undefined ? rawFourBytes : null;
const fetcher = fourBytesFetcher(assetsURLPrefix!);
const { data, error } = useSWRImmutable(["4bytes", fourBytesKey], fetcher);
return error ? undefined : data;
};
export const useMethodSelector = (data: string): [boolean, string, string] => {
const rawFourBytes = extract4Bytes(data);
const fourBytesEntry = use4Bytes(rawFourBytes);
const isSimpleTransfer = data === "0x";
const methodName = isSimpleTransfer
? "transfer"
: fourBytesEntry?.name ?? rawFourBytes ?? "-";
const methodTitle = isSimpleTransfer
? "ETH Transfer"
: methodName === rawFourBytes
? methodName
: `${methodName} [${rawFourBytes}]`;
return [isSimpleTransfer, methodName, methodTitle];
};
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;
};