diff --git a/components/TreasuryAccount/AccountItem.tsx b/components/TreasuryAccount/AccountItem.tsx index 643db0b188..24e17d1647 100644 --- a/components/TreasuryAccount/AccountItem.tsx +++ b/components/TreasuryAccount/AccountItem.tsx @@ -1,67 +1,52 @@ -import { useMemo } from 'react' -import { getTreasuryAccountItemInfoV2 } from '@utils/treasuryTools' -import { AssetAccount } from '@utils/uiTypes/assets' +import { AssetAccount, TreasuryAccountItemInfo } from '@utils/uiTypes/assets' import TokenIcon from '@components/treasuryV2/icons/TokenIcon' import { useTokenMetadata } from '@hooks/queries/tokenMetadata' -import { useJupiterPriceByMintQuery } from '../../hooks/queries/jupiterPrice' -import BigNumber from 'bignumber.js' +import { useMemo, useState } from 'react' + +type AccountItemProps = { + governedAccountTokenAccount: AssetAccount + treasuryInfo: TreasuryAccountItemInfo +} const AccountItem = ({ governedAccountTokenAccount, -}: { - governedAccountTokenAccount: AssetAccount -}) => { + treasuryInfo, +}: AccountItemProps) => { + const [imgError, setImgError] = useState(false) + const { - decimalAdjustedAmount, amountFormatted, logo, name, symbol, - } = getTreasuryAccountItemInfoV2(governedAccountTokenAccount) + displayPrice, + mintPubkey, + } = treasuryInfo || {} - const { data: priceData } = useJupiterPriceByMintQuery( - governedAccountTokenAccount.extensions.mint?.publicKey - ) - - const { data } = useTokenMetadata( - governedAccountTokenAccount.extensions.mint?.publicKey, + const { data: tokenMetadata } = useTokenMetadata( + mintPubkey, !logo ) const symbolFromMeta = useMemo(() => { - // data.symbol is kinda weird - //Handle null characters, whitespace, and ensure fallback to symbol - const cleanSymbol = data?.symbol - ?.replace(/\0/g, '') // Remove null characters - ?.replace(/\s+/g, ' ') // Normalize whitespace to single spaces - ?.trim() // Remove leading/trailing whitespace - - return cleanSymbol || symbol || '' - }, [data?.symbol, symbol]) + const cleanSymbol = tokenMetadata?.symbol + ?.replace(/\0/g, '') + ?.replace(/\s+/g, ' ') + ?.trim() - const displayPrice = useMemo(() => { - if (!decimalAdjustedAmount || !priceData?.result?.price) return '' - - try { - const totalPrice = decimalAdjustedAmount * priceData.result.price - return new BigNumber(totalPrice).toFormat(0) - } catch (error) { - console.error('Error calculating display price:', error) - return '' - } - }, [priceData, decimalAdjustedAmount]) + return cleanSymbol || symbol || '' + }, [tokenMetadata?.symbol, symbol]) return (
- {logo ? ( + {logo && !imgError ? ( { currentTarget.onerror = null - currentTarget.hidden = true + setImgError(true) }} alt={`${name} logo`} /> diff --git a/components/TreasuryAccount/AccountsItems.tsx b/components/TreasuryAccount/AccountsItems.tsx index 04d4afbbde..ea6065f9d9 100644 --- a/components/TreasuryAccount/AccountsItems.tsx +++ b/components/TreasuryAccount/AccountsItems.tsx @@ -1,39 +1,90 @@ import useGovernanceAssets from '@hooks/useGovernanceAssets' -import { getTreasuryAccountItemInfoV2 } from '@utils/treasuryTools' -import React from 'react' +import React, { useMemo } from 'react' import AccountItem from './AccountItem' +import { useJupiterPricesByMintsQuery } from '../../hooks/queries/jupiterPrice' +import { PublicKey } from '@solana/web3.js' +import { WSOL_MINT } from '../instructions/tools' +import BigNumber from 'bignumber.js' +import { getTreasuryAccountItemInfoV3 } from '../../utils/treasuryToolsV3' const AccountsItems = () => { const { governedTokenAccountsWithoutNfts, auxiliaryTokenAccounts, } = useGovernanceAssets() - const accounts = [ - ...governedTokenAccountsWithoutNfts, - ...auxiliaryTokenAccounts, - ] - const accountsSorted = accounts - .sort((a, b) => { - const infoA = getTreasuryAccountItemInfoV2(a) - const infoB = getTreasuryAccountItemInfoV2(b) - return infoB.totalPrice - infoA.totalPrice - }) - .splice( - 0, - Number(process?.env?.MAIN_VIEW_SHOW_MAX_TOP_TOKENS_NUM || accounts.length) - ) + + const accounts = useMemo(() => { + const allAccounts = [ + ...(governedTokenAccountsWithoutNfts || []), + ...(auxiliaryTokenAccounts || []), + ] + return allAccounts.filter(Boolean) + }, [governedTokenAccountsWithoutNfts, auxiliaryTokenAccounts]) + + const mintsToFetch = useMemo(() => { + return [ + ...governedTokenAccountsWithoutNfts, + ...auxiliaryTokenAccounts, + ] + .filter((x) => typeof x.extensions.mint !== 'undefined') + .map((x) => x.extensions.mint!.publicKey) + }, [governedTokenAccountsWithoutNfts, auxiliaryTokenAccounts]) + + const { data: prices } = useJupiterPricesByMintsQuery([ + ...mintsToFetch, + new PublicKey(WSOL_MINT), + ]) + + const sortedAccounts = useMemo(() => { + if (!accounts.length || !prices) return [] + + try { + const accountsWithInfo = accounts.map((account) => { + try { + const info = getTreasuryAccountItemInfoV3(account) + // Override the price/total price with Jupiter price data + const mintAddress = account.extensions.mint?.publicKey.toBase58() + const jupiterPrice = mintAddress && Object.keys(prices || {}).length > 0 ? prices[mintAddress]?.price ?? 0 : 0 + const amount = info.decimalAdjustedAmount + const totalPrice = amount * jupiterPrice + + return { + account, + info: { + ...info, + totalPrice, + displayPrice: totalPrice ? new BigNumber(totalPrice).toFormat(0) : '' + } + } + } catch (err) { + console.error(`Error processing account ${account?.pubkey?.toString()}:`, err) + return null + } + }) + + const validAccounts = accountsWithInfo + .filter((item) => item !== null) + .sort((a, b) => b.info.totalPrice - a.info.totalPrice) + + const maxTokens = Number(process?.env?.MAIN_VIEW_SHOW_MAX_TOP_TOKENS_NUM) || accounts.length + return validAccounts.slice(0, maxTokens) + } catch (err) { + console.error('Error sorting accounts:', err) + return [] + } + }, [accounts, prices]) + return (
- {accountsSorted.map((account) => { - return ( - - ) - })} + {sortedAccounts.map(({ account, info }) => ( + + ))}
) } -export default AccountsItems +export default AccountsItems \ No newline at end of file diff --git a/hooks/useGetTreasuryAccountItemInfoV3.ts b/hooks/useGetTreasuryAccountItemInfoV3.ts new file mode 100644 index 0000000000..88dfc9a57b --- /dev/null +++ b/hooks/useGetTreasuryAccountItemInfoV3.ts @@ -0,0 +1,107 @@ +import { getAccountName, WSOL_MINT } from '@components/instructions/tools' +import { BN } from '@coral-xyz/anchor' +import { PublicKey } from '@solana/web3.js' +import { getMintDecimalAmountFromNatural } from '@tools/sdk/units' +import BigNumber from 'bignumber.js' +import { abbreviateAddress } from '@utils/formatting' +import { AccountType, AssetAccount } from '@utils/uiTypes/assets' +import { useJupiterPriceByMintQuery } from './queries/jupiterPrice' +import { useTokenMetadata } from './queries/tokenMetadata' +import { useMemo } from 'react' + +export const useGetTreasuryAccountItemInfoV3 = (account: AssetAccount) => { + // Memoize these values since they're used in multiple places + const mintPubkey = useMemo(() => + account.extensions.mint?.publicKey, + [account.extensions.mint] + ) + + const mintAddress = useMemo(() => + account.type === AccountType.SOL + ? WSOL_MINT + : mintPubkey?.toBase58(), + [account.type, mintPubkey] + ) + + const decimalAdjustedAmount = useMemo(() => + account.extensions.amount && account.extensions.mint + ? getMintDecimalAmountFromNatural( + account.extensions.mint.account, + new BN( + account.isSol + ? account.extensions.solAccount!.lamports + : account.extensions.amount + ) + ).toNumber() + : 0 + , [account]) + + const { data: priceData } = useJupiterPriceByMintQuery(mintPubkey) + const { data: tokenMetadata } = useTokenMetadata(mintPubkey, true) + // const info = tokenPriceService.getTokenInfo(mintAddress!) + + const amountFormatted = useMemo(() => + new BigNumber(decimalAdjustedAmount).toFormat() + , [decimalAdjustedAmount]) + + // Handle symbol with metadata fallback + const symbol = useMemo(() => { + if (account.type === AccountType.NFT) return 'NFTS' + if (account.type === AccountType.SOL) return 'SOL' + + // Try to get from metadata first + const metadataSymbol = tokenMetadata?.symbol + ?.replace(/\0/g, '') + ?.replace(/\s+/g, ' ') + ?.trim() + + if (metadataSymbol) return metadataSymbol + + // Fallback to abbreviated address + return account.extensions.mint + ? abbreviateAddress(account.extensions.mint.publicKey) + : '' + }, [account, tokenMetadata]) + + const accountName = account.pubkey ? getAccountName(account.pubkey) : '' + const name = useMemo(() => + accountName || ( + account.extensions.transferAddress + ? abbreviateAddress(account.extensions.transferAddress as PublicKey) + : '' + ) + , [accountName, account.extensions.transferAddress]) + + const totalPrice = useMemo(() => { + if (!decimalAdjustedAmount || !priceData?.result?.price) return 0 + try { + return decimalAdjustedAmount * priceData.result.price + } catch (error) { + console.error('Error calculating total price:', error) + return 0 + } + }, [decimalAdjustedAmount, priceData]) + + const displayPrice = useMemo(() => { + if (!totalPrice) return '' + try { + return new BigNumber(totalPrice).toFormat(0) + } catch (error) { + console.error('Error formatting display price:', error) + return '' + } + }, [totalPrice]) + + return { + decimalAdjustedAmount, + amountFormatted, + name, + symbol, + totalPrice, + displayPrice, + logo: `https://jito.network/coinsByMint/${mintAddress}.webp`, + // logo: tokenMetadata?.image || '', // Use image instead of logoURI + mintPubkey, + mintAddress, + } +} \ No newline at end of file diff --git a/hooks/useTotalTreasuryPrice.ts b/hooks/useTotalTreasuryPrice.ts index e56ba44bb7..b4fbe048c8 100644 --- a/hooks/useTotalTreasuryPrice.ts +++ b/hooks/useTotalTreasuryPrice.ts @@ -1,3 +1,4 @@ +import { useMemo } from 'react' import { BN } from '@coral-xyz/anchor' import { getMintDecimalAmountFromNatural } from '@tools/sdk/units' import BigNumber from 'bignumber.js' @@ -15,12 +16,14 @@ export function useTotalTreasuryPrice() { auxiliaryTokenAccounts, } = useGovernanceAssets() - const mintsToFetch = [ - ...governedTokenAccountsWithoutNfts, - ...auxiliaryTokenAccounts, - ] - .filter((x) => typeof x.extensions.mint !== 'undefined') - .map((x) => x.extensions.mint!.publicKey) + const mintsToFetch = useMemo(() => { + return [ + ...governedTokenAccountsWithoutNfts, + ...auxiliaryTokenAccounts, + ] + .filter((x) => typeof x.extensions.mint !== 'undefined') + .map((x) => x.extensions.mint!.publicKey) + }, [governedTokenAccountsWithoutNfts, auxiliaryTokenAccounts]) const { data: prices } = useJupiterPricesByMintsQuery([ ...mintsToFetch, @@ -31,41 +34,49 @@ export function useTotalTreasuryPrice() { assetAccounts ) - const totalTokensPrice = [ - ...governedTokenAccountsWithoutNfts, - ...auxiliaryTokenAccounts, - ] - .filter((x) => typeof x.extensions.mint !== 'undefined') - .map((x) => { - return ( - getMintDecimalAmountFromNatural( - x.extensions.mint!.account, - new BN( - x.isSol - ? x.extensions.solAccount!.lamports - : x.isToken || x.type === AccountType.AUXILIARY_TOKEN - ? x.extensions.token!.account?.amount - : 0 - ) - ).toNumber() * - (prices?.[x.extensions.mint!.publicKey.toBase58()]?.price ?? 0) - ) - }) - .reduce((acc, val) => acc + val, 0) + const totalTokensPrice = useMemo(() => { + return [ + ...governedTokenAccountsWithoutNfts, + ...auxiliaryTokenAccounts, + ] + .filter((x) => typeof x.extensions.mint !== 'undefined') + .map((x) => { + return ( + getMintDecimalAmountFromNatural( + x.extensions.mint!.account, + new BN( + x.isSol + ? x.extensions.solAccount!.lamports + : x.isToken || x.type === AccountType.AUXILIARY_TOKEN + ? x.extensions.token!.account?.amount + : 0 + ) + ).toNumber() * + (prices?.[x.extensions.mint!.publicKey.toBase58()]?.price ?? 0) + ) + }) + .reduce((acc, val) => acc + val, 0) + }, [governedTokenAccountsWithoutNfts, auxiliaryTokenAccounts, prices]) - const stakeAccountsTotalPrice = assetAccounts - .filter((x) => x.extensions.stake) - .map((x) => { - return x.extensions.stake!.amount * (prices?.[WSOL_MINT]?.price ?? 0) - }) - .reduce((acc, val) => acc + val, 0) + const stakeAccountsTotalPrice = useMemo(() => { + return assetAccounts + .filter((x) => x.extensions.stake) + .map((x) => { + return x.extensions.stake!.amount * (prices?.[WSOL_MINT]?.price ?? 0) + }) + .reduce((acc, val) => acc + val, 0) + }, [assetAccounts, prices]) - const totalPrice = totalTokensPrice + stakeAccountsTotalPrice + const totalPrice = useMemo(() => { + return totalTokensPrice + stakeAccountsTotalPrice + }, [totalTokensPrice, stakeAccountsTotalPrice]) - const totalPriceFormatted = (governedTokenAccountsWithoutNfts.length - ? new BigNumber(totalPrice) - : new BigNumber(0) - ).plus(mangoAccountsValue) + const totalPriceFormatted = useMemo(() => { + return (governedTokenAccountsWithoutNfts.length + ? new BigNumber(totalPrice) + : new BigNumber(0) + ).plus(mangoAccountsValue) + }, [governedTokenAccountsWithoutNfts.length, totalPrice, mangoAccountsValue]) return { isFetching, diff --git a/hooks/useTreasuryInfo/assembleWallets.tsx b/hooks/useTreasuryInfo/assembleWallets.tsx index bf6c72f61b..91c699c8f9 100644 --- a/hooks/useTreasuryInfo/assembleWallets.tsx +++ b/hooks/useTreasuryInfo/assembleWallets.tsx @@ -214,7 +214,7 @@ export const assembleWallets = async ( upgradeAuthority: account.authority?.toBase58(), walletIsUpgradeAuthority: account.authority?.toBase58() === - walletMap[walletAddress].governanceAddress || + walletMap[walletAddress].governanceAddress || account.authority?.toBase58() === walletAddress, raw: p, })) @@ -272,8 +272,9 @@ export const assembleWallets = async ( ? getAccountName(wallet.governanceAddress) : getAccountName(wallet.address), totalValue: calculateTotalValue( - wallet.assets.map((asset) => - 'value' in asset ? asset.value : new BigNumber(0) + wallet.assets.map((asset) => { + return 'value' in asset ? asset.value : new BigNumber(0) + } ) ), }) @@ -326,16 +327,16 @@ export const assembleWallets = async ( const auxiliaryWallets: AuxiliaryWallet[] = auxiliaryAssets.length ? [ - { - assets: auxiliaryAssets, - name: 'Auxiliary Assets', - totalValue: calculateTotalValue( - auxiliaryAssets.map((asset) => - 'value' in asset ? asset.value : new BigNumber(0) - ) - ), - }, - ] + { + assets: auxiliaryAssets, + name: 'Auxiliary Assets', + totalValue: calculateTotalValue( + auxiliaryAssets.map((asset) => + 'value' in asset ? asset.value : new BigNumber(0) + ) + ), + }, + ] : [] const walletsToMerge = allWallets diff --git a/hooks/useTreasuryInfo/convertAccountToAsset.tsx b/hooks/useTreasuryInfo/convertAccountToAsset.tsx index 86611b7f62..183ca72c9e 100644 --- a/hooks/useTreasuryInfo/convertAccountToAsset.tsx +++ b/hooks/useTreasuryInfo/convertAccountToAsset.tsx @@ -10,6 +10,7 @@ import { abbreviateAddress } from '@utils/formatting' import { getAccountAssetCount } from './getAccountAssetCount' import { fetchJupiterPrice } from '@hooks/queries/jupiterPrice' import { getAccountValue, getStakeAccountValue } from './getAccountValue' +import { getAccountValueV2 } from './getAccountValueV2' export const convertAccountToAsset = async ( account: AssetAccount, @@ -52,7 +53,8 @@ export const convertAccountToAsset = async ( : undefined, } - case AccountType.SOL: + case AccountType.SOL: { + const { value, price } = await getAccountValueV2(account) return { type: AssetType.Sol, address: account.pubkey.toBase58(), @@ -63,17 +65,14 @@ export const convertAccountToAsset = async ( ) : ( ), - price: account.extensions.mint - ? new BigNumber( - (await fetchJupiterPrice(account.extensions.mint.publicKey)) - .result?.price ?? 0 - ) - : undefined, + price, raw: account, - value: getAccountValue(account), + value, } + } - case AccountType.TOKEN: + case AccountType.TOKEN: { + const { value, price } = await getAccountValueV2(account) return { type: AssetType.Token, address: account.pubkey.toBase58(), @@ -87,16 +86,12 @@ export const convertAccountToAsset = async ( logo: info.info?.logoURI, mintAddress: account.extensions.token?.account.mint.toBase58(), name: info.accountName || info.info?.name || info.name || info.symbol, - price: account.extensions.mint - ? new BigNumber( - (await fetchJupiterPrice(account.extensions.mint.publicKey)) - .result?.price ?? 0 - ) - : undefined, + price, raw: account, symbol: info.symbol, - value: getAccountValue(account), + value, } + } case AccountType.STAKE: return { diff --git a/hooks/useTreasuryInfo/getAccountValueV2.ts b/hooks/useTreasuryInfo/getAccountValueV2.ts new file mode 100644 index 0000000000..775ffa6a45 --- /dev/null +++ b/hooks/useTreasuryInfo/getAccountValueV2.ts @@ -0,0 +1,22 @@ +import { BigNumber } from 'bignumber.js' +import { AssetAccount } from '@utils/uiTypes/assets' +import { getAccountAssetCount } from './getAccountAssetCount' +import { fetchJupiterPrice } from '../queries/jupiterPrice' + +export const getAccountValueV2 = async (account: AssetAccount) => { + if (!account.extensions.mint) { + return { + value: new BigNumber(0), + price: new BigNumber(0), + } + } + + const count = getAccountAssetCount(account) + const priceObj = await fetchJupiterPrice(account.extensions.mint.publicKey) + const price = priceObj?.found && priceObj?.result?.price ? priceObj.result.price : 0 + return { + value: count.multipliedBy(new BigNumber(price)), + price: new BigNumber(price), + } +} + diff --git a/utils/services/tokenPrice.tsx b/utils/services/tokenPrice.tsx index 1cc3bf6f09..cbe75852bb 100644 --- a/utils/services/tokenPrice.tsx +++ b/utils/services/tokenPrice.tsx @@ -10,7 +10,7 @@ import { USDC_MINT } from '@blockworks-foundation/mango-v4' //this service provide prices it is not recommended to get anything more from here besides token name or price. //decimals from metadata can be different from the realm on chain one -const priceEndpoint = 'https://price.jup.ag/v4/price' +const priceEndpoint = 'https://api.jup.ag/price/v2' const tokenListUrl = 'https://token.jup.ag/strict' export type TokenInfoWithoutDecimals = Omit @@ -69,10 +69,11 @@ class TokenPriceService { ...keyValue, } } catch (e) { - notify({ - type: 'error', - message: 'unable to fetch token prices', - }) + console.log("Price error", e) + // notify({ + // type: 'error', + // message: 'unable to fetch token prices', + // }) } } const USDC_MINT_BASE = USDC_MINT.toBase58() diff --git a/utils/treasuryTools.tsx b/utils/treasuryTools.tsx index 6f57cd095d..73f3281aaf 100644 --- a/utils/treasuryTools.tsx +++ b/utils/treasuryTools.tsx @@ -68,6 +68,5 @@ export const getTreasuryAccountItemInfoV2 = (account: AssetAccount) => { info, symbol, totalPrice, - } } diff --git a/utils/treasuryToolsV3.tsx b/utils/treasuryToolsV3.tsx new file mode 100644 index 0000000000..b70f5eeb50 --- /dev/null +++ b/utils/treasuryToolsV3.tsx @@ -0,0 +1,61 @@ +import { getAccountName, WSOL_MINT } from '@components/instructions/tools' +import { BN } from '@coral-xyz/anchor' +import { PublicKey } from '@solana/web3.js' +import { getMintDecimalAmountFromNatural } from '@tools/sdk/units' +import BigNumber from 'bignumber.js' +import { abbreviateAddress } from './formatting' +import tokenPriceService from './services/tokenPrice' +import { AccountType, AssetAccount } from './uiTypes/assets' + +export const getTreasuryAccountItemInfoV3 = (account: AssetAccount) => { + const mintAddress = + account.type === AccountType.SOL + ? WSOL_MINT + : account.extensions.mint?.publicKey.toBase58() + + const amount = + account.extensions.amount && account.extensions.mint + ? getMintDecimalAmountFromNatural( + account.extensions.mint.account, + new BN( + account.isSol + ? account.extensions.solAccount!.lamports + : account.extensions.amount + ) + ).toNumber() + : 0 + const info = tokenPriceService.getTokenInfo(mintAddress!) + + const symbol = + account.type === AccountType.NFT + ? 'NFTS' + : account.type === AccountType.SOL + ? 'SOL' + : info?.symbol + ? info.address === WSOL_MINT + ? 'wSOL' + : info?.symbol + : account.extensions.mint + ? abbreviateAddress(account.extensions.mint.publicKey) + : '' + const amountFormatted = new BigNumber(amount).toFormat() + + const logo = info?.logoURI || '' + const accountName = account.pubkey ? getAccountName(account.pubkey) : '' + const name = accountName + ? accountName + : account.extensions.transferAddress + ? abbreviateAddress(account.extensions.transferAddress as PublicKey) + : '' + + return { + decimalAdjustedAmount: amount, + accountName, + amountFormatted, + // logo: mintAddress ? `https://jito.network/coinsByMint/${mintAddress}.webp` : logo ? logo : undefined, + logo: logo || '', + name, + info, + symbol, + } +} diff --git a/utils/uiTypes/assets.ts b/utils/uiTypes/assets.ts index 5305d424ec..33ffc0da87 100644 --- a/utils/uiTypes/assets.ts +++ b/utils/uiTypes/assets.ts @@ -19,6 +19,7 @@ interface AccountExtension { export type GovernanceProgramAccountWithNativeTreasuryAddress = ProgramAccount & { nativeTreasuryAddress: PublicKey } + export interface AssetAccount { governance: GovernanceProgramAccountWithNativeTreasuryAddress pubkey: PublicKey @@ -29,6 +30,18 @@ export interface AssetAccount { isToken?: boolean } +export type TreasuryAccountItemInfo = { + decimalAdjustedAmount: number + amountFormatted: string + name: string + symbol: string + totalPrice?: number + displayPrice?: string + logo: string + mintPubkey?: PublicKey + mintAddress?: string +} + export enum AccountType { TOKEN, SOL,