Repository Public
1// Live READ-ONLY check of the TRON integration against mainnet. Nothing is signed with a real key, nothing is broadcast.2// npx tsx scripts/smoke-tron.mts3// 1. health + identity (genesis block id) on both sources, 2. balances of a well-known funded address,4// 3. prepare() for TRX and USDT transfers from that address (two-source agreement, activation and energy accounting),5// 4. byte-equality: our encoder vs what each full node builds itself (/wallet/createtransaction, /wallet/triggersmartcontract),6// 5. tracking and deposit scan on irreversible data. Exits 1 on any mismatch.7import { buildTronTx, formatUnits, NETWORKS, tronAddressBytes, type Intent } from '@spw/core';89// the API config module wants its environment; this script uses none of it (no database, no signer, no broadcast)10for (const [k, v] of Object.entries({ SPW_ORIGIN: 'http://localhost', SPW_RP_ID: 'localhost', DATABASE_URL: 'postgres://unused', SPW_SIGNER_SECRET: 'unused', SPW_BROADCAST: 'off', SPW_WORKERS: 'off' })) process.env[k] ??= v;11const { tronAdapter } = await import('../services/api/src/chains/tron.ts');1213const n = NETWORKS.tron, a = tronAdapter(n);14const USDT = n.tokens[0]!;15const FUNDED = process.env.TRON_ADDR ?? 'TV6MuMXfmLbBqPZvBHdwFsDnQeVfnmiuSi'; // long-lived public exchange wallet holding TRX and USDT16const EXISTING = 'TKHuVq1oKVruCGLvqVexFs6dawKv6fQgFs'; // Tether treasury: activated account17const NEVER_USED = 'TQGfKPHs3AwiBT44ibkCU64u1G4ttojUXU'; // valid address with no account on-chain (checked below)18const KNOWN_TX = '9dc1c48fc17d0bb3ff8942095b24d3a2d4f147e0debc779d0372126d4d00d1b3'; // irreversible TRX transfer to FUNDED (block 87880468)19let failures = 0;20const check = (ok: boolean, label: string, detail = '') => { console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ' ' + detail : ''}`); if (!ok) failures += 1; };21const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));22const trx = (v: bigint) => `${formatUnits(v, 6)} TRX`;23const hex = (b: Uint8Array) => Buffer.from(b).toString('hex');2425async function node(url: string, path: string, body: unknown): Promise<any> {26 for (let k = 0; ; k++) {27 await sleep(/trongrid/.test(url) ? 450 : 60);28 const r = await fetch(url + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(15000) });29 if (r.status === 429 && k < 3) { await sleep(5500); continue; }30 if (!r.ok) throw new Error(`${url}${path}: http ${r.status}`);31 return r.json();32 }33}3435// ---- 1. health / identity36const h = await a.health();37check(h.ok && h.identityVerified, 'health: both sources up, genesis block id verified', `height=${h.height} ${h.endpoints.map((e) => `${e.url}:${e.ok ? e.ms + 'ms' : 'DOWN'}`).join(' ')}`);38for (const url of n.rpc) {39 const g = await node(url, '/wallet/getblockbynum', { num: 0 });40 check(g.blockID === n.chainRef, `genesis on ${new URL(url).host}`, g.blockID);41}42check((a.disabledAssets?.() ?? []).length === 0, 'USDT contract reports decimals=6 symbol=USDT on-chain (token not disabled)');4344// ---- 2. balances45const bal = await a.balances(FUNDED);46check((bal.native ?? 0n) > 0n && (bal[USDT.id] ?? 0n) > 0n, `balances of ${FUNDED}`, `${trx(bal.native ?? 0n)}, ${formatUnits(bal[USDT.id] ?? 0n, 6)} USDT`);47const fresh = await a.balances(NEVER_USED);48check(fresh.native === 0n && fresh[USDT.id] === 0n, `balances of a never-used address are zero (${NEVER_USED})`);4950// ---- 3 + 4. prepare() and byte-equality against the nodes51const now = () => Math.floor(Date.now() / 1000);52const intentOf = (asset: string, to: string, amount: bigint, x: Record<string, string>, maxFee: bigint): Intent => ({53 v: 1, proposalId: '6f1c1f0e-8f5f-4b1e-9c56-0d6b5f0f1a11', keyId: 'smoke-test-only', network: n.id, chainRef: n.chainRef, asset,54 symbol: asset === 'native' ? 'TRX' : USDT.symbol, decimals: 6, amount: amount.toString(), from: FUNDED, to, maxFee: maxFee.toString(),55 createdAt: now(), expiresAt: now() + 120, policyVersion: 0, x });5657async function scenario(label: string, asset: string, to: string, amount: bigint): Promise<void> {58 console.log(`\n-- ${label}`);59 const p = await a.prepare({ asset, from: FUNDED, to, amount });60 const x = (p.section as { x: Record<string, string> }).x;61 console.log(` prepare: x=${JSON.stringify(x)} feeEstimate=${trx(p.feeEstimate)} maxFee=${trx(p.maxFee)} warnings=[${p.warnings.join(',')}] ttl=${p.ttlSec}s`);62 // the prepared intent goes through the same builder the signer uses63 const mine = buildTronTx(intentOf(asset, to, amount, x, p.maxFee), n);64 check(mine.worstFee <= p.maxFee, 'prepared intent builds; fee within the approved ceiling', `txid=${hex(mine.txid)} bandwidth=${mine.bandwidthBytes} bytes fee_limit=${trx(mine.feeLimit)}`);65 // the reference block chosen by prepare() really is that block on both nodes66 for (const url of n.rpc) {67 const sh = await node(url, '/walletsolidity/getnowblock', {});68 const num = Number(sh.block_header.raw_data.number);69 const cand = [num, num - 1, num - 2, num - 3, num - 4, num - 5, num - 6, num - 7, num - 8].find((k) => (k & 0xffff) === parseInt(x.ref_block_bytes!, 16));70 const b = cand === undefined ? null : await node(url, '/wallet/getblockbynum', { num: cand });71 check(!!b && b.blockID.slice(16, 32) === x.ref_block_hash, `reference block is irreversible and identical on ${new URL(url).host}`, b ? `#${cand} ${b.blockID}` : 'not found in the last irreversible blocks');72 }73 // what the node builds for the same transfer (it picks its own reference block and timestamps: feed those to our encoder)74 for (const url of n.rpc) {75 const host = new URL(url).host;76 const r = asset === 'native'77 ? await node(url, '/wallet/createtransaction', { owner_address: FUNDED, to_address: to, amount: Number(amount), visible: true })78 : (await node(url, '/wallet/triggersmartcontract', { owner_address: FUNDED, contract_address: asset, function_selector: 'transfer(address,uint256)',79 parameter: hex(tronAddressBytes(to).subarray(1)).padStart(64, '0') + amount.toString(16).padStart(64, '0'), fee_limit: Number(x.fee_limit), call_value: 0, visible: true })).transaction;80 if (!r?.raw_data_hex) { check(false, `${host} built the transaction`, JSON.stringify(r).slice(0, 200)); continue; }81 const rd = r.raw_data;82 const nx: Record<string, string> = { ref_block_bytes: rd.ref_block_bytes, ref_block_hash: rd.ref_block_hash, expiration: String(rd.expiration), timestamp: String(rd.timestamp),83 ...(asset === 'native' ? {} : { fee_limit: String(rd.fee_limit) }) };84 const ours = buildTronTx(intentOf(asset, to, amount, nx, p.maxFee), n);85 check(hex(ours.rawData) === r.raw_data_hex && hex(ours.txid) === r.txID, `byte-identical to ${host} ${asset === 'native' ? '/wallet/createtransaction' : '/wallet/triggersmartcontract'}`,86 `txID=${r.txID} (${r.raw_data_hex.length / 2} bytes of raw_data)`);87 if (hex(ours.rawData) !== r.raw_data_hex) console.log(` node: ${r.raw_data_hex}\n ours: ${hex(ours.rawData)}`);88 }89}90await scenario('TRX 1.5 -> activated account', 'native', EXISTING, 1_500_000n);91await scenario('TRX 2 -> never-used address (activation)', 'native', NEVER_USED, 2_000_000n);92await scenario('USDT 3.25 -> account that already holds USDT', USDT.id, EXISTING, 3_250_000n);93await scenario('USDT 1 -> never-used address (first-time holder: more energy)', USDT.id, NEVER_USED, 1_000_000n);9495// ---- prepare() refusals (read-only)96console.log('\n-- refusals');97const refuses = async (label: string, f: () => Promise<unknown>, re: RegExp) => { try { await f(); check(false, label, 'was accepted'); } catch (e) { check(re.test((e as Error).message), label, (e as Error).message.slice(0, 110)); } };98await refuses('amount above the TRX balance', () => a.prepare({ asset: 'native', from: FUNDED, to: EXISTING, amount: (bal.native ?? 0n) + 1n }), /insufficient/);99await refuses('amount above the USDT balance', () => a.prepare({ asset: USDT.id, from: FUNDED, to: EXISTING, amount: (bal[USDT.id] ?? 0n) + 1n }), /insufficient/);100await refuses('USDT from an account with no USDT and no TRX', () => a.prepare({ asset: USDT.id, from: NEVER_USED, to: EXISTING, amount: 1n }), /insufficient/);101await refuses('TRX from an account that does not exist', () => a.prepare({ asset: 'native', from: NEVER_USED, to: EXISTING, amount: 1n }), /insufficient/);102await refuses('destination = USDT contract', () => a.prepare({ asset: 'native', from: FUNDED, to: USDT.id, amount: 1n }), /token contract/);103104// ---- 5. tracking + deposits (irreversible data only)105console.log('\n-- tracking / deposits');106const t1 = await a.track({ txid: KNOWN_TX, intent: { x: { expiration: '1794315258000' } }, blockRef: null, broadcastAt: 0 });107check(t1.state === 'finalized' && t1.blockRef === '87880468', 'track: known irreversible transfer is finalized', JSON.stringify(t1));108const ghost = 'f'.repeat(64);109const t2 = await a.track({ txid: ghost, intent: { x: { expiration: String(Date.now() - 3_600_000) } }, blockRef: null, broadcastAt: 0 });110check(t2.state === 'failed' && /expired/.test(t2.error ?? ''), 'track: unknown txid past its expiration (both sources agree) is failed/expired', JSON.stringify(t2));111const t3 = await a.track({ txid: ghost, intent: { x: { expiration: String(Date.now() + 200_000) } }, blockRef: null, broadcastAt: 0 });112check(t3.state === 'broadcast' && t3.rebroadcast === true, 'track: unknown txid before its expiration stays "broadcast" (a hash is never success)', JSON.stringify(t3));113const dep = await a.deposits(FUNDED, null);114const kinds = dep.items.reduce((m, d) => { const k = d.asset === 'native' ? 'TRX' : 'USDT'; m[k] = (m[k] ?? 0) + 1; return m; }, {} as Record<string, number>);115check(dep.items.length > 0 && dep.finalizedHeight > 87_000_000, 'deposits: incoming TRX / USDT transfers re-verified on irreversible node data', `${JSON.stringify(kinds)} cursor=${dep.cursor} solidified=${dep.finalizedHeight}`);116for (const d of dep.items.slice(-3)) console.log(` ${d.asset === 'native' ? 'TRX ' : 'USDT'} ${formatUnits(d.amount, 6).padStart(14)} from ${d.from} block ${d.block} ${d.txid}`);117const dep2 = await a.deposits(FUNDED, dep.cursor);118check(dep2.cursor >= dep.cursor && dep2.items.every((d) => !dep.items.some((o) => o.txid === d.txid && o.idx === d.idx)), 'deposits: scanning again from the cursor returns nothing already seen', `new=${dep2.items.length}`);119120console.log(`\n${failures === 0 ? 'ALL CHECKS PASSED' : `${failures} CHECK(S) FAILED`} — read-only, nothing signed with a real key, nothing broadcast`);121process.exit(failures === 0 ? 0 : 1);122