import { EVMVM } from "./evm"; import { SVMVM } from "./svm"; import { ChainWalletConfig, vmTypes } from "./types"; import { sha256 } from "@noble/hashes/sha2"; import { Connection, PublicKey } from "@solana/web3.js"; import { createPublicClientFromChainConfig, discoverTokens as discoverEvmTokens, getNativeBalance as getEvmNativeBalance, getTokenBalance as getEvmTokenBalance } from "./evm/utils"; import { discoverTokens as discoverSvmTokens, getSvmNativeBalance } from "./svm/utils"; import { Hex } from "viem"; import BN from "bn.js"; import { fetchPrices } from "./price"; export const getPrivateKeyFromAnother = (privateKey: any, fromVm: vmTypes, toVm: vmTypes) => { if (fromVm === "EVM") { if (toVm === "SVM") { return SVMVM.convertFromEntropyToPrivateKey(sha256(privateKey).toString()) } } else if (fromVm === "SVM") { if (toVm === "EVM") { return EVMVM.convertFromEntropyToPrivateKey(sha256(privateKey).toString()) } } else { throw new Error("No conversion available for this vms yet") } } export interface AddressPortfolioItem { tokenAddress: string | "native"; symbol: string; decimals: number; balanceRaw: string; balanceFormatted: number; priceUsd: number | null; priceChange24h: number | null; valueUsd: number | null; } export interface AddressPortfolioResult { address: string; vmType: vmTypes; chainId: number; items: AddressPortfolioItem[]; totals: { valueUsd: number; pricedItems: number; unpricedItems: number; }; } export interface AddressPortfolioParams { chain: ChainWalletConfig; address: string; vmType?: vmTypes; includeNative?: boolean; tokenAddresses?: string[]; } const EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/; export const detectVmTypeFromAddress = (address: string): vmTypes => { if (EVM_ADDRESS_REGEX.test(address)) { return "EVM"; } try { new PublicKey(address); return "SVM"; } catch { throw new Error(`Could not infer VM type from address: ${address}`); } }; const normalizePriceMap = ( prices: Array<{ tokenAddress: string; price: number; priceChange24h?: number }> ) => { const priceMap = new Map(); for (const item of prices) { if (!item?.tokenAddress || typeof item.price !== "number") continue; priceMap.set(item.tokenAddress.toLowerCase(), { priceUsd: item.price, priceChange24h: typeof item.priceChange24h === "number" ? item.priceChange24h : null, }); } return priceMap; }; const enrichWithUsdValues = ( items: Omit[], priceMap: Map ): AddressPortfolioResult["items"] => { return items.map((item) => { const key = String(item.tokenAddress).toLowerCase(); const priceEntry = priceMap.get(key); const priceUsd = priceEntry?.priceUsd ?? null; const priceChange24h = priceEntry?.priceChange24h ?? null; const valueUsd = priceUsd === null ? null : item.balanceFormatted * priceUsd; return { ...item, priceUsd, priceChange24h, valueUsd, }; }); }; const buildTotals = (items: AddressPortfolioItem[]) => { let valueUsd = 0; let pricedItems = 0; let unpricedItems = 0; for (const item of items) { if (item.valueUsd === null) { unpricedItems += 1; continue; } pricedItems += 1; valueUsd += item.valueUsd; } return { valueUsd, pricedItems, unpricedItems }; }; export const getAddressPortfolioValue = async (params: AddressPortfolioParams): Promise => { const { chain, address, includeNative = true, tokenAddresses } = params; const vmType = params.vmType ?? detectVmTypeFromAddress(address); if (vmType === "EVM" && !EVM_ADDRESS_REGEX.test(address)) { throw new Error(`Invalid EVM address: ${address}`); } if (vmType === "SVM") { // Throws if invalid new PublicKey(address); } if (vmType === "EVM") { const client = createPublicClientFromChainConfig(chain); const items: Omit[] = []; const priceTargets = new Set(); if (includeNative) { const nativeBalance = await getEvmNativeBalance(address as Hex, client); const nativeRaw = nativeBalance.balance.toString(); items.push({ tokenAddress: "native", symbol: chain.nativeToken.symbol, decimals: chain.nativeToken.decimals, balanceRaw: nativeRaw, balanceFormatted: nativeBalance.formatted, }); priceTargets.add("native"); } if (tokenAddresses && tokenAddresses.length > 0) { const tokenResults = await Promise.all( tokenAddresses.map(async (tokenAddress) => { const balance = await getEvmTokenBalance(tokenAddress as Hex, address as Hex, client); const raw = balance.balance.toString(); return { tokenAddress, symbol: tokenAddress, decimals: balance.decimal, balanceRaw: raw, balanceFormatted: balance.formatted, }; }) ); for (const token of tokenResults) { items.push(token); priceTargets.add(token.tokenAddress); } } else { const discovered = await discoverEvmTokens(address, chain); for (const token of discovered) { const raw = String(token.balance); const decimals = token.decimals ?? 0; const rawBn = new BN(raw); const divisor = Math.pow(10, decimals); const formatted = divisor === 0 ? 0 : Number(rawBn.toString()) / divisor; items.push({ tokenAddress: token.address, symbol: token.symbol, decimals, balanceRaw: raw, balanceFormatted: formatted, }); priceTargets.add(token.address); } } const priceResult = await fetchPrices({ vm: "EVM", chainId: chain.chainId, tokenAddresses: Array.from(priceTargets), }); const priceMap = normalizePriceMap(priceResult.data?.prices ?? []); const enrichedItems = enrichWithUsdValues(items, priceMap); return { address, vmType, chainId: chain.chainId, items: enrichedItems, totals: buildTotals(enrichedItems), }; } const svmAddress = new PublicKey(address); const connection = new Connection(chain.rpcUrl); const items: Omit[] = []; const priceTargets = new Set(); if (includeNative) { const nativeBalance = await getSvmNativeBalance(svmAddress, connection); items.push({ tokenAddress: "native", symbol: chain.nativeToken.symbol, decimals: chain.nativeToken.decimals, balanceRaw: nativeBalance.balance.toString(), balanceFormatted: nativeBalance.formatted, }); priceTargets.add("native"); } if (tokenAddresses && tokenAddresses.length > 0) { const tokenResults = await Promise.all( tokenAddresses.map(async (tokenAddress) => { const pubkey = new PublicKey(tokenAddress); const tokenBalance = await SVMVM.getTokenBalance(svmAddress, pubkey, connection); return { tokenAddress, symbol: tokenAddress, decimals: tokenBalance.decimal, balanceRaw: tokenBalance.balance.toString(), balanceFormatted: tokenBalance.formatted, }; }) ); for (const token of tokenResults) { items.push(token); priceTargets.add(token.tokenAddress); } } else { const discovered = await discoverSvmTokens(svmAddress, connection); for (const token of discovered) { const raw = String(token.balance); const decimals = token.decimals ?? 0; const rawBn = new BN(raw); const divisor = Math.pow(10, decimals); const formatted = divisor === 0 ? 0 : Number(rawBn.toString()) / divisor; items.push({ tokenAddress: token.address, symbol: token.symbol, decimals, balanceRaw: raw, balanceFormatted: formatted, }); priceTargets.add(token.address); } } const priceResult = await fetchPrices({ vm: "SVM", chainId: chain.chainId, tokenAddresses: Array.from(priceTargets), }); const priceMap = normalizePriceMap(priceResult.data?.prices ?? []); const enrichedItems = enrichWithUsdValues(items, priceMap); return { address, vmType, chainId: chain.chainId, items: enrichedItems, totals: buildTotals(enrichedItems), }; };