Merge branch 'feature/erc20-address-resolver' into develop

This commit is contained in:
Willian Mitsuda 2021-11-01 04:50:14 -03:00
commit f2c75ca6e2
19 changed files with 341 additions and 172 deletions

View File

@ -11,11 +11,13 @@ import {
TokenTransfer, TokenTransfer,
TransactionData, TransactionData,
} from "./types"; } from "./types";
import { ResolvedAddresses } from "./api/address-resolver";
type TokenTransferItemProps = { type TokenTransferItemProps = {
t: TokenTransfer; t: TokenTransfer;
txData: TransactionData; txData: TransactionData;
tokenMeta?: TokenMeta | undefined; tokenMeta?: TokenMeta | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
// TODO: handle partial // TODO: handle partial
@ -23,6 +25,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
t, t,
txData, txData,
tokenMeta, tokenMeta,
resolvedAddresses,
}) => ( }) => (
<div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100"> <div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100">
<span className="text-gray-500"> <span className="text-gray-500">
@ -64,10 +67,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
<AddressHighlighter address={t.token}> <AddressHighlighter address={t.token}>
<DecoratedAddressLink <DecoratedAddressLink
address={t.token} address={t.token}
text={ resolvedAddresses={resolvedAddresses}
tokenMeta ? `${tokenMeta.name} (${tokenMeta.symbol})` : undefined
}
tokenMeta={tokenMeta}
/> />
</AddressHighlighter> </AddressHighlighter>
</div> </div>

View File

@ -1,26 +1,28 @@
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { IAddressResolver } from "./address-resolver"; import { IAddressResolver } from "./address-resolver";
export class CompositeAddressResolver implements IAddressResolver { export type SelectedResolvedName<T> = [IAddressResolver<T>, T] | null;
private resolvers: IAddressResolver[] = [];
addResolver(resolver: IAddressResolver) { export class CompositeAddressResolver<T = any>
implements IAddressResolver<SelectedResolvedName<T>>
{
private resolvers: IAddressResolver<T>[] = [];
addResolver(resolver: IAddressResolver<T>) {
this.resolvers.push(resolver); this.resolvers.push(resolver);
} }
async resolveAddress( async resolveAddress(
provider: BaseProvider, provider: BaseProvider,
address: string address: string
): Promise<string | undefined> { ): Promise<SelectedResolvedName<T> | undefined> {
for (const r of this.resolvers) { for (const r of this.resolvers) {
const name = r.resolveAddress(provider, address); const resolvedAddress = await r.resolveAddress(provider, address);
if (name !== undefined) { if (resolvedAddress !== undefined) {
return name; return [r, resolvedAddress];
} }
} }
return undefined; return null;
// TODO: fallback to address itself
// return address;
} }
} }

View File

@ -1,7 +1,7 @@
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { IAddressResolver } from "./address-resolver"; import { IAddressResolver } from "./address-resolver";
export class ENSAddressResolver implements IAddressResolver { export class ENSAddressResolver implements IAddressResolver<string> {
async resolveAddress( async resolveAddress(
provider: BaseProvider, provider: BaseProvider,
address: string address: string

View File

@ -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<TokenMeta> {
async resolveAddress(
provider: BaseProvider,
address: string
): Promise<TokenMeta | undefined> {
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;
}
}

View File

@ -1,8 +1,16 @@
import React from "react";
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
export interface IAddressResolver { export interface IAddressResolver<T> {
resolveAddress( resolveAddress(
provider: BaseProvider, provider: BaseProvider,
address: string address: string
): Promise<string | undefined>; ): Promise<T | undefined>;
} }
export type ResolvedAddressRenderer<T> = (
address: string,
resolvedAddress: T,
linkable: boolean,
dontOverrideColors: boolean
) => React.ReactElement;

View File

@ -1,34 +1,57 @@
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { IAddressResolver } from "./address-resolver"; import { ensRenderer } from "../../components/ENSName";
import { CompositeAddressResolver } from "./CompositeAddressResolver"; import { tokenRenderer } from "../../components/TokenName";
import { IAddressResolver, ResolvedAddressRenderer } from "./address-resolver";
import {
CompositeAddressResolver,
SelectedResolvedName,
} from "./CompositeAddressResolver";
import { ENSAddressResolver } from "./ENSAddressResolver"; import { ENSAddressResolver } from "./ENSAddressResolver";
import { ERCTokenResolver } from "./ERCTokenResolver";
export type ResolvedAddresses = Record<string, string>; export type ResolvedAddresses = Record<string, SelectedResolvedName<any>>;
// Create and configure the main resolver // Create and configure the main resolver
export const ensResolver = new ENSAddressResolver();
export const ercTokenResolver = new ERCTokenResolver();
const _mainResolver = new CompositeAddressResolver(); const _mainResolver = new CompositeAddressResolver();
_mainResolver.addResolver(new ENSAddressResolver()); _mainResolver.addResolver(ensResolver);
_mainResolver.addResolver(ercTokenResolver);
export const mainResolver: IAddressResolver = _mainResolver; export const mainResolver: IAddressResolver<SelectedResolvedName<any>> =
_mainResolver;
export const resolverRendererRegistry = new Map<
IAddressResolver<any>,
ResolvedAddressRenderer<any>
>();
resolverRendererRegistry.set(ensResolver, ensRenderer);
resolverRendererRegistry.set(ercTokenResolver, tokenRenderer);
// TODO: implement progressive resolving
export const batchPopulate = async ( export const batchPopulate = async (
provider: BaseProvider, provider: BaseProvider,
addresses: string[] addresses: string[],
currentMap: ResolvedAddresses | undefined
): Promise<ResolvedAddresses> => { ): Promise<ResolvedAddresses> => {
const solvers: Promise<string | undefined>[] = []; const solvers: Promise<SelectedResolvedName<any> | undefined>[] = [];
for (const a of addresses) { const unresolvedAddresses = addresses.filter(
(a) => currentMap?.[a] === undefined
);
for (const a of unresolvedAddresses) {
solvers.push(mainResolver.resolveAddress(provider, a)); solvers.push(mainResolver.resolveAddress(provider, a));
} }
const resultMap: ResolvedAddresses = currentMap ? { ...currentMap } : {};
const results = await Promise.all(solvers); const results = await Promise.all(solvers);
const cache: ResolvedAddresses = {};
for (let i = 0; i < results.length; i++) { for (let i = 0; i < results.length; i++) {
const r = results[i]; const r = results[i];
if (r === undefined) { if (r === undefined) {
continue; continue;
} }
cache[addresses[i]] = r; resultMap[unresolvedAddresses[i]] = r;
} }
return cache; return resultMap;
}; };

View File

@ -1,13 +0,0 @@
import React from "react";
type AddressProps = {
address: string;
};
const Address: React.FC<AddressProps> = ({ address }) => (
<span className="font-address text-gray-400 truncate" title={address}>
{address}
</span>
);
export default Address;

View File

@ -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<AddressLinkProps> = ({
address,
text,
dontOverrideColors,
}) => (
<NavLink
className={`${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} font-address truncate`}
to={`/address/${address}`}
title={text ?? address}
>
{text ?? address}
</NavLink>
);
export default AddressLink;

View File

@ -1,14 +1,13 @@
import React from "react"; import React from "react";
import Address from "./Address"; import {
import AddressLink from "./AddressLink"; ResolvedAddresses,
import ENSName from "./ENSName"; resolverRendererRegistry,
import ENSNameLink from "./ENSNameLink"; } from "../api/address-resolver";
import { ResolvedAddresses } from "../api/address-resolver"; import PlainAddress from "./PlainAddress";
type AddressOrENSNameProps = { type AddressOrENSNameProps = {
address: string; address: string;
selectedAddress?: string; selectedAddress?: string;
text?: string;
dontOverrideColors?: boolean; dontOverrideColors?: boolean;
resolvedAddresses?: ResolvedAddresses | undefined; resolvedAddresses?: ResolvedAddresses | undefined;
}; };
@ -16,40 +15,35 @@ type AddressOrENSNameProps = {
const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({ const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({
address, address,
selectedAddress, selectedAddress,
text,
dontOverrideColors, dontOverrideColors,
resolvedAddresses, resolvedAddresses,
}) => { }) => {
const name = resolvedAddresses?.[address]; const resolvedAddress = resolvedAddresses?.[address];
return ( const linkable = address !== selectedAddress;
<>
{address === selectedAddress ? ( if (!resolvedAddress) {
<> return (
{name ? ( <PlainAddress
<ENSName name={name} address={address} /> address={address}
) : ( linkable={linkable}
<Address address={address} /> dontOverrideColors={dontOverrideColors}
)} />
</> );
) : ( }
<>
{name ? ( const [resolver, resolvedName] = resolvedAddress;
<ENSNameLink const renderer = resolverRendererRegistry.get(resolver);
name={name} if (renderer === undefined) {
address={address} return (
dontOverrideColors={dontOverrideColors} <PlainAddress
/> address={address}
) : ( linkable={linkable}
<AddressLink dontOverrideColors={dontOverrideColors}
address={address} />
text={text} );
dontOverrideColors={dontOverrideColors} }
/>
)} return renderer(address, resolvedName, linkable, !!dontOverrideColors);
</>
)}
</>
);
}; };
export default AddressOrENSName; export default AddressOrENSName;

View File

@ -5,36 +5,31 @@ import { faBomb } from "@fortawesome/free-solid-svg-icons/faBomb";
import { faMoneyBillAlt } from "@fortawesome/free-solid-svg-icons/faMoneyBillAlt"; import { faMoneyBillAlt } from "@fortawesome/free-solid-svg-icons/faMoneyBillAlt";
import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn"; import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn";
import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins"; import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins";
import TokenLogo from "./TokenLogo";
import AddressOrENSName from "./AddressOrENSName"; import AddressOrENSName from "./AddressOrENSName";
import { AddressContext, TokenMeta, ZERO_ADDRESS } from "../types"; import { AddressContext, ZERO_ADDRESS } from "../types";
import { ResolvedAddresses } from "../api/address-resolver"; import { ResolvedAddresses } from "../api/address-resolver";
type DecoratedAddressLinkProps = { type DecoratedAddressLinkProps = {
address: string; address: string;
selectedAddress?: string; selectedAddress?: string;
text?: string;
addressCtx?: AddressContext; addressCtx?: AddressContext;
creation?: boolean; creation?: boolean;
miner?: boolean; miner?: boolean;
selfDestruct?: boolean; selfDestruct?: boolean;
txFrom?: boolean; txFrom?: boolean;
txTo?: boolean; txTo?: boolean;
tokenMeta?: TokenMeta;
resolvedAddresses?: ResolvedAddresses | undefined; resolvedAddresses?: ResolvedAddresses | undefined;
}; };
const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
address, address,
selectedAddress, selectedAddress,
text,
addressCtx, addressCtx,
creation, creation,
miner, miner,
selfDestruct, selfDestruct,
txFrom, txFrom,
txTo, txTo,
tokenMeta,
resolvedAddresses, resolvedAddresses,
}) => { }) => {
const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS; const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS;
@ -75,15 +70,9 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
<FontAwesomeIcon icon={faCoins} size="1x" /> <FontAwesomeIcon icon={faCoins} size="1x" />
</span> </span>
)} )}
{tokenMeta && (
<div className="self-center">
<TokenLogo address={address} name={tokenMeta.name} />
</div>
)}
<AddressOrENSName <AddressOrENSName
address={address} address={address}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
text={text}
dontOverrideColors={mint || burn} dontOverrideColors={mint || burn}
resolvedAddresses={resolvedAddresses} resolvedAddresses={resolvedAddresses}
/> />

View File

@ -1,25 +1,75 @@
import React from "react"; import React from "react";
import { NavLink } from "react-router-dom";
import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver";
import ENSLogo from "./ensLogo.svg"; import ENSLogo from "./ensLogo.svg";
type ENSNameProps = { type ENSNameProps = {
name: string; name: string;
address: string; address: string;
linkable: boolean;
dontOverrideColors?: boolean;
}; };
const ENSName: React.FC<ENSNameProps> = ({ name, address }) => ( const ENSName: React.FC<ENSNameProps> = ({
<div name,
className="flex items-baseline space-x-1 font-sans text-gray-700 truncate" address,
title={`${name}: ${address}`} linkable,
> dontOverrideColors,
}) => {
if (linkable) {
return (
<NavLink
className={`flex items-baseline space-x-1 font-sans ${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} truncate`}
to={`/address/${name}`}
title={`${name}: ${address}`}
>
<Content linkable={true} name={name} />
</NavLink>
);
}
return (
<div
className="flex items-baseline space-x-1 font-sans text-gray-700 truncate"
title={`${name}: ${address}`}
>
<Content linkable={false} name={name} />
</div>
);
};
type ContentProps = {
linkable: boolean;
name: string;
};
const Content: React.FC<ContentProps> = ({ linkable, name }) => (
<>
<img <img
className="self-center filter grayscale" className={`self-center ${linkable ? "" : "filter grayscale"}`}
src={ENSLogo} src={ENSLogo}
alt="ENS Logo" alt="ENS Logo"
width={12} width={12}
height={12} height={12}
/> />
<span className="truncate">{name}</span> <span className="truncate">{name}</span>
</div> </>
);
export const ensRenderer: ResolvedAddressRenderer<string> = (
address,
resolvedAddress,
linkable,
dontOverrideColors
) => (
<ENSName
address={address}
name={resolvedAddress}
linkable={linkable}
dontOverrideColors={dontOverrideColors}
/>
); );
export default ENSName; export default ENSName;

View File

@ -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<ENSNameLinkProps> = ({
name,
address,
dontOverrideColors,
}) => (
<NavLink
className={`flex items-baseline space-x-1 font-sans ${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} truncate`}
to={`/address/${name}`}
title={`${name}: ${address}`}
>
<img
className="self-center"
src={ENSLogo}
alt="ENS Logo"
width={12}
height={12}
/>
<span className="truncate">{name}</span>
</NavLink>
);
export default ENSNameLink;

View File

@ -3,17 +3,23 @@ import InternalTransfer from "./InternalTransfer";
import InternalSelfDestruct from "./InternalSelfDestruct"; import InternalSelfDestruct from "./InternalSelfDestruct";
import InternalCreate from "./InternalCreate"; import InternalCreate from "./InternalCreate";
import { TransactionData, InternalOperation, OperationType } from "../types"; import { TransactionData, InternalOperation, OperationType } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
type InternalTransactionOperationProps = { type InternalTransactionOperationProps = {
txData: TransactionData; txData: TransactionData;
internalOp: InternalOperation; internalOp: InternalOperation;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const InternalTransactionOperation: React.FC<InternalTransactionOperationProps> = const InternalTransactionOperation: React.FC<InternalTransactionOperationProps> =
({ txData, internalOp }) => ( ({ txData, internalOp, resolvedAddresses }) => (
<> <>
{internalOp.type === OperationType.TRANSFER && ( {internalOp.type === OperationType.TRANSFER && (
<InternalTransfer txData={txData} internalOp={internalOp} /> <InternalTransfer
txData={txData}
internalOp={internalOp}
resolvedAddresses={resolvedAddresses}
/>
)} )}
{internalOp.type === OperationType.SELF_DESTRUCT && ( {internalOp.type === OperationType.SELF_DESTRUCT && (
<InternalSelfDestruct txData={txData} internalOp={internalOp} /> <InternalSelfDestruct txData={txData} internalOp={internalOp} />

View File

@ -5,15 +5,18 @@ import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
import AddressHighlighter from "./AddressHighlighter"; import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import { TransactionData, InternalOperation } from "../types"; import { TransactionData, InternalOperation } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
type InternalTransferProps = { type InternalTransferProps = {
txData: TransactionData; txData: TransactionData;
internalOp: InternalOperation; internalOp: InternalOperation;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const InternalTransfer: React.FC<InternalTransferProps> = ({ const InternalTransfer: React.FC<InternalTransferProps> = ({
txData, txData,
internalOp, internalOp,
resolvedAddresses,
}) => { }) => {
const fromMiner = const fromMiner =
txData.confirmedData?.miner !== undefined && txData.confirmedData?.miner !== undefined &&

View File

@ -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<PlainAddressProps> = ({
address,
linkable,
dontOverrideColors,
}) => {
if (linkable) {
return (
<NavLink
className={`${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} font-address truncate`}
to={`/address/${address}`}
title={address}
>
{address}
</NavLink>
);
}
return (
<span className="font-address text-gray-400 truncate" title={address}>
{address}
</span>
);
};
export default PlainAddress;

View File

@ -9,7 +9,7 @@ type TokenLogoProps = {
}; };
const TokenLogo: React.FC<TokenLogoProps> = (props) => ( const TokenLogo: React.FC<TokenLogoProps> = (props) => (
<Suspense fallback={<></>}> <Suspense fallback={null}>
<InternalTokenLogo {...props} /> <InternalTokenLogo {...props} />
</Suspense> </Suspense>
); );

View File

@ -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<TokenNameProps> = ({
address,
name,
symbol,
linkable,
dontOverrideColors,
}) => {
if (linkable) {
return (
<NavLink
className={`flex items-baseline space-x-1 font-sans ${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} truncate`}
to={`/address/${address}`}
title={`${name} (${symbol}): ${address}`}
>
<Content
address={address}
linkable={true}
name={name}
symbol={symbol}
/>
</NavLink>
);
}
return (
<div
className="flex items-baseline space-x-1 font-sans text-gray-700 truncate"
title={`${name} (${symbol}): ${address}`}
>
<Content address={address} linkable={false} name={name} symbol={symbol} />
</div>
);
};
type ContentProps = {
address: string;
name: string;
symbol: string;
linkable: boolean;
};
const Content: React.FC<ContentProps> = ({
address,
name,
symbol,
linkable,
}) => (
<>
<div
className={`self-center w-5 h-5 ${linkable ? "" : "filter grayscale"}`}
>
<TokenLogo address={address} name={name} />
</div>
<span className="truncate">
{name} ({symbol})
</span>
</>
);
export const tokenRenderer: ResolvedAddressRenderer<TokenMeta> = (
address,
tokenMeta,
linkable,
dontOverrideColors
) => (
<TokenName
address={address}
name={tokenMeta.name}
symbol={tokenMeta.symbol}
linkable={linkable}
dontOverrideColors={dontOverrideColors}
/>
);
export default TokenName;

View File

@ -206,6 +206,7 @@ const Details: React.FC<DetailsProps> = ({
key={i} key={i}
txData={txData} txData={txData}
internalOp={op} internalOp={op}
resolvedAddresses={resolvedAddresses}
/> />
))} ))}
</div> </div>
@ -225,6 +226,7 @@ const Details: React.FC<DetailsProps> = ({
t={t} t={t}
txData={txData} txData={txData}
tokenMeta={txData.tokenMetas[t.token]} tokenMeta={txData.tokenMetas[t.token]}
resolvedAddresses={resolvedAddresses}
/> />
))} ))}
</div> </div>

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { JsonRpcProvider } from "@ethersproject/providers"; import { JsonRpcProvider } from "@ethersproject/providers";
import { ProcessedTransaction, TransactionData } from "./types"; import { ProcessedTransaction, TransactionData } from "./types";
import { batchPopulate, ResolvedAddresses } from "./api/address-resolver"; import { batchPopulate, ResolvedAddresses } from "./api/address-resolver";
@ -66,19 +66,27 @@ export const useResolvedAddresses = (
addrCollector: AddressCollector addrCollector: AddressCollector
) => { ) => {
const [names, setNames] = useState<ResolvedAddresses>(); const [names, setNames] = useState<ResolvedAddresses>();
const ref = useRef<ResolvedAddresses | undefined>();
useEffect(() => { useEffect(() => {
if (!provider) { ref.current = names;
return; });
}
const populate = async () => { useEffect(
const _addresses = addrCollector(); () => {
const _names = await batchPopulate(provider, _addresses); if (!provider) {
setNames(_names); return;
}; }
populate();
}, [provider, addrCollector]); 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; return names;
}; };