import { formatUnits, isAddress, ZeroAddress } from 'ethers'; import { CHAIN } from './vesting.js'; // Indexed balances discover addresses; spending amounts use a fresh balanceOf read. export async function discoverTokens(account, { fetcher = fetch, signal } = {}) { if (!isAddress(account)) throw Error('Invalid wallet address.'); const tokens = new Map(), seen = new Set(); let query = new URLSearchParams({ type: 'ERC-20' }); for (let page = 0; page < 10; page++) { const key = query.toString(); if (seen.has(key)) throw Error('Token indexer repeated a page. Retry or enter a token address manually.'); seen.add(key); const response = await fetcher(`${CHAIN.blockExplorerUrls[0]}/api/v2/addresses/${account}/tokens?${query}`, { signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(12000)]) : AbortSignal.timeout(12000), }); if (!response.ok) throw Error('Token indexer is unavailable. Retry or enter a token address manually.'); const data = await response.json(); if (!Array.isArray(data.items)) throw Error('Token indexer returned an invalid response. Use a manual token address.'); for (const item of data.items) { const token = item?.token, address = token?.address_hash; if (token?.type !== 'ERC-20' || !isAddress(address || '') || address === ZeroAddress || !/^\d+$/.test(String(item.value)) || BigInt(item.value) <= 0n) continue; tokens.set(address.toLowerCase(), { address, symbol: String(token.symbol || 'ERC-20').slice(0, 24), name: String(token.name || '').slice(0, 60) }); } if (!data.next_page_params) return { tokens: [...tokens.values()], truncated: false }; query = new URLSearchParams({ type: 'ERC-20' }); for (const [name, value] of Object.entries(data.next_page_params)) { if (name !== 'type' && value != null && ['string', 'number', 'boolean'].includes(typeof value)) query.set(name, String(value)); } } return { tokens: [...tokens.values()], truncated: true }; } export function percentageAmount(balance, decimals, percentage) { if (!Number.isInteger(decimals) || decimals < 0 || decimals > 80 || ![10, 50, 100].includes(percentage) || BigInt(balance) < 0n) throw Error('Invalid balance or percentage.'); return formatUnits(BigInt(balance) * BigInt(percentage) / 100n, decimals); } export async function readWalletToken(token, address, account) { const [balance, precision, symbol] = await Promise.all([token.balanceOf(account), token.decimals(), token.symbol().catch(() => '')]); const decimals = Number(precision); if (!Number.isInteger(decimals) || decimals < 0 || decimals > 80) throw Error('Token decimals are unsupported.'); return { balance: BigInt(balance), decimals, symbol: String(symbol).trim().slice(0, 24) || `${address.slice(0, 6)}…${address.slice(-4)}` }; }