diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index 2cf91a0..940d9e2 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCaretRight } from "@fortawesome/free-solid-svg-icons/faCaretRight"; import TransactionAddress from "./components/TransactionAddress"; @@ -9,13 +9,17 @@ import { ChecksummedAddress, TokenMeta, TokenTransfer, + TransactionData, } from "./types"; +import { RuntimeContext } from "./useRuntime"; +import { useHasCode } from "./useErigonHooks"; import { Metadata } from "./sourcify/useSourcify"; type TokenTransferItemProps = { t: TokenTransfer; tokenMeta?: TokenMeta | null | undefined; metadatas: Record; + txData: TransactionData; }; // TODO: handle partial @@ -23,42 +27,59 @@ const TokenTransferItem: React.FC = ({ t, tokenMeta, metadatas, -}) => ( -
- - - -
-
- From - -
-
- To - -
-
- For - - - - - - + txData, +}) => { + const { provider } = useContext(RuntimeContext); + const fromHasCode = useHasCode( + provider, + t.from, + txData.confirmedData ? txData.confirmedData.blockNumber - 1 : undefined + ); + const toHasCode = useHasCode( + provider, + t.to, + txData.confirmedData ? txData.confirmedData.blockNumber - 1 : undefined + ); + + return ( +
+ + + +
+
+ From + +
+
+ To + +
+
+ For + + + + + + +
-
-); + ); +}; export default React.memo(TokenTransferItem); diff --git a/src/components/AddressOrENSName.tsx b/src/components/AddressOrENSName.tsx deleted file mode 100644 index bacdea8..0000000 --- a/src/components/AddressOrENSName.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useContext } from "react"; -import PlainAddress from "./PlainAddress"; -import { resolverRendererRegistry } from "../api/address-resolver"; -import { useResolvedAddress } from "../useResolvedAddresses"; -import { RuntimeContext } from "../useRuntime"; -import { ChecksummedAddress } from "../types"; - -type AddressOrENSNameProps = { - address: ChecksummedAddress; - selectedAddress?: string; - dontOverrideColors?: boolean; -}; - -const AddressOrENSName: React.FC = ({ - address, - selectedAddress, - dontOverrideColors, -}) => { - const { provider } = useContext(RuntimeContext); - const resolvedAddress = useResolvedAddress(provider, address); - const linkable = address !== selectedAddress; - - if (!provider || !resolvedAddress) { - return ( - - ); - } - - const [resolver, resolvedName] = resolvedAddress; - const renderer = resolverRendererRegistry.get(resolver); - if (renderer === undefined) { - return ( - - ); - } - - return renderer( - provider.network.chainId, - address, - resolvedName, - linkable, - !!dontOverrideColors - ); -}; - -export default AddressOrENSName; diff --git a/src/components/AddressOrENSNameInvalidNonce.tsx b/src/components/AddressOrENSNameInvalidNonce.tsx index a92496a..1b71288 100644 --- a/src/components/AddressOrENSNameInvalidNonce.tsx +++ b/src/components/AddressOrENSNameInvalidNonce.tsx @@ -1,7 +1,7 @@ import React from "react"; import StandardSubtitle from "../StandardSubtitle"; import ContentFrame from "../ContentFrame"; -import AddressOrENSName from "./AddressOrENSName"; +import DecoratedAddressLink from "./DecoratedAddressLink"; type AddressOrENSNameInvalidNonceProps = { addressOrENSName: string; @@ -15,7 +15,7 @@ const AddressOrENSNameInvalidNonce: React.FC< Transaction Details
- + : no transaction found for nonce="{nonce}".
diff --git a/src/components/AddressOrENSNameNoTx.tsx b/src/components/AddressOrENSNameNoTx.tsx index c4f6178..33e18fe 100644 --- a/src/components/AddressOrENSNameNoTx.tsx +++ b/src/components/AddressOrENSNameNoTx.tsx @@ -1,7 +1,7 @@ import React from "react"; import StandardSubtitle from "../StandardSubtitle"; import ContentFrame from "../ContentFrame"; -import AddressOrENSName from "./AddressOrENSName"; +import DecoratedAddressLink from "./DecoratedAddressLink"; type AddressOrENSNameNoTxProps = { addressOrENSName: string; @@ -14,7 +14,7 @@ const AddressOrENSNameNoTx: React.FC = ({ Transaction Details
- + : no outbound transactions found.
diff --git a/src/components/DecoratedAddressLink.tsx b/src/components/DecoratedAddressLink.tsx index 1b3870a..d59ba9d 100644 --- a/src/components/DecoratedAddressLink.tsx +++ b/src/components/DecoratedAddressLink.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import { NavLink } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faStar } from "@fortawesome/free-solid-svg-icons/faStar"; @@ -6,21 +6,25 @@ import { faBomb } from "@fortawesome/free-solid-svg-icons/faBomb"; import { faMoneyBillAlt } from "@fortawesome/free-solid-svg-icons/faMoneyBillAlt"; import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn"; import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins"; -import AddressOrENSName from "./AddressOrENSName"; import SourcifyLogo from "../sourcify/SourcifyLogo"; -import { AddressContext, ZERO_ADDRESS } from "../types"; +import PlainAddress from "./PlainAddress"; import { Metadata } from "../sourcify/useSourcify"; +import { RuntimeContext } from "../useRuntime"; +import { useResolvedAddress } from "../useResolvedAddresses"; +import { AddressContext, ChecksummedAddress, ZERO_ADDRESS } from "../types"; +import { resolverRendererRegistry } from "../api/address-resolver"; type DecoratedAddressLinkProps = { - address: string; - selectedAddress?: string; - addressCtx?: AddressContext; - creation?: boolean; - miner?: boolean; - selfDestruct?: boolean; - txFrom?: boolean; - txTo?: boolean; + address: ChecksummedAddress; + selectedAddress?: ChecksummedAddress | undefined; + addressCtx?: AddressContext | undefined; + creation?: boolean | undefined; + miner?: boolean | undefined; + selfDestruct?: boolean | undefined; + txFrom?: boolean | undefined; + txTo?: boolean | undefined; metadata?: Metadata | null | undefined; + eoa?: boolean | undefined; }; const DecoratedAddressLink: React.FC = ({ @@ -33,6 +37,7 @@ const DecoratedAddressLink: React.FC = ({ txFrom, txTo, metadata, + eoa, }) => { const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS; const burn = addressCtx === AddressContext.TO && address === ZERO_ADDRESS; @@ -80,13 +85,84 @@ const DecoratedAddressLink: React.FC = ({ )} - + {!mint && !burn && ( + <> + {eoa === true && ( + + [EOA] + + )} + {eoa === false && ( + [C] + )} + + )}
); }; +type ResolvedAddressProps = { + address: ChecksummedAddress; + selectedAddress?: ChecksummedAddress | undefined; + dontOverrideColors?: boolean; +}; + +const ResolvedAddress: React.FC = ({ + address, + selectedAddress, + dontOverrideColors, +}) => { + const { provider } = useContext(RuntimeContext); + const resolvedAddress = useResolvedAddress(provider, address); + const linkable = address !== selectedAddress; + + if (!provider || !resolvedAddress) { + return ( + + ); + } + + const [resolver, resolvedName] = resolvedAddress; + const renderer = resolverRendererRegistry.get(resolver); + if (renderer === undefined) { + return ( + + ); + } + + return renderer( + provider.network.chainId, + address, + resolvedName, + linkable, + !!dontOverrideColors + ); +}; + +type AddressLegendProps = { + title: string; +}; + +const AddressLegend: React.FC = ({ title, children }) => ( + + {children} + +); + export default React.memo(DecoratedAddressLink); diff --git a/src/components/InternalTransfer.tsx b/src/components/InternalTransfer.tsx index 0600e95..5a9f8cd 100644 --- a/src/components/InternalTransfer.tsx +++ b/src/components/InternalTransfer.tsx @@ -1,9 +1,11 @@ -import React from "react"; +import React, { useContext } from "react"; import { formatEther } from "@ethersproject/units"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import AddressHighlighter from "./AddressHighlighter"; import DecoratedAddressLink from "./DecoratedAddressLink"; +import { RuntimeContext } from "../useRuntime"; +import { useHasCode } from "../useErigonHooks"; import { TransactionData, InternalOperation } from "../types"; type InternalTransferProps = { @@ -22,6 +24,18 @@ const InternalTransfer: React.FC = ({ txData.confirmedData?.miner !== undefined && internalOp.to === txData.confirmedData.miner; + const { provider } = useContext(RuntimeContext); + const fromHasCode = useHasCode( + provider, + internalOp.from, + txData.confirmedData ? txData.confirmedData.blockNumber - 1 : undefined + ); + const toHasCode = useHasCode( + provider, + internalOp.to, + txData.confirmedData ? txData.confirmedData.blockNumber - 1 : undefined + ); + return (
@@ -41,6 +55,7 @@ const InternalTransfer: React.FC = ({ miner={fromMiner} txFrom={internalOp.from === txData.from} txTo={internalOp.from === txData.to} + eoa={fromHasCode === undefined ? undefined : !fromHasCode} />
@@ -58,6 +73,7 @@ const InternalTransfer: React.FC = ({ miner={toMiner} txFrom={internalOp.to === txData.from} txTo={internalOp.to === txData.to} + eoa={toHasCode === undefined ? undefined : !toHasCode} />
diff --git a/src/components/TransactionAddress.tsx b/src/components/TransactionAddress.tsx index a313b24..6a76dea 100644 --- a/src/components/TransactionAddress.tsx +++ b/src/components/TransactionAddress.tsx @@ -9,12 +9,14 @@ type TransactionAddressProps = { address: string; addressCtx?: AddressContext | undefined; metadata?: Metadata | null | undefined; + eoa?: boolean | undefined; }; const TransactionAddress: React.FC = ({ address, addressCtx, metadata, + eoa, }) => { const txData = useSelectedTransaction(); // TODO: push down creation coloring logic into DecoratedAddressLink @@ -30,6 +32,7 @@ const TransactionAddress: React.FC = ({ txTo={address === txData?.to || creation} creation={creation} metadata={metadata} + eoa={eoa} /> ); diff --git a/src/search/TransactionItem.tsx b/src/search/TransactionItem.tsx index daa0088..58de25b 100644 --- a/src/search/TransactionItem.tsx +++ b/src/search/TransactionItem.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import { BlockTag } from "@ethersproject/abstract-provider"; import { BigNumber } from "@ethersproject/bignumber"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -16,6 +16,8 @@ import TransactionDirection, { import TransactionValue from "../components/TransactionValue"; import { ChecksummedAddress, ProcessedTransaction } from "../types"; import { FeeDisplay } from "./useFeeToggler"; +import { RuntimeContext } from "../useRuntime"; +import { useHasCode } from "../useErigonHooks"; import { formatValue } from "../components/formatter"; import ETH2USDValue from "../components/ETH2USDValue"; import { Metadata } from "../sourcify/useSourcify"; @@ -35,6 +37,13 @@ const TransactionItem: React.FC = ({ priceMap, metadatas, }) => { + const { provider } = useContext(RuntimeContext); + const toHasCode = useHasCode( + provider, + tx.to ?? undefined, + tx.blockNumber - 1 + ); + let direction: Direction | undefined; if (selectedAddress) { if (tx.from === selectedAddress && tx.to === selectedAddress) { @@ -107,6 +116,7 @@ const TransactionItem: React.FC = ({ selectedAddress={selectedAddress} miner={tx.miner === tx.to} metadata={metadatas[tx.to]} + eoa={toHasCode === undefined ? undefined : !toHasCode} /> ) : ( @@ -116,6 +126,7 @@ const TransactionItem: React.FC = ({ selectedAddress={selectedAddress} creation metadata={metadatas[tx.createdContractAddress!]} + eoa={false} /> )} diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index 261e651..fd8acab 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -45,7 +45,7 @@ import { import { DevDoc, Metadata, useError, UserDoc } from "../sourcify/useSourcify"; import { RuntimeContext } from "../useRuntime"; import { useContractsMetadata } from "../hooks"; -import { useTransactionError } from "../useErigonHooks"; +import { useHasCode, useTransactionError } from "../useErigonHooks"; type DetailsProps = { txData: TransactionData; @@ -118,6 +118,12 @@ const Details: React.FC = ({ : undefined; const [expanded, setExpanded] = useState(false); + const toHasCode = useHasCode( + provider, + txData.to, + txData.confirmedData ? txData.confirmedData.blockNumber - 1 : undefined + ); + return ( @@ -264,6 +270,7 @@ const Details: React.FC = ({ @@ -307,6 +314,7 @@ const Details: React.FC = ({ t={t} tokenMeta={txData.tokenMetas[t.token]} metadatas={metadatas} + txData={txData} /> ))} diff --git a/src/transaction/Trace.tsx b/src/transaction/Trace.tsx index a04bcdb..5a2a4f9 100644 --- a/src/transaction/Trace.tsx +++ b/src/transaction/Trace.tsx @@ -24,7 +24,12 @@ const Trace: React.FC = ({ txData }) => {
{traces.map((t, i, a) => ( - + ))}
diff --git a/src/transaction/TraceInput.tsx b/src/transaction/TraceInput.tsx index 0524501..68cca6c 100644 --- a/src/transaction/TraceInput.tsx +++ b/src/transaction/TraceInput.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBomb } from "@fortawesome/free-solid-svg-icons/faBomb"; import TransactionAddress from "../components/TransactionAddress"; @@ -6,18 +6,21 @@ import FormattedBalance from "../components/FormattedBalance"; import FunctionSignature from "./FunctionSignature"; import InputDecoder from "./decoder/InputDecoder"; import ExpanderSwitch from "../components/ExpanderSwitch"; -import { TraceEntry } from "../useErigonHooks"; +import { RuntimeContext } from "../useRuntime"; +import { TraceEntry, useHasCode } from "../useErigonHooks"; import { extract4Bytes, use4Bytes, useTransactionDescription, } from "../use4Bytes"; +import { TransactionData } from "../types"; type TraceInputProps = { t: TraceEntry; + txData: TransactionData; }; -const TraceInput: React.FC = ({ t }) => { +const TraceInput: React.FC = ({ t, txData }) => { const raw4Bytes = extract4Bytes(t.input); const fourBytes = use4Bytes(raw4Bytes); const sigText = @@ -32,6 +35,15 @@ const TraceInput: React.FC = ({ t }) => { const [expanded, setExpanded] = useState(false); + const { provider } = useContext(RuntimeContext); + const toHasCode = useHasCode( + provider, + t.to, + txData.confirmedData !== undefined + ? txData.confirmedData.blockNumber - 1 + : undefined + ); + return (
= ({ t }) => { ) : ( <> - + {t.type !== "CREATE" && t.type !== "CREATE2" && ( <> diff --git a/src/transaction/TraceItem.tsx b/src/transaction/TraceItem.tsx index d6a4d7d..5dd4a3d 100644 --- a/src/transaction/TraceItem.tsx +++ b/src/transaction/TraceItem.tsx @@ -5,13 +5,15 @@ import { faMinusSquare } from "@fortawesome/free-regular-svg-icons/faMinusSquare import { Switch } from "@headlessui/react"; import { TraceGroup } from "../useErigonHooks"; import TraceInput from "./TraceInput"; +import { TransactionData } from "../types"; type TraceItemProps = { t: TraceGroup; last: boolean; + txData: TransactionData; }; -const TraceItem: React.FC = ({ t, last }) => { +const TraceItem: React.FC = ({ t, last, txData }) => { const [expanded, setExpanded] = useState(true); return ( @@ -33,7 +35,7 @@ const TraceItem: React.FC = ({ t, last }) => { /> )} - +
{t.children && (
= ({ t, last }) => { expanded ? "" : "hidden" }`} > - +
)} @@ -50,16 +52,19 @@ const TraceItem: React.FC = ({ t, last }) => { type TraceChildrenProps = { c: TraceGroup[]; + txData: TransactionData; }; -const TraceChildren: React.FC = React.memo(({ c }) => { - return ( - <> - {c.map((tc, i, a) => ( - - ))} - - ); -}); +const TraceChildren: React.FC = React.memo( + ({ c, txData }) => { + return ( + <> + {c.map((tc, i, a) => ( + + ))} + + ); + } +); export default TraceItem; diff --git a/src/useErigonHooks.ts b/src/useErigonHooks.ts index cbc6208..c949481 100644 --- a/src/useErigonHooks.ts +++ b/src/useErigonHooks.ts @@ -1,5 +1,9 @@ import { useState, useEffect } from "react"; -import { Block, BlockWithTransactions } from "@ethersproject/abstract-provider"; +import { + Block, + BlockWithTransactions, + BlockTag, +} from "@ethersproject/abstract-provider"; import { JsonRpcProvider } from "@ethersproject/providers"; import { getAddress } from "@ethersproject/address"; import { Contract } from "@ethersproject/contracts"; @@ -7,6 +11,7 @@ import { defaultAbiCoder } from "@ethersproject/abi"; import { BigNumber } from "@ethersproject/bignumber"; import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes"; import useSWR, { useSWRConfig } from "swr"; +import useSWRImmutable from "swr/immutable"; import { getInternalOperations } from "./nodeFunctions"; import { TokenMetas, @@ -679,3 +684,43 @@ export const useAddressBalance = ( return balance; }; + +/** + * This is a generic fetch for SWR, where the key is an array, whose + * element 0 is the JSON-RPC method, and the remaining are the method + * arguments. + */ +export const providerFetcher = + (provider: JsonRpcProvider | undefined) => + async (...key: any[]): Promise => { + if (provider === undefined) { + return undefined; + } + for (const a of key) { + if (a === undefined) { + return undefined; + } + } + + const method = key[0]; + const args = key.slice(1); + const result = await provider.send(method, args); + // console.log(`providerFetcher: ${method} ${args} === ${result}`); + return result; + }; + +export const useHasCode = ( + provider: JsonRpcProvider | undefined, + address: ChecksummedAddress | undefined, + blockTag: BlockTag = "latest" +): boolean | undefined => { + const fetcher = providerFetcher(provider); + const { data, error } = useSWRImmutable( + ["ots_hasCode", address, blockTag], + fetcher + ); + if (error) { + return undefined; + } + return data as boolean | undefined; +};