From f39cc4184f00f22bba7fb29d538ab78d1e6e9f5d Mon Sep 17 00:00:00 2001 From: Willian Mitsuda Date: Thu, 7 Apr 2022 22:48:59 -0300 Subject: [PATCH 1/3] First working version of token/usd oracle support; add support to token transfers section on tx details --- src/TokenTransferItem.tsx | 20 ++++++++- src/components/USDAmount.tsx | 44 ++++++++++++++++++++ src/usePriceOracle.ts | 80 ++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/components/USDAmount.tsx diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index f616922..5838428 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -1,17 +1,21 @@ -import React from "react"; +import React, { useContext } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCaretRight } from "@fortawesome/free-solid-svg-icons/faCaretRight"; import { faSackDollar } from "@fortawesome/free-solid-svg-icons/faSackDollar"; import TransactionAddress from "./components/TransactionAddress"; import ValueHighlighter from "./components/ValueHighlighter"; import FormattedBalance from "./components/FormattedBalance"; +import USDAmount from "./components/USDAmount"; import { AddressContext, ChecksummedAddress, TokenMeta, TokenTransfer, } from "./types"; +import { RuntimeContext } from "./useRuntime"; +import { useBlockNumberContext } from "./useBlockTagContext"; import { Metadata } from "./sourcify/useSourcify"; +import { useTokenUSDOracle } from "./usePriceOracle"; type TokenTransferItemProps = { t: TokenTransfer; @@ -25,6 +29,10 @@ const TokenTransferItem: React.FC = ({ tokenMeta, metadatas, }) => { + const { provider } = useContext(RuntimeContext); + const blockNumber = useBlockNumberContext(); + const [quote, decimals] = useTokenUSDOracle(provider, blockNumber, t.token); + return (
@@ -60,6 +68,16 @@ const TokenTransferItem: React.FC = ({ + {tokenMeta && quote !== undefined && decimals !== undefined && ( + + + + )}
diff --git a/src/components/USDAmount.tsx b/src/components/USDAmount.tsx new file mode 100644 index 0000000..9ba7221 --- /dev/null +++ b/src/components/USDAmount.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { BigNumber, FixedNumber } from "@ethersproject/bignumber"; +import { commify } from "@ethersproject/units"; + +type USDAmountProps = { + amount: BigNumber; + amountDecimals: number; + quote: BigNumber; + quoteDecimals: number; +}; + +// TODO: fix the duplication mess with other currency display components + +/** + * Basic display of USD amount WITHOUT box decoration, only + * text formatting. + * + * USD amounts are displayed commified with 2 decimals places and $ prefix, + * i.e., "$1,000.00". + */ +const USDAmount: React.FC = ({ + amount, + amountDecimals, + quote, + quoteDecimals, +}) => { + const value = amount.mul(quote); + const decimals = amountDecimals + quoteDecimals; + + return ( + + $ + + {commify( + FixedNumber.fromValue(value, decimals, `ufixed256x${decimals}`) + .round(2) + .toString() + )} + + + ); +}; + +export default React.memo(USDAmount); diff --git a/src/usePriceOracle.ts b/src/usePriceOracle.ts index 2e49afd..737c398 100644 --- a/src/usePriceOracle.ts +++ b/src/usePriceOracle.ts @@ -3,6 +3,85 @@ import { JsonRpcProvider, BlockTag } from "@ethersproject/providers"; import { Contract } from "@ethersproject/contracts"; import { BigNumber } from "@ethersproject/bignumber"; import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json"; +import FeedRegistryInterface from "@chainlink/contracts/abi/v0.8/FeedRegistryInterface.json"; +import { ChecksummedAddress } from "./types"; + +const FEED_REGISTRY_MAINNET: ChecksummedAddress = + "0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDf"; + +// The USD "token" address for Chainlink feed registry's purposes +const USD = "0x0000000000000000000000000000000000000348"; + +export const useTokenUSDOracle = ( + provider: JsonRpcProvider | undefined, + blockTag: BlockTag | undefined, + tokenAddress: ChecksummedAddress +): [BigNumber | undefined, number | undefined] => { + const feedRegistry = useMemo(() => { + // It work works on ethereum mainnet and kovan, see: + // https://docs.chain.link/docs/feed-registry/ + if (!provider || provider.network.chainId !== 1) { + return undefined; + } + + try { + return new Contract( + FEED_REGISTRY_MAINNET, + FeedRegistryInterface, + provider + ); + } catch (err) { + console.error(err); + return undefined; + } + }, [provider]); + + const [decimals, setDecimals] = useState(); + useEffect(() => { + if (!feedRegistry || blockTag === undefined) { + return; + } + + const readData = async () => { + try { + const _decimals = await feedRegistry.decimals(tokenAddress, USD, { + blockTag, + }); + setDecimals(_decimals); + } catch (err) { + // Silently ignore on purpose; it means the network or block number does + // not contain the chainlink feed contract + return undefined; + } + }; + readData(); + }, [feedRegistry, blockTag, tokenAddress]); + + const [latestPriceData, setLatestPriceData] = useState(); + useEffect(() => { + if (!feedRegistry || blockTag === undefined) { + return; + } + + const readData = async () => { + try { + const priceData = await feedRegistry.latestRoundData( + tokenAddress, + USD, + { blockTag } + ); + setLatestPriceData(BigNumber.from(priceData.answer)); + } catch (err) { + // Silently ignore on purpose; it means the network or block number does + // not contain the chainlink feed contract + return undefined; + } + }; + readData(); + }, [feedRegistry, blockTag, tokenAddress]); + + return [latestPriceData, decimals]; +}; export const useETHUSDOracle = ( provider: JsonRpcProvider | undefined, @@ -22,6 +101,7 @@ export const useMultipleETHUSDOracle = ( blockTags: (BlockTag | undefined)[] ) => { const ethFeed = useMemo(() => { + // TODO: it currently is hardcoded to support only mainnet if (!provider || provider.network.chainId !== 1) { return undefined; } From e9907f5925f8ae668ba067ba3dc6efdbc66f3b2d Mon Sep 17 00:00:00 2001 From: Willian Mitsuda Date: Sun, 10 Apr 2022 01:35:01 -0300 Subject: [PATCH 2/3] Apply SWR; extract fetcher --- src/usePriceOracle.ts | 116 +++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 64 deletions(-) diff --git a/src/usePriceOracle.ts b/src/usePriceOracle.ts index 737c398..3a9cda6 100644 --- a/src/usePriceOracle.ts +++ b/src/usePriceOracle.ts @@ -4,6 +4,7 @@ import { Contract } from "@ethersproject/contracts"; import { BigNumber } from "@ethersproject/bignumber"; import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json"; import FeedRegistryInterface from "@chainlink/contracts/abi/v0.8/FeedRegistryInterface.json"; +import useSWRImmutable from "swr/immutable"; import { ChecksummedAddress } from "./types"; const FEED_REGISTRY_MAINNET: ChecksummedAddress = @@ -12,75 +13,62 @@ const FEED_REGISTRY_MAINNET: ChecksummedAddress = // The USD "token" address for Chainlink feed registry's purposes const USD = "0x0000000000000000000000000000000000000348"; +const feedRegistryFetcherKey = ( + tokenAddress: ChecksummedAddress, + blockTag: BlockTag | undefined +): [ChecksummedAddress, BlockTag] | null => { + if (blockTag === undefined) { + return null; + } + return [tokenAddress, blockTag]; +}; + +const feedRegistryFetcher = + (provider: JsonRpcProvider | undefined) => + async ( + tokenAddress: ChecksummedAddress, + blockTag: BlockTag + ): Promise<[BigNumber | undefined, number | undefined]> => { + // It work works on ethereum mainnet and kovan, see: + // https://docs.chain.link/docs/feed-registry/ + if (!provider || provider.network.chainId !== 1) { + return [undefined, undefined]; + } + + try { + const feedRegistry = new Contract( + FEED_REGISTRY_MAINNET, + FeedRegistryInterface, + provider + ); + const priceData = await feedRegistry.latestRoundData(tokenAddress, USD, { + blockTag, + }); + const quote = BigNumber.from(priceData.answer); + const decimals = await feedRegistry.decimals(tokenAddress, USD, { + blockTag, + }); + return [quote, decimals]; + } catch (err) { + console.error(err); + return [undefined, undefined]; + } + }; + export const useTokenUSDOracle = ( provider: JsonRpcProvider | undefined, blockTag: BlockTag | undefined, tokenAddress: ChecksummedAddress ): [BigNumber | undefined, number | undefined] => { - const feedRegistry = useMemo(() => { - // It work works on ethereum mainnet and kovan, see: - // https://docs.chain.link/docs/feed-registry/ - if (!provider || provider.network.chainId !== 1) { - return undefined; - } - - try { - return new Contract( - FEED_REGISTRY_MAINNET, - FeedRegistryInterface, - provider - ); - } catch (err) { - console.error(err); - return undefined; - } - }, [provider]); - - const [decimals, setDecimals] = useState(); - useEffect(() => { - if (!feedRegistry || blockTag === undefined) { - return; - } - - const readData = async () => { - try { - const _decimals = await feedRegistry.decimals(tokenAddress, USD, { - blockTag, - }); - setDecimals(_decimals); - } catch (err) { - // Silently ignore on purpose; it means the network or block number does - // not contain the chainlink feed contract - return undefined; - } - }; - readData(); - }, [feedRegistry, blockTag, tokenAddress]); - - const [latestPriceData, setLatestPriceData] = useState(); - useEffect(() => { - if (!feedRegistry || blockTag === undefined) { - return; - } - - const readData = async () => { - try { - const priceData = await feedRegistry.latestRoundData( - tokenAddress, - USD, - { blockTag } - ); - setLatestPriceData(BigNumber.from(priceData.answer)); - } catch (err) { - // Silently ignore on purpose; it means the network or block number does - // not contain the chainlink feed contract - return undefined; - } - }; - readData(); - }, [feedRegistry, blockTag, tokenAddress]); - - return [latestPriceData, decimals]; + const fetcher = feedRegistryFetcher(provider); + const { data, error } = useSWRImmutable( + feedRegistryFetcherKey(tokenAddress, blockTag), + fetcher + ); + if (error) { + return [undefined, undefined]; + } + return data ?? [undefined, undefined]; }; export const useETHUSDOracle = ( From c2e9ad7bd823b0ed5bd60e11c244b3a733a7cfeb Mon Sep 17 00:00:00 2001 From: Willian Mitsuda Date: Sun, 10 Apr 2022 01:51:52 -0300 Subject: [PATCH 3/3] Simplify SWR code --- src/usePriceOracle.ts | 51 +++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/usePriceOracle.ts b/src/usePriceOracle.ts index 3a9cda6..f39230f 100644 --- a/src/usePriceOracle.ts +++ b/src/usePriceOracle.ts @@ -4,6 +4,7 @@ import { Contract } from "@ethersproject/contracts"; import { BigNumber } from "@ethersproject/bignumber"; import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json"; import FeedRegistryInterface from "@chainlink/contracts/abi/v0.8/FeedRegistryInterface.json"; +import { Fetcher } from "swr"; import useSWRImmutable from "swr/immutable"; import { ChecksummedAddress } from "./types"; @@ -13,10 +14,13 @@ const FEED_REGISTRY_MAINNET: ChecksummedAddress = // The USD "token" address for Chainlink feed registry's purposes const USD = "0x0000000000000000000000000000000000000348"; +type FeedRegistryFetcherKey = [ChecksummedAddress, BlockTag]; +type FeedRegistryFetcherData = [BigNumber | undefined, number | undefined]; + const feedRegistryFetcherKey = ( tokenAddress: ChecksummedAddress, blockTag: BlockTag | undefined -): [ChecksummedAddress, BlockTag] | null => { +): FeedRegistryFetcherKey | null => { if (blockTag === undefined) { return null; } @@ -24,35 +28,30 @@ const feedRegistryFetcherKey = ( }; const feedRegistryFetcher = - (provider: JsonRpcProvider | undefined) => - async ( - tokenAddress: ChecksummedAddress, - blockTag: BlockTag - ): Promise<[BigNumber | undefined, number | undefined]> => { + ( + provider: JsonRpcProvider | undefined + ): Fetcher => + async (tokenAddress, blockTag) => { // It work works on ethereum mainnet and kovan, see: // https://docs.chain.link/docs/feed-registry/ - if (!provider || provider.network.chainId !== 1) { - return [undefined, undefined]; + if (provider!.network.chainId !== 1) { + throw new Error("FeedRegistry is supported only on mainnet"); } - try { - const feedRegistry = new Contract( - FEED_REGISTRY_MAINNET, - FeedRegistryInterface, - provider - ); - const priceData = await feedRegistry.latestRoundData(tokenAddress, USD, { - blockTag, - }); - const quote = BigNumber.from(priceData.answer); - const decimals = await feedRegistry.decimals(tokenAddress, USD, { - blockTag, - }); - return [quote, decimals]; - } catch (err) { - console.error(err); - return [undefined, undefined]; - } + // Let SWR handle error + const feedRegistry = new Contract( + FEED_REGISTRY_MAINNET, + FeedRegistryInterface, + provider + ); + const priceData = await feedRegistry.latestRoundData(tokenAddress, USD, { + blockTag, + }); + const quote = BigNumber.from(priceData.answer); + const decimals = await feedRegistry.decimals(tokenAddress, USD, { + blockTag, + }); + return [quote, decimals]; }; export const useTokenUSDOracle = (