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; }