#!/usr/bin/env node /** * tron-refund-validate.cjs — validate R1 (self-refund after timeout) against a * well-behaved standard TRC-20 (MockUsdt), isolating the lock contract's refund * logic from the Nile faucet USDT's non-standard `transfer`. * * deploy MockUsdt → deploy UsdtLockNile(mock, me) → approve + lock 0.2 → * wait past the deadline → refund → assert the locker's balance is restored. * * Run: node scripts/tron-refund-validate.cjs --yes */ const fs = require('fs'); const path = require('path'); const solc = require('solc'); const mod = require('tronweb'); const TronWeb = mod.TronWeb ?? mod.default ?? mod; const ROOT = path.join(__dirname, '..'); const KEYS = JSON.parse(fs.readFileSync(path.join(ROOT, '.tron-nile-keys.json'), 'utf8')); const FULL_HOST = 'https://nile.trongrid.io'; const BCH_RECIPIENT = '0x76a9149aa6baabb2bbb1d1940e4ec82b9640a7ab62b8dc88ac'; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const big = (v) => BigInt((v?.toString?.() ?? v)); function compile(file, name) { const input = { language: 'Solidity', sources: { [file]: { content: fs.readFileSync(path.join(ROOT, 'contracts/tron', file), 'utf8') } }, settings: { optimizer: { enabled: true, runs: 200 }, evmVersion: 'istanbul', outputSelection: { '*': { '*': ['abi', 'evm.bytecode.object'] } } }, }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const errs = (out.errors || []).filter((e) => e.severity === 'error'); if (errs.length) { errs.forEach((e) => console.error(e.formattedMessage)); throw new Error('solc'); } const c = out.contracts[file][name]; return { abi: c.abi, bytecode: c.evm.bytecode.object }; } async function main() { if (!process.argv.includes('--yes')) { console.log('Re-run with --yes.'); return; } const tw = new TronWeb({ fullHost: FULL_HOST, privateKey: KEYS.privateKey }); tw.setAddress(KEYS.address); const me = KEYS.address; const chainNow = async () => Number(big((await tw.trx.getCurrentBlock()).block_header.raw_data.timestamp) / 1000n); const mockArt = compile('MockUsdt.sol', 'MockUsdt'); const lockArt = compile('UsdtLockNile.sol', 'UsdtLockNile'); console.log('deploy MockUsdt (1000 mUSDT)…'); const mock = await tw.contract().new({ abi: mockArt.abi, bytecode: mockArt.bytecode, feeLimit: 1500_000_000, parameters: ['1000000000'] }); const mockAddr = tw.address.fromHex(mock.address); console.log(' MockUsdt @ ' + mockAddr); console.log('deploy UsdtLockNile(mock, me)…'); const lockI = await tw.contract().new({ abi: lockArt.abi, bytecode: lockArt.bytecode, feeLimit: 1500_000_000, parameters: [mockAddr, me] }); const lockAddr = tw.address.fromHex(lockI.address); console.log(' lock @ ' + lockAddr); const usdt = tw.contract(mockArt.abi, mockAddr); const lock = tw.contract(lockArt.abi, lockAddr); const bal = async () => big(await usdt.balanceOf(me).call()); console.log('approve + lock 0.2 mUSDT…'); await usdt.approve(lockAddr, '10000000').send({ feeLimit: 200_000_000, shouldPollResponse: false }); await sleep(7000); const id = big(await lock.nextId().call()); const before = await bal(); const dl = (await chainNow()) + 60; await lock.lock('200000', BCH_RECIPIENT, dl).send({ feeLimit: 300_000_000, shouldPollResponse: false }); await sleep(8000); const afterLock = await bal(); console.log(` locked id ${id}: ${before} → ${afterLock}; waiting past deadline…`); while ((await chainNow()) < dl + 4) await sleep(8000); console.log('refund…'); const txid = await lock.refund(id.toString()).send({ feeLimit: 500_000_000, shouldPollResponse: false }); await sleep(12000); const tx = await tw.trx.getTransaction(txid); const ret = tx?.ret?.[0]?.contractRet; const afterRefund = await bal(); const ok = ret === 'SUCCESS' && afterRefund === before; console.log(` refund ${ret}: ${afterLock} → ${afterRefund} (restored=${ok})`); console.log('\n=== RESULT ==='); console.log(` ${ok ? 'PASS' : 'FAIL'} R1 self-refund after timeout returns the locked token to the locker`); console.log(ok ? 'VALIDATED — the refund safety valve works with a standard TRC-20. Non-custody holds; the Nile USDT failure was a token quirk (non-standard transfer), now handled by SafeERC20.' : 'FAIL.'); if (!ok) process.exitCode = 1; } main().catch((e) => { console.error('FAILED:', e?.message ?? e); process.exitCode = 1; });