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..f39230f 100644 --- a/src/usePriceOracle.ts +++ b/src/usePriceOracle.ts @@ -3,6 +3,72 @@ 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 { Fetcher } from "swr"; +import useSWRImmutable from "swr/immutable"; +import { ChecksummedAddress } from "./types"; + +const FEED_REGISTRY_MAINNET: ChecksummedAddress = + "0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDf"; + +// 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 +): FeedRegistryFetcherKey | null => { + if (blockTag === undefined) { + return null; + } + return [tokenAddress, blockTag]; +}; + +const feedRegistryFetcher = + ( + 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!.network.chainId !== 1) { + throw new Error("FeedRegistry is supported only on mainnet"); + } + + // 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 = ( + provider: JsonRpcProvider | undefined, + blockTag: BlockTag | undefined, + tokenAddress: ChecksummedAddress +): [BigNumber | undefined, number | undefined] => { + const fetcher = feedRegistryFetcher(provider); + const { data, error } = useSWRImmutable( + feedRegistryFetcherKey(tokenAddress, blockTag), + fetcher + ); + if (error) { + return [undefined, undefined]; + } + return data ?? [undefined, undefined]; +}; export const useETHUSDOracle = ( provider: JsonRpcProvider | undefined, @@ -22,6 +88,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; }