import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import ganache from 'ganache'; import { BrowserProvider, ContractFactory, JsonRpcProvider } from 'ethers'; import { readPublicSchedule, publicLink } from '../src/public-reader.js'; const artifacts=JSON.parse(readFileSync('artifacts/contracts.json','utf8')); test('public viewer reads another recipient’s schedule without wallet access or signer',async t=>{ const server=ganache.server({logging:{quiet:true},chain:{chainId:4663,hardfork:'shanghai'},miner:{timestampIncrement:0}}); await server.listen(0,'127.0.0.1');t.after(()=>server.close()); const setup=new BrowserProvider(server.provider);setup.pollingInterval=10; const signer=await setup.getSigner(),recipient=await (await setup.getSigner(1)).getAddress(); const deploy=async name=>{const c=await new ContractFactory(artifacts[name].abi,artifacts[name].bytecode,signer).deploy();await c.waitForDeployment();return c;}; const workspace=await deploy('Veylock'),token=await deploy('TestToken'); const address=await workspace.getAddress(),tokenAddress=await token.getAddress(); const start=Number((await setup.getBlock('latest')).timestamp)+100; await(await token.approve(address,1001n)).wait(); await(await workspace.createSchedule(tokenAddress,recipient,1001n,start,start+100,start+400)).wait(); await server.provider.request({method:'evm_setTime',params:[(start+200)*1000]}); await server.provider.request({method:'evm_mine',params:[]}); // Production reader receives only a public HTTP provider, never the setup wallet. const reader=new JsonRpcProvider(`http://127.0.0.1:${server.address().port}`,undefined,{batchMaxCount:1});t.after(()=>reader.destroy()); const calls=[];reader.on('debug',event=>{if(event.action==='sendRpcPayload')calls.push(...[event.payload].flat().map(p=>p.method));}); const row=await readPublicSchedule(reader,artifacts.Veylock,address,'1'); assert.equal(row.total,'1001');assert.equal(row.vested,'500');assert.equal(row.claimable,'500');assert.equal(row.released,'0'); assert.equal(row.beneficiary,recipient);assert.equal(row.status,'active');assert.equal(row.token,tokenAddress); await assert.rejects(readPublicSchedule(reader,artifacts.Veylock,address,'2'),/not found/); await assert.rejects(readPublicSchedule(reader,artifacts.Veylock,tokenAddress,'1'),/No matching/); await assert.rejects(readPublicSchedule(reader,artifacts.Veylock,address,'0'),/positive schedule ID/); assert.ok(calls.length>0);assert.equal(calls.some(method=>/accounts|sign|sendTransaction/i.test(method)),false); }); test('public viewer rejects wrong networks and creates portable detail URLs',async()=>{ await assert.rejects(readPublicSchedule({getNetwork:async()=>({chainId:1n})},artifacts.Veylock,'0x1111111111111111111111111111111111111111','1'),/wrong network/); const link=new URL(publicLink('https://example.com','0x1111111111111111111111111111111111111111','12')); assert.equal(link.pathname,'/vesting.html');assert.equal(link.searchParams.get('schedule'),'12'); });