Repository Public
1// Live READ-ONLY check of the TON adapter against mainnet: health + chain identity (zero-state hash), balances, deposit scan,2// prepare() from a funded public wallet v4R2 (proves both sources agree on state, seqno and the bounce flag; runs estimateFee3// and the simulation), and tracking of a real / an unknown message. Nothing is signed with a real key and nothing is broadcast.4// Run: npx tsx scripts/smoke-ton.mts (TON_FROM / TON_TO override the public addresses used)5import { createHash } from 'node:crypto';6for (const [k, v] of Object.entries({ SPW_ORIGIN: 'https://smoke.invalid', SPW_RP_ID: 'smoke.invalid', DATABASE_URL: 'postgres:///unused', SPW_SIGNER_SECRET: 'unused' })) process.env[k] ??= v;7const { NETWORKS, formatUnits, intentHash, pluginFor, tonFormatAddress, tonNormalizeAddress, tonRawAddress, toHex } = await import('@spw/core');8const { tonAdapter } = await import('../services/api/src/chains/ton.ts');910const n = NETWORKS.ton, a = tonAdapter(n), ton = (v: bigint) => `${formatUnits(v, 9)} TON`;11// a public, active wallet v4R2 with the default subwallet id (picked from a recent block; any other such wallet works)12const FUNDED = tonNormalizeAddress(process.env.TON_FROM ?? '0:1de184a0c1767bcea7c803fdcbfe6304907a0cef39b117a664c0579150fe903b');13const ACTIVE = tonNormalizeAddress(process.env.TON_TO ?? 'EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N'); // TON Foundation wallet (an initialized contract)14const FRESH = tonFormatAddress(createHash('sha256').update(`spw-smoke-nonexistent-${Date.now()}`).digest()); // nobody's address: no contract, no coins15const TEST_WALLET = 'UQAzWZa6nM5mJev91wGc7VCSfBoIsYRqKJpV78N8Add9-RKY'; // v4R2 wallet of the public test mnemonic at m/44'/607'/0' (never funded)16let failed = 0;17const step = async (name: string, f: () => Promise<unknown>) => {18 try { console.log(name.padEnd(40), JSON.stringify(await f(), (_k, v) => (typeof v === 'bigint' ? v.toString() : v))); }19 catch (e) { failed++; console.log(name.padEnd(40), 'ERR', (e as { code?: string }).code ?? '', (e as Error).message); }20};21const expectErr = (code: string, f: () => Promise<unknown>) => async () => { try { await f(); } catch (e) { if ((e as { code?: string }).code === code) return `refused as expected: ${(e as Error).message}`; throw e; } throw new Error(`expected ${code}`); };22/** prepare, then rebuild the exact signing message with the plugin from the resulting intent (as the API does before approval) */23const prepareAndBuild = async (to: string, amount: bigint, memo?: string) => {24 const p = await a.prepare({ asset: 'native', from: FUNDED, to, amount, memo });25 const now = Math.floor(Date.now() / 1000);26 const intent = { v: 1 as const, proposalId: '6f1c1f0e-8f5f-4b1e-9c56-0d6b5f0f1a11', keyId: 'spw-smoke-0000000', network: n.id, chainRef: n.chainRef, asset: 'native', symbol: 'TON', decimals: 9,27 amount: amount.toString(), from: FUNDED, to, maxFee: p.maxFee.toString(), createdAt: now, expiresAt: now + p.ttlSec, policyVersion: 2, ...(memo ? { memo } : {}), ...(p.section as { x: Record<string, string> }) };28 const built = pluginFor(n).build(intent, n);29 return { x: intent.x, feeEstimate: ton(p.feeEstimate), maxFee: ton(p.maxFee), warnings: p.warnings, ttlSec: p.ttlSec, intentHash: toHex(intentHash(intent)), signingHash: built.describe.signingHash };30};3132console.log('sources', n.rpc.join(' , '), '| chainRef (zero-state root hash)', n.chainRef);33console.log('addresses', JSON.stringify({ FUNDED, ACTIVE, FRESH, TEST_WALLET }));34await step('health', async () => { const h = await a.health(); if (!h.ok || !h.identityVerified) throw new Error(JSON.stringify(h)); return h; });35let balance = 0n;36await step('balance (funded public wallet)', async () => { balance = (await a.balances(FUNDED)).native!; if (balance <= 0n) throw new Error('zero'); return ton(balance); });37await step('balance (address with nothing on it)', async () => { const b = await a.balances(FRESH); if (b.native !== 0n) throw new Error('non-zero'); return ton(b.native); });38await step('deposits (first scan)', async () => {39 const d = await a.deposits(FUNDED, null);40 if (d.items.length > 0) await step('deposits (incremental)', async () => { const e = await a.deposits(FUNDED, d.items[0]!.block - 1); if (!e.items.some((x) => x.txid === d.items[0]!.txid)) throw new Error('missed'); return { items: e.items.length, cursor: e.cursor }; });41 return { items: d.items.length, cursor: d.cursor, finalizedHeight: d.finalizedHeight, newest: d.items[0] && { ...d.items[0], amount: ton(d.items[0].amount) } };42});43const small = balance / 20n > 0n ? balance / 20n : 1n;44await step('prepare → initialized contract', async () => { const r = await prepareAndBuild(ACTIVE, small); if (r.x.bounce !== '1' || r.x.stateInit !== '0') throw new Error(JSON.stringify(r.x)); return r; });45await step('prepare → empty address, comment', async () => { const r = await prepareAndBuild(FRESH, small, 'smoke test ✓'); if (r.x.bounce !== '0') throw new Error('expected non-bounceable'); return r; });46await step('prepare ← wallet holding nothing', expectErr('insufficient_funds', () => a.prepare({ asset: 'native', from: TEST_WALLET, to: ACTIVE, amount: 1_000_000n })));47await step('prepare, more than the balance', expectErr('insufficient_funds', () => a.prepare({ asset: 'native', from: FUNDED, to: ACTIVE, amount: balance + 1n })));48await step('prepare, a jetton', expectErr('unsupported_asset', () => a.prepare({ asset: 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs', from: FUNDED, to: ACTIVE, amount: 1n })));4950// tracking: the wallet's latest outgoing transfer, straight from the second source (normalized message hash, destination, value)51const nowSec = Math.floor(Date.now() / 1000);52let recent: { transactions?: any[] } = {};53for (let k = 0; k < 6 && !recent.transactions; k++) { // keyless sources answer 429 after a burst54 const r = await fetch(`${n.rpc[1]}/blockchain/accounts/${tonRawAddress(FUNDED)}/transactions?limit=20`);55 if (r.ok) recent = (await r.json()) as { transactions: any[] }; else await new Promise((w) => setTimeout(w, 1500));56}57const sent = (recent.transactions ?? []).find((t) => t.in_msg?.msg_type === 'ext_in_msg' && t.out_msgs?.length === 1 && t.out_msgs[0].destination?.address?.startsWith('0:'));58const seqnoNow = Number((await a.prepare({ asset: 'native', from: FUNDED, to: ACTIVE, amount: small }).then((p) => (p.section as any).x.seqno)));59if (sent) {60 const real = { from: FUNDED, to: tonNormalizeAddress(sent.out_msgs[0].destination.address), amount: String(sent.out_msgs[0].value), x: { validUntil: String(sent.utime + 60), seqno: '0' } };61 await step('track (real included transfer)', async () => { const t = await a.track({ txid: sent.in_msg.hash, intent: real, blockRef: null, broadcastAt: sent.utime }); if (t.state !== 'finalized') throw new Error(JSON.stringify(t)); return { ...t, txid: sent.in_msg.hash }; });62 await step('track (same, other amount approved)', async () => { try { await a.track({ txid: sent.in_msg.hash, intent: { ...real, amount: '1' }, blockRef: null, broadcastAt: sent.utime }); } catch (e) { return `no verdict, as expected: ${(e as Error).message}`; } throw new Error('expected a refusal'); });63} else { failed++; console.log('track (real included transfer)'.padEnd(40), 'ERR no outgoing transfer among the last 20 transactions of this wallet'); }64const ghost = (validUntil: number, seqno: number) => ({ txid: 'ab'.repeat(32), intent: { from: FUNDED, to: ACTIVE, amount: '1', x: { validUntil: String(validUntil), seqno: String(seqno) } }, blockRef: null, broadcastAt: nowSec });65await step('track (unknown, still valid)', async () => { const t = await a.track(ghost(nowSec + 600, seqnoNow)); if (t.state !== 'broadcast' || !t.rebroadcast) throw new Error(JSON.stringify(t)); return t; });66await step('track (unknown, expired, seqno unused)', async () => { const t = await a.track(ghost(nowSec - 600, seqnoNow)); if (t.state !== 'failed') throw new Error(JSON.stringify(t)); return t; });67await step('track (unknown, expired, seqno used)', async () => { const t = await a.track(ghost(nowSec - 600, seqnoNow - 1)); if (t.state !== 'broadcast' || t.rebroadcast) throw new Error(JSON.stringify(t)); return t; });68console.log(failed ? `FAILED: ${failed} step(s)` : 'OK: all steps passed (read-only, nothing signed or broadcast)');69process.exit(failed ? 1 : 0);70