diff --git a/src/api/address-resolver/UniswapV3Resolver.ts b/src/api/address-resolver/UniswapV3Resolver.ts new file mode 100644 index 0000000..835e823 --- /dev/null +++ b/src/api/address-resolver/UniswapV3Resolver.ts @@ -0,0 +1,90 @@ +import { BaseProvider } from "@ethersproject/providers"; +import { Contract } from "@ethersproject/contracts"; +import { IAddressResolver } from "./address-resolver"; +import { ChecksummedAddress, TokenMeta } from "../../types"; +import { ERCTokenResolver } from "./ERCTokenResolver"; + +const UNISWAP_V3_FACTORY = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; + +const UNISWAP_V3_FACTORY_ABI = [ + "function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool)", +]; + +const UNISWAP_V3_PAIR_ABI = [ + "function factory() external view returns (address)", + "function token0() external view returns (address)", + "function token1() external view returns (address)", + "function fee() external view returns (uint24)", +]; + +export type UniswapV3TokenMeta = { + address: ChecksummedAddress; +} & TokenMeta; + +export type UniswapV3PairMeta = { + pair: ChecksummedAddress; + token0: UniswapV3TokenMeta; + token1: UniswapV3TokenMeta; + fee: number; +}; + +const ercResolver = new ERCTokenResolver(); + +export class UniswapV3Resolver implements IAddressResolver { + async resolveAddress( + provider: BaseProvider, + address: string + ): Promise { + const poolContract = new Contract(address, UNISWAP_V3_PAIR_ABI, provider); + const factoryContract = new Contract( + UNISWAP_V3_FACTORY, + UNISWAP_V3_FACTORY_ABI, + provider + ); + + try { + // First, probe the factory() function; if it responds with UniswapV2 factory + // address, it may be a pair + const factoryAddress = (await poolContract.factory()) as string; + if (factoryAddress !== UNISWAP_V3_FACTORY) { + return undefined; + } + + // Probe the token0/token1/fee + const [token0, token1, fee] = await Promise.all([ + poolContract.token0() as string, + poolContract.token1() as string, + poolContract.fee() as number, + ]); + + // Probe the factory to ensure it is a legit pair + const expectedPoolAddress = await factoryContract.getPool( + token0, + token1, + fee + ); + if (expectedPoolAddress !== address) { + return undefined; + } + + const [meta0, meta1] = await Promise.all([ + ercResolver.resolveAddress(provider, token0), + ercResolver.resolveAddress(provider, token1), + ]); + if (meta0 === undefined || meta1 === undefined) { + return undefined; + } + + return { + pair: address, + token0: { address: token0, ...meta0 }, + token1: { address: token1, ...meta1 }, + fee, + }; + } 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/index.ts b/src/api/address-resolver/index.ts index 5899231..c6a1687 100644 --- a/src/api/address-resolver/index.ts +++ b/src/api/address-resolver/index.ts @@ -4,6 +4,7 @@ import { plainStringRenderer } from "../../components/PlainString"; import { tokenRenderer } from "../../components/TokenName"; import { uniswapV1PairRenderer } from "../../components/UniswapV1ExchangeName"; import { uniswapV2PairRenderer } from "../../components/UniswapV2PairName"; +import { uniswapV3PairRenderer } from "../../components/UniswapV3PoolName"; import { IAddressResolver, ResolvedAddressRenderer } from "./address-resolver"; import { CompositeAddressResolver, @@ -12,6 +13,7 @@ import { import { ENSAddressResolver } from "./ENSAddressResolver"; import { UniswapV1Resolver } from "./UniswapV1Resolver"; import { UniswapV2Resolver } from "./UniswapV2Resolver"; +import { UniswapV3Resolver } from "./UniswapV3Resolver"; import { ERCTokenResolver } from "./ERCTokenResolver"; import { HardcodedAddressResolver } from "./HardcodedAddressResolver"; @@ -19,13 +21,15 @@ export type ResolvedAddresses = Record>; // Create and configure the main resolver export const ensResolver = new ENSAddressResolver(); -export const uniswapV2Resolver = new UniswapV2Resolver(); export const uniswapV1Resolver = new UniswapV1Resolver(); +export const uniswapV2Resolver = new UniswapV2Resolver(); +export const uniswapV3Resolver = new UniswapV3Resolver(); export const ercTokenResolver = new ERCTokenResolver(); export const hardcodedResolver = new HardcodedAddressResolver(); const _mainResolver = new CompositeAddressResolver(); _mainResolver.addResolver(ensResolver); +_mainResolver.addResolver(uniswapV3Resolver); _mainResolver.addResolver(uniswapV2Resolver); _mainResolver.addResolver(uniswapV1Resolver); _mainResolver.addResolver(ercTokenResolver); @@ -39,8 +43,9 @@ export const resolverRendererRegistry = new Map< ResolvedAddressRenderer >(); resolverRendererRegistry.set(ensResolver, ensRenderer); -resolverRendererRegistry.set(uniswapV2Resolver, uniswapV2PairRenderer); resolverRendererRegistry.set(uniswapV1Resolver, uniswapV1PairRenderer); +resolverRendererRegistry.set(uniswapV2Resolver, uniswapV2PairRenderer); +resolverRendererRegistry.set(uniswapV3Resolver, uniswapV3PairRenderer); resolverRendererRegistry.set(ercTokenResolver, tokenRenderer); resolverRendererRegistry.set(hardcodedResolver, plainStringRenderer); diff --git a/src/components/UniswapV3PoolName.tsx b/src/components/UniswapV3PoolName.tsx new file mode 100644 index 0000000..a21b186 --- /dev/null +++ b/src/components/UniswapV3PoolName.tsx @@ -0,0 +1,119 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; +import TokenLogo from "./TokenLogo"; +import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver"; +import { + UniswapV3PairMeta, + UniswapV3TokenMeta, +} from "../api/address-resolver/UniswapV3Resolver"; +import { ChecksummedAddress } from "../types"; + +type UniswapV3PoolNameProps = { + address: string; + token0: UniswapV3TokenMeta; + token1: UniswapV3TokenMeta; + fee: number; + linkable: boolean; + dontOverrideColors?: boolean; +}; + +const UniswapV3PairName: React.FC = ({ + address, + token0, + token1, + fee, + linkable, + dontOverrideColors, +}) => { + if (linkable) { + return ( + + Uniswap V3 LP: + + / + + / {fee / 10000}% + + ); + } + + return ( +
+ Uniswap V3 LP: + + / + + / {fee / 10000}% +
+ ); +}; + +type ContentProps = { + linkable: boolean; + address: ChecksummedAddress; + name: string; + symbol: string; +}; + +const Content: React.FC = ({ + address, + name, + symbol, + linkable, +}) => ( + <> +
+ +
+ {symbol} + +); + +export const uniswapV3PairRenderer: ResolvedAddressRenderer = + (address, tokenMeta, linkable, dontOverrideColors) => ( + + ); + +export default UniswapV3PairName;