diff --git a/components/App.tsx b/components/App.tsx
index a199520b8..c066ce559 100644
--- a/components/App.tsx
+++ b/components/App.tsx
@@ -105,7 +105,7 @@ export function AppContents(props: Props) {
handleRouterHistory()
useHandleGovernanceAssetsStore()
useEffect(() => {
- tokenPriceService.fetchSolanaTokenList()
+ tokenPriceService.fetchSolanaTokenListV2()
}, [])
const { getOwnedDeposits, resetDepositState } = useDepositStore()
diff --git a/components/TreasuryAccount/AccountItem.tsx b/components/TreasuryAccount/AccountItem.tsx
index 4fdd1e30c..d9f712255 100644
--- a/components/TreasuryAccount/AccountItem.tsx
+++ b/components/TreasuryAccount/AccountItem.tsx
@@ -1,5 +1,5 @@
-import { useMemo } from 'react'
-import { getTreasuryAccountItemInfoV2 } from '@utils/treasuryTools'
+import { useEffect, useState, useMemo } from 'react'
+import { getTreasuryAccountItemInfoV2Async } from '@utils/treasuryTools'
import { AssetAccount } from '@utils/uiTypes/assets'
import TokenIcon from '@components/treasuryV2/icons/TokenIcon'
import { useTokenMetadata } from '@hooks/queries/tokenMetadata'
@@ -9,23 +9,38 @@ const AccountItem = ({
}: {
governedAccountTokenAccount: AssetAccount
}) => {
- const {
- amountFormatted,
- logo,
- name,
- symbol,
- displayPrice,
- } = getTreasuryAccountItemInfoV2(governedAccountTokenAccount)
+ const [accountAssetInfo, setAccountAssetInfo] = useState({
+ amountFormatted: '',
+ logo: '',
+ name: '',
+ symbol: '',
+ displayPrice: '',
+ })
+
+ useEffect(() => {
+ const fetchAccounAssetInfo = async () => {
+ try {
+ const info = await getTreasuryAccountItemInfoV2Async(governedAccountTokenAccount)
+ setAccountAssetInfo(info)
+ } catch (error) {
+ console.error('Error fetching treasury account info:', error)
+ }
+ }
+
+ fetchAccounAssetInfo()
+ }, [governedAccountTokenAccount])
const { data } = useTokenMetadata(
governedAccountTokenAccount.extensions.mint?.publicKey,
- !logo
+ !accountAssetInfo.logo
)
-
+
const symbolFromMeta = useMemo(() => {
return data?.symbol
}, [data?.symbol])
+ const { amountFormatted, logo, name, symbol, displayPrice } = accountAssetInfo
+
return (
([])
+ const [isLoading, setIsLoading] = useState(true)
+
+ useEffect(() => {
+ const sortAccounts = async () => {
+ try {
+ setIsLoading(true)
+ const accounts = [
+ ...governedTokenAccountsWithoutNfts,
+ ...auxiliaryTokenAccounts,
+ ]
+
+ // Get all account info in parallel
+ const accountsWithInfo = await Promise.all(
+ accounts.map(async (account) => ({
+ account,
+ info: await getTreasuryAccountItemInfoV2Async(account)
+ }))
+ )
+
+ // Sort based on the fetched info
+ const sorted = accountsWithInfo
+ .sort((a, b) => b.info.totalPrice - a.info.totalPrice)
+ .map(({ account }) => account)
+ .splice(
+ 0,
+ Number(process?.env?.MAIN_VIEW_SHOW_MAX_TOP_TOKENS_NUM || accounts.length)
+ )
+
+ setSortedAccounts(sorted)
+ } catch (error) {
+ console.error('Error sorting accounts:', error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ sortAccounts()
+ }, [])
+
+ if (isLoading) {
+ return
+ }
+
return (
- {accountsSorted.map((account) => {
- return (
-
- )
- })}
+ {sortedAccounts.map((account) => (
+
+ ))}
)
}
-export default AccountsItems
+export default AccountsItems
\ No newline at end of file
diff --git a/components/instructions/programs/stake.tsx b/components/instructions/programs/stake.tsx
index 4b97a7b49..cbc50f8e6 100644
--- a/components/instructions/programs/stake.tsx
+++ b/components/instructions/programs/stake.tsx
@@ -17,7 +17,12 @@ export const STAKE_INSTRUCTIONS = {
BufferLayout.u32('instruction'),
BufferLayout.ns64('lamports'),
]).decode(Buffer.from(_data))
- const rent = await _connection.getMinimumBalanceForRentExemption(200)
+
+ // const rent = await _connection.getMinimumBalanceForRentExemption(200)
+
+ // > solana rent 200 --lamports
+ // Rent-exempt minimum: 2282880 lamports
+ const rent = 2282880
return (
<>
diff --git a/hooks/queries/jupiterPrice.ts b/hooks/queries/jupiterPrice.ts
index 0c153837c..8fcc90d43 100644
--- a/hooks/queries/jupiterPrice.ts
+++ b/hooks/queries/jupiterPrice.ts
@@ -2,23 +2,51 @@ import { PublicKey } from '@solana/web3.js'
import { useQuery } from '@tanstack/react-query'
import queryClient from './queryClient'
-const URL = 'https://price.jup.ag/v4/price'
+const URL = 'https://api.jup.ag/price/v2'
/* example query
-GET https://price.jup.ag/v4/price?ids=SOL
-response: {"data":{"SOL":{"id":"So11111111111111111111111111111111111111112","mintSymbol":"SOL","vsToken":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v","vsTokenSymbol":"USDC","price":26.649616441}},"timeTaken":0.0002587199999766199}
+# Unit price of 1 JUP & 1 SOL based on the Derived Price in USDC
+https://api.jup.ag/price/v2?ids=JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN,So11111111111111111111111111111111111111112
+
+{
+ "data": {
+ "So11111111111111111111111111111111111111112": {
+ "id": "So11111111111111111111111111111111111111112",
+ "type": "derivedPrice",
+ "price": "133.890945000"
+ },
+ "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN": {
+ "id": "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
+ "type": "derivedPrice",
+ "price": "0.751467"
+ }
+ },
+ "timeTaken": 0.00395219
+}
*/
/* example intentionally broken query
-GET https://price.jup.ag/v4/price?ids=bingus
-response: {"data":{},"timeTaken":0.00010941000005004753}
+curl -X 'GET' 'https://api.jup.ag/price/v2?ids=So11111111111111111111111111111111111111112&showExtraInfo=true'
+{
+ "data": {
+ "So11111111111111111111111111111111111111112": {
+ "id": "So11111111111111111111111111111111111111112",
+ "type": "derivedPrice",
+ "price": "134.170633378"
+ },
+ "8agCopCHWdpj7mHk3JUWrzt8pHAxMiPX5hLVDJh9TXWv": null
+ },
+ "timeTaken": 0.003186833
+}
*/
type Price = {
id: string // pubkey,
- mintSymbol: string
- vsToken: string // pubkey,
- vsTokenSymbol: string
+ // price is in USD
price: number
+ // removed in v2 API
+ // mintSymbol: string
+ // vsToken: string // pubkey,
+ // vsTokenSymbol: string
}
type Response = {
data: Record //uses whatever you input (so, pubkey OR symbol). no entry if data not found
@@ -76,11 +104,13 @@ export const getJupiterPriceSync = (mint: PublicKey) =>
export const useJupiterPricesByMintsQuery = (mints: PublicKey[]) => {
const enabled = mints.length > 0
+ const deduped = new Set(mints)
+ const dedupedMints = Array.from(deduped)
return useQuery({
enabled,
- queryKey: jupiterPriceQueryKeys.byMints(mints),
+ queryKey: jupiterPriceQueryKeys.byMints(dedupedMints),
queryFn: async () => {
- const batches = [...chunks(mints, 100)]
+ const batches = [...chunks(dedupedMints, 100)]
const responses = await Promise.all(
batches.map(async (batch) => {
const x = await fetch(`${URL}?ids=${batch.join(',')}`)
@@ -106,7 +136,7 @@ export const useJupiterPricesByMintsQuery = (mints: PublicKey[]) => {
return data
},
onSuccess: (data) => {
- mints.forEach((mint) =>
+ dedupedMints.forEach((mint) =>
queryClient.setQueryData(
jupiterPriceQueryKeys.byMint(mint),
data[mint.toString()]
@@ -117,3 +147,30 @@ export const useJupiterPricesByMintsQuery = (mints: PublicKey[]) => {
},
})
}
+
+// function is used to get fresh token prices
+export const getJupiterPricesByMintStrings = async (mints: string[]) => {
+ if (mints.length === 0) return {}
+ const deduped = new Set(mints)
+ const dedupedMints = Array.from(deduped)
+ try {
+ const x = await fetch(`${URL}?ids=${dedupedMints.join(',')}`)
+ const response = (await x.json()) as Response
+ const data = response.data
+
+ //override chai price if its broken
+ const chaiMint = '3jsFX1tx2Z8ewmamiwSU851GzyzM2DJMq7KWW5DM8Py3'
+ const chaiData = data[chaiMint]
+
+ if (chaiData?.price && (chaiData.price > 1.3 || chaiData.price < 0.9)) {
+ data[chaiMint] = {
+ ...chaiData,
+ price: 1,
+ }
+ }
+ return data
+ } catch (error) {
+ console.error('Error fetching Jupiter prices:', error)
+ throw error
+ }
+}
\ No newline at end of file
diff --git a/hooks/useLocalStorage.ts b/hooks/useLocalStorage.ts
new file mode 100644
index 000000000..8d87fe0a0
--- /dev/null
+++ b/hooks/useLocalStorage.ts
@@ -0,0 +1,33 @@
+type UseStorageReturnValue = {
+ getItem: (key: string) => string;
+ setItem: (key: string, value: string) => boolean;
+ removeItem: (key: string) => void;
+ };
+
+ export const useLocalStorage = (): UseStorageReturnValue => {
+ const isBrowser: boolean = ((): boolean => typeof window !== "undefined")();
+
+ const getItem = (key: string): string => {
+ return isBrowser ? window.localStorage[key] : "";
+ };
+
+ const setItem = (key: string, value: string): boolean => {
+ if (isBrowser) {
+ window.localStorage.setItem(key, value);
+ return true;
+ }
+
+ return false;
+ };
+
+ const removeItem = (key: string): void => {
+ window.localStorage.removeItem(key);
+ };
+
+ return {
+ getItem,
+ setItem,
+ removeItem,
+ };
+ };
+
\ No newline at end of file
diff --git a/hooks/useTreasuryInfo/convertAccountToAsset.tsx b/hooks/useTreasuryInfo/convertAccountToAsset.tsx
index 86611b7f6..a03f0446a 100644
--- a/hooks/useTreasuryInfo/convertAccountToAsset.tsx
+++ b/hooks/useTreasuryInfo/convertAccountToAsset.tsx
@@ -2,7 +2,7 @@ import { BigNumber } from 'bignumber.js'
import { AccountType, AssetAccount } from '@utils/uiTypes/assets'
import { AssetType, Asset } from '@models/treasury/Asset'
-import { getTreasuryAccountItemInfoV2 } from '@utils/treasuryTools'
+import { getTreasuryAccountItemInfoV2Async } from '@utils/treasuryTools'
import TokenIcon from '@components/treasuryV2/icons/TokenIcon'
import { WSOL_MINT } from '@components/instructions/tools'
import { abbreviateAddress } from '@utils/formatting'
@@ -10,13 +10,14 @@ import { abbreviateAddress } from '@utils/formatting'
import { getAccountAssetCount } from './getAccountAssetCount'
import { fetchJupiterPrice } from '@hooks/queries/jupiterPrice'
import { getAccountValue, getStakeAccountValue } from './getAccountValue'
+import tokenPriceService from '@utils/services/tokenPrice'
export const convertAccountToAsset = async (
account: AssetAccount,
councilMintAddress?: string,
communityMintAddress?: string
): Promise => {
- const info = getTreasuryAccountItemInfoV2(account)
+ const info = await getTreasuryAccountItemInfoV2Async(account)
switch (account.type) {
case AccountType.AUXILIARY_TOKEN:
@@ -65,8 +66,7 @@ export const convertAccountToAsset = async (
),
price: account.extensions.mint
? new BigNumber(
- (await fetchJupiterPrice(account.extensions.mint.publicKey))
- .result?.price ?? 0
+ await tokenPriceService.fetchTokenPrice(account.extensions.mint.publicKey.toString()) ?? 0
)
: undefined,
raw: account,
@@ -89,8 +89,7 @@ export const convertAccountToAsset = async (
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
+ await tokenPriceService.fetchTokenPrice(account.extensions.mint.publicKey.toString()) ?? 0
)
: undefined,
raw: account,
diff --git a/hooks/useTreasuryInfo/index.tsx b/hooks/useTreasuryInfo/index.tsx
index b9f3d8b8a..e28b235f2 100644
--- a/hooks/useTreasuryInfo/index.tsx
+++ b/hooks/useTreasuryInfo/index.tsx
@@ -67,7 +67,6 @@ export default function useTreasuryInfo(
if (!loadingGovernedAccounts && accounts.length && getNftsAndDomains) {
setDomainsLoading(true)
setBuildingWallets(true)
-
getDomains(
accounts.filter((acc) => acc.isSol),
connection.current
diff --git a/hub/components/GlobalStats/data/index.ts b/hub/components/GlobalStats/data/index.ts
index 6347d7f7d..8146e645a 100644
--- a/hub/components/GlobalStats/data/index.ts
+++ b/hub/components/GlobalStats/data/index.ts
@@ -275,8 +275,8 @@ export async function fetchData(
logger.log('fetching tokens and prices...');
logger.log(`token count: ${tokenAmountMap.size}`);
-
- await tokenPriceService.fetchSolanaTokenList();
+ // already called inside fetchTokenPrices()
+ // await tokenPriceService.fetchSolanaTokenListV2();
const relevantTokens = Array.from(tokenAmountMap.keys()).filter(
(key) => !!tokenAmountMap.get(key)?.isGreaterThan(1),
);
diff --git a/pages/api/daoStatistics.api.ts b/pages/api/daoStatistics.api.ts
index dc59aa37e..2f43304e6 100644
--- a/pages/api/daoStatistics.api.ts
+++ b/pages/api/daoStatistics.api.ts
@@ -166,7 +166,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
console.log('fetching tokens and prices...')
console.log('token count', tokenAmountMap.size)
- await tokenPriceService.fetchSolanaTokenList()
+ // already called inside fetchTokenPrices()
+ // await tokenPriceService.fetchSolanaTokenListV2()
for (const chunk of chunks([...tokenAmountMap.keys()], 50)) {
await tokenPriceService.fetchTokenPrices(chunk)
diff --git a/pages/dao/[symbol]/params/components/AccountsView.tsx b/pages/dao/[symbol]/params/components/AccountsView.tsx
index 46005357e..94298356d 100644
--- a/pages/dao/[symbol]/params/components/AccountsView.tsx
+++ b/pages/dao/[symbol]/params/components/AccountsView.tsx
@@ -1,4 +1,4 @@
-import { getTreasuryAccountItemInfoV2 } from '@utils/treasuryTools'
+import { getTreasuryAccountItemInfoV2Async } from '@utils/treasuryTools'
import { AccountType } from '@utils/uiTypes/assets'
import { AddressField } from '../index'
import useGovernanceAssets from '@hooks/useGovernanceAssets'
@@ -20,8 +20,8 @@ const AccountsView = ({
activeGovernance.pubkey.toBase58()
: auxiliaryMode
)
- .map((x) => {
- const info = getTreasuryAccountItemInfoV2(x)
+ .map(async (x) => {
+ const info = await getTreasuryAccountItemInfoV2Async(x)
if (x.isToken || x.isSol || x.type === AccountType.AUXILIARY_TOKEN) {
return (
{
+ const getAssetAccountMetadata = async (
+ mintAssetAccount: AssetAccount,
+ base: boolean
+ ) => {
+ const {
+ logo,
+ name,
+ symbol,
+ displayPrice,
+ } = await getTreasuryAccountItemInfoV2Async(mintAssetAccount)
+ if (base) {
+ setBaseMetadata({
+ logo,
+ name,
+ symbol,
+ displayPrice,
+ decimals: mintAssetAccount.extensions.mint!.account.decimals,
+ })
+ } else {
+ setQuoteMetadata({
+ logo,
+ name,
+ symbol,
+ displayPrice,
+ decimals: mintAssetAccount.extensions.mint!.account.decimals,
+ })
+ }
+ }
function getInstruction(): Promise
{
return getConfigInstruction({
connection,
@@ -75,37 +104,21 @@ const StakingOption = ({
{ governedAccount: governedAccount, getInstruction },
index
)
- if (form.baseTreasury && form.baseTreasury.extensions.mint && form.baseTreasury.extensions.mint.account.decimals) {
- const {
- logo,
- name,
- symbol,
- displayPrice,
- } = getTreasuryAccountItemInfoV2(form.baseTreasury)
- setBaseMetadata({
- logo,
- name,
- symbol,
- displayPrice,
- decimals: form.baseTreasury.extensions.mint.account.decimals
- })
+ if (
+ form.baseTreasury &&
+ form.baseTreasury.extensions.mint &&
+ form.baseTreasury.extensions.mint.account.decimals
+ ) {
+ getAssetAccountMetadata(form.baseTreasury, true)
} else {
setBaseMetadata(undefined)
}
- if (form.quoteTreasury && form.quoteTreasury.extensions.mint && form.quoteTreasury.extensions.mint.account.decimals) {
- const {
- logo,
- name,
- symbol,
- displayPrice,
- } = getTreasuryAccountItemInfoV2(form.quoteTreasury)
- setQuoteMetadata({
- logo,
- name,
- symbol,
- displayPrice,
- decimals: form.quoteTreasury.extensions.mint.account.decimals
- })
+ if (
+ form.quoteTreasury &&
+ form.quoteTreasury.extensions.mint &&
+ form.quoteTreasury.extensions.mint.account.decimals
+ ) {
+ getAssetAccountMetadata(form.quoteTreasury, false)
} else {
setQuoteMetadata(undefined)
}
@@ -255,26 +268,27 @@ const StakingOption = ({
{baseMetadata && quoteMetadata && (
<>
- {form.strike / form.lotSize * 10 ** (-quoteMetadata.decimals + baseMetadata.decimals) * (Number(form.numTokens) / 10 ** baseMetadata.decimals)}
-
{
- currentTarget.onerror = null // prevents looping
- currentTarget.hidden = true
- }}
- />
- =
- {Number(form.numTokens) / 10 ** baseMetadata.decimals}
-
{
- currentTarget.onerror = null // prevents looping
- currentTarget.hidden = true
- }}
- />
- div >
+ {(form.strike / form.lotSize) *
+ 10 ** (-quoteMetadata.decimals + baseMetadata.decimals) *
+ (Number(form.numTokens) / 10 ** baseMetadata.decimals)}
+
{
+ currentTarget.onerror = null // prevents looping
+ currentTarget.hidden = true
+ }}
+ />
+ ={Number(form.numTokens) / 10 ** baseMetadata.decimals}
+
{
+ currentTarget.onerror = null // prevents looping
+ currentTarget.hidden = true
+ }}
+ />
+
>
)}
>
diff --git a/stores/useGovernanceAssetsStore.tsx b/stores/useGovernanceAssetsStore.tsx
index 766079496..2427e584a 100644
--- a/stores/useGovernanceAssetsStore.tsx
+++ b/stores/useGovernanceAssetsStore.tsx
@@ -546,10 +546,12 @@ const getSolAccountObj = async (
}
}
- const minRentAmount = await connection.current.getMinimumBalanceForRentExemption(
- 0
- )
-
+ // const minRentAmount = await connection.current.getMinimumBalanceForRentExemption(
+ // 0
+ // )
+ // > solana rent 0 --lamports
+ // Rent-exempt minimum: 890880 lamports
+ const minRentAmount = 890880
const solAccount = acc as AccountInfoGen
solAccount.lamports =
diff --git a/utils/services/tokenPrice.tsx b/utils/services/tokenPrice.tsx
index 1cc3bf6f0..2a649c187 100644
--- a/utils/services/tokenPrice.tsx
+++ b/utils/services/tokenPrice.tsx
@@ -7,11 +7,14 @@ import overrides from 'public/realms/token-overrides.json'
import { Price, TokenInfo } from './types'
import { chunks } from '@utils/helpers'
import { USDC_MINT } from '@blockworks-foundation/mango-v4'
+import { useLocalStorage } from '@hooks/useLocalStorage'
+import { getJupiterPricesByMintStrings } from '@hooks/queries/jupiterPrice'
//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 tokenListUrl = 'https://token.jup.ag/strict'
+// const priceEndpoint = 'https://price.jup.ag/v4/price'
+const tokenListUrl = 'https://tokens.jup.ag/tokens?tags=verified'
+const CACHE_TTL_MS = 1000 * 60 * 60 * 24 // 24 hours
export type TokenInfoWithoutDecimals = Omit
@@ -21,9 +24,12 @@ class TokenPriceService {
_tokenPriceToUSDlist: {
[mintAddress: string]: Price
}
+ _unverifiedTokenCache: { [mintAddress: string]: TokenInfoWithoutDecimals }
+
constructor() {
this._tokenList = []
this._tokenPriceToUSDlist = {}
+ this._unverifiedTokenCache = {}
}
async fetchSolanaTokenList() {
try {
@@ -48,20 +54,76 @@ class TokenPriceService {
})
}
}
+
+ async fetchSolanaTokenListV2(): Promise {
+ const storage = useLocalStorage()
+ const tokenListRaw = storage.getItem('tokenList')
+ const ttl = storage.getItem('tokenListTTL')
+
+ let tokenList: TokenInfo[] = []
+
+ try {
+ // If the data is not in storage, or the TTL has expired
+ if (!tokenListRaw || !ttl || Date.now() > Number(ttl)) {
+ // Fetch fresh data
+ const response = await axios.get(tokenListUrl)
+ const tokens = response.data as TokenInfo[]
+
+ if (tokens && tokens.length) {
+ tokenList = tokens.map((token) => {
+ const override = overrides[token.address]
+
+ if (override) {
+ return mergeDeepRight(token, override)
+ }
+
+ return token
+ })
+
+ // Update cache and TTL
+ storage.setItem('tokenList', JSON.stringify(tokenList))
+ storage.setItem('tokenListTTL', String(Date.now() + CACHE_TTL_MS))
+ }
+ } else {
+ // Use cached data
+ tokenList = JSON.parse(tokenListRaw)
+ }
+
+ this._tokenList = tokenList
+ return tokenList
+ } catch (e) {
+ console.log(e)
+ notify({
+ type: 'error',
+ message: 'Unable to fetch token list',
+ })
+ return []
+ }
+ }
+
async fetchTokenPrices(mintAddresses: string[]) {
if (mintAddresses.length) {
+ const tokenList = await this.fetchSolanaTokenListV2()
+ const symbolMap = Object.fromEntries(
+ tokenList.map((token) => [token.address, token.symbol])
+ )
+
//can query only 100 at once
const mintAddressesWithSol = chunks([...mintAddresses, WSOL_MINT], 100)
for (const mintChunk of mintAddressesWithSol) {
- const symbols = mintChunk.join(',')
try {
- const response = await axios.get(`${priceEndpoint}?ids=${symbols}`)
- const priceToUsd: Price[] = response?.data?.data
- ? Object.values(response.data.data)
+ const response = await getJupiterPricesByMintStrings(mintChunk)
+ const priceToUsd: Price[] = response
+ ? Object.entries(response).map(([address, data]) => ({
+ ...data,
+ mintSymbol: symbolMap[address] || 'Unknown',
+ vsToken: USDC_MINT.toBase58(),
+ vsTokenSymbol: 'USDC',
+ }))
: []
+
const keyValue = Object.fromEntries(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- Object.entries(priceToUsd).map(([key, val]) => [val.id, val])
+ priceToUsd.map((val) => [val.id, val])
)
this._tokenPriceToUSDlist = {
@@ -69,12 +131,14 @@ class TokenPriceService {
...keyValue,
}
} catch (e) {
+ console.error(e)
notify({
type: 'error',
message: 'unable to fetch token prices',
})
}
}
+
const USDC_MINT_BASE = USDC_MINT.toBase58()
if (!this._tokenPriceToUSDlist[USDC_MINT_BASE]) {
this._tokenPriceToUSDlist[USDC_MINT_BASE] = {
@@ -98,6 +162,36 @@ class TokenPriceService {
}
}
}
+ async fetchTokenPrice(mintAddress: string): Promise {
+ try {
+ if (this._tokenPriceToUSDlist[mintAddress]) {
+ return this._tokenPriceToUSDlist[mintAddress].price
+ }
+
+ const response = await getJupiterPricesByMintStrings([mintAddress])
+
+ if (!response || !response[mintAddress]) {
+ return null
+ }
+
+ let tokenInfo: TokenInfoWithoutDecimals | undefined =
+ this._unverifiedTokenCache[mintAddress] || undefined
+ if (!tokenInfo) {
+ tokenInfo = (await this.getTokenInfoAsync(mintAddress)) || undefined
+ }
+ this._tokenPriceToUSDlist[mintAddress] = {
+ id: mintAddress,
+ mintSymbol: tokenInfo?.symbol || 'Unknown',
+ price: response[mintAddress].price,
+ vsToken: USDC_MINT.toBase58(),
+ vsTokenSymbol: 'USDC',
+ }
+ return response[mintAddress].price
+ } catch (e) {
+ console.error('Error fetching token price:', e)
+ return null
+ }
+ }
/**
* @deprecated
* seriously do not use this. use fetchJupiterPrice
@@ -114,6 +208,64 @@ class TokenPriceService {
)
return tokenListRecord
}
+
+ // This async method is used to lookup additional tokens not on JUP's strict list
+ async getTokenInfoAsync(
+ mintAddress: string
+ ): Promise {
+ let tokenInfo: TokenInfoWithoutDecimals | undefined =
+ this._unverifiedTokenCache[mintAddress] || undefined
+
+ if (tokenInfo) {
+ return tokenInfo
+ }
+ if (!mintAddress || mintAddress.trim() === '') {
+ return undefined
+ }
+ // Check the strict token list first
+ let tokenListRecord = this._tokenList?.find(
+ (x) => x.address === mintAddress
+ )
+ if (tokenListRecord) {
+ return tokenListRecord
+ }
+ // Check the unverified token list cache next to avoid repeatedly loading token metadata
+ if (this._unverifiedTokenCache[mintAddress]) {
+ return this._unverifiedTokenCache[mintAddress]
+ }
+ // Get the token data from JUP's api
+ try {
+ const requestURL = `https://tokens.jup.ag/token/${mintAddress}`
+ const response = await axios.get(requestURL)
+ if (response.data) {
+ // Remove decimals and add chainId to match the TokenInfoWithoutDecimals struct
+ const { decimals, ...tokenInfoWithoutDecimals } = response.data
+
+ const finalTokenInfo = {
+ ...tokenInfoWithoutDecimals,
+ chainId: 101,
+ }
+ // Add to unverified token cache
+ this._unverifiedTokenCache[mintAddress] = finalTokenInfo
+ return finalTokenInfo
+ } else {
+ console.error(`Metadata retrieving failed for ${mintAddress}`)
+ return undefined
+ }
+ } catch {
+ console.error(`Metadata retrieving failed for ${mintAddress}`)
+ return undefined
+ }
+ }
+ catch(e) {
+ console.error(e)
+ notify({
+ type: 'error',
+ message: 'Unable to fetch token information',
+ })
+ return undefined
+ }
+
/**
* For decimals use on chain tryGetMint
*/
diff --git a/utils/treasuryTools.tsx b/utils/treasuryTools.tsx
index dcb18f659..460c465f8 100644
--- a/utils/treasuryTools.tsx
+++ b/utils/treasuryTools.tsx
@@ -4,9 +4,20 @@ 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 tokenPriceService, { TokenInfoWithoutDecimals } from './services/tokenPrice'
import { AccountType, AssetAccount } from './uiTypes/assets'
+interface TreasuryAccountInfo {
+ accountName: string;
+ amountFormatted: string;
+ logo: string;
+ name: string;
+ displayPrice: string;
+ info: TokenInfoWithoutDecimals | undefined;
+ symbol: string;
+ totalPrice: number;
+}
+
export const getTreasuryAccountItemInfoV2 = (account: AssetAccount) => {
const mintAddress =
account.type === AccountType.SOL
@@ -30,7 +41,6 @@ export const getTreasuryAccountItemInfoV2 = (account: AssetAccount) => {
? new BigNumber(totalPrice).toFormat(0)
: ''
const info = tokenPriceService.getTokenInfo(mintAddress!)
-
const symbol =
account.type === AccountType.NFT
? 'NFTS'
@@ -69,3 +79,101 @@ export const getTreasuryAccountItemInfoV2 = (account: AssetAccount) => {
totalPrice,
}
}
+const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes in milliseconds
+
+export const getTreasuryAccountItemInfoV2Async = async (account: AssetAccount) => {
+ const cacheKey = `tokenAccountInfoItem_${account.pubkey.toString()}`;
+
+ // Check cache first
+ const cachedData = localStorage.getItem(cacheKey);
+ const cacheTTL = localStorage.getItem("tokenAccountInfoItem_ttl");
+
+ // If we have valid cached data, return it
+ if (cachedData && cacheTTL && Date.now() < Number(cacheTTL)) {
+ return JSON.parse(cachedData) as TreasuryAccountInfo;
+ }
+
+ // If no cache or expired, fetch fresh data
+ // this is safe because we are using a browser local storage
+ await tokenPriceService.fetchSolanaTokenListV2();
+
+ 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;
+
+ let price = tokenPriceService.getUSDTokenPrice(mintAddress!) || null;
+ if (!price) {
+ price = await tokenPriceService.fetchTokenPrice(mintAddress!) || 0;
+ }
+
+ const totalPrice = amount * price;
+ const totalPriceFormatted = amount
+ ? new BigNumber(totalPrice).toFormat(0)
+ : '';
+
+ let info = tokenPriceService.getTokenInfo(mintAddress!);
+ if (!info) {
+ info = await tokenPriceService.getTokenInfoAsync(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)
+ : '';
+
+ const displayPrice =
+ totalPriceFormatted && totalPriceFormatted !== '0'
+ ? totalPriceFormatted
+ : '';
+
+ const result: TreasuryAccountInfo = {
+ accountName,
+ amountFormatted,
+ logo,
+ name,
+ displayPrice,
+ info,
+ symbol,
+ totalPrice,
+ };
+
+ try {
+ // Store in cache with TTL
+ localStorage.setItem(cacheKey, JSON.stringify(result));
+ localStorage.setItem(`tokenAccountInfoItem_ttl`, String(Date.now() + CACHE_TTL_MS));
+ } catch (e) {
+ console.warn('Failed to cache treasury account info:', e);
+ }
+
+ return result;
+};
\ No newline at end of file