Merge branch 'feature/topic0-event-decoding' into develop
This commit is contained in:
commit
6fa78bcdc6
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
34
nginx.conf
34
nginx.conf
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 |
|
@ -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 />
|
||||
|
|
|
@ -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,7 +17,31 @@ type LogEntryProps = {
|
|||
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?.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">
|
||||
|
@ -48,15 +73,15 @@ const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => (
|
|||
<ModeTab>Raw</ModeTab>
|
||||
</div>
|
||||
</Tab.List>
|
||||
<Tab.Panels as={Fragment}>
|
||||
<Tab.Panels as={React.Fragment}>
|
||||
<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="col-start-2 flex space-x-2 items-center col-span-11">
|
||||
Waiting for data...
|
||||
</div>
|
||||
</div>
|
||||
) : logDesc === null ? (
|
||||
) : 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
|
||||
|
@ -66,15 +91,18 @@ const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => (
|
|||
<>
|
||||
<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} />
|
||||
<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={logDesc.args}
|
||||
paramTypes={logDesc.eventFragment.inputs}
|
||||
args={resolvedLogDesc.args}
|
||||
paramTypes={resolvedLogDesc.eventFragment.inputs}
|
||||
txData={txData}
|
||||
hasParamNames={resolvedLogDesc === logDesc}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -112,5 +140,6 @@ const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => (
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(LogEntry);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 52559d5690d491f8191a2d3fdb3c037516adc68f
|
Loading…
Reference in New Issue