First working version of non-verified contract topic0 decoder

This commit is contained in:
Willian Mitsuda 2021-10-08 02:38:46 -03:00
parent 903d5eacdd
commit ccad1f123a
3 changed files with 194 additions and 89 deletions

View File

@ -1,6 +1,6 @@
import React, { Fragment } from "react"; import React, { useMemo } from "react";
import { Log } from "@ethersproject/abstract-provider"; import { Log } from "@ethersproject/abstract-provider";
import { LogDescription } from "@ethersproject/abi"; import { Fragment, Interface, LogDescription } from "@ethersproject/abi";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import AddressHighlighter from "../components/AddressHighlighter"; import AddressHighlighter from "../components/AddressHighlighter";
import DecoratedAddressLink from "../components/DecoratedAddressLink"; import DecoratedAddressLink from "../components/DecoratedAddressLink";
@ -9,6 +9,7 @@ import ModeTab from "../components/ModeTab";
import DecodedParamsTable from "./decoder/DecodedParamsTable"; import DecodedParamsTable from "./decoder/DecodedParamsTable";
import DecodedLogSignature from "./decoder/DecodedLogSignature"; import DecodedLogSignature from "./decoder/DecodedLogSignature";
import { TransactionData } from "../types"; import { TransactionData } from "../types";
import { useTopic0 } from "../useTopic0";
type LogEntryProps = { type LogEntryProps = {
txData: TransactionData; txData: TransactionData;
@ -16,7 +17,29 @@ type LogEntryProps = {
logDesc: LogDescription | null | undefined; logDesc: LogDescription | null | undefined;
}; };
const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => ( const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => {
const rawTopic0 = log.topics[0];
const topic0 = useTopic0(rawTopic0);
const topic0LogDesc = useMemo(() => {
if (!topic0?.signature) {
return undefined;
}
const sig = topic0.signature;
const logFragment = Fragment.fromString(`event ${sig}`);
const intf = new Interface([logFragment]);
try {
return intf.parseLog(log);
} catch (err) {
// TODO: try other indexed/non-indexed combinations?
console.error(err);
return undefined;
}
}, [topic0, log]);
const resolvedLogDesc = logDesc ?? topic0LogDesc;
return (
<div className="flex space-x-10 py-5"> <div className="flex space-x-10 py-5">
<div> <div>
<span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500"> <span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500">
@ -48,15 +71,15 @@ const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => (
<ModeTab>Raw</ModeTab> <ModeTab>Raw</ModeTab>
</div> </div>
</Tab.List> </Tab.List>
<Tab.Panels as={Fragment}> <Tab.Panels as={React.Fragment}>
<Tab.Panel className="space-y-2"> <Tab.Panel className="space-y-2">
{logDesc === undefined ? ( {resolvedLogDesc === undefined ? (
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="col-start-2 flex space-x-2 items-center col-span-11"> <div className="col-start-2 flex space-x-2 items-center col-span-11">
Waiting for data... Waiting for data...
</div> </div>
</div> </div>
) : logDesc === null ? ( ) : resolvedLogDesc === null ? (
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="col-start-2 flex space-x-2 items-center col-span-11"> <div className="col-start-2 flex space-x-2 items-center col-span-11">
No decoded data No decoded data
@ -66,14 +89,16 @@ const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => (
<> <>
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="col-start-2 flex space-x-2 items-center col-span-11 font-mono"> <div className="col-start-2 flex space-x-2 items-center col-span-11 font-mono">
<DecodedLogSignature event={logDesc.eventFragment} /> <DecodedLogSignature
event={resolvedLogDesc.eventFragment}
/>
</div> </div>
</div> </div>
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="col-start-2 flex space-x-2 items-center col-span-11"> <div className="col-start-2 flex space-x-2 items-center col-span-11">
<DecodedParamsTable <DecodedParamsTable
args={logDesc.args} args={resolvedLogDesc.args}
paramTypes={logDesc.eventFragment.inputs} paramTypes={resolvedLogDesc.eventFragment.inputs}
txData={txData} txData={txData}
/> />
</div> </div>
@ -112,5 +137,6 @@ const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => (
</div> </div>
</div> </div>
); );
};
export default React.memo(LogEntry); export default React.memo(LogEntry);

View File

@ -5,6 +5,9 @@ export const fourBytesURL = (
fourBytes: string fourBytes: string
): string => `${assetsURLPrefix}/signatures/${fourBytes}`; ): string => `${assetsURLPrefix}/signatures/${fourBytes}`;
export const topic0URL = (assetsURLPrefix: string, topic0: string): string =>
`${assetsURLPrefix}/topic0/${topic0}`;
export const tokenLogoURL = ( export const tokenLogoURL = (
assetsURLPrefix: string, assetsURLPrefix: string,
address: string address: string

76
src/useTopic0.ts Normal file
View File

@ -0,0 +1,76 @@
import { useState, useEffect, useContext } from "react";
import { RuntimeContext } from "./useRuntime";
import { topic0URL } from "./url";
export type Topic0Entry = {
signature: string | undefined;
};
const fullCache = new Map<string, Topic0Entry | null>();
/**
* Extract topic0 DB info
*
* @param rawTopic0 an hex string containing the keccak256 of event signature
*/
export const useTopic0 = (
rawTopic0: string
): Topic0Entry | null | undefined => {
if (rawTopic0.length !== 66 || !rawTopic0.startsWith("0x")) {
throw new Error(
`rawTopic0 must contain a 32 bytes hex event signature starting with 0x; received value: "${rawTopic0}"`
);
}
const runtime = useContext(RuntimeContext);
const assetsURLPrefix = runtime.config?.assetsURLPrefix;
const topic0 = rawTopic0.slice(2);
const [entry, setEntry] = useState<Topic0Entry | null | undefined>(
fullCache.get(topic0)
);
useEffect(() => {
if (assetsURLPrefix === undefined) {
return;
}
const signatureURL = topic0URL(assetsURLPrefix, topic0);
fetch(signatureURL)
.then(async (res) => {
if (!res.ok) {
console.error(`Signature does not exist in topic0 DB: ${topic0}`);
fullCache.set(topic0, null);
setEntry(null);
return;
}
// Get only the first occurrence, for now ignore alternative param names
const sig = await res.text();
const entry: Topic0Entry = {
signature: sig,
};
setEntry(entry);
fullCache.set(topic0, entry);
})
.catch((err) => {
console.error(`Couldn't fetch signature URL ${signatureURL}`, err);
setEntry(null);
fullCache.set(topic0, null);
});
}, [topic0, assetsURLPrefix]);
if (assetsURLPrefix === undefined) {
return undefined;
}
// Try to resolve topic0 name
if (entry === null || entry === undefined) {
return entry;
}
// Simulates LRU
// TODO: implement LRU purging
fullCache.delete(topic0);
fullCache.set(topic0, entry);
return entry;
};