First working version of non-verified contract topic0 decoder
This commit is contained in:
parent
903d5eacdd
commit
ccad1f123a
|
@ -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,101 +17,126 @@ 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 }) => {
|
||||||
<div className="flex space-x-10 py-5">
|
const rawTopic0 = log.topics[0];
|
||||||
<div>
|
const topic0 = useTopic0(rawTopic0);
|
||||||
<span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500">
|
|
||||||
{log.logIndex}
|
const topic0LogDesc = useMemo(() => {
|
||||||
</span>
|
if (!topic0?.signature) {
|
||||||
</div>
|
return undefined;
|
||||||
<div className="w-full space-y-2">
|
}
|
||||||
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
|
const sig = topic0.signature;
|
||||||
<div className="font-bold text-right">Address</div>
|
const logFragment = Fragment.fromString(`event ${sig}`);
|
||||||
<div className="col-span-11 mr-auto">
|
const intf = new Interface([logFragment]);
|
||||||
<div className="flex items-baseline space-x-2 -ml-1 mr-3">
|
try {
|
||||||
<AddressHighlighter address={log.address}>
|
return intf.parseLog(log);
|
||||||
<DecoratedAddressLink
|
} catch (err) {
|
||||||
address={log.address}
|
// TODO: try other indexed/non-indexed combinations?
|
||||||
miner={log.address === txData.confirmedData?.miner}
|
console.error(err);
|
||||||
txFrom={log.address === txData.from}
|
return undefined;
|
||||||
txTo={log.address === txData.to}
|
}
|
||||||
/>
|
}, [topic0, log]);
|
||||||
</AddressHighlighter>
|
|
||||||
<Copy value={log.address} />
|
const resolvedLogDesc = logDesc ?? topic0LogDesc;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex space-x-10 py-5">
|
||||||
|
<div>
|
||||||
|
<span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500">
|
||||||
|
{log.logIndex}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full space-y-2">
|
||||||
|
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
|
||||||
|
<div className="font-bold text-right">Address</div>
|
||||||
|
<div className="col-span-11 mr-auto">
|
||||||
|
<div className="flex items-baseline space-x-2 -ml-1 mr-3">
|
||||||
|
<AddressHighlighter address={log.address}>
|
||||||
|
<DecoratedAddressLink
|
||||||
|
address={log.address}
|
||||||
|
miner={log.address === txData.confirmedData?.miner}
|
||||||
|
txFrom={log.address === txData.from}
|
||||||
|
txTo={log.address === txData.to}
|
||||||
|
/>
|
||||||
|
</AddressHighlighter>
|
||||||
|
<Copy value={log.address} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Tab.Group>
|
||||||
<Tab.Group>
|
<Tab.List className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
|
||||||
<Tab.List className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
|
<div className="text-right">Parameters</div>
|
||||||
<div className="text-right">Parameters</div>
|
<div className="col-span-11 flex space-x-1 mb-1">
|
||||||
<div className="col-span-11 flex space-x-1 mb-1">
|
<ModeTab>Decoded</ModeTab>
|
||||||
<ModeTab>Decoded</ModeTab>
|
<ModeTab>Raw</ModeTab>
|
||||||
<ModeTab>Raw</ModeTab>
|
</div>
|
||||||
</div>
|
</Tab.List>
|
||||||
</Tab.List>
|
<Tab.Panels as={React.Fragment}>
|
||||||
<Tab.Panels as={Fragment}>
|
<Tab.Panel className="space-y-2">
|
||||||
<Tab.Panel className="space-y-2">
|
{resolvedLogDesc === undefined ? (
|
||||||
{logDesc === undefined ? (
|
|
||||||
<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">
|
|
||||||
Waiting for data...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : logDesc === null ? (
|
|
||||||
<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">
|
|
||||||
No decoded data
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<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">
|
|
||||||
<DecodedLogSignature event={logDesc.eventFragment} />
|
|
||||||
</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
|
Waiting for data...
|
||||||
args={logDesc.args}
|
|
||||||
paramTypes={logDesc.eventFragment.inputs}
|
|
||||||
txData={txData}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
) : resolvedLogDesc === null ? (
|
||||||
)}
|
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
|
||||||
</Tab.Panel>
|
<div className="col-start-2 flex space-x-2 items-center col-span-11">
|
||||||
<Tab.Panel className="space-y-2">
|
No decoded data
|
||||||
{log.topics.map((t, i) => (
|
</div>
|
||||||
<div
|
</div>
|
||||||
className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"
|
) : (
|
||||||
key={i}
|
<>
|
||||||
>
|
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
|
||||||
<div className="text-right">{i === 0 && "Topics"}</div>
|
<div className="col-start-2 flex space-x-2 items-center col-span-11 font-mono">
|
||||||
<div className="flex space-x-2 items-center col-span-11 font-mono">
|
<DecodedLogSignature
|
||||||
<span className="rounded bg-gray-100 text-gray-500 px-2 py-1 text-xs">
|
event={resolvedLogDesc.eventFragment}
|
||||||
{i}
|
/>
|
||||||
</span>
|
</div>
|
||||||
<span>{t}</span>
|
</div>
|
||||||
|
<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">
|
||||||
|
<DecodedParamsTable
|
||||||
|
args={resolvedLogDesc.args}
|
||||||
|
paramTypes={resolvedLogDesc.eventFragment.inputs}
|
||||||
|
txData={txData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="space-y-2">
|
||||||
|
{log.topics.map((t, i) => (
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
<div className="text-right">{i === 0 && "Topics"}</div>
|
||||||
|
<div className="flex space-x-2 items-center col-span-11 font-mono">
|
||||||
|
<span className="rounded bg-gray-100 text-gray-500 px-2 py-1 text-xs">
|
||||||
|
{i}
|
||||||
|
</span>
|
||||||
|
<span>{t}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
|
||||||
|
<div className="text-right pt-2">Data</div>
|
||||||
|
<div className="col-span-11">
|
||||||
|
<textarea
|
||||||
|
className="w-full h-40 bg-gray-50 font-mono focus:outline-none border rounded p-2"
|
||||||
|
value={log.data}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</Tab.Panel>
|
||||||
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
|
</Tab.Panels>
|
||||||
<div className="text-right pt-2">Data</div>
|
</Tab.Group>
|
||||||
<div className="col-span-11">
|
</div>
|
||||||
<textarea
|
|
||||||
className="w-full h-40 bg-gray-50 font-mono focus:outline-none border rounded p-2"
|
|
||||||
value={log.data}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab.Panel>
|
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
export default React.memo(LogEntry);
|
export default React.memo(LogEntry);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
Loading…
Reference in New Issue