diff --git a/src/Address.tsx b/src/Address.tsx index 891a315..583966f 100644 --- a/src/Address.tsx +++ b/src/Address.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useContext, useCallback, useMemo } from "react"; +import React, { useEffect, useContext, useCallback } from "react"; import { useParams, useNavigate, @@ -23,9 +23,9 @@ import Contracts from "./address/Contracts"; import { RuntimeContext } from "./useRuntime"; import { useAppConfigContext } from "./useAppConfig"; import { useAddressOrENS } from "./useResolvedAddresses"; -import { useMultipleMetadata } from "./sourcify/useSourcify"; +import { useSourcifyMetadata } from "./sourcify/useSourcify"; import { ChecksummedAddress } from "./types"; -import { useAddressesWithCode } from "./useErigonHooks"; +import { useHasCode } from "./useErigonHooks"; import { useChainInfo } from "./useChainInfo"; const AddressTransactionByNonce = React.lazy( @@ -65,25 +65,13 @@ const Address: React.FC = () => { } }, [addressOrName, checksummedAddress, isENS]); + const hasCode = useHasCode(provider, checksummedAddress, "latest"); const { sourcifySource } = useAppConfigContext(); - const checksummedAddressAsArray = useMemo( - () => (checksummedAddress !== undefined ? [checksummedAddress] : []), - [checksummedAddress] - ); - const contractAddresses = useAddressesWithCode( - provider, - checksummedAddressAsArray - ); - const metadatas = useMultipleMetadata( - undefined, - contractAddresses, + const addressMetadata = useSourcifyMetadata( + hasCode ? checksummedAddress : undefined, provider?.network.chainId, sourcifySource ); - const addressMetadata = - checksummedAddress !== undefined - ? metadatas[checksummedAddress] - : undefined; const { network, faucets } = useChainInfo(); @@ -134,7 +122,7 @@ const Address: React.FC = () => { Overview - {(contractAddresses?.length ?? 0) > 0 && ( + {hasCode && ( { element={ } /> diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index 5838428..0284a58 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -6,28 +6,20 @@ import TransactionAddress from "./components/TransactionAddress"; import ValueHighlighter from "./components/ValueHighlighter"; import FormattedBalance from "./components/FormattedBalance"; import USDAmount from "./components/USDAmount"; -import { - AddressContext, - ChecksummedAddress, - TokenMeta, - TokenTransfer, -} from "./types"; +import { AddressContext, TokenMeta, TokenTransfer } from "./types"; import { RuntimeContext } from "./useRuntime"; import { useBlockNumberContext } from "./useBlockTagContext"; -import { Metadata } from "./sourcify/useSourcify"; import { useTokenUSDOracle } from "./usePriceOracle"; type TokenTransferItemProps = { t: TokenTransfer; tokenMeta?: TokenMeta | null | undefined; - metadatas: Record; }; // TODO: handle partial const TokenTransferItem: React.FC = ({ t, tokenMeta, - metadatas, }) => { const { provider } = useContext(RuntimeContext); const blockNumber = useBlockNumberContext(); @@ -40,7 +32,6 @@ const TokenTransferItem: React.FC = ({ @@ -51,7 +42,6 @@ const TokenTransferItem: React.FC = ({ @@ -67,7 +57,7 @@ const TokenTransferItem: React.FC = ({ /> - + {tokenMeta && quote !== undefined && decimals !== undefined && ( import("./transaction/Details")); +const Logs = React.lazy(() => import("./transaction/Logs")); +const Trace = React.lazy(() => import("./transaction/Trace")); const Transaction: React.FC = () => { - const { txhash } = useParams(); - if (txhash === undefined) { + const { txhash: txHash } = useParams(); + if (txHash === undefined) { throw new Error("txhash couldn't be undefined here"); } - return ; + + const { provider } = useContext(RuntimeContext); + const txData = useTxData(provider, txHash); + const selectionCtx = useSelection(); + + return ( + + + + Transaction Details + {txData === null && ( + +
+ Transaction {txHash} not + found. +
+
+ )} + {txData && ( + + + + Overview + {txData.confirmedData?.blockNumber !== undefined && ( + + Logs + {txData && + ` (${txData.confirmedData?.logs?.length ?? 0})`} + + )} + Trace + + + + + } /> + } /> + } /> + + + + )} +
+
+
+ ); }; export default Transaction; diff --git a/src/TransactionPageContent.tsx b/src/TransactionPageContent.tsx deleted file mode 100644 index 7320a2e..0000000 --- a/src/TransactionPageContent.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { useContext, useMemo } from "react"; -import { Route, Routes } from "react-router-dom"; -import { Tab } from "@headlessui/react"; -import StandardFrame from "./StandardFrame"; -import StandardSubtitle from "./StandardSubtitle"; -import ContentFrame from "./ContentFrame"; -import NavTab from "./components/NavTab"; -import { RuntimeContext } from "./useRuntime"; -import { useInternalOperations, useTxData } from "./useErigonHooks"; -import { SelectionContext, useSelection } from "./useSelection"; -import { SelectedTransactionContext } from "./useSelectedTransaction"; -import { BlockNumberContext } from "./useBlockTagContext"; -import { useAppConfigContext } from "./useAppConfig"; -import { useSourcify, useTransactionDescription } from "./sourcify/useSourcify"; - -const Details = React.lazy(() => import("./transaction/Details")); -const Logs = React.lazy(() => import("./transaction/Logs")); -const Trace = React.lazy(() => import("./transaction/Trace")); - -type TransactionPageContentProps = { - txHash: string; -}; - -const TransactionPageContent: React.FC = ({ - txHash, -}) => { - const { provider } = useContext(RuntimeContext); - - const txData = useTxData(provider, txHash); - const internalOps = useInternalOperations(provider, txData); - const sendsEthToMiner = useMemo(() => { - if (!txData || !internalOps) { - return false; - } - - for (const t of internalOps) { - if (t.to === txData.confirmedData?.miner) { - return true; - } - } - return false; - }, [txData, internalOps]); - - const selectionCtx = useSelection(); - - const { sourcifySource } = useAppConfigContext(); - const metadata = useSourcify( - txData?.to, - provider?.network.chainId, - sourcifySource - ); - const txDesc = useTransactionDescription(metadata, txData); - - return ( - - - - Transaction Details - {txData === null && ( - -
- Transaction {txHash} not - found. -
-
- )} - {txData && ( - - - - Overview - {txData.confirmedData?.blockNumber !== undefined && ( - - Logs - {txData && - ` (${txData.confirmedData?.logs?.length ?? 0})`} - - )} - Trace - - - - - - } - /> - } - /> - } /> - - - - )} -
-
-
- ); -}; - -export default TransactionPageContent; diff --git a/src/address/AddressTransactionResults.tsx b/src/address/AddressTransactionResults.tsx index c70edba..a6e1fc3 100644 --- a/src/address/AddressTransactionResults.tsx +++ b/src/address/AddressTransactionResults.tsx @@ -18,7 +18,6 @@ import { useMultipleETHUSDOracle } from "../usePriceOracle"; import { RuntimeContext } from "../useRuntime"; import { useParams, useSearchParams } from "react-router-dom"; import { ChecksummedAddress, ProcessedTransaction } from "../types"; -import { useContractsMetadata } from "../hooks"; import { useAddressBalance, useContractCreator } from "../useErigonHooks"; import { BlockNumberContext } from "../useBlockTagContext"; @@ -112,22 +111,6 @@ const AddressTransactionResults: React.FC = ({ }, [page]); const priceMap = useMultipleETHUSDOracle(provider, blockTags); - // Calculate Sourcify metadata for all addresses that appear on this page results - const addresses = useMemo(() => { - const _addresses = [address]; - if (page) { - for (const t of page) { - if (t.to) { - _addresses.push(t.to); - } - if (t.createdContractAddress) { - _addresses.push(t.createdContractAddress); - } - } - } - return _addresses; - }, [address, page]); - const metadatas = useContractsMetadata(addresses, provider); const balance = useAddressBalance(provider, address); const creator = useContractCreator(provider, address); @@ -181,7 +164,6 @@ const AddressTransactionResults: React.FC = ({ selectedAddress={address} feeDisplay={feeDisplay} priceMap={priceMap} - metadatas={metadatas} /> ))} diff --git a/src/address/Contract.tsx b/src/address/Contract.tsx index 360e8bc..2f366a8 100644 --- a/src/address/Contract.tsx +++ b/src/address/Contract.tsx @@ -1,40 +1,19 @@ import React from "react"; import { SyntaxHighlighter, docco } from "../highlight-init"; -import { useContract } from "../sourcify/useSourcify"; -import { useAppConfigContext } from "../useAppConfig"; type ContractProps = { - checksummedAddress: string; - networkId: number; - filename: string; - source: any; + content: any; }; -const Contract: React.FC = ({ - checksummedAddress, - networkId, - filename, - source, -}) => { - const { sourcifySource } = useAppConfigContext(); - const content = useContract( - checksummedAddress, - networkId, - filename, - source, - sourcifySource - ); - - return ( - - {content ?? ""} - - ); -}; +const Contract: React.FC = ({ content }) => ( + + {content ?? ""} + +); export default React.memo(Contract); diff --git a/src/address/ContractFromRepo.tsx b/src/address/ContractFromRepo.tsx new file mode 100644 index 0000000..568babf --- /dev/null +++ b/src/address/ContractFromRepo.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { SyntaxHighlighter, docco } from "../highlight-init"; +import { useContract } from "../sourcify/useSourcify"; +import { useAppConfigContext } from "../useAppConfig"; + +type ContractFromRepoProps = { + checksummedAddress: string; + networkId: number; + filename: string; +}; + +const ContractFromRepo: React.FC = ({ + checksummedAddress, + networkId, + filename, +}) => { + const { sourcifySource } = useAppConfigContext(); + const content = useContract( + checksummedAddress, + networkId, + filename, + sourcifySource + ); + + return ( + + {content ?? ""} + + ); +}; + +export default React.memo(ContractFromRepo); diff --git a/src/address/Contracts.tsx b/src/address/Contracts.tsx index 19ce669..d3e4179 100644 --- a/src/address/Contracts.tsx +++ b/src/address/Contracts.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useContext, Fragment } from "react"; +import React, { useState, useEffect, useContext } from "react"; import { commify } from "@ethersproject/units"; import { Menu } from "@headlessui/react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -6,6 +6,7 @@ import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown"; import ContentFrame from "../ContentFrame"; import InfoRow from "../components/InfoRow"; import Contract from "./Contract"; +import ContractFromRepo from "./ContractFromRepo"; import { RuntimeContext } from "../useRuntime"; import { Metadata } from "../sourcify/useSourcify"; import ExternalLink from "../components/ExternalLink"; @@ -101,7 +102,7 @@ const Contracts: React.FC = ({ className={`flex text-sm px-2 py-1 ${ selected === k ? "font-bold bg-gray-200 text-gray-500" - : "hover:border-orange-200 hover:text-gray-500 text-gray-400 transition-transform transition-colors duration-75" + : "hover:text-gray-500 text-gray-400 transition-colors duration-75" }`} onClick={() => setSelected(k)} > @@ -113,12 +114,17 @@ const Contracts: React.FC = ({ {selected && ( - + <> + {rawMetadata.sources[selected].content ? ( + + ) : ( + + )} + )} diff --git a/src/block/BlockTransactionResults.tsx b/src/block/BlockTransactionResults.tsx index 2d768cc..5c1824f 100644 --- a/src/block/BlockTransactionResults.tsx +++ b/src/block/BlockTransactionResults.tsx @@ -8,10 +8,9 @@ import TransactionItem from "../search/TransactionItem"; import { useFeeToggler } from "../search/useFeeToggler"; import { RuntimeContext } from "../useRuntime"; import { SelectionContext, useSelection } from "../useSelection"; -import { ChecksummedAddress, ProcessedTransaction } from "../types"; +import { ProcessedTransaction } from "../types"; import { PAGE_SIZE } from "../params"; import { useMultipleETHUSDOracle } from "../usePriceOracle"; -import { useContractsMetadata } from "../hooks"; type BlockTransactionResultsProps = { blockTag: BlockTag; @@ -32,24 +31,6 @@ const BlockTransactionResults: React.FC = ({ const blockTags = useMemo(() => [blockTag], [blockTag]); const priceMap = useMultipleETHUSDOracle(provider, blockTags); - const addresses = useMemo((): ChecksummedAddress[] => { - if (!page) { - return []; - } - - const _addresses: ChecksummedAddress[] = []; - for (const t of page) { - if (t.to) { - _addresses.push(t.to); - } - if (t.createdContractAddress) { - _addresses.push(t.createdContractAddress); - } - } - return _addresses; - }, [page]); - const metadatas = useContractsMetadata(addresses, provider); - return (
@@ -78,7 +59,6 @@ const BlockTransactionResults: React.FC = ({ tx={tx} feeDisplay={feeDisplay} priceMap={priceMap} - metadatas={metadatas} /> ))}
diff --git a/src/components/DecoratedAddressLink.tsx b/src/components/DecoratedAddressLink.tsx index e85753e..62ccd3a 100644 --- a/src/components/DecoratedAddressLink.tsx +++ b/src/components/DecoratedAddressLink.tsx @@ -8,8 +8,9 @@ import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn"; import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins"; import SourcifyLogo from "../sourcify/SourcifyLogo"; import PlainAddress from "./PlainAddress"; -import { Metadata } from "../sourcify/useSourcify"; import { RuntimeContext } from "../useRuntime"; +import { useAppConfigContext } from "../useAppConfig"; +import { useSourcifyMetadata } from "../sourcify/useSourcify"; import { useResolvedAddress } from "../useResolvedAddresses"; import { AddressContext, ChecksummedAddress, ZERO_ADDRESS } from "../types"; import { resolverRendererRegistry } from "../api/address-resolver"; @@ -23,7 +24,6 @@ type DecoratedAddressLinkProps = { selfDestruct?: boolean | undefined; txFrom?: boolean | undefined; txTo?: boolean | undefined; - metadata?: Metadata | null | undefined; eoa?: boolean | undefined; }; @@ -36,9 +36,16 @@ const DecoratedAddressLink: React.FC = ({ selfDestruct, txFrom, txTo, - metadata, eoa, }) => { + const { provider } = useContext(RuntimeContext); + const { sourcifySource } = useAppConfigContext(); + const metadata = useSourcifyMetadata( + address, + provider?.network.chainId, + sourcifySource + ); + const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS; const burn = addressCtx === AddressContext.TO && address === ZERO_ADDRESS; diff --git a/src/components/TransactionAddress.tsx b/src/components/TransactionAddress.tsx index 46f427f..823bd50 100644 --- a/src/components/TransactionAddress.tsx +++ b/src/components/TransactionAddress.tsx @@ -5,20 +5,17 @@ import { useSelectedTransaction } from "../useSelectedTransaction"; import { useBlockNumberContext } from "../useBlockTagContext"; import { RuntimeContext } from "../useRuntime"; import { useHasCode } from "../useErigonHooks"; -import { Metadata } from "../sourcify/useSourcify"; import { AddressContext, ChecksummedAddress } from "../types"; type TransactionAddressProps = { address: ChecksummedAddress; addressCtx?: AddressContext | undefined; - metadata?: Metadata | null | undefined; showCodeIndicator?: boolean; }; const TransactionAddress: React.FC = ({ address, addressCtx, - metadata, showCodeIndicator = false, }) => { const txData = useSelectedTransaction(); @@ -46,7 +43,6 @@ const TransactionAddress: React.FC = ({ txFrom={address === txData?.from} txTo={address === txData?.to || creation} creation={creation} - metadata={metadata} eoa={ showCodeIndicator && blockNumber !== undefined ? !toHasCode diff --git a/src/hooks.ts b/src/hooks.ts deleted file mode 100644 index e924314..0000000 --- a/src/hooks.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useMemo } from "react"; -import { JsonRpcProvider } from "@ethersproject/providers"; -import { ChecksummedAddress } from "./types"; -import { Metadata, useMultipleMetadata } from "./sourcify/useSourcify"; -import { useAppConfigContext } from "./useAppConfig"; -import { useAddressesWithCode } from "./useErigonHooks"; - -export const useDedupedAddresses = ( - addresses: ChecksummedAddress[] -): ChecksummedAddress[] => { - return useMemo(() => { - const deduped = new Set(addresses); - return [...deduped]; - }, [addresses]); -}; - -export const useContractsMetadata = ( - addresses: ChecksummedAddress[], - provider: JsonRpcProvider | undefined, - baseMetadatas?: Record -) => { - const deduped = useDedupedAddresses(addresses); - const contracts = useAddressesWithCode(provider, deduped); - const { sourcifySource } = useAppConfigContext(); - const metadatas = useMultipleMetadata( - baseMetadatas, - contracts, - provider?.network.chainId, - sourcifySource - ); - - return metadatas; -}; diff --git a/src/search/TransactionItem.tsx b/src/search/TransactionItem.tsx index 21e1b84..3fee32c 100644 --- a/src/search/TransactionItem.tsx +++ b/src/search/TransactionItem.tsx @@ -14,20 +14,18 @@ import TransactionDirection, { Flags, } from "../components/TransactionDirection"; import TransactionValue from "../components/TransactionValue"; -import { ChecksummedAddress, ProcessedTransaction } from "../types"; +import { 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"; type TransactionItemProps = { tx: ProcessedTransaction; selectedAddress?: string; feeDisplay: FeeDisplay; priceMap: Record; - metadatas: Record; }; const TransactionItem: React.FC = ({ @@ -35,7 +33,6 @@ const TransactionItem: React.FC = ({ selectedAddress, feeDisplay, priceMap, - metadatas, }) => { const { provider } = useContext(RuntimeContext); const toHasCode = useHasCode( @@ -113,7 +110,6 @@ const TransactionItem: React.FC = ({ address={tx.to} selectedAddress={selectedAddress} miner={tx.miner === tx.to} - metadata={metadatas[tx.to]} eoa={toHasCode === undefined ? undefined : !toHasCode} /> @@ -123,7 +119,6 @@ const TransactionItem: React.FC = ({ address={tx.createdContractAddress!} selectedAddress={selectedAddress} creation - metadata={metadatas[tx.createdContractAddress!]} eoa={false} /> diff --git a/src/sourcify/useSourcify.ts b/src/sourcify/useSourcify.ts index 4f8f31f..0ae3958 100644 --- a/src/sourcify/useSourcify.ts +++ b/src/sourcify/useSourcify.ts @@ -1,6 +1,7 @@ -import { useState, useEffect, useMemo } from "react"; +import { useMemo } from "react"; import { Interface } from "@ethersproject/abi"; import { ErrorDescription } from "@ethersproject/abi/lib/interface"; +import useSWRImmutable from "swr/immutable"; import { ChecksummedAddress, TransactionData } from "../types"; import { sourcifyMetadata, SourcifySource, sourcifySourceFile } from "../url"; @@ -80,148 +81,67 @@ export type Metadata = { }; }; -const fetchSourcifyMetadata = async ( - address: ChecksummedAddress, - chainId: number, - source: SourcifySource, - abortController: AbortController -): Promise => { +const sourcifyFetcher = async (url: string) => { try { - const metadataURL = sourcifyMetadata(address, chainId, source); - const result = await fetch(metadataURL, { - signal: abortController.signal, - }); - if (result.ok) { - return await result.json(); + const res = await fetch(url); + if (res.ok) { + return res.json(); } - return null; } catch (err) { - console.error(err); + console.warn( + `error while getting Sourcify metadata: url=${url} err=${err}` + ); return null; } }; -// TODO: replace every occurrence with the multiple version one -export const useSourcify = ( +export const useSourcifyMetadata = ( address: ChecksummedAddress | undefined, chainId: number | undefined, source: SourcifySource ): Metadata | null | undefined => { - const [rawMetadata, setRawMetadata] = useState(); - - useEffect(() => { - if (!address || chainId === undefined) { - return; - } - setRawMetadata(undefined); - - const abortController = new AbortController(); - const fetchMetadata = async () => { - const _metadata = await fetchSourcifyMetadata( - address, - chainId, - source, - abortController - ); - setRawMetadata(_metadata); - }; - fetchMetadata(); - - return () => { - abortController.abort(); - }; - }, [address, chainId, source]); - - return rawMetadata; + const metadataURL = () => + address === undefined || chainId === undefined + ? null + : sourcifyMetadata(address, chainId, source); + const { data, error } = useSWRImmutable( + metadataURL, + sourcifyFetcher + ); + if (error) { + return null; + } + return data; }; -export const useMultipleMetadata = ( - baseMetadatas: Record | undefined, - addresses: ChecksummedAddress[] | undefined, - chainId: number | undefined, - source: SourcifySource -): Record => { - const [rawMetadata, setRawMetadata] = useState< - Record - >({}); - useEffect(() => { - if (addresses === undefined || chainId === undefined) { - return; - } - setRawMetadata({}); - - const abortController = new AbortController(); - const fetchMetadata = async (_addresses: string[]) => { - const fetchers: Promise[] = []; - for (const address of _addresses) { - fetchers.push( - fetchSourcifyMetadata(address, chainId, source, abortController) - ); - } - - const results = await Promise.all(fetchers); - if (abortController.signal.aborted) { - return; - } - let metadatas: Record = {}; - if (baseMetadatas) { - metadatas = { ...baseMetadatas }; - } - for (let i = 0; i < results.length; i++) { - metadatas[_addresses[i]] = results[i]; - } - setRawMetadata(metadatas); - }; - - const filtered = addresses.filter((a) => baseMetadatas?.[a] === undefined); - fetchMetadata(filtered); - - return () => { - abortController.abort(); - }; - }, [baseMetadatas, addresses, chainId, source]); - - return rawMetadata; +const contractFetcher = async (url: string): Promise => { + const res = await fetch(url); + if (res.ok) { + return await res.text(); + } + return null; }; export const useContract = ( checksummedAddress: string, networkId: number, filename: string, - source: any, sourcifySource: SourcifySource ) => { - const [content, setContent] = useState(source.content); + const normalizedFilename = filename.replaceAll(/[@:]/g, "_"); + const url = sourcifySourceFile( + checksummedAddress, + networkId, + normalizedFilename, + sourcifySource + ); - useEffect(() => { - if (source.content) { - return; - } - - const abortController = new AbortController(); - const readContent = async () => { - const normalizedFilename = filename.replaceAll(/[@:]/g, "_"); - const url = sourcifySourceFile( - checksummedAddress, - networkId, - normalizedFilename, - sourcifySource - ); - const res = await fetch(url, { signal: abortController.signal }); - if (res.ok) { - const _content = await res.text(); - setContent(_content); - } - }; - readContent(); - - return () => { - abortController.abort(); - }; - }, [checksummedAddress, networkId, filename, source.content, sourcifySource]); - - return content; + const { data, error } = useSWRImmutable(url, contractFetcher); + if (error) { + return undefined; + } + return data; }; export const useTransactionDescription = ( diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index 9b9449f..a34a212 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -1,6 +1,5 @@ import React, { useContext, useMemo, useState } from "react"; import { Tab } from "@headlessui/react"; -import { TransactionDescription } from "@ethersproject/abi"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; import { faCube } from "@fortawesome/free-solid-svg-icons/faCube"; @@ -25,11 +24,7 @@ import USDValue from "../components/USDValue"; import FormattedBalance from "../components/FormattedBalance"; import ETH2USDValue from "../components/ETH2USDValue"; import TokenTransferItem from "../TokenTransferItem"; -import { - TransactionData, - InternalOperation, - ChecksummedAddress, -} from "../types"; +import { TransactionData } from "../types"; import PercentageBar from "../components/PercentageBar"; import ExternalLink from "../components/ExternalLink"; import RelativePosition from "../components/RelativePosition"; @@ -41,32 +36,24 @@ import { use4Bytes, useTransactionDescription, } from "../use4Bytes"; -import { DevDoc, Metadata, useError, UserDoc } from "../sourcify/useSourcify"; +import { useAppConfigContext } from "../useAppConfig"; +import { + useError, + useSourcifyMetadata, + useTransactionDescription as useSourcifyTransactionDescription, +} from "../sourcify/useSourcify"; import { RuntimeContext } from "../useRuntime"; -import { useContractsMetadata } from "../hooks"; -import { useTransactionError } from "../useErigonHooks"; +import { useInternalOperations, useTransactionError } from "../useErigonHooks"; import { useChainInfo } from "../useChainInfo"; import { useETHUSDOracle } from "../usePriceOracle"; type DetailsProps = { txData: TransactionData; - txDesc: TransactionDescription | null | undefined; - toMetadata: Metadata | null | undefined; - userDoc?: UserDoc | undefined; - devDoc?: DevDoc | undefined; - internalOps?: InternalOperation[]; - sendsEthToMiner: boolean; }; -const Details: React.FC = ({ - txData, - txDesc, - toMetadata, - userDoc, - devDoc, - internalOps, - sendsEthToMiner, -}) => { +const Details: React.FC = ({ txData }) => { + const { provider } = useContext(RuntimeContext); + const hasEIP1559 = txData.confirmedData?.blockBaseFeePerGas !== undefined && txData.confirmedData?.blockBaseFeePerGas !== null; @@ -80,11 +67,34 @@ const Details: React.FC = ({ txData.value ); + const internalOps = useInternalOperations(provider, txData); + const sendsEthToMiner = useMemo(() => { + if (!txData || !internalOps) { + return false; + } + + for (const t of internalOps) { + if (t.to === txData.confirmedData?.miner) { + return true; + } + } + return false; + }, [txData, internalOps]); + + const { sourcifySource } = useAppConfigContext(); + const metadata = useSourcifyMetadata( + txData?.to, + provider?.network.chainId, + sourcifySource + ); + + const txDesc = useSourcifyTransactionDescription(metadata, txData); + const userDoc = metadata?.output.userdoc; + const devDoc = metadata?.output.devdoc; const resolvedTxDesc = txDesc ?? fourBytesTxDesc; const userMethod = txDesc ? userDoc?.methods[txDesc.signature] : undefined; const devMethod = txDesc ? devDoc?.methods[txDesc.signature] : undefined; - const { provider } = useContext(RuntimeContext); const { nativeCurrency: { name, symbol }, } = useChainInfo(); @@ -94,28 +104,12 @@ const Details: React.FC = ({ txData?.confirmedData?.blockNumber ); - 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 metadatas = useContractsMetadata(addresses, provider); const [errorMsg, outputData, isCustomError] = useTransactionError( provider, txData.transactionHash ); const errorDescription = useError( - toMetadata, + metadata, isCustomError ? outputData : undefined ); const userError = errorDescription @@ -269,11 +263,7 @@ const Details: React.FC = ({ {txData.to ? (
- +
) : txData.confirmedData === undefined ? ( @@ -284,9 +274,6 @@ const Details: React.FC = ({
@@ -316,7 +303,6 @@ const Details: React.FC = ({ key={i} t={t} tokenMeta={txData.tokenMetas[t.token]} - metadatas={metadatas} /> ))}
diff --git a/src/transaction/LogEntry.tsx b/src/transaction/LogEntry.tsx index a6597eb..d67d30e 100644 --- a/src/transaction/LogEntry.tsx +++ b/src/transaction/LogEntry.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from "react"; +import React, { useContext, useMemo } from "react"; import { Log } from "@ethersproject/abstract-provider"; -import { Fragment, Interface, LogDescription } from "@ethersproject/abi"; +import { Fragment, Interface } from "@ethersproject/abi"; import { Tab } from "@headlessui/react"; import TransactionAddress from "../components/TransactionAddress"; import Copy from "../components/Copy"; @@ -8,16 +8,41 @@ import ModeTab from "../components/ModeTab"; import DecodedParamsTable from "./decoder/DecodedParamsTable"; import DecodedLogSignature from "./decoder/DecodedLogSignature"; import { useTopic0 } from "../useTopic0"; -import { ChecksummedAddress } from "../types"; -import { Metadata } from "../sourcify/useSourcify"; +import { RuntimeContext } from "../useRuntime"; +import { useAppConfigContext } from "../useAppConfig"; +import { useSourcifyMetadata } from "../sourcify/useSourcify"; type LogEntryProps = { log: Log; - logDesc: LogDescription | null | undefined; - metadatas: Record; }; -const LogEntry: React.FC = ({ log, logDesc, metadatas }) => { +const LogEntry: React.FC = ({ log }) => { + const { provider } = useContext(RuntimeContext); + const { sourcifySource } = useAppConfigContext(); + const metadata = useSourcifyMetadata( + log.address, + provider?.network.chainId, + sourcifySource + ); + + const logDesc = useMemo(() => { + if (!metadata) { + return metadata; + } + + const abi = metadata.output.abi; + const intf = new Interface(abi as any); + try { + return intf.parseLog({ + topics: log.topics, + data: log.data, + }); + } catch (err) { + console.warn("Couldn't find function signature", err); + return null; + } + }, [log, metadata]); + const rawTopic0 = log.topics[0]; const topic0 = useTopic0(rawTopic0); @@ -56,10 +81,7 @@ const LogEntry: React.FC = ({ log, logDesc, metadatas }) => {
Address
- +
diff --git a/src/transaction/Logs.tsx b/src/transaction/Logs.tsx index 9b903b7..01bb1d8 100644 --- a/src/transaction/Logs.tsx +++ b/src/transaction/Logs.tsx @@ -1,82 +1,28 @@ -import React, { useContext, useMemo } from "react"; -import { Interface } from "@ethersproject/abi"; +import React from "react"; import ContentFrame from "../ContentFrame"; import LogEntry from "./LogEntry"; import { TransactionData } from "../types"; -import { Metadata } from "../sourcify/useSourcify"; -import { RuntimeContext } from "../useRuntime"; -import { useContractsMetadata } from "../hooks"; type LogsProps = { txData: TransactionData; - metadata: Metadata | null | undefined; }; -const Logs: React.FC = ({ txData, metadata }) => { - const baseMetadatas = useMemo((): Record => { - if (!txData.to || metadata === undefined) { - return {}; - } - - const md: Record = {}; - md[txData.to] = metadata; - return md; - }, [txData.to, metadata]); - - const logAddresses = useMemo( - () => txData.confirmedData?.logs.map((l) => l.address) ?? [], - [txData] - ); - const { provider } = useContext(RuntimeContext); - const metadatas = useContractsMetadata(logAddresses, provider, baseMetadatas); - - const logDescs = useMemo(() => { - if (!txData) { - return undefined; - } - - return txData.confirmedData?.logs.map((l) => { - const mt = metadatas[l.address]; - if (!mt) { - return mt; - } - - const abi = mt.output.abi; - const intf = new Interface(abi as any); - try { - return intf.parseLog({ - topics: l.topics, - data: l.data, - }); - } catch (err) { - console.warn("Couldn't find function signature", err); - return null; - } - }); - }, [metadatas, txData]); - - return ( - - {txData.confirmedData && ( - <> - {txData.confirmedData.logs.length > 0 ? ( - <> - {txData.confirmedData.logs.map((l, i) => ( - - ))} - - ) : ( -
Transaction didn't emit any logs
- )} - - )} -
- ); -}; +const Logs: React.FC = ({ txData }) => ( + + {txData.confirmedData && ( + <> + {txData.confirmedData.logs.length > 0 ? ( + <> + {txData.confirmedData.logs.map((l, i) => ( + + ))} + + ) : ( +
Transaction didn't emit any logs
+ )} + + )} +
+); export default React.memo(Logs); diff --git a/src/url.ts b/src/url.ts index 6a7a411..69d8397 100644 --- a/src/url.ts +++ b/src/url.ts @@ -53,6 +53,10 @@ const resolveSourcifySource = (source: SourcifySource) => { throw new Error(`Unknown Sourcify integration source code: ${source}`); }; +/** + * Builds a complete Sourcify metadata.json URL given the contract address + * and chain. + */ export const sourcifyMetadata = ( address: ChecksummedAddress, chainId: number, diff --git a/src/use4Bytes.ts b/src/use4Bytes.ts index 85f4e98..ddf8e2b 100644 --- a/src/use4Bytes.ts +++ b/src/use4Bytes.ts @@ -5,6 +5,7 @@ import { TransactionDescription, } from "@ethersproject/abi"; import { BigNumberish } from "@ethersproject/bignumber"; +import { Fetcher } from "swr"; import useSWRImmutable from "swr/immutable"; import { RuntimeContext } from "./useRuntime"; import { fourBytesURL } from "./url"; @@ -29,16 +30,32 @@ export const extract4Bytes = (rawInput: string): string | null => { return rawInput.slice(0, 10); }; -const fetch4Bytes = async ( - assetsURLPrefix: string, - fourBytes: string -): Promise => { - const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes); +type FourBytesKey = [id: "4bytes", fourBytes: string]; +type FourBytesFetcher = Fetcher< + FourBytesEntry | null | undefined, + FourBytesKey +>; + +const fourBytesFetcher = + (assetsURLPrefix: string): FourBytesFetcher => + async (_, key) => { + if (key === null || key === "0x") { + return undefined; + } + + // Handle simple transfers with invalid selector like tx: + // 0x8bcbdcc1589b5c34c1e55909c8269a411f0267a4fed59a73dd4348cc71addbb9, + // which contains 0x00 as data + if (key.length !== 10) { + return undefined; + } + + const fourBytes = key.slice(2); + const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes); - try { const res = await fetch(signatureURL); if (!res.ok) { - console.error(`Signature does not exist in 4bytes DB: ${fourBytes}`); + console.warn(`Signature does not exist in 4bytes DB: ${fourBytes}`); return null; } @@ -53,11 +70,7 @@ const fetch4Bytes = async ( signature: sig, }; return entry; - } catch (err) { - console.error(`Couldn't fetch signature URL ${signatureURL}`, err); - return null; - } -}; + }; /** * Extract 4bytes DB info @@ -75,26 +88,10 @@ export const use4Bytes = ( const { config } = useContext(RuntimeContext); const assetsURLPrefix = config?.assetsURLPrefix; + const fourBytesKey = assetsURLPrefix !== undefined ? rawFourBytes : null; - const fourBytesFetcher = (key: string | null) => { - if (key === null || key === "0x") { - return undefined; - } - - // Handle simple transfers with invalid selector like tx: - // 0x8bcbdcc1589b5c34c1e55909c8269a411f0267a4fed59a73dd4348cc71addbb9, - // which contains 0x00 as data - if (key.length !== 10) { - return undefined; - } - - return fetch4Bytes(assetsURLPrefix!, key.slice(2)); - }; - - const { data, error } = useSWRImmutable( - assetsURLPrefix !== undefined ? rawFourBytes : null, - fourBytesFetcher - ); + const fetcher = fourBytesFetcher(assetsURLPrefix!); + const { data, error } = useSWRImmutable(["4bytes", fourBytesKey], fetcher); return error ? undefined : data; }; diff --git a/src/useErigonHooks.ts b/src/useErigonHooks.ts index c949481..6382877 100644 --- a/src/useErigonHooks.ts +++ b/src/useErigonHooks.ts @@ -427,44 +427,6 @@ export const useTraceTransaction = ( return traceGroups; }; -const hasCode = async ( - provider: JsonRpcProvider, - address: ChecksummedAddress -): Promise => { - const result = await provider.send("ots_hasCode", [address, "latest"]); - return result as boolean; -}; - -export const useAddressesWithCode = ( - provider: JsonRpcProvider | undefined, - addresses: ChecksummedAddress[] -): ChecksummedAddress[] | undefined => { - const [results, setResults] = useState(); - - useEffect(() => { - // Reset - setResults(undefined); - - if (provider === undefined) { - return; - } - - const readCodes = async () => { - const checkers: Promise[] = []; - for (const a of addresses) { - checkers.push(hasCode(provider, a)); - } - - const result = await Promise.all(checkers); - const filtered = addresses.filter((_, i) => result[i]); - setResults(filtered); - }; - readCodes(); - }, [provider, addresses]); - - return results; -}; - // Error(string) const ERROR_MESSAGE_SELECTOR = "0x08c379a0"; @@ -582,7 +544,6 @@ export const prefetchTransactionBySenderAndNonce = ( } return getTransactionBySenderAndNonceFetcher(provider)(key); }); - // } }; export const useTransactionBySenderAndNonce = ( @@ -705,7 +666,6 @@ export const providerFetcher = const method = key[0]; const args = key.slice(1); const result = await provider.send(method, args); - // console.log(`providerFetcher: ${method} ${args} === ${result}`); return result; }; diff --git a/src/useTopic0.ts b/src/useTopic0.ts index adcef1e..2ebadf0 100644 --- a/src/useTopic0.ts +++ b/src/useTopic0.ts @@ -1,4 +1,5 @@ -import { useState, useEffect, useContext } from "react"; +import { useContext } from "react"; +import useSWRImmutable from "swr/immutable"; import { RuntimeContext } from "./useRuntime"; import { topic0URL } from "./url"; @@ -6,7 +7,28 @@ export type Topic0Entry = { signatures: string[] | undefined; }; -const fullCache = new Map(); +const topic0Fetcher = async ( + signatureURL: string +): Promise => { + try { + const res = await fetch(signatureURL); + if (!res.ok) { + console.error(`Signature does not exist in topic0 DB: ${signatureURL}`); + return null; + } + + // Get only the first occurrence, for now ignore alternative param names + const sig = await res.text(); + const sigs = sig.split(";"); + const entry: Topic0Entry = { + signatures: sigs, + }; + return entry; + } catch (err) { + console.error(`Couldn't fetch signature URL ${signatureURL}`, err); + return null; + } +}; /** * Extract topic0 DB info @@ -26,52 +48,11 @@ export const useTopic0 = ( const assetsURLPrefix = runtime.config?.assetsURLPrefix; const topic0 = rawTopic0.slice(2); - const [entry, setEntry] = useState( - fullCache.get(topic0) - ); - useEffect(() => { - if (assetsURLPrefix === undefined) { - return; - } - - const signatureURL = topic0URL(assetsURLPrefix, topic0); - fetch(signatureURL) - .then(async (res) => { - if (!res.ok) { - console.error(`Signature does not exist in topic0 DB: ${topic0}`); - fullCache.set(topic0, null); - setEntry(null); - return; - } - - // Get only the first occurrence, for now ignore alternative param names - const sig = await res.text(); - const sigs = sig.split(";"); - const entry: Topic0Entry = { - signatures: sigs, - }; - setEntry(entry); - fullCache.set(topic0, entry); - }) - .catch((err) => { - console.error(`Couldn't fetch signature URL ${signatureURL}`, err); - setEntry(null); - fullCache.set(topic0, null); - }); - }, [topic0, assetsURLPrefix]); - - if (assetsURLPrefix === undefined) { - return undefined; + const signatureURL = () => + assetsURLPrefix === undefined ? null : topic0URL(assetsURLPrefix, topic0); + const { data, error } = useSWRImmutable(signatureURL, topic0Fetcher); + if (error) { + return null; } - - // Try to resolve topic0 name - if (entry === null || entry === undefined) { - return entry; - } - - // Simulates LRU - // TODO: implement LRU purging - fullCache.delete(topic0); - fullCache.set(topic0, entry); - return entry; + return data; };