diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index 083f7f0..03fa198 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -11,11 +11,13 @@ import { TokenTransfer, TransactionData, } from "./types"; +import { ResolvedAddresses } from "./api/address-resolver"; type TokenTransferItemProps = { t: TokenTransfer; txData: TransactionData; tokenMeta?: TokenMeta | undefined; + resolvedAddresses: ResolvedAddresses | undefined; }; // TODO: handle partial @@ -23,6 +25,7 @@ const TokenTransferItem: React.FC = ({ t, txData, tokenMeta, + resolvedAddresses, }) => (
@@ -64,10 +67,7 @@ const TokenTransferItem: React.FC = ({
diff --git a/src/api/address-resolver/CompositeAddressResolver.ts b/src/api/address-resolver/CompositeAddressResolver.ts index bee73da..55a7318 100644 --- a/src/api/address-resolver/CompositeAddressResolver.ts +++ b/src/api/address-resolver/CompositeAddressResolver.ts @@ -1,26 +1,28 @@ import { BaseProvider } from "@ethersproject/providers"; import { IAddressResolver } from "./address-resolver"; -export class CompositeAddressResolver implements IAddressResolver { - private resolvers: IAddressResolver[] = []; +export type SelectedResolvedName = [IAddressResolver, T] | null; - addResolver(resolver: IAddressResolver) { +export class CompositeAddressResolver + implements IAddressResolver> +{ + private resolvers: IAddressResolver[] = []; + + addResolver(resolver: IAddressResolver) { this.resolvers.push(resolver); } async resolveAddress( provider: BaseProvider, address: string - ): Promise { + ): Promise | undefined> { for (const r of this.resolvers) { - const name = r.resolveAddress(provider, address); - if (name !== undefined) { - return name; + const resolvedAddress = await r.resolveAddress(provider, address); + if (resolvedAddress !== undefined) { + return [r, resolvedAddress]; } } - return undefined; - // TODO: fallback to address itself - // return address; + return null; } } diff --git a/src/api/address-resolver/ENSAddressResolver.ts b/src/api/address-resolver/ENSAddressResolver.ts index ef45a64..820a51f 100644 --- a/src/api/address-resolver/ENSAddressResolver.ts +++ b/src/api/address-resolver/ENSAddressResolver.ts @@ -1,7 +1,7 @@ import { BaseProvider } from "@ethersproject/providers"; import { IAddressResolver } from "./address-resolver"; -export class ENSAddressResolver implements IAddressResolver { +export class ENSAddressResolver implements IAddressResolver { async resolveAddress( provider: BaseProvider, address: string diff --git a/src/api/address-resolver/ERCTokenResolver.ts b/src/api/address-resolver/ERCTokenResolver.ts new file mode 100644 index 0000000..02625dc --- /dev/null +++ b/src/api/address-resolver/ERCTokenResolver.ts @@ -0,0 +1,30 @@ +import { BaseProvider } from "@ethersproject/providers"; +import { Contract } from "@ethersproject/contracts"; +import { IAddressResolver } from "./address-resolver"; +import erc20 from "../../erc20.json"; +import { TokenMeta } from "../../types"; + +export class ERCTokenResolver implements IAddressResolver { + async resolveAddress( + provider: BaseProvider, + address: string + ): Promise { + const erc20Contract = new Contract(address, erc20, provider); + try { + const [name, symbol, decimals] = await Promise.all([ + erc20Contract.name(), + erc20Contract.symbol(), + erc20Contract.decimals(), + ]); + return { + name, + symbol, + decimals, + }; + } catch (err) { + // Ignore on purpose; this indicates the probe failed and the address + // is not a token + } + return undefined; + } +} diff --git a/src/api/address-resolver/address-resolver.ts b/src/api/address-resolver/address-resolver.ts index 246db34..12d289e 100644 --- a/src/api/address-resolver/address-resolver.ts +++ b/src/api/address-resolver/address-resolver.ts @@ -1,8 +1,16 @@ +import React from "react"; import { BaseProvider } from "@ethersproject/providers"; -export interface IAddressResolver { +export interface IAddressResolver { resolveAddress( provider: BaseProvider, address: string - ): Promise; + ): Promise; } + +export type ResolvedAddressRenderer = ( + address: string, + resolvedAddress: T, + linkable: boolean, + dontOverrideColors: boolean +) => React.ReactElement; diff --git a/src/api/address-resolver/index.ts b/src/api/address-resolver/index.ts index cc4fe1b..dc72a9f 100644 --- a/src/api/address-resolver/index.ts +++ b/src/api/address-resolver/index.ts @@ -1,34 +1,57 @@ import { BaseProvider } from "@ethersproject/providers"; -import { IAddressResolver } from "./address-resolver"; -import { CompositeAddressResolver } from "./CompositeAddressResolver"; +import { ensRenderer } from "../../components/ENSName"; +import { tokenRenderer } from "../../components/TokenName"; +import { IAddressResolver, ResolvedAddressRenderer } from "./address-resolver"; +import { + CompositeAddressResolver, + SelectedResolvedName, +} from "./CompositeAddressResolver"; import { ENSAddressResolver } from "./ENSAddressResolver"; +import { ERCTokenResolver } from "./ERCTokenResolver"; -export type ResolvedAddresses = Record; +export type ResolvedAddresses = Record>; // Create and configure the main resolver +export const ensResolver = new ENSAddressResolver(); +export const ercTokenResolver = new ERCTokenResolver(); + const _mainResolver = new CompositeAddressResolver(); -_mainResolver.addResolver(new ENSAddressResolver()); +_mainResolver.addResolver(ensResolver); +_mainResolver.addResolver(ercTokenResolver); -export const mainResolver: IAddressResolver = _mainResolver; +export const mainResolver: IAddressResolver> = + _mainResolver; +export const resolverRendererRegistry = new Map< + IAddressResolver, + ResolvedAddressRenderer +>(); +resolverRendererRegistry.set(ensResolver, ensRenderer); +resolverRendererRegistry.set(ercTokenResolver, tokenRenderer); + +// TODO: implement progressive resolving export const batchPopulate = async ( provider: BaseProvider, - addresses: string[] + addresses: string[], + currentMap: ResolvedAddresses | undefined ): Promise => { - const solvers: Promise[] = []; - for (const a of addresses) { + const solvers: Promise | undefined>[] = []; + const unresolvedAddresses = addresses.filter( + (a) => currentMap?.[a] === undefined + ); + for (const a of unresolvedAddresses) { solvers.push(mainResolver.resolveAddress(provider, a)); } + const resultMap: ResolvedAddresses = currentMap ? { ...currentMap } : {}; 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; + resultMap[unresolvedAddresses[i]] = r; } - return cache; + return resultMap; }; diff --git a/src/components/Address.tsx b/src/components/Address.tsx deleted file mode 100644 index 478f4f6..0000000 --- a/src/components/Address.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; - -type AddressProps = { - address: string; -}; - -const Address: React.FC = ({ address }) => ( - - {address} - -); - -export default Address; diff --git a/src/components/AddressLink.tsx b/src/components/AddressLink.tsx deleted file mode 100644 index 8353457..0000000 --- a/src/components/AddressLink.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; -import { NavLink } from "react-router-dom"; - -type AddressLinkProps = { - address: string; - text?: string; - dontOverrideColors?: boolean; -}; - -const AddressLink: React.FC = ({ - address, - text, - dontOverrideColors, -}) => ( - - {text ?? address} - -); - -export default AddressLink; diff --git a/src/components/AddressOrENSName.tsx b/src/components/AddressOrENSName.tsx index 23ba6cd..62ee289 100644 --- a/src/components/AddressOrENSName.tsx +++ b/src/components/AddressOrENSName.tsx @@ -1,14 +1,13 @@ import React from "react"; -import Address from "./Address"; -import AddressLink from "./AddressLink"; -import ENSName from "./ENSName"; -import ENSNameLink from "./ENSNameLink"; -import { ResolvedAddresses } from "../api/address-resolver"; +import { + ResolvedAddresses, + resolverRendererRegistry, +} from "../api/address-resolver"; +import PlainAddress from "./PlainAddress"; type AddressOrENSNameProps = { address: string; selectedAddress?: string; - text?: string; dontOverrideColors?: boolean; resolvedAddresses?: ResolvedAddresses | undefined; }; @@ -16,40 +15,35 @@ type AddressOrENSNameProps = { const AddressOrENSName: React.FC = ({ address, selectedAddress, - text, dontOverrideColors, resolvedAddresses, }) => { - const name = resolvedAddresses?.[address]; - return ( - <> - {address === selectedAddress ? ( - <> - {name ? ( - - ) : ( -
- )} - - ) : ( - <> - {name ? ( - - ) : ( - - )} - - )} - - ); + const resolvedAddress = resolvedAddresses?.[address]; + const linkable = address !== selectedAddress; + + if (!resolvedAddress) { + return ( + + ); + } + + const [resolver, resolvedName] = resolvedAddress; + const renderer = resolverRendererRegistry.get(resolver); + if (renderer === undefined) { + return ( + + ); + } + + return renderer(address, resolvedName, linkable, !!dontOverrideColors); }; export default AddressOrENSName; diff --git a/src/components/DecoratedAddressLink.tsx b/src/components/DecoratedAddressLink.tsx index ee88b31..cadc77f 100644 --- a/src/components/DecoratedAddressLink.tsx +++ b/src/components/DecoratedAddressLink.tsx @@ -5,36 +5,31 @@ 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 TokenLogo from "./TokenLogo"; import AddressOrENSName from "./AddressOrENSName"; -import { AddressContext, TokenMeta, ZERO_ADDRESS } from "../types"; +import { AddressContext, ZERO_ADDRESS } from "../types"; import { ResolvedAddresses } from "../api/address-resolver"; type DecoratedAddressLinkProps = { address: string; selectedAddress?: string; - text?: string; addressCtx?: AddressContext; creation?: boolean; miner?: boolean; selfDestruct?: boolean; txFrom?: boolean; txTo?: boolean; - tokenMeta?: TokenMeta; resolvedAddresses?: ResolvedAddresses | undefined; }; const DecoratedAddressLink: React.FC = ({ address, selectedAddress, - text, addressCtx, creation, miner, selfDestruct, txFrom, txTo, - tokenMeta, resolvedAddresses, }) => { const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS; @@ -75,15 +70,9 @@ const DecoratedAddressLink: React.FC = ({ )} - {tokenMeta && ( -
- -
- )} diff --git a/src/components/ENSName.tsx b/src/components/ENSName.tsx index 95f1e1f..f12f416 100644 --- a/src/components/ENSName.tsx +++ b/src/components/ENSName.tsx @@ -1,25 +1,75 @@ import React from "react"; +import { NavLink } from "react-router-dom"; +import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver"; import ENSLogo from "./ensLogo.svg"; type ENSNameProps = { name: string; address: string; + linkable: boolean; + dontOverrideColors?: boolean; }; -const ENSName: React.FC = ({ name, address }) => ( -
+const ENSName: React.FC = ({ + name, + address, + linkable, + dontOverrideColors, +}) => { + if (linkable) { + return ( + + + + ); + } + + return ( +
+ +
+ ); +}; + +type ContentProps = { + linkable: boolean; + name: string; +}; + +const Content: React.FC = ({ linkable, name }) => ( + <> ENS Logo {name} -
+ +); + +export const ensRenderer: ResolvedAddressRenderer = ( + address, + resolvedAddress, + linkable, + dontOverrideColors +) => ( + ); export default ENSName; diff --git a/src/components/ENSNameLink.tsx b/src/components/ENSNameLink.tsx deleted file mode 100644 index ad5df4d..0000000 --- a/src/components/ENSNameLink.tsx +++ /dev/null @@ -1,34 +0,0 @@ -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 = ({ - name, - address, - dontOverrideColors, -}) => ( - - ENS Logo - {name} - -); - -export default ENSNameLink; diff --git a/src/components/InternalTransactionOperation.tsx b/src/components/InternalTransactionOperation.tsx index 095162f..7de3bb2 100644 --- a/src/components/InternalTransactionOperation.tsx +++ b/src/components/InternalTransactionOperation.tsx @@ -3,17 +3,23 @@ import InternalTransfer from "./InternalTransfer"; import InternalSelfDestruct from "./InternalSelfDestruct"; import InternalCreate from "./InternalCreate"; import { TransactionData, InternalOperation, OperationType } from "../types"; +import { ResolvedAddresses } from "../api/address-resolver"; type InternalTransactionOperationProps = { txData: TransactionData; internalOp: InternalOperation; + resolvedAddresses: ResolvedAddresses | undefined; }; const InternalTransactionOperation: React.FC = - ({ txData, internalOp }) => ( + ({ txData, internalOp, resolvedAddresses }) => ( <> {internalOp.type === OperationType.TRANSFER && ( - + )} {internalOp.type === OperationType.SELF_DESTRUCT && ( diff --git a/src/components/InternalTransfer.tsx b/src/components/InternalTransfer.tsx index dc53d05..2958452 100644 --- a/src/components/InternalTransfer.tsx +++ b/src/components/InternalTransfer.tsx @@ -5,15 +5,18 @@ import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import AddressHighlighter from "./AddressHighlighter"; import DecoratedAddressLink from "./DecoratedAddressLink"; import { TransactionData, InternalOperation } from "../types"; +import { ResolvedAddresses } from "../api/address-resolver"; type InternalTransferProps = { txData: TransactionData; internalOp: InternalOperation; + resolvedAddresses: ResolvedAddresses | undefined; }; const InternalTransfer: React.FC = ({ txData, internalOp, + resolvedAddresses, }) => { const fromMiner = txData.confirmedData?.miner !== undefined && diff --git a/src/components/PlainAddress.tsx b/src/components/PlainAddress.tsx new file mode 100644 index 0000000..fbc37ca --- /dev/null +++ b/src/components/PlainAddress.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; + +type PlainAddressProps = { + address: string; + linkable: boolean; + dontOverrideColors: boolean | undefined; +}; + +const PlainAddress: React.FC = ({ + address, + linkable, + dontOverrideColors, +}) => { + if (linkable) { + return ( + + {address} + + ); + } + + return ( + + {address} + + ); +}; + +export default PlainAddress; diff --git a/src/components/TokenLogo.tsx b/src/components/TokenLogo.tsx index c6f5b8b..8568a0b 100644 --- a/src/components/TokenLogo.tsx +++ b/src/components/TokenLogo.tsx @@ -9,7 +9,7 @@ type TokenLogoProps = { }; const TokenLogo: React.FC = (props) => ( - }> + ); diff --git a/src/components/TokenName.tsx b/src/components/TokenName.tsx new file mode 100644 index 0000000..5c3e91b --- /dev/null +++ b/src/components/TokenName.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; +import TokenLogo from "./TokenLogo"; +import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver"; +import { TokenMeta } from "../types"; + +type TokenNameProps = { + address: string; + name: string; + symbol: string; + linkable: boolean; + dontOverrideColors?: boolean; +}; + +const TokenName: React.FC = ({ + address, + name, + symbol, + linkable, + dontOverrideColors, +}) => { + if (linkable) { + return ( + + + + ); + } + + return ( +
+ +
+ ); +}; + +type ContentProps = { + address: string; + name: string; + symbol: string; + linkable: boolean; +}; + +const Content: React.FC = ({ + address, + name, + symbol, + linkable, +}) => ( + <> +
+ +
+ + {name} ({symbol}) + + +); + +export const tokenRenderer: ResolvedAddressRenderer = ( + address, + tokenMeta, + linkable, + dontOverrideColors +) => ( + +); + +export default TokenName; diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index 72f448a..9c9e29d 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -206,6 +206,7 @@ const Details: React.FC = ({ key={i} txData={txData} internalOp={op} + resolvedAddresses={resolvedAddresses} /> ))} @@ -225,6 +226,7 @@ const Details: React.FC = ({ t={t} txData={txData} tokenMeta={txData.tokenMetas[t.token]} + resolvedAddresses={resolvedAddresses} /> ))} diff --git a/src/useResolvedAddresses.ts b/src/useResolvedAddresses.ts index 74042e5..1936da0 100644 --- a/src/useResolvedAddresses.ts +++ b/src/useResolvedAddresses.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { JsonRpcProvider } from "@ethersproject/providers"; import { ProcessedTransaction, TransactionData } from "./types"; import { batchPopulate, ResolvedAddresses } from "./api/address-resolver"; @@ -66,19 +66,27 @@ export const useResolvedAddresses = ( addrCollector: AddressCollector ) => { const [names, setNames] = useState(); - + const ref = useRef(); useEffect(() => { - if (!provider) { - return; - } + ref.current = names; + }); - const populate = async () => { - const _addresses = addrCollector(); - const _names = await batchPopulate(provider, _addresses); - setNames(_names); - }; - populate(); - }, [provider, addrCollector]); + useEffect( + () => { + if (!provider) { + return; + } + + const populate = async () => { + const _addresses = addrCollector(); + const _names = await batchPopulate(provider, _addresses, ref.current); + setNames(_names); + }; + populate(); + }, + // DON'T put names variables in dependency array; this is intentional; useRef + [provider, addrCollector] + ); return names; };