import { test } from 'node:test'; import assert from 'node:assert/strict'; import { parseUnits } from 'ethers'; import { discoverTokens, percentageAmount, readWalletToken } from '../src/wallet-tokens.js'; const account='0x1111111111111111111111111111111111111111'; const item=(address=account, overrides={})=>({value:'100',token:{address_hash:address,type:'ERC-20',symbol:'TEST',name:'Test',...overrides}}); test('percentages use integer base units with exact precision and round down',()=>{ assert.equal(percentageAmount(19n,0,10),'1'); assert.equal(percentageAmount(1n,18,50),'0.0'); const balance=parseUnits('12345678901234567890.123456789012345678',18); assert.equal(parseUnits(percentageAmount(balance,18,100),18),balance); assert.equal(parseUnits(percentageAmount(balance,18,50),18),balance/2n); assert.equal(percentageAmount(1234567n,6,10),'0.123456'); assert.throws(()=>percentageAmount(balance,18,200)); }); test('token discovery paginates, deduplicates and excludes NFTs, zero and invalid balances',async()=>{ const urls=[]; const result=await discoverTokens(account,{fetcher:async url=>{ urls.push(url); return {ok:true,json:async()=>urls.length===1?{items:[item(),item('bad'),item(account,{type:'ERC-721'}),{...item(),value:'invalid'}],next_page_params:{items_count:50,value:'99',token_name:null}}:{items:[item(),item('0x2222222222222222222222222222222222222222'),{...item('0x3333333333333333333333333333333333333333'),value:'0'}],next_page_params:null}}; }}); assert.equal(result.tokens.length,2);assert.equal(result.truncated,false); assert.match(urls[1],/type=ERC-20/);assert.match(urls[1],/items_count=50/); }); test('empty balances, errors, malformed responses and repeated pagination stay distinguishable',async()=>{ assert.deepEqual(await discoverTokens(account,{fetcher:async()=>({ok:true,json:async()=>({items:[]})})}),{tokens:[],truncated:false}); await assert.rejects(discoverTokens(account,{fetcher:async()=>({ok:false})}),/unavailable/); await assert.rejects(discoverTokens(account,{fetcher:async()=>({ok:true,json:async()=>({})})}),/invalid response/); await assert.rejects(discoverTokens(account,{fetcher:async()=>({ok:true,json:async()=>({items:[],next_page_params:{items_count:50}})})}),/repeated a page/); }); test('discovery propagates cancellation to fetch',async()=>{ const controller=new AbortController();controller.abort(); await assert.rejects(discoverTokens(account,{signal:controller.signal,fetcher:async(url,{signal})=>{signal.throwIfAborted();}}),{name:'AbortError'}); }); test('wallet balance and decimals are mandatory; symbol failure has an address fallback',async()=>{ const result=await readWalletToken({balanceOf:async owner=>{assert.equal(owner,account);return 12000001n;},decimals:async()=>6n,symbol:async()=>{throw Error('no symbol');}},account,account); assert.equal(result.balance,12000001n);assert.equal(result.decimals,6);assert.match(result.symbol,/0x1111/); await assert.rejects(readWalletToken({balanceOf:async()=>1n,decimals:async()=>{throw Error('no decimals');},symbol:async()=>''},account,account),/no decimals/); });