@shogun-sdk/money-legos
Version:
Shogun Money Legos: clients and types for quotes, memes, prices, balances, fees, validations, etc.
731 lines • 31.3 kB
JavaScript
import { GraphQL } from '@codex-data/sdk';
import { AccountLayout, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { PublicKey } from '@solana/web3.js';
import { formatUnits } from 'viem';
import { arbitrum, base, berachain, avalanche, bsc, mainnet, sonic, polygon } from 'viem/chains';
import { CHAIN_CONFIGS, CHAIN_MAP, EVM_CHAIN_IDS, isEVMChain, isNativeAddress, SOLANA_CHAIN_ID, } from '../config/chains.js';
import { compareAddresses, compareChains } from '../utils/address.js';
import { NATIVE_TOKEN } from '../config/addresses.js';
import { getEvmJsonRpcProvider, getSolanaProvider } from '../utils/rpc.js';
import { erc20Abi } from '../config/index.js';
import { getNormalizedSolanaToken } from './solana.js';
import { HyperEVM } from '../utils/viemChains.custom.js';
const TokenRankingAttribute = GraphQL.TokenRankingAttribute;
const RankingDirection = GraphQL.RankingDirection;
export const getEVMWalletBalance = async (sdk, address) => {
if (!address)
return null;
const CHAIN_NAMES = {
1: 'ethereum',
42161: 'arbitrum',
8453: 'base',
80094: 'berachain',
56: 'bsc',
[sonic.id]: 'sonic',
43114: 'avalanche',
137: 'polygon',
999: 'hyperevm'
// Add new chains here easily
};
// Generate variables dynamically
const variables = EVM_CHAIN_IDS.reduce((acc, chainId) => ({
...acc,
[`${CHAIN_NAMES[chainId]}Address`]: `${address}:${chainId}`,
}), {});
return await sdk.send(`query Balances(
$ethereumAddress: String!
$arbitrumAddress: String!
$baseAddress: String!
$berachainAddress: String!
$bscAddress: String!
$sonicAddress: String!
$avalancheAddress: String!
$polygonAddress: String!
$hyperevmAddress: String!
) {
ethereum: balances(input: { walletId: $ethereumAddress }) {
items {
walletId
tokenId
balance
shiftedBalance
}
}
arbitrum: balances(input: { walletId: $arbitrumAddress }) {
items {
walletId
tokenId
balance
shiftedBalance
}
}
base: balances(input: { walletId: $baseAddress }) {
items {
walletId
tokenId
balance
shiftedBalance
}
}
berachain: balances(input: { walletId: $berachainAddress }) {
items {
walletId
tokenId
balance
shiftedBalance
}
}
bsc: balances(input: { walletId: $bscAddress }) {
items {
walletId
tokenId
balance
shiftedBalance
}
}
sonic: balances(input: { walletId: $sonicAddress }) {
items {
walletId
tokenId
balance
shiftedBalance
}
}
avalanche: balances(input: { walletId: $avalancheAddress }) {
items {
walletId
tokenId
balance
shiftedBalance
}
}
polygon: balances(input: { walletId: $polygonAddress }) {
items {
walletId
tokenId
balance
shiftedBalance
}
}
hyperevm: balances(input: { walletId: $hyperevmAddress }) {
items {
walletId
tokenId
balance
shiftedBalance
}
}
}`, variables);
};
const getBalance = async ({ walletAddress, chainId }) => {
const publicClient = getEvmJsonRpcProvider(chainId);
// Fetch token data using multicall
const balance = await publicClient.getBalance({
address: walletAddress,
});
return {
balance: balance || BigInt(0),
};
};
export const getPortfolioTokens = async (sdk, data, walletAddress, fallbackTokens) => {
const wrappedNativeAddresses = CHAIN_CONFIGS.filter((c) => c.isEVM).map((c) => c.wrapped);
const chainIds = CHAIN_CONFIGS.filter((c) => c.isEVM).map((c) => c.id);
const walletTokensFiltered = data.filter((token) => token.balance !== '0');
const walletTokensAddresses = [
...new Set(data
.map((token) => token.tokenId.split(':')[0])
.filter((address) => address !== 'native' && address !== 'nta')),
];
const safeFilterTokens = async () => {
try {
const tokens = [...walletTokensAddresses, ...wrappedNativeAddresses];
const chunks = [];
for (let i = 0; i < tokens.length; i += 200)
chunks.push(tokens.slice(i, i + 200));
const results = [];
for (const chunk of chunks) {
try {
const res = await sdk.queries.filterTokens({
tokens: chunk,
filters: { network: [...chainIds] },
limit: 200,
});
if (res?.filterTokens?.results?.length)
results.push(...res.filterTokens.results);
}
catch (err) {
console.error("[filterTokens] Chunk fetch failed:", err);
}
}
return { filterTokens: { results } };
}
catch (err) {
console.error("[filterTokens] Failed to fetch tokens:", err);
return { filterTokens: { results: [] } };
}
};
const [codexInfo, ethNativeBalance, baseNativeBalance, arbNativeBalance, berachainNativeBalance, bscinNativeBalance, sonicNativeBalance, avalancheNativeBalance, polygonNativeBalance, hyperevmNativeBalance,] = await Promise.all([
safeFilterTokens(),
getBalance({ walletAddress, chainId: mainnet.id, }), getBalance({ walletAddress, chainId: base.id, }), getBalance({ walletAddress, chainId: arbitrum.id, }), getBalance({ walletAddress, chainId: berachain.id, }), getBalance({ walletAddress, chainId: bsc.id, }), getBalance({ walletAddress, chainId: sonic.id, }), getBalance({ walletAddress, chainId: CHAIN_MAP[avalanche.id]?.id ?? avalanche.id, }), getBalance({ walletAddress, chainId: polygon.id, }), getBalance({ walletAddress, chainId: HyperEVM.id, }),
]);
if (!codexInfo?.filterTokens?.results || codexInfo.filterTokens.results.length === 0) {
console.warn("[getBalances] No Codex token data found.");
return [];
}
const tokenResults = codexInfo.filterTokens.results.filter((result) => result?.token !== null &&
result?.token !== undefined &&
typeof result.priceUSD === 'string' &&
typeof result.token.address === 'string' &&
typeof result.token.symbol === 'string' &&
typeof result.token.decimals === 'number');
let mappedTokenBalances = mapEvmTokenBalancesToInfo(walletTokensFiltered, tokenResults).filter(Boolean);
// Add fallback tokens if they exist and not already in results
if (fallbackTokens) {
const fallbackTokensToAdd = [];
for (const token of [fallbackTokens.tokenIn, fallbackTokens.tokenOut].filter((t) => !!t && isEVMChain(t.chainId) && !compareAddresses(t.address, NATIVE_TOKEN.ETH))) {
if (!token?.address || !token.chainId)
continue;
// Check if token already exists in results
const exists = mappedTokenBalances
.filter((t) => !!t)
.some((t) => compareAddresses(t.tokenAddress, token.address) && t.network && compareChains(t.network, token.chainId));
if (!exists) {
try {
const publicClient = getEvmJsonRpcProvider(token.chainId);
// Fetch token data using multicall
const [balance, decimals, symbol, name] = await publicClient.multicall({
// @ts-ignore
contracts: [
{
address: token.address,
abi: erc20Abi,
functionName: 'balanceOf',
args: [walletAddress],
},
{
address: token.address,
abi: erc20Abi,
functionName: 'decimals',
},
{
address: token.address,
abi: erc20Abi,
functionName: 'symbol',
},
{
address: token.address,
abi: erc20Abi,
functionName: 'name',
},
],
multicallAddress: token.chainId === berachain.id ? '0x8adce287716648df409faccbbecf76fe25c8fe22' : undefined,
});
const tokenDecimals = decimals.result || token.decimals || 18;
const balanceFormatted = formatUnits(balance.result || BigInt(0), tokenDecimals);
fallbackTokensToAdd.push({
tokenAddress: token.address,
symbol: symbol.result || token.symbol,
name: name.result || token.name || symbol.result || token.symbol,
decimals: tokenDecimals,
balance: (balance.result || BigInt(0)).toString(),
balanceFormatted,
totalSupply: '',
mcap: '',
logo: token.image || `/tokens/${token.address.toLowerCase()}.svg`,
usdPrice: 0,
usdValue: 0,
nativeToken: false,
network: token.chainId.toString(),
walletAddress,
isFallbackToken: true, // Mark as fallback token
});
}
catch (err) {
console.error(`Error fetching fallback token data for ${token.address}:`, err);
}
}
}
// Add fallback tokens to results
mappedTokenBalances = [...mappedTokenBalances, ...fallbackTokensToAdd];
}
const mappedNativeBalances = mapEvmNativeBalancesToInfo(ethNativeBalance.balance, arbNativeBalance.balance, baseNativeBalance.balance, berachainNativeBalance.balance, bscinNativeBalance.balance, sonicNativeBalance.balance, avalancheNativeBalance.balance, polygonNativeBalance.balance, hyperevmNativeBalance.balance, walletAddress, tokenResults);
// Group tokens by network
const tokensByNetwork = mappedTokenBalances.reduce((acc, token) => {
if (!token?.network || !token.tokenAddress)
return acc;
if (!acc[token.network]) {
acc[token.network] = [];
}
acc[token.network]?.push(token);
return acc;
}, {});
console.log('useRefetchSwapBalancesUntilDataChanges tokensByNetwork', tokensByNetwork);
// Get updated balances for each network using multicall
const updatedBalances = await Promise.all(Object.entries(tokensByNetwork).map(async ([network, tokens]) => {
try {
const publicClient = getEvmJsonRpcProvider(Number(network));
const balanceResults = await publicClient.multicall({
contracts: tokens
.filter((t) => !!t)
.map((token) => ({
address: token.tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [walletAddress],
})),
multicallAddress: Number(network) === berachain.id ? '0x8adce287716648df409faccbbecf76fe25c8fe22' : undefined,
});
return tokens
.filter((t) => !!t)
.map((token, i) => {
try {
if (balanceResults[i]?.error) {
return token;
}
const balance = balanceResults[i]?.result;
const balanceFormatted = formatUnits(BigInt(balance ?? 0), token.decimals || 18);
return {
...token,
balance: balance?.toString() ?? '0',
balanceFormatted,
usdValue: Number(balanceFormatted) * (token.usdPrice || 0),
};
}
catch (err) {
return token;
}
});
}
catch (err) {
return tokens;
}
}));
// Flatten the updated balances array
mappedTokenBalances = updatedBalances.flat();
const tokens = [...mappedTokenBalances, ...mappedNativeBalances].map((token) => ({
...token,
symbol: token?.symbol ?? '',
name: token?.name ?? '',
decimals: token?.decimals ?? 0,
}));
const filteredTokens = filterTokensByValue(tokens);
return filteredTokens;
};
export const mapEvmTokenBalancesToInfo = (balances, tokenInfos) => {
return balances
.map((item) => {
const [tokenAddress, network] = item.tokenId.split(':');
const walletAddress = item.walletId.split(':')[0];
const tokenInfo = tokenInfos.find((info) => compareAddresses(info?.token?.address, tokenAddress) && info?.token?.networkId === Number(network));
if (!tokenInfo) {
return null;
}
const balanceFormatted = item.shiftedBalance;
const usdPrice = Number(tokenInfo.priceUSD);
const usdValue = balanceFormatted * usdPrice;
return {
tokenAddress: tokenAddress,
symbol: tokenInfo?.token?.symbol,
name: tokenInfo?.token?.name,
decimals: tokenInfo?.token?.decimals,
balance: item.balance,
balanceFormatted: balanceFormatted.toString(),
totalSupply: tokenInfo?.token?.info?.totalSupply ?? '',
mcap: tokenInfo.marketCap ?? '',
logo: tokenInfo?.token?.info?.imageSmallUrl ?? `/tokens/${tokenAddress?.toLowerCase()}.svg`,
usdPrice,
usdPrice1hrPercentChange: tokenInfo.change1 ? Number(tokenInfo.change1) * 100 : 0,
usdPrice4hrPercentChange: tokenInfo.change4 ? Number(tokenInfo.change4) * 100 : 0,
usdPrice12hrPercentChange: tokenInfo.change12 ? Number(tokenInfo.change12) * 100 : 0,
usdPrice24hrPercentChange: tokenInfo.change24 ? Number(tokenInfo.change24) * 100 : 0,
usdValue,
nativeToken: false,
network: network ?? tokenInfo.token?.networkId,
walletAddress,
};
})
.filter((result) => result?.balance !== null);
};
export const mapEvmNativeBalancesToInfo = (ethBalance, arbBalance, baseBalance, berachainBalance, bscBalance, sonicBalance, avalancheBalance, polygonBalance, hyperevmBalance, walletAddress, tokenInfos) => {
const balances = [
{ balance: ethBalance, network: 1 },
{ balance: arbBalance, network: 42161 },
{ balance: baseBalance, network: 8453 },
{ balance: berachainBalance, network: 80094 },
{ balance: bscBalance, network: 56 },
{ balance: sonicBalance, network: sonic.id },
{ balance: avalancheBalance, network: avalanche.id },
{ balance: polygonBalance, network: polygon.id },
{ balance: hyperevmBalance, network: HyperEVM.id },
];
return balances
.map(({ balance, network }) => {
const tokenInfo = tokenInfos.find((info) => compareAddresses(info?.token?.address, CHAIN_MAP[network]?.wrapped));
if (!tokenInfo) {
return null;
}
const balanceFormatted = formatUnits(balance, tokenInfo?.token?.decimals || 18);
const usdPrice = parseFloat(tokenInfo?.priceUSD || '0');
const usdValue = Number(balanceFormatted) * usdPrice;
const currencyName = CHAIN_MAP[network]?.nativeCurrency?.name;
return {
tokenAddress: NATIVE_TOKEN.ETH,
symbol: CHAIN_MAP[network]?.nativeCurrency?.symbol,
name: currencyName,
decimals: tokenInfo?.token?.decimals,
balance: balance.toString(),
balanceFormatted: balanceFormatted.toString(),
totalSupply: tokenInfo?.token?.info?.totalSupply || '',
mcap: tokenInfo.marketCap ?? '',
logo: tokenInfo?.token?.info?.imageSmallUrl ?? `/tokens/${NATIVE_TOKEN.ETH.toLowerCase()}.svg`,
usdPrice,
usdPrice1hrPercentChange: tokenInfo.change1 ? Number(tokenInfo.change1) * 100 : 0,
usdPrice4hrPercentChange: tokenInfo.change4 ? Number(tokenInfo.change4) * 100 : 0,
usdPrice12hrPercentChange: tokenInfo.change12 ? Number(tokenInfo.change12) * 100 : 0,
usdPrice24hrPercentChange: tokenInfo.change24 ? Number(tokenInfo.change24) * 100 : 0,
usdValue,
nativeToken: true,
network: tokenInfo.token?.networkId ?? network,
walletAddress,
};
})
.filter((result) => result !== null);
};
export const getSolanaTokenBalances = async (sdk, walletAddress) => {
// --- 1️⃣ Input validation
if (!walletAddress) {
console.error("[getSolanaTokenBalances] Missing wallet address");
return [];
}
let solanaProvider;
try {
solanaProvider = await getSolanaProvider();
}
catch (err) {
console.error("[getSolanaTokenBalances] Failed to get Solana provider:", err);
return [];
}
let rpcAccounts_1, rpcAccounts_2, nativeBalance;
try {
[rpcAccounts_1, rpcAccounts_2, nativeBalance] = await Promise.all([
solanaProvider.getTokenAccountsByOwner(new PublicKey(walletAddress), {
programId: TOKEN_PROGRAM_ID,
}),
solanaProvider.getTokenAccountsByOwner(new PublicKey(walletAddress), {
programId: TOKEN_2022_PROGRAM_ID,
}),
solanaProvider.getBalance(new PublicKey(walletAddress)),
]);
}
catch (err) {
console.error("[getSolanaTokenBalances] Error fetching token accounts or balance:", err);
return [createNativeSolToken(walletAddress, 0)];
}
const rpcAccounts = [...(rpcAccounts_1?.value || []), ...(rpcAccounts_2?.value || [])];
if (!rpcAccounts.length) {
console.warn("[getSolanaTokenBalances] No token accounts found for wallet:", walletAddress);
return [createNativeSolToken(walletAddress, nativeBalance)];
}
// --- 2️⃣ Decode and filter valid tokens
const filteredRpcAccounts = [];
rpcAccounts.forEach((account) => {
try {
const buffer = account.account.data;
const accountInfo = AccountLayout.decode(buffer);
const token = {
mint: accountInfo.mint.toString(),
amount: accountInfo.amount.toString(),
};
if (token.amount !== "0")
filteredRpcAccounts.push(token);
}
catch (err) {
console.warn("[getSolanaTokenBalances] Failed to decode token account:", err);
}
});
const tokensAddresses = [
...filteredRpcAccounts.map((token) => (!isPumpfun(token.mint) ? token.mint : "")).filter(Boolean),
"So11111111111111111111111111111111111111112",
];
const pumpAddresses = filteredRpcAccounts
.map((token) => (isPumpfun(token.mint) ? token.mint : ""))
.filter(Boolean);
const allAddresses = [...tokensAddresses, ...pumpAddresses];
if (!allAddresses.length) {
console.warn("[getSolanaTokenBalances] No valid token addresses found.");
return [createNativeSolToken(walletAddress, nativeBalance)];
}
// --- 3️⃣ Split into chunks (max 200 tokens per query)
const chunkSize = 200;
const addressChunks = [];
for (let i = 0; i < allAddresses.length; i += chunkSize) {
addressChunks.push(allAddresses.slice(i, i + chunkSize));
}
// --- 4️⃣ Fetch token metadata in chunks with try/catch
const allResults = [];
for (const chunk of addressChunks) {
try {
const response = await sdk.queries.filterTokens({
tokens: chunk,
filters: { network: [1399811149] },
});
const results = response?.filterTokens?.results || [];
if (results.length)
allResults.push(...results);
}
catch (err) {
console.error("[getSolanaTokenBalances] Error fetching filterTokens for chunk:", err?.message || err);
// Continue with next chunk instead of failing everything
continue;
}
}
// --- 5️⃣ Handle empty result
if (!allResults.length) {
console.warn("[getSolanaTokenBalances] No Codex token data found.");
return [
...filteredRpcAccounts.map((token) => mapSolanaToken(token, walletAddress)),
createNativeSolToken(walletAddress, nativeBalance),
];
}
// --- 6️⃣ Normalize Codex tokens
const codexTokens = allResults.map((t) => {
if (t?.token?.info?.address === "So11111111111111111111111111111111111111112" &&
t?.token?.info?.symbol === "SOL") {
t.token.info.symbol = "wSOL";
t.token.symbol = "wSOL";
}
return t;
});
// --- 7️⃣ Merge wallet tokens with Codex data
const tokens = filteredRpcAccounts
.map((token) => {
const codexToken = codexTokens.find((ct) => compareAddresses(ct.token.address, token.mint));
return codexToken ? mapSolanaToken(token, walletAddress, false, codexToken) : null;
})
.filter((t) => t !== null); // type-safe filter
const codexNativeToken = codexTokens.find((ct) => compareAddresses(ct.token.address, "So11111111111111111111111111111111111111112"));
if (codexNativeToken?.token) {
codexNativeToken.token.address = "So11111111111111111111111111111111111111111";
}
// --- 8️⃣ Add native SOL token
tokens.push(createNativeSolToken(walletAddress, nativeBalance, codexNativeToken));
const filteredTokens = tokens.filter((t) => t !== null);
// --- ✅ 9️⃣ Final safe return
return filterTokensByValue(filteredTokens);
};
export const createNativeSolToken = (walletAddress, nativeBalance, codexData) => {
return mapSolanaToken({
mint: 'So11111111111111111111111111111111111111111',
amount: nativeBalance?.toString() || '0',
}, walletAddress, true, codexData);
};
export const isValidSolanaOrEvMaddreess = (address) => {
try {
// EVM address validation (0x followed by 40 hexadecimal characters)
const evmAddressRegex = /^0x[a-fA-F0-9]{40}$/;
// Solana address validation (base58 string of length 32-44)
const solanaAddressRegex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
return evmAddressRegex.test(address) || solanaAddressRegex.test(address);
}
catch (error) {
return false;
}
};
export const mapSolanaToken = (token, walletAddress, isNative = false, codexToken) => {
const balanceFormatted = token.amount && codexToken?.token?.decimals ? formatUnits(BigInt(token.amount), codexToken.token.decimals) : '0';
return {
network: SOLANA_CHAIN_ID,
walletAddress,
tokenAddress: codexToken?.token?.address || 'So11111111111111111111111111111111111111111',
symbol: codexToken?.token?.address === 'So11111111111111111111111111111111111111111'
? 'SOL'
: codexToken?.token?.symbol || 'SOL',
name: codexToken?.token?.address === 'So11111111111111111111111111111111111111111'
? 'SOL'
: codexToken?.token?.name || 'SOL',
decimals: codexToken?.token?.decimals ? Number(codexToken.token.decimals) : 9,
balance: token.amount || '0',
balanceFormatted,
usdPrice: codexToken?.priceUSD ? Number(codexToken.priceUSD) : 0,
usdValue: balanceFormatted && codexToken?.priceUSD ? Number(balanceFormatted) * Number(codexToken.priceUSD) : 0,
nativeToken: isNative,
usdPrice1hrPercentChange: codexToken?.change1 ? Number(codexToken.change1) * 100 : 0,
usdPrice4hrPercentChange: codexToken?.change4 ? Number(codexToken.change4) * 100 : 0,
usdPrice12hrPercentChange: codexToken?.change12 ? Number(codexToken.change12) * 100 : 0,
usdPrice24hrPercentChange: codexToken?.change24 ? Number(codexToken.change24) * 100 : 0,
totalSupply: codexToken?.token?.info?.totalSupply || '',
mcap: codexToken?.marketCap ?? '',
logo: codexToken?.token?.info?.imageSmallUrl ??
`/tokens/${(codexToken?.token?.address || 'So11111111111111111111111111111111111111111')?.toLowerCase()}.svg`,
};
};
export function isPumpfun(tokenAddress) {
return tokenAddress.endsWith('pump');
}
export const normalizeCodexSolanaChainId = (chainId) => {
if (chainId === 1399811149) {
return SOLANA_CHAIN_ID;
}
else {
return chainId;
}
};
// Update filterTokensByValue to skip filtering for fallback tokens
export const filterTokensByValue = (tokens) => {
const [fallbackTokens, regularTokens] = tokens.reduce(([fall, reg], token) => {
if (token.isFallbackToken) {
fall.push(token);
}
else {
reg.push(token);
}
return [fall, reg];
}, [[], []]);
const filteredRegularTokens = regularTokens
.filter((token) => Number(token.usdValue ?? 0) > Number(0.01))
.sort((a, b) => Number(b.usdValue || 0) - Number(a.usdValue || 0));
return [...fallbackTokens, ...filteredRegularTokens];
};
const priceCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes cache duration
export const SOLANA_NETWORK_ID = 1399811149;
export const getTokenUSDPrice = async (sdk, tokenAddress, chainId) => {
try {
tokenAddress = getNormalizedSolanaToken(tokenAddress);
if (isNativeAddress(tokenAddress) && CHAIN_MAP[chainId]?.wrapped) {
tokenAddress = CHAIN_MAP[chainId]?.wrapped;
}
// Create cache key combining address and chain
const cacheKey = `${tokenAddress.toLowerCase()}-${chainId}`;
const now = Date.now();
// Check cache
const cached = priceCache.get(cacheKey);
if (cached && now - cached.timestamp < CACHE_DURATION) {
return cached.price;
}
// Convert Solana chain ID if needed
const networkId = chainId === SOLANA_CHAIN_ID ? SOLANA_NETWORK_ID : chainId;
// Fetch price
const response = await sdk.queries.getTokenPrices({
inputs: [
{
address: tokenAddress,
networkId,
},
],
});
// Find matching token
const token = response.getTokenPrices?.find((t) => t?.address.toLowerCase() === tokenAddress.toLowerCase()) || null;
// Update cache
priceCache.set(cacheKey, {
price: token,
timestamp: now,
});
return token;
}
catch (error) {
console.error('Error fetching token price:', error);
return null;
}
};
// Cache for storing top tokens data
const topTokensCache = {
data: null,
timestamp: 0,
};
const TOKEN_LIMIT = 100;
const SUPPORTED_NETWORKS = [...EVM_CHAIN_IDS, SOLANA_NETWORK_ID];
export const getTopTokens = async (sdk) => {
try {
const now = Date.now();
// Return cached data if valid
if (topTokensCache.data && now - topTokensCache.timestamp < CACHE_DURATION) {
return topTokensCache.data;
}
// Fetch data for all networks in parallel
const networkPromises = SUPPORTED_NETWORKS.map((networkId) => sdk.queries.listTopTokens({
limit: TOKEN_LIMIT,
networkFilter: [networkId],
}));
const responses = await Promise.all(networkPromises);
const filteredResponse = responses.map((response) => response.listTopTokens?.filter((token) => Number(token.liquidity) >= 50_000));
// Combine and map tokens from all networks
const mappedTokens = filteredResponse.flatMap((response) => response?.map((token) => ({
symbol: token.symbol,
address: token.address,
decimals: token.decimals,
chainId: normalizeCodexSolanaChainId(token.networkId),
image: token.imageSmallUrl,
name: token.name,
mcap: token.marketCap,
})) || []);
// Remove duplicates based on address and chainId
const uniqueTokens = Array.from(new Map(mappedTokens.map((token) => [`${token.address}-${token.chainId}`, token])).values());
// Update cache
topTokensCache.data = uniqueTokens;
topTokensCache.timestamp = now;
return uniqueTokens;
}
catch (error) {
console.error('Error fetching top tokens:', error);
// Return cached data if available, even if expired
if (topTokensCache.data) {
return topTokensCache.data;
}
return [];
}
};
export const searchToken = async (sdk, symbolOrAddress) => {
const isAddress = isValidSolanaOrEvMaddreess(symbolOrAddress);
const searchParams = isAddress ? { tokens: [symbolOrAddress] } : { phrase: symbolOrAddress };
const additionalParams = !isAddress
? {
liquidity: {
gte: 50_000,
},
volume24: {
gte: 100,
},
}
: {};
const token = await sdk.queries.filterTokens({
...searchParams,
filters: {
network: [...EVM_CHAIN_IDS, 1399811149], // Including both EVM chains and Solana
...additionalParams,
},
rankings: { attribute: TokenRankingAttribute.MarketCap, direction: RankingDirection.Desc },
});
return (token.filterTokens?.results?.map((result) => ({
symbol: result?.token?.symbol,
address: result?.token?.address,
decimals: result?.token?.decimals,
chainId: normalizeCodexSolanaChainId(result?.token?.networkId),
image: result?.token?.info?.imageSmallUrl,
name: result?.token?.name,
mcap: result?.marketCap,
})) || []);
};
export const searchTokenByAddress = async (sdk, options) => {
const { network, addresses } = options;
const networkIds = network ?? [...EVM_CHAIN_IDS, SOLANA_NETWORK_ID];
const tokens = await sdk.queries.filterTokens({
tokens: addresses,
limit: 20,
filters: {
network: networkIds,
},
});
return tokens.filterTokens?.results;
};
export const getTokenInfo = async (sdk, tokenAddress, chainId) => {
const data = await sdk.queries.filterTokens({
tokens: [tokenAddress],
filters: {
network: [chainId],
},
limit: 1,
});
return data?.filterTokens?.results?.[0];
};
//# sourceMappingURL=codex.js.map