From b791923c79f0e518d1de97235ac056e6d42a7887 Mon Sep 17 00:00:00 2001 From: Evan Gray <56235822+evan-gray@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:55:54 -0500 Subject: [PATCH] dashboard: accountant TVL and TVM --- dashboard/src/components/Accountant.tsx | 68 +++++++++++++++++++++++-- dashboard/src/hooks/useTokenData.ts | 45 ++++++++++++++++ 2 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 dashboard/src/hooks/useTokenData.ts diff --git a/dashboard/src/components/Accountant.tsx b/dashboard/src/components/Accountant.tsx index 8a942467..61db0fe7 100644 --- a/dashboard/src/components/Accountant.tsx +++ b/dashboard/src/components/Accountant.tsx @@ -33,8 +33,15 @@ import { } from '@wormhole-foundation/wormhole-monitor-common'; import CollapsibleSection from './CollapsibleSection'; import Table from './Table'; +import useTokenData, { TokenDataEntry } from '../hooks/useTokenData'; +import numeral from 'numeral'; type PendingTransferForAcct = PendingTransfer & { isEnqueuedInGov: boolean }; +type AccountWithTokenData = Account & { + tokenData?: TokenDataEntry; + tvlTvm: number; + adjBalance: number; +}; function getNumSignatures(signatures: string) { let bitfield = Number(signatures); @@ -182,7 +189,7 @@ const pendingTransferColumns = [ }), ]; -const accountsColumnHelper = createColumnHelper(); +const accountsColumnHelper = createColumnHelper(); const accountsColumns = [ accountsColumnHelper.accessor('key.chain_id', { @@ -195,11 +202,41 @@ const accountsColumns = [ cell: (info) => `${chainIdToName(info.getValue())} (${info.getValue()})`, sortingFn: `text`, }), + accountsColumnHelper.accessor('tokenData.native_address', { + header: () => 'Native Address', + }), + accountsColumnHelper.accessor('tokenData.name', { + header: () => 'Name', + }), + accountsColumnHelper.accessor('tokenData.symbol', { + header: () => 'Symbol', + }), + accountsColumnHelper.accessor('tokenData.price_usd', { + header: () => 'Price', + cell: (info) => (info.getValue() ? numeral(info.getValue()).format('$0,0.0000') : ''), + }), + accountsColumnHelper.accessor('adjBalance', { + header: () => 'Adjusted Balance', + cell: (info) => + info.getValue() < 1 + ? info.getValue().toFixed(4) + : numeral(info.getValue()).format('0,0.0000'), + }), + accountsColumnHelper.accessor('tvlTvm', { + header: () => 'TVL/TVM', + cell: (info) => + info.getValue() < 1 + ? `$${info.getValue().toFixed(4)}` + : numeral(info.getValue()).format('$0,0.0000'), + }), + accountsColumnHelper.accessor('tokenData.decimals', { + header: () => 'Decimals', + }), accountsColumnHelper.accessor('key.token_address', { header: () => 'Token Address', }), accountsColumnHelper.accessor('balance', { - header: () => 'Balance', + header: () => 'Raw Balance', }), ]; @@ -208,6 +245,8 @@ function Accountant({ governorInfo }: { governorInfo: CloudGovernorInfo }) { const accountsInfo = useGetAccountantAccounts(); + const tokenData = useTokenData(); + const pendingTransfersForAcct: PendingTransferForAcct[] = useMemo( () => pendingTransferInfo.map((transfer) => ({ @@ -238,6 +277,27 @@ function Accountant({ governorInfo }: { governorInfo: CloudGovernorInfo }) { } return stats; }, [pendingTransferInfo]); + + const accountsWithTokenData: AccountWithTokenData[] = useMemo(() => { + return accountsInfo.map((a) => { + const thisTokenData = tokenData?.[`${a.key.token_chain}/${a.key.token_address}`]; + if (!thisTokenData) + return { + ...a, + adjBalance: 0, + tvlTvm: 0, + }; + const adjBalance = Number(a.balance) / 10 ** thisTokenData.decimals; + const tvlTvm = adjBalance * Number(thisTokenData.price_usd); + return { + ...a, + tokenData: thisTokenData, + adjBalance, + tvlTvm, + }; + }); + }, [accountsInfo, tokenData]); + const [guardianSigningSorting, setGuardianSigningSorting] = useState([]); const guardianSigning = useReactTable({ columns: guardianSigningColumns, @@ -267,7 +327,7 @@ function Accountant({ governorInfo }: { governorInfo: CloudGovernorInfo }) { const [accountsSorting, setAccountsSorting] = useState([]); const accounts = useReactTable({ columns: accountsColumns, - data: accountsInfo, + data: accountsWithTokenData, state: { sorting: accountsSorting, }, @@ -359,7 +419,7 @@ function Accountant({ governorInfo }: { governorInfo: CloudGovernorInfo }) { Accounts ({accountsInfo.length}) - table={accounts} paginated /> + table={accounts} paginated /> diff --git a/dashboard/src/hooks/useTokenData.ts b/dashboard/src/hooks/useTokenData.ts new file mode 100644 index 00000000..e445c8ce --- /dev/null +++ b/dashboard/src/hooks/useTokenData.ts @@ -0,0 +1,45 @@ +import axios from 'axios'; +import { useEffect, useState } from 'react'; + +export type TokenDataEntry = { + token_chain: number; + token_address: string; + native_address: string; + coin_gecko_coin_id: string; + decimals: number; + symbol: string; + name: string; + price_usd: string; +}; + +export type TokenDataByChainAddress = { + [chainAddress: string]: TokenDataEntry; +}; + +function useTokenData(): TokenDataByChainAddress | null { + const [tokenData, setTokenData] = useState(null); + useEffect(() => { + let cancelled = false; + (async () => { + while (!cancelled) { + const response = await axios.get<{ data: TokenDataEntry[] }>( + 'https://europe-west3-wormhole-message-db-mainnet.cloudfunctions.net/latest-tokendata' + ); + if (!cancelled) { + setTokenData( + response.data?.data.reduce((obj, tokenData) => { + obj[`${tokenData.token_chain}/${tokenData.token_address}`] = tokenData; + return obj; + }, {}) || null + ); + await new Promise((resolve) => setTimeout(resolve, 60000)); + } + } + })(); + return () => { + cancelled = true; + }; + }, []); + return tokenData; +} +export default useTokenData;