Merge branch 'release/v2021.11.03-otterscan'

This commit is contained in:
Willian Mitsuda 2021-11-22 19:10:29 -03:00
commit 36985ed0f8
51 changed files with 1615 additions and 993 deletions

2
.nvmrc
View File

@ -1 +1 @@
v14.17.3 v16.13.0

View File

@ -1,5 +1,4 @@
FROM node:14.17.3-alpine3.14 AS builder FROM node:16.13.0-alpine3.14 AS builder
RUN npm i -g npm@7.19.1
WORKDIR /otterscan-build WORKDIR /otterscan-build
COPY ["package.json", "package-lock.json", "/otterscan-build/"] COPY ["package.json", "package-lock.json", "/otterscan-build/"]
RUN npm install RUN npm install

View File

@ -118,7 +118,7 @@ server {
index index.html; index index.html;
try_files $uri /index.html; try_files $uri /index.html;
brotli_static on; # brotli_static on;
} }
#error_page 404 /404.html; #error_page 404 /404.html;

View File

@ -4,7 +4,7 @@ worker_processes auto;
error_log /var/log/nginx/error.log notice; error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid; pid /var/run/nginx.pid;
load_module modules/ngx_http_brotli_static_module.so; #load_module modules/ngx_http_brotli_static_module.so;
events { events {
worker_connections 1024; worker_connections 1024;

743
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
"@blackbox-vision/react-qr-reader": "^5.0.0", "@blackbox-vision/react-qr-reader": "^5.0.0",
"@chainlink/contracts": "^0.2.2", "@chainlink/contracts": "^0.2.2",
"@craco/craco": "^6.4.0", "@craco/craco": "^6.4.0",
"@fontsource/fira-code": "^4.5.1", "@fontsource/fira-code": "^4.5.2",
"@fontsource/roboto": "^4.5.1", "@fontsource/roboto": "^4.5.1",
"@fontsource/roboto-mono": "^4.5.0", "@fontsource/roboto-mono": "^4.5.0",
"@fontsource/space-grotesk": "^4.5.0", "@fontsource/space-grotesk": "^4.5.0",
@ -16,21 +16,21 @@
"@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16", "@fortawesome/react-fontawesome": "^0.1.16",
"@headlessui/react": "^1.4.1", "@headlessui/react": "^1.4.2",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.15.0",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/node": "^14.17.5", "@types/node": "^14.17.5",
"@types/react": "^17.0.33", "@types/react": "^17.0.35",
"@types/react-blockies": "^1.4.1", "@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.10", "@types/react-dom": "^17.0.11",
"@types/react-highlight": "^0.12.5", "@types/react-highlight": "^0.12.5",
"@types/react-router-dom": "^5.3.2", "@types/react-router-dom": "^5.3.2",
"@types/react-syntax-highlighter": "^13.5.2", "@types/react-syntax-highlighter": "^13.5.2",
"chart.js": "^3.5.1", "chart.js": "^3.5.1",
"ethers": "^5.5.1", "ethers": "^5.5.1",
"highlightjs-solidity": "^2.0.1", "highlightjs-solidity": "^2.0.2",
"query-string": "^7.0.1", "query-string": "^7.0.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-blockies": "^1.4.1", "react-blockies": "^1.4.1",
@ -41,8 +41,8 @@
"react-image": "^4.0.3", "react-image": "^4.0.3",
"react-router-dom": "^5.3.0", "react-router-dom": "^5.3.0",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"react-syntax-highlighter": "^15.4.4", "react-syntax-highlighter": "^15.4.5",
"serve": "^12.0.1", "serve": "^13.0.2",
"typescript": "^4.4.4", "typescript": "^4.4.4",
"use-keyboard-shortcut": "^1.0.6", "use-keyboard-shortcut": "^1.0.6",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"

View File

@ -31,7 +31,8 @@ import { useFeeToggler } from "./search/useFeeToggler";
import { SelectionContext, useSelection } from "./useSelection"; import { SelectionContext, useSelection } from "./useSelection";
import { useMultipleETHUSDOracle } from "./usePriceOracle"; import { useMultipleETHUSDOracle } from "./usePriceOracle";
import { useAppConfigContext } from "./useAppConfig"; import { useAppConfigContext } from "./useAppConfig";
import { useSourcify } from "./useSourcify"; import { useMultipleMetadata } from "./useSourcify";
import { ChecksummedAddress } from "./types";
import SourcifyLogo from "./sourcify.svg"; import SourcifyLogo from "./sourcify.svg";
type BlockParams = { type BlockParams = {
@ -181,12 +182,31 @@ const AddressTransactions: React.FC = () => {
const [feeDisplay, feeDisplayToggler] = useFeeToggler(); const [feeDisplay, feeDisplayToggler] = useFeeToggler();
const selectionCtx = useSelection(); const selectionCtx = useSelection();
const addresses = useMemo(() => {
const _addresses: ChecksummedAddress[] = [];
if (checksummedAddress) {
_addresses.push(checksummedAddress);
}
if (page) {
for (const t of page) {
if (t.to) {
_addresses.push(t.to);
}
}
}
return _addresses;
}, [checksummedAddress, page]);
const { sourcifySource } = useAppConfigContext(); const { sourcifySource } = useAppConfigContext();
const rawMetadata = useSourcify( const metadatas = useMultipleMetadata(
checksummedAddress, undefined,
addresses,
provider?.network.chainId, provider?.network.chainId,
sourcifySource sourcifySource
); );
const addressMetadata =
checksummedAddress !== undefined
? metadatas[checksummedAddress]
: undefined;
return ( return (
<StandardFrame> <StandardFrame>
@ -224,18 +244,18 @@ const AddressTransactions: React.FC = () => {
<NavTab href={`/address/${checksummedAddress}/contract`}> <NavTab href={`/address/${checksummedAddress}/contract`}>
<span <span
className={`flex items-baseline space-x-2 ${ className={`flex items-baseline space-x-2 ${
rawMetadata === undefined ? "italic opacity-50" : "" addressMetadata === undefined ? "italic opacity-50" : ""
}`} }`}
> >
<span>Contract</span> <span>Contract</span>
{rawMetadata === undefined ? ( {addressMetadata === undefined ? (
<span className="self-center"> <span className="self-center">
<FontAwesomeIcon <FontAwesomeIcon
className="animate-spin" className="animate-spin"
icon={faCircleNotch} icon={faCircleNotch}
/> />
</span> </span>
) : rawMetadata === null ? ( ) : addressMetadata === null ? (
<span className="self-center text-red-500"> <span className="self-center text-red-500">
<FontAwesomeIcon icon={faQuestionCircle} /> <FontAwesomeIcon icon={faQuestionCircle} />
</span> </span>
@ -288,6 +308,7 @@ const AddressTransactions: React.FC = () => {
selectedAddress={checksummedAddress} selectedAddress={checksummedAddress}
feeDisplay={feeDisplay} feeDisplay={feeDisplay}
priceMap={priceMap} priceMap={priceMap}
metadatas={metadatas}
/> />
))} ))}
<div className="flex justify-between items-baseline py-3"> <div className="flex justify-between items-baseline py-3">
@ -316,7 +337,7 @@ const AddressTransactions: React.FC = () => {
<Route path="/address/:addressOrName/contract" exact> <Route path="/address/:addressOrName/contract" exact>
<Contracts <Contracts
checksummedAddress={checksummedAddress} checksummedAddress={checksummedAddress}
rawMetadata={rawMetadata} rawMetadata={addressMetadata}
/> />
</Route> </Route>
</Switch> </Switch>

View File

@ -1,31 +1,31 @@
import React from "react"; import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCaretRight } from "@fortawesome/free-solid-svg-icons/faCaretRight"; import { faCaretRight } from "@fortawesome/free-solid-svg-icons/faCaretRight";
import AddressHighlighter from "./components/AddressHighlighter"; import TransactionAddress from "./components/TransactionAddress";
import ValueHighlighter from "./components/ValueHighlighter"; import ValueHighlighter from "./components/ValueHighlighter";
import DecoratedAddressLink from "./components/DecoratedAddressLink";
import FormattedBalance from "./components/FormattedBalance"; import FormattedBalance from "./components/FormattedBalance";
import { import {
AddressContext, AddressContext,
ChecksummedAddress,
TokenMeta, TokenMeta,
TokenTransfer, TokenTransfer,
TransactionData,
} from "./types"; } from "./types";
import { ResolvedAddresses } from "./api/address-resolver"; import { ResolvedAddresses } from "./api/address-resolver";
import { Metadata } from "./useSourcify";
type TokenTransferItemProps = { type TokenTransferItemProps = {
t: TokenTransfer; t: TokenTransfer;
txData: TransactionData; tokenMeta?: TokenMeta | null | undefined;
tokenMeta?: TokenMeta | undefined;
resolvedAddresses: ResolvedAddresses | undefined; resolvedAddresses: ResolvedAddresses | undefined;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
}; };
// TODO: handle partial // TODO: handle partial
const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
t, t,
txData,
tokenMeta, tokenMeta,
resolvedAddresses, resolvedAddresses,
metadatas,
}) => ( }) => (
<div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100"> <div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100">
<span className="text-gray-500"> <span className="text-gray-500">
@ -34,25 +34,21 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
<div className="grid grid-cols-5 gap-x-1"> <div className="grid grid-cols-5 gap-x-1">
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">From</span> <span className="font-bold">From</span>
<AddressHighlighter address={t.from}> <TransactionAddress
<DecoratedAddressLink
address={t.from} address={t.from}
addressCtx={AddressContext.FROM} addressCtx={AddressContext.FROM}
txFrom={t.from === txData.from} resolvedAddresses={resolvedAddresses}
txTo={t.from === txData.to} metadata={metadatas[t.from]}
/> />
</AddressHighlighter>
</div> </div>
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">To</span> <span className="font-bold">To</span>
<AddressHighlighter address={t.to}> <TransactionAddress
<DecoratedAddressLink
address={t.to} address={t.to}
addressCtx={AddressContext.TO} addressCtx={AddressContext.TO}
txFrom={t.to === txData.from} resolvedAddresses={resolvedAddresses}
txTo={t.to === txData.to} metadata={metadatas[t.to]}
/> />
</AddressHighlighter>
</div> </div>
<div className="col-span-3 flex space-x-1"> <div className="col-span-3 flex space-x-1">
<span className="font-bold">For</span> <span className="font-bold">For</span>
@ -64,12 +60,11 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
/> />
</ValueHighlighter> </ValueHighlighter>
</span> </span>
<AddressHighlighter address={t.token}> <TransactionAddress
<DecoratedAddressLink
address={t.token} address={t.token}
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
metadata={metadatas[t.token]}
/> />
</AddressHighlighter>
</div> </div>
</div> </div>
</div> </div>

View File

@ -15,6 +15,7 @@ import {
transactionDataCollector, transactionDataCollector,
useResolvedAddresses, useResolvedAddresses,
} from "./useResolvedAddresses"; } from "./useResolvedAddresses";
import { SelectedTransactionContext } from "./useSelectedTransaction";
const Details = React.lazy( const Details = React.lazy(
() => () =>
@ -29,6 +30,12 @@ const Logs = React.lazy(
/* webpackChunkName: "txlogs", webpackPrefetch: true */ "./transaction/Logs" /* webpackChunkName: "txlogs", webpackPrefetch: true */ "./transaction/Logs"
) )
); );
const Trace = React.lazy(
() =>
import(
/* webpackChunkName: "txtrace", webpackPrefetch: true */ "./transaction/Trace"
)
);
type TransactionParams = { type TransactionParams = {
txhash: string; txhash: string;
@ -76,6 +83,7 @@ const Transaction: React.FC = () => {
const txDesc = useTransactionDescription(metadata, txData); const txDesc = useTransactionDescription(metadata, txData);
return ( return (
<SelectedTransactionContext.Provider value={txData}>
<StandardFrame> <StandardFrame>
<StandardSubtitle>Transaction Details</StandardSubtitle> <StandardSubtitle>Transaction Details</StandardSubtitle>
{txData === null && ( {txData === null && (
@ -96,6 +104,7 @@ const Transaction: React.FC = () => {
{txData && ` (${txData.confirmedData?.logs?.length ?? 0})`} {txData && ` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab> </NavTab>
)} )}
<NavTab href={`/tx/${txhash}/trace`}>Trace</NavTab>
</Tab.List> </Tab.List>
</Tab.Group> </Tab.Group>
<React.Suspense fallback={null}> <React.Suspense fallback={null}>
@ -119,11 +128,18 @@ const Transaction: React.FC = () => {
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
/> />
</Route> </Route>
<Route path="/tx/:txhash/trace" exact>
<Trace
txData={txData}
resolvedAddresses={resolvedAddresses}
/>
</Route>
</Switch> </Switch>
</React.Suspense> </React.Suspense>
</SelectionContext.Provider> </SelectionContext.Provider>
)} )}
</StandardFrame> </StandardFrame>
</SelectedTransactionContext.Provider>
); );
}; };

View File

@ -11,11 +11,17 @@ export class ERCTokenResolver implements IAddressResolver<TokenMeta> {
): Promise<TokenMeta | undefined> { ): Promise<TokenMeta | undefined> {
const erc20Contract = new Contract(address, erc20, provider); const erc20Contract = new Contract(address, erc20, provider);
try { try {
const [name, symbol, decimals] = await Promise.all([ const [name, symbol, decimals] = (await Promise.all([
erc20Contract.name(), erc20Contract.name(),
erc20Contract.symbol(), erc20Contract.symbol(),
erc20Contract.decimals(), erc20Contract.decimals(),
]); ])) as [string, string, number];
// Prevent faulty tokens with empty name/symbol
if (!name.trim() || !symbol.trim()) {
return undefined;
}
return { return {
name, name,
symbol, symbol,

View File

@ -0,0 +1,22 @@
import { BaseProvider } from "@ethersproject/providers";
import { IAddressResolver } from "./address-resolver";
type HardcodedAddressMap = Record<string, string | undefined>;
export class HardcodedAddressResolver implements IAddressResolver<string> {
async resolveAddress(
provider: BaseProvider,
address: string
): Promise<string | undefined> {
try {
const addressMap: HardcodedAddressMap = await import(
`./hardcoded-addresses/${provider.network.chainId}.json`
);
return addressMap[address];
} catch (err) {
// Ignore on purpose
return undefined;
}
}
}

View File

@ -0,0 +1,13 @@
{
"0x7d655c57f71464B6f83811C55D84009Cd9f5221C": "Gitcoin: Bulk Checkout",
"0xf2354570bE2fB420832Fb7Ff6ff0AE0dF80CF2c6": "Gitcoin: GR8 Matching Payout",
"0x3342E3737732D879743f2682A3953a730ae4F47C": "Gitcoin: GR9 Matching Payout",
"0x3ebAFfe01513164e638480404c651E885cCA0AA4": "Gitcoin: GR10 Matching Payout",
"0x0EbD2E2130b73107d0C45fF2E16c93E7e2e10e3a": "Gitcoin: GR11 Matching Payout",
"0x722122dF12D4e14e13Ac3b6895a86e84145b6967": "Tornado Cash: Proxy",
"0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95": "Uniswap V1: Factory",
"0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f": "Uniswap V2: Factory",
"0xf164fC0Ec4E93095b804a4795bBe1e041497b92a": "Uniswap V2: Router 1",
"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D": "Uniswap V2: Router 2",
"0x1F98431c8aD98523631AE4a59f267346ea31F984": "Uniswap V3: Router"
}

View File

@ -1,5 +1,6 @@
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { ensRenderer } from "../../components/ENSName"; import { ensRenderer } from "../../components/ENSName";
import { plainStringRenderer } from "../../components/PlainString";
import { tokenRenderer } from "../../components/TokenName"; import { tokenRenderer } from "../../components/TokenName";
import { IAddressResolver, ResolvedAddressRenderer } from "./address-resolver"; import { IAddressResolver, ResolvedAddressRenderer } from "./address-resolver";
import { import {
@ -8,16 +9,19 @@ import {
} from "./CompositeAddressResolver"; } from "./CompositeAddressResolver";
import { ENSAddressResolver } from "./ENSAddressResolver"; import { ENSAddressResolver } from "./ENSAddressResolver";
import { ERCTokenResolver } from "./ERCTokenResolver"; import { ERCTokenResolver } from "./ERCTokenResolver";
import { HardcodedAddressResolver } from "./HardcodedAddressResolver";
export type ResolvedAddresses = Record<string, SelectedResolvedName<any>>; export type ResolvedAddresses = Record<string, SelectedResolvedName<any>>;
// Create and configure the main resolver // Create and configure the main resolver
export const ensResolver = new ENSAddressResolver(); export const ensResolver = new ENSAddressResolver();
export const ercTokenResolver = new ERCTokenResolver(); export const ercTokenResolver = new ERCTokenResolver();
export const hardcodedResolver = new HardcodedAddressResolver();
const _mainResolver = new CompositeAddressResolver(); const _mainResolver = new CompositeAddressResolver();
_mainResolver.addResolver(ensResolver); _mainResolver.addResolver(ensResolver);
_mainResolver.addResolver(ercTokenResolver); _mainResolver.addResolver(ercTokenResolver);
_mainResolver.addResolver(hardcodedResolver);
export const mainResolver: IAddressResolver<SelectedResolvedName<any>> = export const mainResolver: IAddressResolver<SelectedResolvedName<any>> =
_mainResolver; _mainResolver;
@ -28,6 +32,7 @@ export const resolverRendererRegistry = new Map<
>(); >();
resolverRendererRegistry.set(ensResolver, ensRenderer); resolverRendererRegistry.set(ensResolver, ensRenderer);
resolverRendererRegistry.set(ercTokenResolver, tokenRenderer); resolverRendererRegistry.set(ercTokenResolver, tokenRenderer);
resolverRendererRegistry.set(hardcodedResolver, plainStringRenderer);
// TODO: implement progressive resolving // TODO: implement progressive resolving
export const batchPopulate = async ( export const batchPopulate = async (

View File

@ -9,9 +9,11 @@ import { useFeeToggler } from "../search/useFeeToggler";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { SelectionContext, useSelection } from "../useSelection"; import { SelectionContext, useSelection } from "../useSelection";
import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses"; import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses";
import { ProcessedTransaction } from "../types"; import { ChecksummedAddress, ProcessedTransaction } from "../types";
import { PAGE_SIZE } from "../params"; import { PAGE_SIZE } from "../params";
import { useMultipleETHUSDOracle } from "../usePriceOracle"; import { useMultipleETHUSDOracle } from "../usePriceOracle";
import { useAppConfigContext } from "../useAppConfig";
import { useMultipleMetadata } from "../useSourcify";
type BlockTransactionResultsProps = { type BlockTransactionResultsProps = {
blockTag: BlockTag; blockTag: BlockTag;
@ -34,6 +36,21 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
const blockTags = useMemo(() => [blockTag], [blockTag]); const blockTags = useMemo(() => [blockTag], [blockTag]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags); const priceMap = useMultipleETHUSDOracle(provider, blockTags);
const addresses = useMemo((): ChecksummedAddress[] => {
if (!page) {
return [];
}
return page.map((t) => t.to).filter((to): to is string => to !== undefined);
}, [page]);
const { sourcifySource } = useAppConfigContext();
const metadatas = useMultipleMetadata(
undefined,
addresses,
provider?.network.chainId,
sourcifySource
);
return ( return (
<ContentFrame> <ContentFrame>
<div className="flex justify-between items-baseline py-3"> <div className="flex justify-between items-baseline py-3">
@ -63,6 +80,7 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
feeDisplay={feeDisplay} feeDisplay={feeDisplay}
priceMap={priceMap} priceMap={priceMap}
metadatas={metadatas}
/> />
))} ))}
<div className="flex justify-between items-baseline py-3"> <div className="flex justify-between items-baseline py-3">

View File

@ -1,40 +1,22 @@
import React, { useMemo } from "react"; import React from "react";
import { useSelectionContext } from "../useSelection"; import SelectionHighlighter, { addressSelector } from "./SelectionHighlighter";
type AddressHighlighterProps = React.PropsWithChildren<{ type AddressHighlighterProps = React.PropsWithChildren<{
address: string; address: string;
}>; }>;
// TODO: replace all occurences with SelectionHighlighter and remove this component
const AddressHighlighter: React.FC<AddressHighlighterProps> = ({ const AddressHighlighter: React.FC<AddressHighlighterProps> = ({
address, address,
children, children,
}) => { }) => (
const [selection, setSelection] = useSelectionContext(); <SelectionHighlighter
const [select, deselect] = useMemo(() => { myType="address"
const _select = () => { myContent={address}
setSelection({ type: "address", content: address }); selector={addressSelector}
};
const _deselect = () => {
setSelection(null);
};
return [_select, _deselect];
}, [setSelection, address]);
return (
<div
className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 truncate ${
selection !== null &&
selection.type === "address" &&
selection.content === address
? "border-orange-400 bg-yellow-100"
: "border-transparent"
}`}
onMouseEnter={select}
onMouseLeave={deselect}
> >
{children} {children}
</div> </SelectionHighlighter>
); );
};
export default React.memo(AddressHighlighter); export default AddressHighlighter;

View File

@ -0,0 +1,27 @@
import React from "react";
import { NavLink } from "react-router-dom";
type AddressLinkProps = {
address: string;
text?: string;
dontOverrideColors?: boolean;
};
const AddressLink: React.FC<AddressLinkProps> = ({
address,
text,
dontOverrideColors,
}) => (
<NavLink
className={`${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} font-address truncate`}
to={`/address/${address}`}
>
<span className="truncate" title={text ?? address}>
{text ?? address}
</span>
</NavLink>
);
export default AddressLink;

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { NavLink } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faStar } from "@fortawesome/free-solid-svg-icons/faStar"; import { faStar } from "@fortawesome/free-solid-svg-icons/faStar";
import { faBomb } from "@fortawesome/free-solid-svg-icons/faBomb"; import { faBomb } from "@fortawesome/free-solid-svg-icons/faBomb";
@ -8,6 +9,8 @@ import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins";
import AddressOrENSName from "./AddressOrENSName"; import AddressOrENSName from "./AddressOrENSName";
import { AddressContext, ZERO_ADDRESS } from "../types"; import { AddressContext, ZERO_ADDRESS } from "../types";
import { ResolvedAddresses } from "../api/address-resolver"; import { ResolvedAddresses } from "../api/address-resolver";
import { Metadata } from "../useSourcify";
import SourcifyLogo from "../sourcify.svg";
type DecoratedAddressLinkProps = { type DecoratedAddressLinkProps = {
address: string; address: string;
@ -19,6 +22,7 @@ type DecoratedAddressLinkProps = {
txFrom?: boolean; txFrom?: boolean;
txTo?: boolean; txTo?: boolean;
resolvedAddresses?: ResolvedAddresses | undefined; resolvedAddresses?: ResolvedAddresses | undefined;
metadata?: Metadata | null | undefined;
}; };
const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
@ -31,6 +35,7 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
txFrom, txFrom,
txTo, txTo,
resolvedAddresses, resolvedAddresses,
metadata,
}) => { }) => {
const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS; const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS;
const burn = addressCtx === AddressContext.TO && address === ZERO_ADDRESS; const burn = addressCtx === AddressContext.TO && address === ZERO_ADDRESS;
@ -70,6 +75,20 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
<FontAwesomeIcon icon={faCoins} size="1x" /> <FontAwesomeIcon icon={faCoins} size="1x" />
</span> </span>
)} )}
{metadata && (
<NavLink
className="self-center flex-shrink-0 flex items-center"
to={`/address/${address}/contract`}
>
<img
src={SourcifyLogo}
alt="Sourcify logo"
title="Verified by Sourcify"
width={16}
height={16}
/>
</NavLink>
)}
<AddressOrENSName <AddressOrENSName
address={address} address={address}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}

View File

@ -0,0 +1,34 @@
import React from "react";
import { NavLink } from "react-router-dom";
import ENSLogo from "./ensLogo.svg";
type ENSNameLinkProps = {
name: string;
address: string;
dontOverrideColors?: boolean;
};
const ENSNameLink: React.FC<ENSNameLinkProps> = ({
name,
address,
dontOverrideColors,
}) => (
<NavLink
className={`flex items-baseline space-x-1 font-sans ${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} truncate`}
to={`/address/${name}`}
title={`${name}: ${address}`}
>
<img
className="self-center"
src={ENSLogo}
alt="ENS Logo"
width={12}
height={12}
/>
<span className="truncate">{name}</span>
</NavLink>
);
export default ENSNameLink;

View File

@ -1,45 +1,44 @@
import React from "react"; import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
import TransactionAddress from "./TransactionAddress";
import AddressHighlighter from "./AddressHighlighter"; import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import { TransactionData, InternalOperation } from "../types"; import { InternalOperation } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
type InternalCreateProps = { type InternalCreateProps = {
txData: TransactionData;
internalOp: InternalOperation; internalOp: InternalOperation;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const InternalCreate: React.FC<InternalCreateProps> = ({ const InternalCreate: React.FC<InternalCreateProps> = ({
txData,
internalOp, internalOp,
}) => { resolvedAddresses,
return ( }) => (
<> <div className="flex items-baseline space-x-1 whitespace-nowrap">
<div className="flex items-baseline space-x-1 text-xs">
<span className="text-gray-500"> <span className="text-gray-500">
<FontAwesomeIcon icon={faAngleRight} size="1x" /> CREATE <FontAwesomeIcon icon={faAngleRight} size="1x" /> CREATE
</span> </span>
<span>Contract</span> <span>Contract</span>
<div className="flex items-baseline"> <div className="flex items-baseline">
<AddressHighlighter address={internalOp.to}> <AddressHighlighter address={internalOp.to}>
<DecoratedAddressLink address={internalOp.to} creation /> <DecoratedAddressLink
address={internalOp.to}
creation
resolvedAddresses={resolvedAddresses}
/>
</AddressHighlighter> </AddressHighlighter>
</div> </div>
<span className="flex items-baseline text-gray-400"> <span className="flex items-baseline text-gray-400">
(Creator:{" "} (Creator:{" "}
<AddressHighlighter address={internalOp.from}> <TransactionAddress
<DecoratedAddressLink
address={internalOp.from} address={internalOp.from}
txFrom={internalOp.from === txData.from} resolvedAddresses={resolvedAddresses}
txTo={internalOp.from === txData.to}
/> />
</AddressHighlighter>
) )
</span> </span>
</div> </div>
</> );
);
};
export default React.memo(InternalCreate); export default InternalCreate;

View File

@ -1,52 +1,57 @@
import React, { useContext } from "react"; import React from "react";
import { formatEther } from "@ethersproject/units"; import { formatEther } from "@ethersproject/units";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
import AddressHighlighter from "./AddressHighlighter"; import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import { RuntimeContext } from "../useRuntime";
import { TransactionData, InternalOperation } from "../types"; import { TransactionData, InternalOperation } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
const CHI_ADDRESS = "0x0000000000004946c0e9F43F4Dee607b0eF1fA1c"; import TransactionAddress from "./TransactionAddress";
const GST2_ADDRESS = "0x0000000000b3F879cb30FE243b4Dfee438691c04";
type InternalSelfDestructProps = { type InternalSelfDestructProps = {
txData: TransactionData; txData: TransactionData;
internalOp: InternalOperation; internalOp: InternalOperation;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
txData, txData,
internalOp, internalOp,
resolvedAddresses,
}) => { }) => {
const { provider } = useContext(RuntimeContext);
const network = provider?.network;
const toMiner = const toMiner =
txData.confirmedData?.miner !== undefined && txData.confirmedData?.miner !== undefined &&
internalOp.to === txData.confirmedData.miner; internalOp.to === txData.confirmedData.miner;
return ( return (
<> <>
<div className="flex items-baseline space-x-1 text-xs"> <div className="flex items-baseline space-x-1 whitespace-nowrap">
<span className="text-gray-500"> <span className="text-gray-500">
<FontAwesomeIcon icon={faAngleRight} size="1x" /> SELF DESTRUCT <FontAwesomeIcon icon={faAngleRight} size="1x" /> SELF DESTRUCT
</span> </span>
<span>Contract</span> <span>Contract</span>
<div className="flex items-baseline"> <div className="flex items-baseline">
<AddressHighlighter address={internalOp.from}> <AddressHighlighter address={internalOp.from}>
<DecoratedAddressLink address={internalOp.from} selfDestruct /> <DecoratedAddressLink
address={internalOp.from}
selfDestruct
resolvedAddresses={resolvedAddresses}
/>
</AddressHighlighter> </AddressHighlighter>
</div> </div>
{network?.chainId === 1 && internalOp.to === CHI_ADDRESS && ( {internalOp.value.isZero() && (
<span className="text-gray-400">(Chi Gastoken)</span> <div className="flex items-baseline text-gray-400">
)} (To:{" "}
{network?.chainId === 1 && internalOp.to === GST2_ADDRESS && ( <TransactionAddress
<span className="text-gray-400">(GST2 Gastoken)</span> address={internalOp.to}
resolvedAddresses={resolvedAddresses}
/>
)
</div>
)} )}
</div> </div>
{!internalOp.value.isZero() && ( {!internalOp.value.isZero() && (
<div className="ml-5 flex items-baseline space-x-1 text-xs"> <div className="ml-5 flex items-baseline space-x-1">
<span className="text-gray-500"> <span className="text-gray-500">
<FontAwesomeIcon icon={faAngleRight} size="1x" /> TRANSFER <FontAwesomeIcon icon={faAngleRight} size="1x" /> TRANSFER
</span> </span>
@ -59,7 +64,11 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
toMiner ? "rounded px-2 py-1 bg-yellow-100" : "" toMiner ? "rounded px-2 py-1 bg-yellow-100" : ""
}`} }`}
> >
<DecoratedAddressLink address={internalOp.to} miner={toMiner} /> <DecoratedAddressLink
address={internalOp.to}
miner={toMiner}
resolvedAddresses={resolvedAddresses}
/>
</div> </div>
</AddressHighlighter> </AddressHighlighter>
</div> </div>
@ -69,4 +78,4 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
); );
}; };
export default React.memo(InternalSelfDestruct); export default InternalSelfDestruct;

View File

@ -22,11 +22,18 @@ const InternalTransactionOperation: React.FC<InternalTransactionOperationProps>
/> />
)} )}
{internalOp.type === OperationType.SELF_DESTRUCT && ( {internalOp.type === OperationType.SELF_DESTRUCT && (
<InternalSelfDestruct txData={txData} internalOp={internalOp} /> <InternalSelfDestruct
txData={txData}
internalOp={internalOp}
resolvedAddresses={resolvedAddresses}
/>
)} )}
{(internalOp.type === OperationType.CREATE || {(internalOp.type === OperationType.CREATE ||
internalOp.type === OperationType.CREATE2) && ( internalOp.type === OperationType.CREATE2) && (
<InternalCreate txData={txData} internalOp={internalOp} /> <InternalCreate
internalOp={internalOp}
resolvedAddresses={resolvedAddresses}
/>
)} )}
</> </>
); );

View File

@ -26,7 +26,7 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
internalOp.to === txData.confirmedData.miner; internalOp.to === txData.confirmedData.miner;
return ( return (
<div className="flex items-baseline space-x-1 text-xs"> <div className="flex items-baseline space-x-1 whitespace-nowrap">
<span className="text-gray-500"> <span className="text-gray-500">
<FontAwesomeIcon icon={faAngleRight} size="1x" /> TRANSFER <FontAwesomeIcon icon={faAngleRight} size="1x" /> TRANSFER
</span> </span>
@ -44,6 +44,7 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
miner={fromMiner} miner={fromMiner}
txFrom={internalOp.from === txData.from} txFrom={internalOp.from === txData.from}
txTo={internalOp.from === txData.to} txTo={internalOp.from === txData.to}
resolvedAddresses={resolvedAddresses}
/> />
</div> </div>
</AddressHighlighter> </AddressHighlighter>
@ -61,6 +62,7 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
miner={toMiner} miner={toMiner}
txFrom={internalOp.to === txData.from} txFrom={internalOp.to === txData.from}
txTo={internalOp.to === txData.to} txTo={internalOp.to === txData.to}
resolvedAddresses={resolvedAddresses}
/> />
</div> </div>
</AddressHighlighter> </AddressHighlighter>
@ -69,4 +71,4 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
); );
}; };
export default React.memo(InternalTransfer); export default InternalTransfer;

View File

@ -1,13 +1,20 @@
import React from "react"; import React from "react";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
const ModeTab: React.FC = ({ children }) => ( type ModeTabProps = {
disabled?: boolean | undefined;
};
const ModeTab: React.FC<ModeTabProps> = ({ disabled, children }) => (
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
`border rounded-lg px-2 py-1 bg-gray-100 hover:bg-gray-200 hover:shadow text-xs text-gray-500 hover:text-gray-600 ${ `border rounded-lg px-2 py-1 bg-gray-100 ${
selected ? "border-blue-300" : "" disabled
}` ? "border-gray-100 text-gray-300 cursor-default"
: "hover:bg-gray-200 hover:shadow text-gray-500 hover:text-gray-600"
} text-xs ${selected ? "border-blue-300" : ""}`
} }
disabled={disabled}
> >
{children} {children}
</Tab> </Tab>

View File

@ -0,0 +1,53 @@
import React from "react";
import { NavLink } from "react-router-dom";
import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver";
type PlainStringProps = {
address: string;
name: string;
linkable: boolean;
dontOverrideColors: boolean | undefined;
};
const PlainString: React.FC<PlainStringProps> = ({
address,
name,
linkable,
dontOverrideColors,
}) => {
if (linkable) {
return (
<NavLink
className={`${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} truncate`}
to={`/address/${address}`}
title={name}
>
{name}
</NavLink>
);
}
return (
<span className="text-gray-400 truncate" title={name}>
{name}
</span>
);
};
export const plainStringRenderer: ResolvedAddressRenderer<string> = (
address,
resolvedAddress,
linkable,
dontOverrideColors
) => (
<PlainString
address={address}
name={resolvedAddress}
linkable={linkable}
dontOverrideColors={dontOverrideColors}
/>
);
export default PlainString;

View File

@ -0,0 +1,78 @@
import React, { useMemo } from "react";
import {
useSelectionContext,
OptionalSelection,
SelectionType,
} from "../useSelection";
export type ContentSelector = (
selection: OptionalSelection,
content: string
) => boolean;
export const genericSelector =
(type: SelectionType) =>
(selection: OptionalSelection, content: string): boolean =>
selection !== null &&
selection.type === type &&
selection.content === content;
export const addressSelector: ContentSelector = genericSelector("address");
export const functionSigSelector: ContentSelector =
genericSelector("functionSig");
type SelectionHighlighterProps = React.PropsWithChildren<{
myType: SelectionType;
myContent: string;
selector: ContentSelector;
}>;
const SelectionHighlighter: React.FC<SelectionHighlighterProps> = ({
myType,
myContent,
selector,
children,
}) => {
const [selection, setSelection] = useSelectionContext();
const [select, deselect] = useMemo(() => {
const _select = () => {
setSelection({ type: myType, content: myContent });
};
const _deselect = () => {
setSelection(null);
};
return [_select, _deselect];
}, [setSelection, myType, myContent]);
return (
<HighlighterBox
selected={selector(selection, myContent)}
select={select}
deselect={deselect}
>
{children}
</HighlighterBox>
);
};
type HighlighterBoxProps = {
selected: boolean;
select: () => void;
deselect: () => void;
};
const HighlighterBox: React.FC<HighlighterBoxProps> = React.memo(
({ selected, select, deselect, children }) => (
<div
className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 truncate ${
selected ? "border-orange-400 bg-yellow-100" : "border-transparent"
}`}
onMouseEnter={select}
onMouseLeave={deselect}
>
{children}
</div>
)
);
export default SelectionHighlighter;

View File

@ -0,0 +1,42 @@
import React from "react";
import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink";
import { ResolvedAddresses } from "../api/address-resolver";
import { useSelectedTransaction } from "../useSelectedTransaction";
import { AddressContext } from "../types";
import { Metadata } from "../useSourcify";
type TransactionAddressProps = {
address: string;
addressCtx?: AddressContext | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
metadata?: Metadata | null | undefined;
};
const TransactionAddress: React.FC<TransactionAddressProps> = ({
address,
addressCtx,
resolvedAddresses,
metadata,
}) => {
const txData = useSelectedTransaction();
// TODO: push down creation coloring logic into DecoratedAddressLink
const creation = address === txData?.confirmedData?.createdContractAddress;
return (
<AddressHighlighter address={address}>
<DecoratedAddressLink
address={address}
addressCtx={addressCtx}
miner={address === txData?.confirmedData?.miner}
txFrom={address === txData?.from}
txTo={address === txData?.to || creation}
creation={creation}
resolvedAddresses={resolvedAddresses}
metadata={metadata}
/>
</AddressHighlighter>
);
};
export default TransactionAddress;

View File

@ -6,6 +6,7 @@ type ValueHighlighterProps = React.PropsWithChildren<{
value: BigNumber; value: BigNumber;
}>; }>;
// TODO: replace all occurences with SelectionHighlighter and remove this component
const ValueHighlighter: React.FC<ValueHighlighterProps> = ({ const ValueHighlighter: React.FC<ValueHighlighterProps> = ({
value, value,
children, children,

View File

@ -1,3 +1,3 @@
export const MIN_API_LEVEL = 2; export const MIN_API_LEVEL = 3;
export const PAGE_SIZE = 25; export const PAGE_SIZE = 25;

View File

@ -14,11 +14,12 @@ import TransactionDirection, {
Flags, Flags,
} from "../components/TransactionDirection"; } from "../components/TransactionDirection";
import TransactionValue from "../components/TransactionValue"; import TransactionValue from "../components/TransactionValue";
import { ProcessedTransaction } from "../types"; import { ChecksummedAddress, ProcessedTransaction } from "../types";
import { FeeDisplay } from "./useFeeToggler"; import { FeeDisplay } from "./useFeeToggler";
import { formatValue } from "../components/formatter"; import { formatValue } from "../components/formatter";
import ETH2USDValue from "../components/ETH2USDValue"; import ETH2USDValue from "../components/ETH2USDValue";
import { ResolvedAddresses } from "../api/address-resolver"; import { ResolvedAddresses } from "../api/address-resolver";
import { Metadata } from "../useSourcify";
type TransactionItemProps = { type TransactionItemProps = {
tx: ProcessedTransaction; tx: ProcessedTransaction;
@ -26,6 +27,7 @@ type TransactionItemProps = {
selectedAddress?: string; selectedAddress?: string;
feeDisplay: FeeDisplay; feeDisplay: FeeDisplay;
priceMap: Record<BlockTag, BigNumber>; priceMap: Record<BlockTag, BigNumber>;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
}; };
const TransactionItem: React.FC<TransactionItemProps> = ({ const TransactionItem: React.FC<TransactionItemProps> = ({
@ -34,6 +36,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
selectedAddress, selectedAddress,
feeDisplay, feeDisplay,
priceMap, priceMap,
metadatas,
}) => { }) => {
let direction: Direction | undefined; let direction: Direction | undefined;
if (selectedAddress) { if (selectedAddress) {
@ -105,6 +108,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
miner={tx.miner === tx.to} miner={tx.miner === tx.to}
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
metadata={metadatas[tx.to]}
/> />
</AddressHighlighter> </AddressHighlighter>
) : ( ) : (
@ -114,6 +118,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
creation creation
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
metadata={metadatas[tx.createdContractAddress!]}
/> />
</AddressHighlighter> </AddressHighlighter>
)} )}

View File

@ -1,12 +1,6 @@
import React, { useMemo } from "react"; import React, { useContext, useMemo } from "react";
import { import { TransactionDescription } from "@ethersproject/abi";
TransactionDescription,
Fragment,
Interface,
} from "@ethersproject/abi";
import { BigNumber } from "@ethersproject/bignumber"; import { BigNumber } from "@ethersproject/bignumber";
import { toUtf8String } from "@ethersproject/strings";
import { Tab } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
import { faCube } from "@fortawesome/free-solid-svg-icons/faCube"; import { faCube } from "@fortawesome/free-solid-svg-icons/faCube";
@ -15,8 +9,7 @@ import ContentFrame from "../ContentFrame";
import InfoRow from "../components/InfoRow"; import InfoRow from "../components/InfoRow";
import BlockLink from "../components/BlockLink"; import BlockLink from "../components/BlockLink";
import BlockConfirmations from "../components/BlockConfirmations"; import BlockConfirmations from "../components/BlockConfirmations";
import AddressHighlighter from "../components/AddressHighlighter"; import TransactionAddress from "../components/TransactionAddress";
import DecoratedAddressLink from "../components/DecoratedAddressLink";
import Copy from "../components/Copy"; import Copy from "../components/Copy";
import Nonce from "../components/Nonce"; import Nonce from "../components/Nonce";
import Timestamp from "../components/Timestamp"; import Timestamp from "../components/Timestamp";
@ -29,16 +22,25 @@ import USDValue from "../components/USDValue";
import FormattedBalance from "../components/FormattedBalance"; import FormattedBalance from "../components/FormattedBalance";
import ETH2USDValue from "../components/ETH2USDValue"; import ETH2USDValue from "../components/ETH2USDValue";
import TokenTransferItem from "../TokenTransferItem"; import TokenTransferItem from "../TokenTransferItem";
import { TransactionData, InternalOperation } from "../types"; import {
TransactionData,
InternalOperation,
ChecksummedAddress,
} from "../types";
import PercentageBar from "../components/PercentageBar"; import PercentageBar from "../components/PercentageBar";
import ExternalLink from "../components/ExternalLink"; import ExternalLink from "../components/ExternalLink";
import RelativePosition from "../components/RelativePosition"; import RelativePosition from "../components/RelativePosition";
import PercentagePosition from "../components/PercentagePosition"; import PercentagePosition from "../components/PercentagePosition";
import ModeTab from "../components/ModeTab"; import InputDecoder from "./decoder/InputDecoder";
import DecodedParamsTable from "./decoder/DecodedParamsTable"; import {
import { rawInputTo4Bytes, use4Bytes } from "../use4Bytes"; rawInputTo4Bytes,
import { DevDoc, UserDoc } from "../useSourcify"; use4Bytes,
useTransactionDescription,
} from "../use4Bytes";
import { DevDoc, useMultipleMetadata, UserDoc } from "../useSourcify";
import { ResolvedAddresses } from "../api/address-resolver"; import { ResolvedAddresses } from "../api/address-resolver";
import { RuntimeContext } from "../useRuntime";
import { useAppConfigContext } from "../useAppConfig";
type DetailsProps = { type DetailsProps = {
txData: TransactionData; txData: TransactionData;
@ -65,35 +67,42 @@ const Details: React.FC<DetailsProps> = ({
txData.confirmedData?.blockBaseFeePerGas !== undefined && txData.confirmedData?.blockBaseFeePerGas !== undefined &&
txData.confirmedData?.blockBaseFeePerGas !== null; txData.confirmedData?.blockBaseFeePerGas !== null;
const utfInput = useMemo(() => {
try {
return txData && toUtf8String(txData.data);
} catch (err) {
console.warn("Error while converting input data to string");
console.warn(err);
return "<can't decode>";
}
}, [txData]);
const fourBytes = txData.to !== null ? rawInputTo4Bytes(txData.data) : "0x"; const fourBytes = txData.to !== null ? rawInputTo4Bytes(txData.data) : "0x";
const fourBytesEntry = use4Bytes(fourBytes); const fourBytesEntry = use4Bytes(fourBytes);
const fourBytesTxDesc = useMemo(() => { const fourBytesTxDesc = useTransactionDescription(
if (!fourBytesEntry) { fourBytesEntry,
return fourBytesEntry; txData.data,
} txData.value
if (!txData || !fourBytesEntry.signature) { );
return undefined;
}
const sig = fourBytesEntry?.signature;
const functionFragment = Fragment.fromString(`function ${sig}`);
const intf = new Interface([functionFragment]);
return intf.parseTransaction({ data: txData.data, value: txData.value });
}, [txData, fourBytesEntry]);
const resolvedTxDesc = txDesc ?? fourBytesTxDesc; const resolvedTxDesc = txDesc ?? fourBytesTxDesc;
const userMethod = txDesc ? userDoc?.methods[txDesc.signature] : undefined; const userMethod = txDesc ? userDoc?.methods[txDesc.signature] : undefined;
const devMethod = txDesc ? devDoc?.methods[txDesc.signature] : undefined; const devMethod = txDesc ? devDoc?.methods[txDesc.signature] : undefined;
const { provider } = useContext(RuntimeContext);
const addresses = useMemo(() => {
const _addresses: ChecksummedAddress[] = [];
if (txData.to) {
_addresses.push(txData.to);
}
if (txData.confirmedData?.createdContractAddress) {
_addresses.push(txData.confirmedData.createdContractAddress);
}
for (const t of txData.tokenTransfers) {
_addresses.push(t.from);
_addresses.push(t.to);
_addresses.push(t.token);
}
return _addresses;
}, [txData]);
const { sourcifySource } = useAppConfigContext();
const metadatas = useMultipleMetadata(
undefined,
addresses,
provider?.network.chainId,
sourcifySource
);
return ( return (
<ContentFrame tabs> <ContentFrame tabs>
<InfoRow title="Transaction Hash"> <InfoRow title="Transaction Hash">
@ -152,14 +161,10 @@ const Details: React.FC<DetailsProps> = ({
<InfoRow title="From / Nonce"> <InfoRow title="From / Nonce">
<div className="flex divide-x-2 divide-dotted divide-gray-300"> <div className="flex divide-x-2 divide-dotted divide-gray-300">
<div className="flex items-baseline space-x-2 -ml-1 mr-3"> <div className="flex items-baseline space-x-2 -ml-1 mr-3">
<AddressHighlighter address={txData.from}> <TransactionAddress
<DecoratedAddressLink
address={txData.from} address={txData.from}
miner={txData.from === txData.confirmedData?.miner}
txFrom
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
/> />
</AddressHighlighter>
<Copy value={txData.from} /> <Copy value={txData.from} />
</div> </div>
<div className="flex items-baseline pl-3"> <div className="flex items-baseline pl-3">
@ -170,14 +175,11 @@ const Details: React.FC<DetailsProps> = ({
<InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}> <InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}>
{txData.to ? ( {txData.to ? (
<div className="flex items-baseline space-x-2 -ml-1"> <div className="flex items-baseline space-x-2 -ml-1">
<AddressHighlighter address={txData.to}> <TransactionAddress
<DecoratedAddressLink
address={txData.to} address={txData.to}
miner={txData.to === txData.confirmedData?.miner}
txTo
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
metadata={metadatas?.[txData.to]}
/> />
</AddressHighlighter>
<Copy value={txData.to} /> <Copy value={txData.to} />
</div> </div>
) : txData.confirmedData === undefined ? ( ) : txData.confirmedData === undefined ? (
@ -186,21 +188,18 @@ const Details: React.FC<DetailsProps> = ({
</span> </span>
) : ( ) : (
<div className="flex items-baseline space-x-2 -ml-1"> <div className="flex items-baseline space-x-2 -ml-1">
<AddressHighlighter <TransactionAddress
address={txData.confirmedData?.createdContractAddress!} address={txData.confirmedData?.createdContractAddress!}
>
<DecoratedAddressLink
address={txData.confirmedData.createdContractAddress!}
creation
txTo
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
metadata={
metadatas?.[txData.confirmedData?.createdContractAddress!]
}
/> />
</AddressHighlighter>
<Copy value={txData.confirmedData.createdContractAddress!} /> <Copy value={txData.confirmedData.createdContractAddress!} />
</div> </div>
)} )}
{internalOps && internalOps.length > 0 && ( {internalOps && internalOps.length > 0 && (
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1 overflow-x-auto">
{internalOps.map((op, i) => ( {internalOps.map((op, i) => (
<InternalTransactionOperation <InternalTransactionOperation
key={i} key={i}
@ -219,17 +218,15 @@ const Details: React.FC<DetailsProps> = ({
)} )}
{txData.tokenTransfers.length > 0 && ( {txData.tokenTransfers.length > 0 && (
<InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}> <InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}>
<div>
{txData.tokenTransfers.map((t, i) => ( {txData.tokenTransfers.map((t, i) => (
<TokenTransferItem <TokenTransferItem
key={i} key={i}
t={t} t={t}
txData={txData}
tokenMeta={txData.tokenMetas[t.token]} tokenMeta={txData.tokenMetas[t.token]}
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
metadatas={metadatas}
/> />
))} ))}
</div>
</InfoRow> </InfoRow>
)} )}
<InfoRow title="Value"> <InfoRow title="Value">
@ -256,24 +253,16 @@ const Details: React.FC<DetailsProps> = ({
{txData.type === 2 && ( {txData.type === 2 && (
<> <>
<InfoRow title="Max Priority Fee Per Gas"> <InfoRow title="Max Priority Fee Per Gas">
<span>
<FormattedBalance value={txData.maxPriorityFeePerGas!} /> Ether ( <FormattedBalance value={txData.maxPriorityFeePerGas!} /> Ether (
<FormattedBalance <FormattedBalance
value={txData.maxPriorityFeePerGas!} value={txData.maxPriorityFeePerGas!}
decimals={9} decimals={9}
/>{" "} />{" "}
Gwei) Gwei)
</span>
</InfoRow> </InfoRow>
<InfoRow title="Max Fee Per Gas"> <InfoRow title="Max Fee Per Gas">
<span>
<FormattedBalance value={txData.maxFeePerGas!} /> Ether ( <FormattedBalance value={txData.maxFeePerGas!} /> Ether (
<FormattedBalance <FormattedBalance value={txData.maxFeePerGas!} decimals={9} /> Gwei)
value={txData.maxFeePerGas!}
decimals={9}
/>{" "}
Gwei)
</span>
</InfoRow> </InfoRow>
</> </>
)} )}
@ -315,7 +304,6 @@ const Details: React.FC<DetailsProps> = ({
)} )}
{txData.confirmedData && hasEIP1559 && ( {txData.confirmedData && hasEIP1559 && (
<InfoRow title="Block Base Fee"> <InfoRow title="Block Base Fee">
<span>
<FormattedBalance <FormattedBalance
value={txData.confirmedData.blockBaseFeePerGas!} value={txData.confirmedData.blockBaseFeePerGas!}
decimals={9} decimals={9}
@ -326,7 +314,6 @@ const Details: React.FC<DetailsProps> = ({
decimals={0} decimals={0}
/>{" "} />{" "}
wei) wei)
</span>
</InfoRow> </InfoRow>
)} )}
{txData.confirmedData && ( {txData.confirmedData && (
@ -353,47 +340,15 @@ const Details: React.FC<DetailsProps> = ({
</> </>
)} )}
<InfoRow title="Input Data"> <InfoRow title="Input Data">
<Tab.Group> <InputDecoder
<Tab.List className="flex space-x-1 mb-1"> fourBytes={fourBytes}
<ModeTab>Decoded</ModeTab> resolvedTxDesc={resolvedTxDesc}
<ModeTab>Raw</ModeTab>
<ModeTab>UTF-8</ModeTab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>
{fourBytes === "0x" ? (
<>No parameters</>
) : resolvedTxDesc === undefined ? (
<>Waiting for data...</>
) : resolvedTxDesc === null ? (
<>Can't decode data</>
) : (
<DecodedParamsTable
args={resolvedTxDesc.args}
paramTypes={resolvedTxDesc.functionFragment.inputs}
txData={txData}
hasParamNames={resolvedTxDesc === txDesc} hasParamNames={resolvedTxDesc === txDesc}
data={txData.data}
userMethod={userMethod} userMethod={userMethod}
devMethod={devMethod} devMethod={devMethod}
resolvedAddresses={resolvedAddresses}
/> />
)}
</Tab.Panel>
<Tab.Panel>
<textarea
className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2"
value={txData.data}
readOnly
/>
</Tab.Panel>
<Tab.Panel>
<textarea
className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2"
value={utfInput}
readOnly
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</InfoRow> </InfoRow>
</ContentFrame> </ContentFrame>
); );

View File

@ -0,0 +1,34 @@
import React from "react";
import SelectionHighlighter, {
functionSigSelector,
} from "../components/SelectionHighlighter";
type FunctionSignatureProps = {
callType: string;
sig: string;
};
const FunctionSignature: React.FC<FunctionSignatureProps> = ({
callType,
sig,
}) => (
<SelectionHighlighter
myType="functionSig"
myContent={sig}
selector={functionSigSelector}
>
<span
className={`font-bold ${
callType === "STATICCALL"
? "text-red-700"
: callType === "DELEGATECALL" || callType === "CALLCODE"
? "text-gray-400"
: "text-blue-900"
}`}
>
{sig}
</span>
</SelectionHighlighter>
);
export default FunctionSignature;

View File

@ -2,28 +2,28 @@ import React, { useMemo } from "react";
import { Log } from "@ethersproject/abstract-provider"; import { Log } from "@ethersproject/abstract-provider";
import { Fragment, Interface, 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 TransactionAddress from "../components/TransactionAddress";
import DecoratedAddressLink from "../components/DecoratedAddressLink";
import Copy from "../components/Copy"; import Copy from "../components/Copy";
import ModeTab from "../components/ModeTab"; 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 { useTopic0 } from "../useTopic0"; import { useTopic0 } from "../useTopic0";
import { ResolvedAddresses } from "../api/address-resolver"; import { ResolvedAddresses } from "../api/address-resolver";
import { ChecksummedAddress } from "../types";
import { Metadata } from "../useSourcify";
type LogEntryProps = { type LogEntryProps = {
txData: TransactionData;
log: Log; log: Log;
logDesc: LogDescription | null | undefined; logDesc: LogDescription | null | undefined;
resolvedAddresses: ResolvedAddresses | undefined; resolvedAddresses: ResolvedAddresses | undefined;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
}; };
const LogEntry: React.FC<LogEntryProps> = ({ const LogEntry: React.FC<LogEntryProps> = ({
txData,
log, log,
logDesc, logDesc,
resolvedAddresses, resolvedAddresses,
metadatas,
}) => { }) => {
const rawTopic0 = log.topics[0]; const rawTopic0 = log.topics[0];
const topic0 = useTopic0(rawTopic0); const topic0 = useTopic0(rawTopic0);
@ -63,15 +63,11 @@ const LogEntry: React.FC<LogEntryProps> = ({
<div className="font-bold text-right">Address</div> <div className="font-bold text-right">Address</div>
<div className="col-span-11 mr-auto"> <div className="col-span-11 mr-auto">
<div className="flex items-baseline space-x-2 -ml-1 mr-3"> <div className="flex items-baseline space-x-2 -ml-1 mr-3">
<AddressHighlighter address={log.address}> <TransactionAddress
<DecoratedAddressLink
address={log.address} address={log.address}
miner={log.address === txData.confirmedData?.miner}
txFrom={log.address === txData.from}
txTo={log.address === txData.to}
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
metadata={metadatas[log.address]}
/> />
</AddressHighlighter>
<Copy value={log.address} /> <Copy value={log.address} />
</div> </div>
</div> </div>
@ -112,8 +108,8 @@ const LogEntry: React.FC<LogEntryProps> = ({
<DecodedParamsTable <DecodedParamsTable
args={resolvedLogDesc.args} args={resolvedLogDesc.args}
paramTypes={resolvedLogDesc.eventFragment.inputs} paramTypes={resolvedLogDesc.eventFragment.inputs}
txData={txData}
hasParamNames={resolvedLogDesc === logDesc} hasParamNames={resolvedLogDesc === logDesc}
resolvedAddresses={resolvedAddresses}
/> />
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import React, { useMemo } from "react"; import React, { useContext, useMemo } from "react";
import { Interface } from "@ethersproject/abi"; import { Interface } from "@ethersproject/abi";
import ContentFrame from "../ContentFrame"; import ContentFrame from "../ContentFrame";
import LogEntry from "./LogEntry"; import LogEntry from "./LogEntry";
@ -6,6 +6,7 @@ import { TransactionData } from "../types";
import { useAppConfigContext } from "../useAppConfig"; import { useAppConfigContext } from "../useAppConfig";
import { Metadata, useMultipleMetadata } from "../useSourcify"; import { Metadata, useMultipleMetadata } from "../useSourcify";
import { ResolvedAddresses } from "../api/address-resolver"; import { ResolvedAddresses } from "../api/address-resolver";
import { RuntimeContext } from "../useRuntime";
type LogsProps = { type LogsProps = {
txData: TransactionData; txData: TransactionData;
@ -24,15 +25,16 @@ const Logs: React.FC<LogsProps> = ({ txData, metadata, resolvedAddresses }) => {
return md; return md;
}, [txData.to, metadata]); }, [txData.to, metadata]);
const { sourcifySource } = useAppConfigContext();
const logAddresses = useMemo( const logAddresses = useMemo(
() => txData.confirmedData?.logs.map((l) => l.address) ?? [], () => txData.confirmedData?.logs.map((l) => l.address) ?? [],
[txData] [txData]
); );
const { provider } = useContext(RuntimeContext);
const { sourcifySource } = useAppConfigContext();
const metadatas = useMultipleMetadata( const metadatas = useMultipleMetadata(
baseMetadatas, baseMetadatas,
logAddresses, logAddresses,
1, provider?.network.chainId,
sourcifySource sourcifySource
); );
const logDescs = useMemo(() => { const logDescs = useMemo(() => {
@ -69,10 +71,10 @@ const Logs: React.FC<LogsProps> = ({ txData, metadata, resolvedAddresses }) => {
{txData.confirmedData.logs.map((l, i) => ( {txData.confirmedData.logs.map((l, i) => (
<LogEntry <LogEntry
key={i} key={i}
txData={txData}
log={l} log={l}
logDesc={logDescs?.[i]} logDesc={logDescs?.[i]}
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
metadatas={metadatas}
/> />
))} ))}
</> </>

69
src/transaction/Trace.tsx Normal file
View File

@ -0,0 +1,69 @@
import React, { useContext, useMemo } from "react";
import ContentFrame from "../ContentFrame";
import TransactionAddress from "../components/TransactionAddress";
import TraceItem from "./TraceItem";
import { TransactionData } from "../types";
import { useBatch4Bytes } from "../use4Bytes";
import { useTraceTransaction, useUniqueSignatures } from "../useErigonHooks";
import { RuntimeContext } from "../useRuntime";
import { ResolvedAddresses } from "../api/address-resolver";
import { tracesCollector, useResolvedAddresses } from "../useResolvedAddresses";
type TraceProps = {
txData: TransactionData;
resolvedAddresses: ResolvedAddresses | undefined;
};
const Trace: React.FC<TraceProps> = ({ txData, resolvedAddresses }) => {
const { provider } = useContext(RuntimeContext);
const traces = useTraceTransaction(provider, txData.transactionHash);
const uniqueSignatures = useUniqueSignatures(traces);
const sigMap = useBatch4Bytes(uniqueSignatures);
const addrCollector = useMemo(() => tracesCollector(traces), [traces]);
const traceResolvedAddresses = useResolvedAddresses(provider, addrCollector);
const mergedResolvedAddresses = useMemo(() => {
const merge = {};
if (resolvedAddresses) {
Object.assign(merge, resolvedAddresses);
}
if (traceResolvedAddresses) {
Object.assign(merge, traceResolvedAddresses);
}
return merge;
}, [resolvedAddresses, traceResolvedAddresses]);
return (
<ContentFrame tabs>
<div className="mt-4 mb-5 space-y-3 font-code text-sm flex flex-col items-start overflow-x-auto">
{traces ? (
<>
<div className="border hover:border-gray-500 rounded px-1 py-0.5">
<TransactionAddress
address={txData.from}
resolvedAddresses={resolvedAddresses}
/>
</div>
<div className="ml-5 space-y-3 self-stretch">
{traces.map((t, i, a) => (
<TraceItem
key={i}
t={t}
last={i === a.length - 1}
fourBytesMap={sigMap}
resolvedAddresses={mergedResolvedAddresses}
/>
))}
</div>
</>
) : (
<div className="border hover:border-gray-500 rounded px-1 py-1 w-96 h-7">
<div className="animate-pulse w-full h-full rounded bg-gray-200"></div>
</div>
)}
</div>
</ContentFrame>
);
};
export default React.memo(Trace);

View File

@ -0,0 +1,113 @@
import React, { useState } from "react";
import { Switch } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBomb } from "@fortawesome/free-solid-svg-icons/faBomb";
import TransactionAddress from "../components/TransactionAddress";
import FormattedBalance from "../components/FormattedBalance";
import FunctionSignature from "./FunctionSignature";
import InputDecoder from "./decoder/InputDecoder";
import { TraceEntry } from "../useErigonHooks";
import { ResolvedAddresses } from "../api/address-resolver";
import {
extract4Bytes,
FourBytesEntry,
useTransactionDescription,
} from "../use4Bytes";
type TraceInputProps = {
t: TraceEntry;
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
resolvedAddresses: ResolvedAddresses | undefined;
};
const TraceInput: React.FC<TraceInputProps> = ({
t,
fourBytesMap,
resolvedAddresses,
}) => {
const raw4Bytes = extract4Bytes(t.input);
const fourBytes = raw4Bytes !== null ? fourBytesMap[raw4Bytes] : null;
const sigText =
raw4Bytes === null ? "<fallback>" : fourBytes?.name ?? raw4Bytes;
const hasParams = t.input.length > 10;
const fourBytesTxDesc = useTransactionDescription(
fourBytes,
t.input,
t.value
);
const [expanded, setExpanded] = useState<boolean>(false);
return (
<div
className={`ml-5 border hover:border-gray-500 rounded px-1 py-0.5 ${
expanded ? "w-full" : ""
}`}
>
<div className="flex items-baseline">
<span className="text-xs text-gray-400 lowercase">{t.type}</span>
{t.type === "SELFDESTRUCT" ? (
<span className="pl-2 text-red-800" title="Self destruct">
<FontAwesomeIcon icon={faBomb} size="1x" />
</span>
) : (
<>
<span>
<TransactionAddress
address={t.to}
resolvedAddresses={resolvedAddresses}
/>
</span>
{t.type !== "CREATE" && t.type !== "CREATE2" && (
<>
<span>.</span>
<FunctionSignature callType={t.type} sig={sigText} />
{t.value && !t.value.isZero() && (
<span className="text-red-700 whitespace-nowrap">
{"{"}value: <FormattedBalance value={t.value} /> ETH{"}"}
</span>
)}
<span className="whitespace-nowrap">
(
{hasParams && (
<Switch
className="text-xs"
checked={expanded}
onChange={setExpanded}
>
{expanded ? (
<span className="text-gray-400">[-]</span>
) : (
<>[...]</>
)}
</Switch>
)}
{(!hasParams || !expanded) && <>)</>}
</span>
</>
)}
</>
)}
</div>
{hasParams && expanded && (
<>
<div className="ml-5 mr-1 my-2">
<InputDecoder
fourBytes={raw4Bytes ?? "0x"}
resolvedTxDesc={fourBytesTxDesc}
hasParamNames={false}
data={t.input}
userMethod={undefined}
devMethod={undefined}
resolvedAddresses={resolvedAddresses}
/>
</div>
<div>)</div>
</>
)}
</div>
);
};
export default TraceInput;

View File

@ -0,0 +1,92 @@
import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlusSquare } from "@fortawesome/free-regular-svg-icons/faPlusSquare";
import { faMinusSquare } from "@fortawesome/free-regular-svg-icons/faMinusSquare";
import { Switch } from "@headlessui/react";
import { FourBytesEntry } from "../use4Bytes";
import { TraceGroup } from "../useErigonHooks";
import { ResolvedAddresses } from "../api/address-resolver";
import TraceInput from "./TraceInput";
type TraceItemProps = {
t: TraceGroup;
last: boolean;
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
resolvedAddresses: ResolvedAddresses | undefined;
};
const TraceItem: React.FC<TraceItemProps> = ({
t,
last,
fourBytesMap,
resolvedAddresses,
}) => {
const [expanded, setExpanded] = useState<boolean>(true);
return (
<>
<div className="flex relative">
<div className="absolute border-l border-b w-5 h-6 transform -translate-y-3"></div>
{!last && (
<div className="absolute left-0 border-l w-5 h-full transform translate-y-3"></div>
)}
{t.children && (
<Switch
className="absolute left-0 bg-white transform -translate-x-1/2 text-gray-500"
checked={expanded}
onChange={setExpanded}
>
<FontAwesomeIcon
icon={expanded ? faMinusSquare : faPlusSquare}
size="1x"
/>
</Switch>
)}
<TraceInput
t={t}
fourBytesMap={fourBytesMap}
resolvedAddresses={resolvedAddresses}
/>
</div>
{t.children && (
<div
className={`pl-10 ${last ? "" : "border-l"} space-y-3 ${
expanded ? "" : "hidden"
}`}
>
<TraceChildren
c={t.children}
fourBytesMap={fourBytesMap}
resolvedAddresses={resolvedAddresses}
/>
</div>
)}
</>
);
};
type TraceChildrenProps = {
c: TraceGroup[];
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
resolvedAddresses: ResolvedAddresses | undefined;
};
const TraceChildren: React.FC<TraceChildrenProps> = React.memo(
({ c, fourBytesMap, resolvedAddresses }) => {
return (
<>
{c.map((tc, i, a) => (
<TraceItem
key={i}
t={tc}
last={i === a.length - 1}
fourBytesMap={fourBytesMap}
resolvedAddresses={resolvedAddresses}
/>
))}
</>
);
}
);
export default TraceItem;

View File

@ -1,26 +1,21 @@
import React from "react"; import React from "react";
import AddressHighlighter from "../../components/AddressHighlighter"; import TransactionAddress from "../../components/TransactionAddress";
import DecoratedAddressLink from "../../components/DecoratedAddressLink";
import Copy from "../../components/Copy"; import Copy from "../../components/Copy";
import { TransactionData } from "../../types"; import { ResolvedAddresses } from "../../api/address-resolver";
type AddressDecoderProps = { type AddressDecoderProps = {
r: any; r: string;
txData: TransactionData; resolvedAddresses?: ResolvedAddresses | undefined;
}; };
const AddressDecoder: React.FC<AddressDecoderProps> = ({ r, txData }) => ( const AddressDecoder: React.FC<AddressDecoderProps> = ({
r,
resolvedAddresses,
}) => (
<div className="flex items-baseline space-x-2 -ml-1 mr-3"> <div className="flex items-baseline space-x-2 -ml-1 mr-3">
<AddressHighlighter address={r.toString()}> <TransactionAddress address={r} resolvedAddresses={resolvedAddresses} />
<DecoratedAddressLink <Copy value={r} />
address={r.toString()}
miner={r.toString() === txData.confirmedData?.miner}
txFrom={r.toString() === txData.from}
txTo={r.toString() === txData.to}
/>
</AddressHighlighter>
<Copy value={r.toString()} />
</div> </div>
); );
export default React.memo(AddressDecoder); export default AddressDecoder;

View File

@ -8,16 +8,16 @@ import Uint256Decoder from "./Uint256Decoder";
import AddressDecoder from "./AddressDecoder"; import AddressDecoder from "./AddressDecoder";
import BooleanDecoder from "./BooleanDecoder"; import BooleanDecoder from "./BooleanDecoder";
import BytesDecoder from "./BytesDecoder"; import BytesDecoder from "./BytesDecoder";
import { TransactionData } from "../../types"; import { ResolvedAddresses } from "../../api/address-resolver";
type DecodedParamRowProps = { type DecodedParamRowProps = {
prefix?: ReactNode; prefix?: ReactNode;
i?: number | undefined; i?: number | undefined;
r: any; r: any;
paramType: ParamType; paramType: ParamType;
txData: TransactionData;
arrayElem?: number | undefined; arrayElem?: number | undefined;
help?: string | undefined; help?: string | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const DecodedParamRow: React.FC<DecodedParamRowProps> = ({ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
@ -25,9 +25,9 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
i, i,
r, r,
paramType, paramType,
txData,
arrayElem, arrayElem,
help, help,
resolvedAddresses,
}) => { }) => {
const [showHelp, setShowHelp] = useState<boolean>(false); const [showHelp, setShowHelp] = useState<boolean>(false);
@ -72,7 +72,10 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
{paramType.baseType === "uint256" ? ( {paramType.baseType === "uint256" ? (
<Uint256Decoder r={r} /> <Uint256Decoder r={r} />
) : paramType.baseType === "address" ? ( ) : paramType.baseType === "address" ? (
<AddressDecoder r={r} txData={txData} /> <AddressDecoder
r={r.toString()}
resolvedAddresses={resolvedAddresses}
/>
) : paramType.baseType === "bool" ? ( ) : paramType.baseType === "bool" ? (
<BooleanDecoder r={r} /> <BooleanDecoder r={r} />
) : paramType.baseType === "bytes" ? ( ) : paramType.baseType === "bytes" ? (
@ -99,7 +102,7 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
i={idx} i={idx}
r={e} r={e}
paramType={paramType.components[idx]} paramType={paramType.components[idx]}
txData={txData} resolvedAddresses={resolvedAddresses}
/> />
))} ))}
{paramType.baseType === "array" && {paramType.baseType === "array" &&
@ -109,8 +112,8 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
prefix={paramType.name ?? <span className="italic">param_{i}</span>} prefix={paramType.name ?? <span className="italic">param_{i}</span>}
r={e} r={e}
paramType={paramType.arrayChildren} paramType={paramType.arrayChildren}
txData={txData}
arrayElem={idx} arrayElem={idx}
resolvedAddresses={resolvedAddresses}
/> />
))} ))}
</> </>

View File

@ -1,24 +1,24 @@
import React from "react"; import React from "react";
import { ParamType, Result } from "@ethersproject/abi"; import { ParamType, Result } from "@ethersproject/abi";
import DecodedParamRow from "./DecodedParamRow"; import DecodedParamRow from "./DecodedParamRow";
import { TransactionData } from "../../types";
import { DevMethod, UserMethod } from "../../useSourcify"; import { DevMethod, UserMethod } from "../../useSourcify";
import { ResolvedAddresses } from "../../api/address-resolver";
type DecodedParamsTableProps = { type DecodedParamsTableProps = {
args: Result; args: Result;
paramTypes: ParamType[]; paramTypes: ParamType[];
txData: TransactionData;
hasParamNames?: boolean; hasParamNames?: boolean;
userMethod?: UserMethod | undefined; userMethod?: UserMethod | undefined;
devMethod?: DevMethod | undefined; devMethod?: DevMethod | undefined;
resolvedAddresses?: ResolvedAddresses | undefined;
}; };
const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({ const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({
args, args,
paramTypes, paramTypes,
txData,
hasParamNames = true, hasParamNames = true,
devMethod, devMethod,
resolvedAddresses,
}) => ( }) => (
<table className="border w-full"> <table className="border w-full">
<thead> <thead>
@ -46,8 +46,8 @@ const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({
i={i} i={i}
r={r} r={r}
paramType={paramTypes[i]} paramType={paramTypes[i]}
txData={txData}
help={devMethod?.params?.[paramTypes[i].name]} help={devMethod?.params?.[paramTypes[i].name]}
resolvedAddresses={resolvedAddresses}
/> />
))} ))}
</tbody> </tbody>

View File

@ -0,0 +1,83 @@
import React, { useMemo } from "react";
import { TransactionDescription } from "@ethersproject/abi";
import { toUtf8String } from "@ethersproject/strings";
import { Tab } from "@headlessui/react";
import ModeTab from "../../components/ModeTab";
import DecodedParamsTable from "./DecodedParamsTable";
import { DevMethod, UserMethod } from "../../useSourcify";
import { ResolvedAddresses } from "../../api/address-resolver";
type InputDecoderProps = {
fourBytes: string;
resolvedTxDesc: TransactionDescription | null | undefined;
hasParamNames: boolean;
data: string;
userMethod: UserMethod | undefined;
devMethod: DevMethod | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
};
const InputDecoder: React.FC<InputDecoderProps> = ({
fourBytes,
resolvedTxDesc,
hasParamNames,
data,
userMethod,
devMethod,
resolvedAddresses,
}) => {
const utfInput = useMemo(() => {
try {
return toUtf8String(data);
} catch (err) {
// Silently ignore on purpose
return undefined;
}
}, [data]);
return (
<Tab.Group>
<Tab.List className="flex space-x-1 mb-1">
<ModeTab disabled={!resolvedTxDesc}>Decoded</ModeTab>
<ModeTab>Raw</ModeTab>
<ModeTab disabled={utfInput === undefined}>UTF-8</ModeTab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>
{fourBytes === "0x" ? (
<>No parameters</>
) : resolvedTxDesc === undefined ? (
<>Waiting for data...</>
) : resolvedTxDesc === null ? (
<>Can't decode data</>
) : (
<DecodedParamsTable
args={resolvedTxDesc.args}
paramTypes={resolvedTxDesc.functionFragment.inputs}
hasParamNames={hasParamNames}
userMethod={userMethod}
devMethod={devMethod}
resolvedAddresses={resolvedAddresses}
/>
)}
</Tab.Panel>
<Tab.Panel>
<textarea
className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2"
value={data}
readOnly
/>
</Tab.Panel>
<Tab.Panel>
<textarea
className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2"
value={utfInput}
readOnly
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
);
};
export default InputDecoder;

View File

@ -67,6 +67,9 @@ export type ConfirmedTransactionData = {
// The VOID... // The VOID...
export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
// TODO: replace all occurrences of plain string
export type ChecksummedAddress = string;
export enum AddressContext { export enum AddressContext {
FROM, FROM,
TO, TO,
@ -104,4 +107,4 @@ export type TokenMeta = {
decimals: number; decimals: number;
}; };
export type TokenMetas = Record<string, TokenMeta>; export type TokenMetas = Record<string, TokenMeta | null | undefined>;

View File

@ -1,4 +1,5 @@
import { BlockTag } from "@ethersproject/abstract-provider"; import { BlockTag } from "@ethersproject/abstract-provider";
import { ChecksummedAddress } from "./types";
export const fourBytesURL = ( export const fourBytesURL = (
assetsURLPrefix: string, assetsURLPrefix: string,
@ -38,27 +39,27 @@ const resolveSourcifySource = (source: SourcifySource) => {
return sourcifyHttpRepoPrefix; return sourcifyHttpRepoPrefix;
} }
throw new Error(`Unknown Sourcify intergration source code: ${source}`); throw new Error(`Unknown Sourcify integration source code: ${source}`);
}; };
export const sourcifyMetadata = ( export const sourcifyMetadata = (
checksummedAddress: string, address: ChecksummedAddress,
networkId: number, chainId: number,
source: SourcifySource source: SourcifySource
) => ) =>
`${resolveSourcifySource( `${resolveSourcifySource(
source source
)}/contracts/full_match/${networkId}/${checksummedAddress}/metadata.json`; )}/contracts/full_match/${chainId}/${address}/metadata.json`;
export const sourcifySourceFile = ( export const sourcifySourceFile = (
checksummedAddress: string, address: ChecksummedAddress,
networkId: number, chainId: number,
filepath: string, filepath: string,
source: SourcifySource source: SourcifySource
) => ) =>
`${resolveSourcifySource( `${resolveSourcifySource(
source source
)}/contracts/full_match/${networkId}/${checksummedAddress}/sources/${filepath}`; )}/contracts/full_match/${chainId}/${address}/sources/${filepath}`;
export const openInRemixURL = (checksummedAddress: string, networkId: number) => export const openInRemixURL = (checksummedAddress: string, networkId: number) =>
`https://remix.ethereum.org/#call=source-verification//fetchAndSave//${checksummedAddress}//${networkId}`; `https://remix.ethereum.org/#call=source-verification//fetchAndSave//${checksummedAddress}//${networkId}`;

View File

@ -1,21 +1,97 @@
import { useState, useEffect, useContext } from "react"; import { useState, useEffect, useContext, useMemo } from "react";
import {
Fragment,
Interface,
TransactionDescription,
} from "@ethersproject/abi";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { fourBytesURL } from "./url"; import { fourBytesURL } from "./url";
import { BigNumberish } from "@ethersproject/bignumber";
export type FourBytesEntry = { export type FourBytesEntry = {
name: string; name: string;
signature: string | undefined; signature: string | undefined;
}; };
export type FourBytesMap = Record<string, FourBytesEntry | null | undefined>;
const simpleTransfer: FourBytesEntry = { const simpleTransfer: FourBytesEntry = {
name: "Transfer", name: "transfer",
signature: undefined, signature: undefined,
}; };
const fullCache = new Map<string, FourBytesEntry | null>(); const fullCache = new Map<string, FourBytesEntry | null>();
export const extract4Bytes = (rawInput: string): string | null => {
if (rawInput.length < 10) {
return null;
}
return rawInput.slice(0, 10);
};
export const rawInputTo4Bytes = (rawInput: string) => rawInput.slice(0, 10); export const rawInputTo4Bytes = (rawInput: string) => rawInput.slice(0, 10);
const fetch4Bytes = async (
assetsURLPrefix: string,
fourBytes: string
): Promise<FourBytesEntry | null> => {
const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes);
try {
const res = await fetch(signatureURL);
if (!res.ok) {
console.error(`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) {
console.error(`Couldn't fetch signature URL ${signatureURL}`, err);
return null;
}
};
export const useBatch4Bytes = (
rawFourByteSigs: string[] | undefined
): FourBytesMap => {
const runtime = useContext(RuntimeContext);
const assetsURLPrefix = runtime.config?.assetsURLPrefix;
const [fourBytesMap, setFourBytesMap] = useState<FourBytesMap>({});
useEffect(() => {
if (!rawFourByteSigs || assetsURLPrefix === undefined) {
setFourBytesMap({});
return;
}
const loadSigs = async () => {
const promises = rawFourByteSigs.map((s) =>
fetch4Bytes(assetsURLPrefix, s.slice(2))
);
const results = await Promise.all(promises);
const _fourBytesMap: Record<string, FourBytesEntry | null> = {};
for (let i = 0; i < rawFourByteSigs.length; i++) {
_fourBytesMap[rawFourByteSigs[i]] = results[i];
}
setFourBytesMap(_fourBytesMap);
};
loadSigs();
}, [assetsURLPrefix, rawFourByteSigs]);
return fourBytesMap;
};
/** /**
* Extract 4bytes DB info * Extract 4bytes DB info
* *
@ -47,35 +123,12 @@ export const use4Bytes = (
return; return;
} }
const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes); const loadSig = async () => {
fetch(signatureURL) const entry = await fetch4Bytes(assetsURLPrefix, fourBytes);
.then(async (res) => {
if (!res.ok) {
console.error(`Signature does not exist in 4bytes DB: ${fourBytes}`);
fullCache.set(fourBytes, null);
setEntry(null);
return;
}
// 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("(");
let method = sig.slice(0, cut);
method = method.charAt(0).toUpperCase() + method.slice(1);
const entry: FourBytesEntry = {
name: method,
signature: sig,
};
setEntry(entry);
fullCache.set(fourBytes, entry); fullCache.set(fourBytes, entry);
}) setEntry(entry);
.catch((err) => { };
console.error(`Couldn't fetch signature URL ${signatureURL}`, err); loadSig();
setEntry(null);
fullCache.set(fourBytes, null);
});
}, [fourBytes, assetsURLPrefix]); }, [fourBytes, assetsURLPrefix]);
if (rawFourBytes === "0x") { if (rawFourBytes === "0x") {
@ -96,3 +149,29 @@ export const use4Bytes = (
fullCache.set(fourBytes, entry); fullCache.set(fourBytes, entry);
return entry; return entry;
}; };
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;
};

View File

@ -1,10 +1,11 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import { Block, BlockWithTransactions } from "@ethersproject/abstract-provider"; import { Block, BlockWithTransactions } from "@ethersproject/abstract-provider";
import { JsonRpcProvider } from "@ethersproject/providers"; import { JsonRpcProvider } from "@ethersproject/providers";
import { getAddress } from "@ethersproject/address"; import { getAddress } from "@ethersproject/address";
import { Contract } from "@ethersproject/contracts"; import { Contract } from "@ethersproject/contracts";
import { BigNumber } from "@ethersproject/bignumber"; import { BigNumber } from "@ethersproject/bignumber";
import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes"; import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes";
import { extract4Bytes } from "./use4Bytes";
import { getInternalOperations } from "./nodeFunctions"; import { getInternalOperations } from "./nodeFunctions";
import { import {
TokenMetas, TokenMetas,
@ -221,7 +222,7 @@ export const useTxData = (
// Extract token meta // Extract token meta
const tokenMetas: TokenMetas = {}; const tokenMetas: TokenMetas = {};
for (const t of tokenTransfers) { for (const t of tokenTransfers) {
if (tokenMetas[t.token]) { if (tokenMetas[t.token] !== undefined) {
continue; continue;
} }
const erc20Contract = new Contract(t.token, erc20, provider); const erc20Contract = new Contract(t.token, erc20, provider);
@ -237,6 +238,7 @@ export const useTxData = (
decimals, decimals,
}; };
} catch (err) { } catch (err) {
tokenMetas[t.token] = null;
console.warn(`Couldn't get token ${t.token} metadata; ignoring`, err); console.warn(`Couldn't get token ${t.token} metadata; ignoring`, err);
} }
} }
@ -308,3 +310,133 @@ export const useInternalOperations = (
return intTransfers; return intTransfers;
}; };
export type TraceEntry = {
type: string;
depth: number;
from: string;
to: string;
value: BigNumber;
input: string;
};
export type TraceGroup = TraceEntry & {
children: TraceGroup[] | null;
};
export const useTraceTransaction = (
provider: JsonRpcProvider | undefined,
txHash: string
): TraceGroup[] | undefined => {
const [traceGroups, setTraceGroups] = useState<TraceGroup[] | undefined>();
useEffect(() => {
if (!provider) {
setTraceGroups(undefined);
return;
}
const traceTx = async () => {
const results = await provider.send("ots_traceTransaction", [txHash]);
// Implement better formatter
for (let i = 0; i < results.length; i++) {
results[i].from = provider.formatter.address(results[i].from);
results[i].to = provider.formatter.address(results[i].to);
results[i].value =
results[i].value === null
? null
: provider.formatter.bigNumber(results[i].value);
}
// Build trace tree
const buildTraceTree = (
flatList: TraceEntry[],
depth: number = 0
): TraceGroup[] => {
const entries: TraceGroup[] = [];
let children: TraceEntry[] | null = null;
for (let i = 0; i < flatList.length; i++) {
if (flatList[i].depth === depth) {
if (children !== null) {
const childrenTree = buildTraceTree(children, depth + 1);
const prev = entries.pop();
if (prev) {
prev.children = childrenTree;
entries.push(prev);
}
}
entries.push({
...flatList[i],
children: null,
});
children = null;
} else {
if (children === null) {
children = [];
}
children.push(flatList[i]);
}
}
if (children !== null) {
const childrenTree = buildTraceTree(children, depth + 1);
const prev = entries.pop();
if (prev) {
prev.children = childrenTree;
entries.push(prev);
}
}
return entries;
};
const traceTree = buildTraceTree(results);
setTraceGroups(traceTree);
};
traceTx();
}, [provider, txHash]);
return traceGroups;
};
/**
* Flatten a trace tree and extract and dedup 4byte function signatures
*/
export const useUniqueSignatures = (traces: TraceGroup[] | undefined) => {
const uniqueSignatures = useMemo(() => {
if (!traces) {
return undefined;
}
const sigs = new Set<string>();
let nextTraces: TraceGroup[] = [...traces];
while (nextTraces.length > 0) {
const traces = nextTraces;
nextTraces = [];
for (const t of traces) {
if (
t.type === "CALL" ||
t.type === "DELEGATECALL" ||
t.type === "STATICCALL" ||
t.type === "CALLCODE"
) {
const fourBytes = extract4Bytes(t.input);
if (fourBytes) {
sigs.add(fourBytes);
}
}
if (t.children) {
nextTraces.push(...t.children);
}
}
}
return [...sigs];
}, [traces]);
return uniqueSignatures;
};

View File

@ -1,5 +1,9 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { JsonRpcProvider, WebSocketProvider } from "@ethersproject/providers"; import {
JsonRpcProvider,
JsonRpcBatchProvider,
WebSocketProvider,
} from "@ethersproject/providers";
import { ConnectionStatus } from "./types"; import { ConnectionStatus } from "./types";
import { MIN_API_LEVEL } from "./params"; import { MIN_API_LEVEL } from "./params";
@ -35,7 +39,7 @@ export const useProvider = (
if (erigonURL?.startsWith("ws://") || erigonURL?.startsWith("wss://")) { if (erigonURL?.startsWith("ws://") || erigonURL?.startsWith("wss://")) {
provider = new WebSocketProvider(erigonURL); provider = new WebSocketProvider(erigonURL);
} else { } else {
provider = new JsonRpcProvider(erigonURL); provider = new JsonRpcBatchProvider(erigonURL);
} }
// Check if it is at least a regular ETH node // Check if it is at least a regular ETH node

View File

@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from "react";
import { JsonRpcProvider } from "@ethersproject/providers"; import { JsonRpcProvider } from "@ethersproject/providers";
import { ProcessedTransaction, TransactionData } from "./types"; import { ProcessedTransaction, TransactionData } from "./types";
import { batchPopulate, ResolvedAddresses } from "./api/address-resolver"; import { batchPopulate, ResolvedAddresses } from "./api/address-resolver";
import { TraceGroup } from "./useErigonHooks";
export type AddressCollector = () => string[]; export type AddressCollector = () => string[];
@ -61,6 +62,31 @@ export const transactionDataCollector =
return Array.from(uniqueAddresses); return Array.from(uniqueAddresses);
}; };
export const tracesCollector =
(traces: TraceGroup[] | undefined): AddressCollector =>
() => {
if (traces === undefined) {
return [];
}
const uniqueAddresses = new Set<string>();
let searchTraces = [...traces];
while (searchTraces.length > 0) {
const nextSearch: TraceGroup[] = [];
for (const g of searchTraces) {
uniqueAddresses.add(g.from);
uniqueAddresses.add(g.to);
if (g.children) {
nextSearch.push(...g.children);
}
}
searchTraces = nextSearch;
}
return Array.from(uniqueAddresses);
};
export const useResolvedAddresses = ( export const useResolvedAddresses = (
provider: JsonRpcProvider | undefined, provider: JsonRpcProvider | undefined,
addrCollector: AddressCollector addrCollector: AddressCollector

View File

@ -0,0 +1,9 @@
import { createContext, useContext } from "react";
import { TransactionData } from "./types";
export const SelectedTransactionContext = createContext<
TransactionData | null | undefined
>(undefined);
export const useSelectedTransaction = () =>
useContext(SelectedTransactionContext);

View File

@ -6,12 +6,14 @@ import {
useContext, useContext,
} from "react"; } from "react";
export type SelectionType = "address" | "value" | "functionSig";
export type Selection = { export type Selection = {
type: "address" | "value"; type: SelectionType;
content: string; content: string;
}; };
type OptionalSelection = Selection | null; export type OptionalSelection = Selection | null;
export const useSelection = (): [ export const useSelection = (): [
OptionalSelection, OptionalSelection,

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { Interface } from "@ethersproject/abi"; import { Interface } from "@ethersproject/abi";
import { TransactionData } from "./types"; import { ChecksummedAddress, TransactionData } from "./types";
import { sourcifyMetadata, SourcifySource, sourcifySourceFile } from "./url"; import { sourcifyMetadata, SourcifySource, sourcifySourceFile } from "./url";
export type UserMethod = { export type UserMethod = {
@ -65,24 +65,19 @@ export type Metadata = {
}; };
}; };
export const fetchSourcifyMetadata = async ( const fetchSourcifyMetadata = async (
checksummedAddress: string, address: ChecksummedAddress,
chainId: number, chainId: number,
source: SourcifySource, source: SourcifySource,
abortController: AbortController abortController: AbortController
): Promise<Metadata | null> => { ): Promise<Metadata | null> => {
try { try {
const contractMetadataURL = sourcifyMetadata( const metadataURL = sourcifyMetadata(address, chainId, source);
checksummedAddress, const result = await fetch(metadataURL, {
chainId,
source
);
const result = await fetch(contractMetadataURL, {
signal: abortController.signal, signal: abortController.signal,
}); });
if (result.ok) { if (result.ok) {
const _metadata = await result.json(); return await result.json();
return _metadata;
} }
return null; return null;
@ -92,15 +87,16 @@ export const fetchSourcifyMetadata = async (
} }
}; };
// TODO: replace every occurrence with the multiple version one
export const useSourcify = ( export const useSourcify = (
checksummedAddress: string | undefined, address: ChecksummedAddress | undefined,
chainId: number | undefined, chainId: number | undefined,
source: SourcifySource source: SourcifySource
): Metadata | null | undefined => { ): Metadata | null | undefined => {
const [rawMetadata, setRawMetadata] = useState<Metadata | null | undefined>(); const [rawMetadata, setRawMetadata] = useState<Metadata | null | undefined>();
useEffect(() => { useEffect(() => {
if (!checksummedAddress || chainId === undefined) { if (!address || chainId === undefined) {
return; return;
} }
setRawMetadata(undefined); setRawMetadata(undefined);
@ -108,7 +104,7 @@ export const useSourcify = (
const abortController = new AbortController(); const abortController = new AbortController();
const fetchMetadata = async () => { const fetchMetadata = async () => {
const _metadata = await fetchSourcifyMetadata( const _metadata = await fetchSourcifyMetadata(
checksummedAddress, address,
chainId, chainId,
source, source,
abortController abortController
@ -120,47 +116,53 @@ export const useSourcify = (
return () => { return () => {
abortController.abort(); abortController.abort();
}; };
}, [checksummedAddress, chainId, source]); }, [address, chainId, source]);
return rawMetadata; return rawMetadata;
}; };
export const useMultipleMetadata = ( export const useMultipleMetadata = (
baseMetadatas: Record<string, Metadata | null>, baseMetadatas: Record<string, Metadata | null> | undefined,
checksummedAddress: (string | undefined)[], addresses: (ChecksummedAddress | undefined)[],
chainId: number | undefined, chainId: number | undefined,
source: SourcifySource source: SourcifySource
): Record<string, Metadata | null | undefined> => { ): Record<ChecksummedAddress, Metadata | null | undefined> => {
const [rawMetadata, setRawMetadata] = useState< const [rawMetadata, setRawMetadata] = useState<
Record<string, Metadata | null | undefined> Record<string, Metadata | null | undefined>
>({}); >({});
useEffect(() => { useEffect(() => {
if (!checksummedAddress || chainId === undefined) { if (!addresses || chainId === undefined) {
return; return;
} }
setRawMetadata({}); setRawMetadata({});
const abortController = new AbortController(); const abortController = new AbortController();
const fetchMetadata = async (addresses: string[]) => { const fetchMetadata = async (dedupedAddresses: string[]) => {
const promises: Promise<Metadata | null>[] = []; const promises: Promise<Metadata | null>[] = [];
for (const addr of addresses) { for (const address of dedupedAddresses) {
promises.push( promises.push(
fetchSourcifyMetadata(addr, chainId, source, abortController) fetchSourcifyMetadata(address, chainId, source, abortController)
); );
} }
const results = await Promise.all(promises); const results = await Promise.all(promises);
const metadatas: Record<string, Metadata | null> = { ...baseMetadatas }; if (abortController.signal.aborted) {
return;
}
const metadatas: Record<string, Metadata | null> = baseMetadatas
? { ...baseMetadatas }
: {};
for (let i = 0; i < results.length; i++) { for (let i = 0; i < results.length; i++) {
metadatas[addresses[i]] = results[i]; metadatas[dedupedAddresses[i]] = results[i];
} }
setRawMetadata(metadatas); setRawMetadata(metadatas);
}; };
const deduped = new Set( const deduped = new Set(
checksummedAddress.filter( addresses.filter(
(a): a is string => a !== undefined && baseMetadatas[a] === undefined (a): a is ChecksummedAddress =>
a !== undefined && baseMetadatas?.[a] === undefined
) )
); );
fetchMetadata(Array.from(deduped)); fetchMetadata(Array.from(deduped));
@ -168,7 +170,7 @@ export const useMultipleMetadata = (
return () => { return () => {
abortController.abort(); abortController.abort();
}; };
}, [baseMetadatas, checksummedAddress, chainId, source]); }, [baseMetadatas, addresses, chainId, source]);
return rawMetadata; return rawMetadata;
}; };

@ -1 +1 @@
Subproject commit e439c36937deb321ad01c23ac23941e8a491efe9 Subproject commit 30e4ffa0153594b11421cf383b1192e4414d2f66

View File

@ -14,7 +14,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx",
"downlevelIteration": true
}, },
"include": ["src"] "include": ["src"]
} }