Add ERC token address resolver

This commit is contained in:
Willian Mitsuda 2021-10-31 19:51:11 -03:00
parent 59ff4fd401
commit 099c1465b0
7 changed files with 102 additions and 18 deletions

View File

@ -3,7 +3,7 @@ import { IAddressResolver } from "./address-resolver";
export type SelectedResolvedName<T> = [IAddressResolver<T>, T]; export type SelectedResolvedName<T> = [IAddressResolver<T>, T];
export class CompositeAddressResolver<T> export class CompositeAddressResolver<T = any>
implements IAddressResolver<SelectedResolvedName<T>> implements IAddressResolver<SelectedResolvedName<T>>
{ {
private resolvers: IAddressResolver<T>[] = []; private resolvers: IAddressResolver<T>[] = [];
@ -17,9 +17,9 @@ export class CompositeAddressResolver<T>
address: string address: string
): Promise<SelectedResolvedName<T> | undefined> { ): Promise<SelectedResolvedName<T> | undefined> {
for (const r of this.resolvers) { for (const r of this.resolvers) {
const name = await r.resolveAddress(provider, address); const resolvedAddress = await r.resolveAddress(provider, address);
if (name !== undefined) { if (resolvedAddress !== undefined) {
return [r, name]; return [r, resolvedAddress];
} }
} }

View File

@ -2,12 +2,13 @@ import { BaseProvider } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts"; import { Contract } from "@ethersproject/contracts";
import { IAddressResolver } from "./address-resolver"; import { IAddressResolver } from "./address-resolver";
import erc20 from "../../erc20.json"; import erc20 from "../../erc20.json";
import { TokenMeta } from "../../types";
export class ERCTokenResolver implements IAddressResolver<string> { export class ERCTokenResolver implements IAddressResolver<TokenMeta> {
async resolveAddress( async resolveAddress(
provider: BaseProvider, provider: BaseProvider,
address: string address: string
): Promise<string | undefined> { ): Promise<TokenMeta | undefined> {
const erc20Contract = new Contract(address, erc20, provider); const erc20Contract = new Contract(address, erc20, provider);
try { try {
const [name, symbol, decimals] = await Promise.all([ const [name, symbol, decimals] = await Promise.all([
@ -15,9 +16,14 @@ export class ERCTokenResolver implements IAddressResolver<string> {
erc20Contract.symbol(), erc20Contract.symbol(),
erc20Contract.decimals(), erc20Contract.decimals(),
]); ]);
return name; return {
name,
symbol,
decimals,
};
} catch (err) { } catch (err) {
console.warn(`Couldn't get token ${address} metadata; ignoring`, err); // Ignore on purpose; this indicates the probe failed and the address
// is not a token
} }
return undefined; return undefined;
} }

View File

@ -8,9 +8,9 @@ export interface IAddressResolver<T> {
): Promise<T | undefined>; ): Promise<T | undefined>;
} }
export type ResolvedAddressRenderer = ( export type ResolvedAddressRenderer<T> = (
address: string, address: string,
resolvedAddress: string, resolvedAddress: T,
linkable: boolean, linkable: boolean,
dontOverrideColors: boolean dontOverrideColors: boolean
) => React.ReactElement; ) => React.ReactElement;

View File

@ -1,34 +1,39 @@
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { ensRenderer } from "../../components/ENSName"; import { ensRenderer } from "../../components/ENSName";
import { tokenRenderer } from "../../components/TokenName";
import { IAddressResolver, ResolvedAddressRenderer } from "./address-resolver"; import { IAddressResolver, ResolvedAddressRenderer } from "./address-resolver";
import { import {
CompositeAddressResolver, CompositeAddressResolver,
SelectedResolvedName, SelectedResolvedName,
} from "./CompositeAddressResolver"; } from "./CompositeAddressResolver";
import { ENSAddressResolver } from "./ENSAddressResolver"; import { ENSAddressResolver } from "./ENSAddressResolver";
import { ERCTokenResolver } from "./ERCTokenResolver";
export type ResolvedAddresses = Record<string, SelectedResolvedName<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 ensResolver = new ENSAddressResolver();
export const ercTokenResolver = new ERCTokenResolver();
const _mainResolver = new CompositeAddressResolver<string>(); const _mainResolver = new CompositeAddressResolver();
_mainResolver.addResolver(ensResolver); _mainResolver.addResolver(ensResolver);
_mainResolver.addResolver(ercTokenResolver);
export const mainResolver: IAddressResolver<SelectedResolvedName<string>> = export const mainResolver: IAddressResolver<SelectedResolvedName<any>> =
_mainResolver; _mainResolver;
export const resolverRendererRegistry = new Map< export const resolverRendererRegistry = new Map<
IAddressResolver<string>, IAddressResolver<any>,
ResolvedAddressRenderer ResolvedAddressRenderer<any>
>(); >();
resolverRendererRegistry.set(ensResolver, ensRenderer); resolverRendererRegistry.set(ensResolver, ensRenderer);
resolverRendererRegistry.set(ercTokenResolver, tokenRenderer);
export const batchPopulate = async ( export const batchPopulate = async (
provider: BaseProvider, provider: BaseProvider,
addresses: string[] addresses: string[]
): Promise<ResolvedAddresses> => { ): Promise<ResolvedAddresses> => {
const solvers: Promise<SelectedResolvedName<string> | undefined>[] = []; const solvers: Promise<SelectedResolvedName<any> | undefined>[] = [];
for (const a of addresses) { for (const a of addresses) {
solvers.push(mainResolver.resolveAddress(provider, a)); solvers.push(mainResolver.resolveAddress(provider, a));
} }

View File

@ -58,7 +58,7 @@ const Content: React.FC<ContentProps> = ({ linkable, name }) => (
</> </>
); );
export const ensRenderer: ResolvedAddressRenderer = ( export const ensRenderer: ResolvedAddressRenderer<string> = (
address, address,
resolvedAddress, resolvedAddress,
linkable, linkable,

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