Merge branch 'feature/topic0-event-decoding' into develop

This commit is contained in:
Willian Mitsuda 2021-10-18 16:35:59 -03:00
commit 6fa78bcdc6
10 changed files with 313 additions and 97 deletions

3
.gitmodules vendored
View File

@ -4,3 +4,6 @@
[submodule "trustwallet"]
path = trustwallet
url = https://github.com/trustwallet/assets.git
[submodule "topic0"]
path = topic0
url = https://github.com/wmitsuda/topic0.git

View File

@ -19,8 +19,13 @@ WORKDIR /signatures
COPY 4bytes/signatures /signatures/
COPY 4bytes/with_parameter_names /signatures/
FROM alpine:3.14.0 AS topic0builder
WORKDIR /topic0
COPY topic0/with_parameter_names /topic0/
FROM nginx:1.21.1-alpine
RUN apk add jq
COPY --from=topic0builder /topic0 /usr/share/nginx/html/topic0/
COPY --from=fourbytesbuilder /signatures /usr/share/nginx/html/signatures/
COPY --from=logobuilder /assets /usr/share/nginx/html/assets/
COPY nginx.conf /etc/nginx/conf.d/default.conf

View File

@ -43,6 +43,40 @@ server {
}
}
location /topic0 {
root /usr/share/nginx/html;
expires 30d;
# Base on: https://michielkalkman.com/snippets/nginx-cors-open-configuration/
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
#
# Om nom nom cookies
#
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type' always;
}
}
location /assets {
root /usr/share/nginx/html;
expires 30d;

View File

@ -52,8 +52,8 @@
"test": "craco test",
"eject": "react-scripts eject",
"source-map-explorer": "source-map-explorer build/static/js/*.js",
"assets-start": "docker run --rm -p 3001:80 --name otterscan-assets -d -v$(pwd)/4bytes/signatures:/usr/share/nginx/html/signatures/ -v$(pwd)/trustwallet/blockchains/ethereum/assets:/usr/share/nginx/html/assets -v$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine",
"assets-start-with-param-names": "docker run --rm -p 3001:80 --name otterscan-assets -d -v$(pwd)/4bytes/with_parameter_names:/usr/share/nginx/html/signatures/ -v$(pwd)/trustwallet/blockchains/ethereum/assets:/usr/share/nginx/html/assets -v$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine",
"assets-start": "docker run --rm -p 3001:80 --name otterscan-assets -d -v$(pwd)/4bytes/signatures:/usr/share/nginx/html/signatures/ -v$(pwd)/trustwallet/blockchains/ethereum/assets:/usr/share/nginx/html/assets -v$(pwd)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine",
"assets-start-with-param-names": "docker run --rm -p 3001:80 --name otterscan-assets -d -v$(pwd)/4bytes/with_parameter_names:/usr/share/nginx/html/signatures/ -v$(pwd)/trustwallet/blockchains/ethereum/assets:/usr/share/nginx/html/assets -v$(pwd)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine",
"assets-stop": "docker stop otterscan-assets",
"docker-build": "DOCKER_BUILDKIT=1 docker build -t otterscan -f Dockerfile .",
"docker-start": "docker run --rm -p 5000:80 --name otterscan -d otterscan",

63
public/sourcify.svg Normal file
View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="15.875mm"
height="15.875mm"
viewBox="0 0 15.875 15.875"
version="1.1"
id="svg990"
inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
sodipodi:docname="sourcify.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview992"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.57905443"
inkscape:cx="384.24712"
inkscape:cy="511.1782"
inkscape:window-width="1440"
inkscape:window-height="872"
inkscape:window-x="0"
inkscape:window-y="28"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs987" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-3.0286537,-13.080962)">
<g
id="g972"
transform="matrix(0.26458333,0,0,0.26458333,3.0286537,13.080962)">
<path
d="M 0,30 C 0,46.5685 13.4315,60 30,60 46.5685,60 60,46.5685 60,30 60,13.4315 46.5685,0 30,0 13.4315,0 0,13.4315 0,30 Z"
fill="#2b50aa"
id="path874" />
<path
d="m 30.0587,59.413 c 16.2119,0 29.3542,-13.1423 29.3542,-29.3542 0,-16.2119 -13.1423,-29.35421 -29.3542,-29.35421 -16.2118,0 -29.354171,13.14231 -29.354171,29.35421 0,16.2119 13.142371,29.3542 29.354171,29.3542 z"
fill="#2b50aa"
id="path876" />
<path
d="m 21.9326,42.1567 c 0.1284,0.1176 0.2889,0.1819 0.4387,0.1819 0.1497,0 0.3209,-0.0643 0.4387,-0.1819 l 1.4765,-1.4123 c 0.1283,-0.1178 0.2032,-0.289 0.2032,-0.4602 0,-0.1712 -0.0749,-0.3424 -0.2032,-0.46 L 14.0366,30.0343 24.2865,20.2446 c 0.1283,-0.1178 0.2032,-0.289 0.2032,-0.4602 0,-0.1712 -0.0749,-0.3422 -0.2032,-0.46 L 22.81,17.9121 c -0.1285,-0.1176 -0.2782,-0.1819 -0.4387,-0.1819 -0.1605,0 -0.321,0.0643 -0.4387,0.1819 L 9.71403,29.5743 c -0.1284,0.1178 -0.20329,0.2888 -0.20329,0.46 0,0.1712 0.07489,0.3424 0.20329,0.4602 z M 46.0808,30.0343 35.8309,39.8242 c -0.1283,0.1176 -0.2033,0.2888 -0.2033,0.46 0,0.1712 0.075,0.3424 0.2033,0.4602 l 1.4765,1.4123 c 0.1285,0.1176 0.289,0.1819 0.4387,0.1819 0.1498,0 0.321,-0.0643 0.4387,-0.1819 L 50.4034,30.4945 c 0.1283,-0.1178 0.2032,-0.289 0.2032,-0.4602 0,-0.1712 -0.0749,-0.3422 -0.2032,-0.46 L 38.1848,17.9121 c -0.1284,-0.1176 -0.2889,-0.1819 -0.4387,-0.1819 -0.1605,0 -0.3209,0.0643 -0.4387,0.1819 l -1.4765,1.4123 c -0.1283,0.1178 -0.2033,0.2888 -0.2033,0.46 0,0.1712 0.075,0.3424 0.2033,0.4602 z"
fill="#c5d5ea"
id="path878" />
<path
d="m 21.8471,28.8355 c -0.0107,0.4172 0.1498,0.8131 0.4493,1.1126 l 0.0107,0.0107 c 0.2888,0.289 0.6634,0.4495 1.0699,0.4495 0.3746,0 0.7276,-0.1391 1.0058,-0.3853 l 4.1619,-3.7019 V 37.673 c 0,0.8346 0.6848,1.5194 1.5194,1.5194 0.8345,0 1.5193,-0.6848 1.5193,-1.5194 V 26.3104 l 4.1619,3.702 c 0.2783,0.2461 0.6421,0.3851 1.0058,0.3851 0.4066,0 0.7918,-0.1604 1.0806,-0.4494 l 0.0107,-0.0107 c 0.2996,-0.2995 0.4495,-0.6847 0.4495,-1.1127 -0.0108,-0.4172 -0.182,-0.8024 -0.4922,-1.0913 L 31.0805,21.56 c -0.2782,-0.2568 -0.6419,-0.3959 -1.0164,-0.3959 -0.3745,0 -0.7383,0.1391 -1.0165,0.3959 l -6.7191,6.1734 c -0.2996,0.2995 -0.4814,0.6848 -0.4814,1.1021 z"
fill="#c5d5ea"
id="path880" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -13,7 +13,6 @@ import queryString from "query-string";
import Blockies from "react-blockies";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons/faCheckCircle";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestionCircle";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
@ -240,7 +239,13 @@ const AddressTransactions: React.FC = () => {
</span>
) : (
<span className="self-center text-green-500">
<FontAwesomeIcon icon={faCheckCircle} />
<img
src="/sourcify.svg"
alt="Sourcify logo"
title="Verified by Sourcify"
width={16}
height={16}
/>
</span>
)}
</span>
@ -300,10 +305,6 @@ const AddressTransactions: React.FC = () => {
disabled={controller === undefined}
/>
</div>
<ResultHeader
feeDisplay={feeDisplay}
feeDisplayToggler={feeDisplayToggler}
/>
</SelectionContext.Provider>
) : (
<PendingResults />

View File

@ -1,6 +1,6 @@
import React, { Fragment } from "react";
import React, { useMemo } from "react";
import { Log } from "@ethersproject/abstract-provider";
import { LogDescription } from "@ethersproject/abi";
import { Fragment, Interface, LogDescription } from "@ethersproject/abi";
import { Tab } from "@headlessui/react";
import AddressHighlighter from "../components/AddressHighlighter";
import DecoratedAddressLink from "../components/DecoratedAddressLink";
@ -9,6 +9,7 @@ import ModeTab from "../components/ModeTab";
import DecodedParamsTable from "./decoder/DecodedParamsTable";
import DecodedLogSignature from "./decoder/DecodedLogSignature";
import { TransactionData } from "../types";
import { useTopic0 } from "../useTopic0";
type LogEntryProps = {
txData: TransactionData;
@ -16,101 +17,129 @@ type LogEntryProps = {
logDesc: LogDescription | null | undefined;
};
const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => (
<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} />
const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => {
const rawTopic0 = log.topics[0];
const topic0 = useTopic0(rawTopic0);
const topic0LogDesc = useMemo(() => {
if (!topic0?.signatures) {
return undefined;
}
const sigs = topic0.signatures;
for (const sig of sigs) {
const logFragment = Fragment.fromString(`event ${sig}`);
const intf = new Interface([logFragment]);
try {
return intf.parseLog(log);
} catch (err) {
// Ignore on purpose; try to match other sigs
}
}
return undefined;
}, [topic0, log]);
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>
<Tab.Group>
<Tab.List className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="text-right">Parameters</div>
<div className="col-span-11 flex space-x-1 mb-1">
<ModeTab>Decoded</ModeTab>
<ModeTab>Raw</ModeTab>
</div>
</Tab.List>
<Tab.Panels as={Fragment}>
<Tab.Panel className="space-y-2">
{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>
<Tab.Group>
<Tab.List className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="text-right">Parameters</div>
<div className="col-span-11 flex space-x-1 mb-1">
<ModeTab>Decoded</ModeTab>
<ModeTab>Raw</ModeTab>
</div>
</Tab.List>
<Tab.Panels as={React.Fragment}>
<Tab.Panel className="space-y-2">
{resolvedLogDesc === 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">
<DecodedParamsTable
args={logDesc.args}
paramTypes={logDesc.eventFragment.inputs}
txData={txData}
/>
Waiting for data...
</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>
) : resolvedLogDesc === 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={resolvedLogDesc.eventFragment}
/>
</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">
<DecodedParamsTable
args={resolvedLogDesc.args}
paramTypes={resolvedLogDesc.eventFragment.inputs}
txData={txData}
hasParamNames={resolvedLogDesc === logDesc}
/>
</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 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>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</div>
</div>
);
);
};
export default React.memo(LogEntry);

View File

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

77
src/useTopic0.ts Normal file
View File

@ -0,0 +1,77 @@
import { useState, useEffect, useContext } from "react";
import { RuntimeContext } from "./useRuntime";
import { topic0URL } from "./url";
export type Topic0Entry = {
signatures: 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 sigs = sig.split(";");
const entry: Topic0Entry = {
signatures: sigs,
};
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;
};

1
topic0 Submodule

@ -0,0 +1 @@
Subproject commit 52559d5690d491f8191a2d3fdb3c037516adc68f