Merge branch 'release/v2021.11.03-otterscan'
This commit is contained in:
commit
36985ed0f8
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
@ -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>;
|
||||||
|
|
15
src/url.ts
15
src/url.ts
|
@ -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}`;
|
||||||
|
|
139
src/use4Bytes.ts
139
src/use4Bytes.ts
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue