diff --git a/src/AddressTransactions.tsx b/src/AddressTransactions.tsx index 53f92bf..19d41bb 100644 --- a/src/AddressTransactions.tsx +++ b/src/AddressTransactions.tsx @@ -26,7 +26,7 @@ import PendingResults from "./search/PendingResults"; import TransactionItem from "./search/TransactionItem"; import { SearchController } from "./search/search"; import { RuntimeContext } from "./useRuntime"; -import { useENSCache } from "./useReverseCache"; +import { pageCollector, useResolvedAddresses } from "./useResolvedAddresses"; import { useFeeToggler } from "./search/useFeeToggler"; import { SelectionContext, useSelection } from "./useSelection"; import { useMultipleETHUSDOracle } from "./usePriceOracle"; @@ -165,7 +165,8 @@ const AddressTransactions: React.FC = () => { }, [provider, checksummedAddress, params.direction, hash, controller]); const page = useMemo(() => controller?.getPage(), [controller]); - const reverseCache = useENSCache(provider, page); + const addrCollector = useMemo(() => pageCollector(page), [page]); + const resolvedAddresses = useResolvedAddresses(provider, addrCollector); const blockTags: BlockTag[] = useMemo(() => { if (!page) { @@ -277,13 +278,13 @@ const AddressTransactions: React.FC = () => { feeDisplay={feeDisplay} feeDisplayToggler={feeDisplayToggler} /> - {controller ? ( + {page ? ( - {controller.getPage().map((tx) => ( + {page.map((tx) => ( @@ -36,6 +40,11 @@ const Transaction: React.FC = () => { const { txhash } = params; const txData = useTxData(provider, txhash); + const addrCollector = useMemo( + () => transactionDataCollector(txData), + [txData] + ); + const resolvedAddresses = useResolvedAddresses(provider, addrCollector); const internalOps = useInternalOperations(provider, txData); const sendsEthToMiner = useMemo(() => { @@ -100,10 +109,15 @@ const Transaction: React.FC = () => { internalOps={internalOps} sendsEthToMiner={sendsEthToMiner} ethUSDPrice={blockETHUSDPrice} + resolvedAddresses={resolvedAddresses} /> - + diff --git a/src/api/address-resolver/CompositeAddressResolver.ts b/src/api/address-resolver/CompositeAddressResolver.ts new file mode 100644 index 0000000..bee73da --- /dev/null +++ b/src/api/address-resolver/CompositeAddressResolver.ts @@ -0,0 +1,26 @@ +import { BaseProvider } from "@ethersproject/providers"; +import { IAddressResolver } from "./address-resolver"; + +export class CompositeAddressResolver implements IAddressResolver { + private resolvers: IAddressResolver[] = []; + + addResolver(resolver: IAddressResolver) { + this.resolvers.push(resolver); + } + + async resolveAddress( + provider: BaseProvider, + address: string + ): Promise { + for (const r of this.resolvers) { + const name = r.resolveAddress(provider, address); + if (name !== undefined) { + return name; + } + } + + return undefined; + // TODO: fallback to address itself + // return address; + } +} diff --git a/src/api/address-resolver/ENSAddressResolver.ts b/src/api/address-resolver/ENSAddressResolver.ts new file mode 100644 index 0000000..ef45a64 --- /dev/null +++ b/src/api/address-resolver/ENSAddressResolver.ts @@ -0,0 +1,15 @@ +import { BaseProvider } from "@ethersproject/providers"; +import { IAddressResolver } from "./address-resolver"; + +export class ENSAddressResolver implements IAddressResolver { + async resolveAddress( + provider: BaseProvider, + address: string + ): Promise { + const name = await provider.lookupAddress(address); + if (name === null) { + return undefined; + } + return name; + } +} diff --git a/src/api/address-resolver/address-resolver.ts b/src/api/address-resolver/address-resolver.ts new file mode 100644 index 0000000..246db34 --- /dev/null +++ b/src/api/address-resolver/address-resolver.ts @@ -0,0 +1,8 @@ +import { BaseProvider } from "@ethersproject/providers"; + +export interface IAddressResolver { + resolveAddress( + provider: BaseProvider, + address: string + ): Promise; +} diff --git a/src/api/address-resolver/index.ts b/src/api/address-resolver/index.ts new file mode 100644 index 0000000..cc4fe1b --- /dev/null +++ b/src/api/address-resolver/index.ts @@ -0,0 +1,34 @@ +import { BaseProvider } from "@ethersproject/providers"; +import { IAddressResolver } from "./address-resolver"; +import { CompositeAddressResolver } from "./CompositeAddressResolver"; +import { ENSAddressResolver } from "./ENSAddressResolver"; + +export type ResolvedAddresses = Record; + +// Create and configure the main resolver +const _mainResolver = new CompositeAddressResolver(); +_mainResolver.addResolver(new ENSAddressResolver()); + +export const mainResolver: IAddressResolver = _mainResolver; + +export const batchPopulate = async ( + provider: BaseProvider, + addresses: string[] +): Promise => { + const solvers: Promise[] = []; + for (const a of addresses) { + solvers.push(mainResolver.resolveAddress(provider, a)); + } + + const results = await Promise.all(solvers); + const cache: ResolvedAddresses = {}; + for (let i = 0; i < results.length; i++) { + const r = results[i]; + if (r === undefined) { + continue; + } + cache[addresses[i]] = r; + } + + return cache; +}; diff --git a/src/block/BlockTransactionResults.tsx b/src/block/BlockTransactionResults.tsx index b911013..f9cfc27 100644 --- a/src/block/BlockTransactionResults.tsx +++ b/src/block/BlockTransactionResults.tsx @@ -8,7 +8,7 @@ import TransactionItem from "../search/TransactionItem"; import { useFeeToggler } from "../search/useFeeToggler"; import { RuntimeContext } from "../useRuntime"; import { SelectionContext, useSelection } from "../useSelection"; -import { useENSCache } from "../useReverseCache"; +import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses"; import { ProcessedTransaction } from "../types"; import { PAGE_SIZE } from "../params"; import { useMultipleETHUSDOracle } from "../usePriceOracle"; @@ -29,7 +29,8 @@ const BlockTransactionResults: React.FC = ({ const selectionCtx = useSelection(); const [feeDisplay, feeDisplayToggler] = useFeeToggler(); const { provider } = useContext(RuntimeContext); - const reverseCache = useENSCache(provider, page); + const addrCollector = useMemo(() => pageCollector(page), [page]); + const resolvedAddresses = useResolvedAddresses(provider, addrCollector); const blockTags = useMemo(() => [blockTag], [blockTag]); const priceMap = useMultipleETHUSDOracle(provider, blockTags); @@ -59,7 +60,7 @@ const BlockTransactionResults: React.FC = ({ diff --git a/src/components/Address.tsx b/src/components/Address.tsx index d7694e5..478f4f6 100644 --- a/src/components/Address.tsx +++ b/src/components/Address.tsx @@ -6,8 +6,8 @@ type AddressProps = { const Address: React.FC = ({ address }) => ( - {address} + {address} ); -export default React.memo(Address); +export default Address; diff --git a/src/components/AddressLink.tsx b/src/components/AddressLink.tsx index a8fd5c3..8353457 100644 --- a/src/components/AddressLink.tsx +++ b/src/components/AddressLink.tsx @@ -17,11 +17,10 @@ const AddressLink: React.FC = ({ dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover" } font-address truncate`} to={`/address/${address}`} + title={text ?? address} > - - {text ?? address} - + {text ?? address} ); -export default React.memo(AddressLink); +export default AddressLink; diff --git a/src/components/AddressOrENSName.tsx b/src/components/AddressOrENSName.tsx index 9cb2f9e..23ba6cd 100644 --- a/src/components/AddressOrENSName.tsx +++ b/src/components/AddressOrENSName.tsx @@ -3,49 +3,53 @@ import Address from "./Address"; import AddressLink from "./AddressLink"; import ENSName from "./ENSName"; import ENSNameLink from "./ENSNameLink"; +import { ResolvedAddresses } from "../api/address-resolver"; type AddressOrENSNameProps = { address: string; - ensName?: string; selectedAddress?: string; text?: string; dontOverrideColors?: boolean; + resolvedAddresses?: ResolvedAddresses | undefined; }; const AddressOrENSName: React.FC = ({ address, - ensName, selectedAddress, text, dontOverrideColors, -}) => ( - <> - {address === selectedAddress ? ( - <> - {ensName ? ( - - ) : ( -
- )} - - ) : ( - <> - {ensName ? ( - - ) : ( - - )} - - )} - -); + resolvedAddresses, +}) => { + const name = resolvedAddresses?.[address]; + return ( + <> + {address === selectedAddress ? ( + <> + {name ? ( + + ) : ( +
+ )} + + ) : ( + <> + {name ? ( + + ) : ( + + )} + + )} + + ); +}; -export default React.memo(AddressOrENSName); +export default AddressOrENSName; diff --git a/src/components/DecoratedAddressLink.tsx b/src/components/DecoratedAddressLink.tsx index 0b38fab..ee88b31 100644 --- a/src/components/DecoratedAddressLink.tsx +++ b/src/components/DecoratedAddressLink.tsx @@ -8,10 +8,10 @@ import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins"; import TokenLogo from "./TokenLogo"; import AddressOrENSName from "./AddressOrENSName"; import { AddressContext, TokenMeta, ZERO_ADDRESS } from "../types"; +import { ResolvedAddresses } from "../api/address-resolver"; type DecoratedAddressLinkProps = { address: string; - ensName?: string; selectedAddress?: string; text?: string; addressCtx?: AddressContext; @@ -21,11 +21,11 @@ type DecoratedAddressLinkProps = { txFrom?: boolean; txTo?: boolean; tokenMeta?: TokenMeta; + resolvedAddresses?: ResolvedAddresses | undefined; }; -const DecoratedAddresssLink: React.FC = ({ +const DecoratedAddressLink: React.FC = ({ address, - ensName, selectedAddress, text, addressCtx, @@ -35,6 +35,7 @@ const DecoratedAddresssLink: React.FC = ({ txFrom, txTo, tokenMeta, + resolvedAddresses, }) => { const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS; const burn = addressCtx === AddressContext.TO && address === ZERO_ADDRESS; @@ -81,13 +82,13 @@ const DecoratedAddresssLink: React.FC = ({ )} ); }; -export default React.memo(DecoratedAddresssLink); +export default React.memo(DecoratedAddressLink); diff --git a/src/components/ENSName.tsx b/src/components/ENSName.tsx index 7bc8032..95f1e1f 100644 --- a/src/components/ENSName.tsx +++ b/src/components/ENSName.tsx @@ -22,4 +22,4 @@ const ENSName: React.FC = ({ name, address }) => ( ); -export default React.memo(ENSName); +export default ENSName; diff --git a/src/components/ENSNameLink.tsx b/src/components/ENSNameLink.tsx index c1da646..ad5df4d 100644 --- a/src/components/ENSNameLink.tsx +++ b/src/components/ENSNameLink.tsx @@ -31,4 +31,4 @@ const ENSNameLink: React.FC = ({ ); -export default React.memo(ENSNameLink); +export default ENSNameLink; diff --git a/src/search/TransactionItem.tsx b/src/search/TransactionItem.tsx index 76e58e7..2e6132d 100644 --- a/src/search/TransactionItem.tsx +++ b/src/search/TransactionItem.tsx @@ -14,14 +14,15 @@ import TransactionDirection, { Flags, } from "../components/TransactionDirection"; import TransactionValue from "../components/TransactionValue"; -import { ENSReverseCache, ProcessedTransaction } from "../types"; +import { ProcessedTransaction } from "../types"; import { FeeDisplay } from "./useFeeToggler"; import { formatValue } from "../components/formatter"; import ETH2USDValue from "../components/ETH2USDValue"; +import { ResolvedAddresses } from "../api/address-resolver"; type TransactionItemProps = { tx: ProcessedTransaction; - ensCache?: ENSReverseCache; + resolvedAddresses?: ResolvedAddresses; selectedAddress?: string; feeDisplay: FeeDisplay; priceMap: Record; @@ -29,7 +30,7 @@ type TransactionItemProps = { const TransactionItem: React.FC = ({ tx, - ensCache, + resolvedAddresses, selectedAddress, feeDisplay, priceMap, @@ -50,12 +51,6 @@ const TransactionItem: React.FC = ({ } } - const ensFrom = ensCache && tx.from && ensCache[tx.from]; - const ensTo = ensCache && tx.to && ensCache[tx.to]; - const ensCreated = - ensCache && - tx.createdContractAddress && - ensCache[tx.createdContractAddress]; const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction; return ( @@ -87,9 +82,9 @@ const TransactionItem: React.FC = ({ )} @@ -107,18 +102,18 @@ const TransactionItem: React.FC = ({ ) : ( )} @@ -144,4 +139,4 @@ const TransactionItem: React.FC = ({ ); }; -export default React.memo(TransactionItem); +export default TransactionItem; diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index 4746cc5..72f448a 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -38,6 +38,7 @@ import ModeTab from "../components/ModeTab"; import DecodedParamsTable from "./decoder/DecodedParamsTable"; import { rawInputTo4Bytes, use4Bytes } from "../use4Bytes"; import { DevDoc, UserDoc } from "../useSourcify"; +import { ResolvedAddresses } from "../api/address-resolver"; type DetailsProps = { txData: TransactionData; @@ -47,6 +48,7 @@ type DetailsProps = { internalOps?: InternalOperation[]; sendsEthToMiner: boolean; ethUSDPrice: BigNumber | undefined; + resolvedAddresses: ResolvedAddresses | undefined; }; const Details: React.FC = ({ @@ -57,6 +59,7 @@ const Details: React.FC = ({ internalOps, sendsEthToMiner, ethUSDPrice, + resolvedAddresses, }) => { const hasEIP1559 = txData.confirmedData?.blockBaseFeePerGas !== undefined && @@ -154,6 +157,7 @@ const Details: React.FC = ({ address={txData.from} miner={txData.from === txData.confirmedData?.miner} txFrom + resolvedAddresses={resolvedAddresses} /> @@ -171,6 +175,7 @@ const Details: React.FC = ({ address={txData.to} miner={txData.to === txData.confirmedData?.miner} txTo + resolvedAddresses={resolvedAddresses} /> @@ -188,6 +193,7 @@ const Details: React.FC = ({ address={txData.confirmedData.createdContractAddress!} creation txTo + resolvedAddresses={resolvedAddresses} /> diff --git a/src/transaction/LogEntry.tsx b/src/transaction/LogEntry.tsx index 23ab883..2993e16 100644 --- a/src/transaction/LogEntry.tsx +++ b/src/transaction/LogEntry.tsx @@ -10,14 +10,21 @@ import DecodedParamsTable from "./decoder/DecodedParamsTable"; import DecodedLogSignature from "./decoder/DecodedLogSignature"; import { TransactionData } from "../types"; import { useTopic0 } from "../useTopic0"; +import { ResolvedAddresses } from "../api/address-resolver"; type LogEntryProps = { txData: TransactionData; log: Log; logDesc: LogDescription | null | undefined; + resolvedAddresses: ResolvedAddresses | undefined; }; -const LogEntry: React.FC = ({ txData, log, logDesc }) => { +const LogEntry: React.FC = ({ + txData, + log, + logDesc, + resolvedAddresses, +}) => { const rawTopic0 = log.topics[0]; const topic0 = useTopic0(rawTopic0); @@ -62,6 +69,7 @@ const LogEntry: React.FC = ({ txData, log, logDesc }) => { miner={log.address === txData.confirmedData?.miner} txFrom={log.address === txData.from} txTo={log.address === txData.to} + resolvedAddresses={resolvedAddresses} /> diff --git a/src/transaction/Logs.tsx b/src/transaction/Logs.tsx index 45fe491..1fd4207 100644 --- a/src/transaction/Logs.tsx +++ b/src/transaction/Logs.tsx @@ -5,13 +5,15 @@ import LogEntry from "./LogEntry"; import { TransactionData } from "../types"; import { useAppConfigContext } from "../useAppConfig"; import { Metadata, useMultipleMetadata } from "../useSourcify"; +import { ResolvedAddresses } from "../api/address-resolver"; type LogsProps = { txData: TransactionData; metadata: Metadata | null | undefined; + resolvedAddresses: ResolvedAddresses | undefined; }; -const Logs: React.FC = ({ txData, metadata }) => { +const Logs: React.FC = ({ txData, metadata, resolvedAddresses }) => { const baseMetadatas = useMemo((): Record => { if (!txData.to || metadata === undefined) { return {}; @@ -70,6 +72,7 @@ const Logs: React.FC = ({ txData, metadata }) => { txData={txData} log={l} logDesc={logDescs?.[i]} + resolvedAddresses={resolvedAddresses} /> ))} diff --git a/src/types.ts b/src/types.ts index 5904b31..12763bb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,10 +32,6 @@ export type TransactionChunk = { lastPage: boolean; }; -export type ENSReverseCache = { - [address: string]: string; -}; - export type TransactionData = { transactionHash: string; from: string; diff --git a/src/useResolvedAddresses.ts b/src/useResolvedAddresses.ts new file mode 100644 index 0000000..74042e5 --- /dev/null +++ b/src/useResolvedAddresses.ts @@ -0,0 +1,84 @@ +import { useState, useEffect } from "react"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { ProcessedTransaction, TransactionData } from "./types"; +import { batchPopulate, ResolvedAddresses } from "./api/address-resolver"; + +export type AddressCollector = () => string[]; + +export const pageCollector = + (page: ProcessedTransaction[] | undefined): AddressCollector => + () => { + if (!page) { + return []; + } + + const uniqueAddresses = new Set(); + for (const tx of page) { + if (tx.from) { + uniqueAddresses.add(tx.from); + } + if (tx.to) { + uniqueAddresses.add(tx.to); + } + } + + return Array.from(uniqueAddresses); + }; + +export const transactionDataCollector = + (txData: TransactionData | null | undefined): AddressCollector => + () => { + if (!txData) { + return []; + } + + const uniqueAddresses = new Set(); + + // Standard fields + uniqueAddresses.add(txData.from); + if (txData.to) { + uniqueAddresses.add(txData.to); + } + if (txData.confirmedData?.createdContractAddress) { + uniqueAddresses.add(txData.confirmedData?.createdContractAddress); + } + + // Dig token transfers + for (const t of txData.tokenTransfers) { + uniqueAddresses.add(t.from); + uniqueAddresses.add(t.to); + uniqueAddresses.add(t.token); + } + + // Dig log addresses + if (txData.confirmedData) { + for (const l of txData.confirmedData.logs) { + uniqueAddresses.add(l.address); + // TODO: find a way to dig over decoded address log attributes + } + } + + return Array.from(uniqueAddresses); + }; + +export const useResolvedAddresses = ( + provider: JsonRpcProvider | undefined, + addrCollector: AddressCollector +) => { + const [names, setNames] = useState(); + + useEffect(() => { + if (!provider) { + return; + } + + const populate = async () => { + const _addresses = addrCollector(); + const _names = await batchPopulate(provider, _addresses); + setNames(_names); + }; + populate(); + }, [provider, addrCollector]); + + return names; +}; diff --git a/src/useReverseCache.ts b/src/useReverseCache.ts deleted file mode 100644 index 365888a..0000000 --- a/src/useReverseCache.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useState, useEffect } from "react"; -import { JsonRpcProvider } from "@ethersproject/providers"; -import { ENSReverseCache, ProcessedTransaction } from "./types"; - -export const useENSCache = ( - provider?: JsonRpcProvider, - page?: ProcessedTransaction[] -) => { - const [reverseCache, setReverseCache] = useState(); - - useEffect(() => { - if (!provider || !page) { - return; - } - - const addrSet = new Set(); - for (const tx of page) { - if (tx.from) { - addrSet.add(tx.from); - } - if (tx.to) { - addrSet.add(tx.to); - } - } - const addresses = Array.from(addrSet); - - const reverseResolve = async () => { - const solvers: Promise[] = []; - for (const a of addresses) { - solvers.push(provider.lookupAddress(a)); - } - - const results = await Promise.all(solvers); - const cache: ENSReverseCache = {}; - for (let i = 0; i < results.length; i++) { - const r = results[i]; - if (r === null) { - continue; - } - cache[addresses[i]] = r; - } - setReverseCache(cache); - }; - reverseResolve(); - }, [provider, page]); - - return reverseCache; -};