deviant/scripts/tron-bch-lock-mint-trx.mjs
walerikus 56d1181594 feat(build-b): native-coin lock-and-mint (tTRX) validated — List B2
Option 2: the native-coin sibling of the tUSDt rail. TronTRXVault locks NATIVE
TRX via a payable lock() (no transferFrom); DeviantWrapMintIOU mints a floating
1:1 IOU tTRX on BCH (= 1 TRX, pegged to the native unit, NOT a dollar). All 6
checks PASS on real coins: lock 10 TRX -> mint 10,000,000 tTRX -> reconcile
(==) -> replay rejected -> redeem releases the TRX -> reconcile (0:0). Proves
the hub holds volatile L1 coins, not just stablecoins. Trust = oracle/operator
(B6 M-of-N applies identically).

  contracts/tron/TronTRXVault.sol
  scripts/tron-bch-lock-mint-trx.mjs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:04:18 +02:00

165 lines
12 KiB
JavaScript

#!/usr/bin/env node
/**
* tron-bch-lock-mint-trx.mjs — Build B / List B2: native-coin lock-and-mint (τTRX).
*
* The native-coin sibling of tron-bch-lock-mint.mjs (τUSDt). Locks native TRX in
* TronTRXVault (payable) and mints a FLOATING 1:1 IOU τTRX on BCH (τTRX = 1 TRX, pegged
* to the native unit, not a dollar). Same pipeline: real lock → oracle attest → mint →
* replay-reject → redeem → reconcile (Σ τTRX == TRX locked), across both chains.
*
* Run: node --import tsx scripts/tron-bch-lock-mint-trx.mjs --yes
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { TestNetWallet } from 'mainnet-js';
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
import { secp256k1, sha256 as laSha256 } from '@bitauth/libauth';
import solc from 'solc';
import { TronWeb } from 'tronweb';
const ROOT = join(import.meta.dirname, '..');
const WRAP_ART = join(ROOT, 'contracts/dist/DeviantWrapMintIOU.json');
const KEYS = { b: join(ROOT, '.chipnet-keys-b.json'), c: join(ROOT, '.chipnet-keys-c.json'), e: join(ROOT, '.chipnet-keys-e.json') };
const NILE = 'https://nile.trongrid.io';
const LOCK = 10_000_000; // 10 TRX (6 dec) → 10,000,000 τTRX units (1:1 sun)
const SUPPLY = 1_000_000_000n; // τTRX mint reservoir
const DUST = 1000n, MF = 2500n;
const CALL_FEE = 400_000_000, DEPLOY_FEE = 1_500_000_000;
const hexToBin = (h) => { const o = new Uint8Array(h.length / 2); for (let i = 0; i < o.length; i++) o[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16); return o; };
const txidLE = (h) => { const d = hexToBin(h); const o = new Uint8Array(d.length); for (let i = 0; i < d.length; i++) o[i] = d[d.length - 1 - i]; return o; };
const p2pkh = (pkh) => { const o = new Uint8Array(25); o[0] = 0x76; o[1] = 0xa9; o[2] = 0x14; o.set(pkh, 3); o[23] = 0x88; o[24] = 0xac; return o; };
const u32le = (n) => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, Number(n), true); return b; };
const u64le = (n) => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(n), true); return b; };
const oracleSign = (priv, msg) => { const s = secp256k1.signMessageHashSchnorr(priv, laSha256.hash(msg)); if (typeof s === 'string') throw new Error(s); return s; };
const mintMessage = (lb, amt, resTxid, resVout) => { const m = new Uint8Array(69); m.set(lb, 0); m.set(u64le(amt), 25); m.set(txidLE(resTxid), 33); m.set(u32le(resVout), 65); return m; };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const divider = (c = '=') => console.log(c.repeat(80));
const step = (t) => { console.log(''); divider(); console.log(t); divider(); };
const txidOf = (s) => (typeof s === 'string' ? s : s?.txid ?? s?.txId);
function pickBch(u, need) { const p = []; let t = 0n; for (const x of u) { p.push(x); t += BigInt(x.satoshis); if (t >= need) break; } return { p, t }; }
let tronWeb;
function compileVault() {
const out = JSON.parse(solc.compile(JSON.stringify({
language: 'Solidity',
sources: { 'TronTRXVault.sol': { content: readFileSync(join(ROOT, 'contracts/tron/TronTRXVault.sol'), 'utf8') } },
settings: { evmVersion: 'istanbul', optimizer: { enabled: true, runs: 200 }, outputSelection: { '*': { '*': ['abi', 'evm.bytecode.object'] } } },
})));
const errs = (out.errors || []).filter((e) => e.severity === 'error');
if (errs.length) { errs.forEach((e) => console.error(e.formattedMessage)); throw new Error('solc failed'); }
const c = out.contracts['TronTRXVault.sol'].TronTRXVault;
return { abi: c.abi, bytecode: c.evm.bytecode.object };
}
async function sendTx(txid, label) {
let info;
for (let i = 0; i < 20; i++) { await sleep(3000); info = await tronWeb.trx.getTransactionInfo(txid); if (info && info.receipt) break; }
const result = info?.receipt?.result || (info?.blockNumber > 0 ? 'SUCCESS' : 'PENDING');
console.log(` ${label}: ${result} ${txid.slice(0, 12)}…`);
if (result !== 'SUCCESS') throw new Error(`${label} -> ${result}`);
}
async function main() {
if (!process.argv.includes('--yes')) { console.log('Re-run with --yes to broadcast on Nile + chipnet.'); return; }
const art = JSON.parse(readFileSync(WRAP_ART, 'utf8'));
if (!existsSync(KEYS.e)) { const w = await TestNetWallet.newRandom(); writeFileSync(KEYS.e, JSON.stringify({ wif: w.privateKeyWif }, null, 2)); }
const tronKey = JSON.parse(readFileSync(join(ROOT, '.tron-nile-keys.json'), 'utf8'));
tronWeb = new TronWeb({ fullHost: NILE, privateKey: tronKey.privateKey });
const ALICE_TRON = tronKey.address;
const ALICE = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.b, 'utf8')).wif);
const C = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.c, 'utf8')).wif);
const E = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.e, 'utf8')).wif);
const provider = new ElectrumNetworkProvider('chipnet');
const sigC = new SignatureTemplate(C.privateKey);
const sigA = new SignatureTemplate(ALICE.privateKey);
const wrap = (cat) => new Contract(art, [txidLE(cat), E.publicKey], { provider });
const ftBal = async (w, cat) => (await provider.getUtxos(w.cashaddr)).filter((x) => x.token?.category === cat && !x.token?.nft).reduce((s, x) => s + BigInt(x.token.amount), 0n);
async function pick(w, need) { const u = (await provider.getUtxos(w.cashaddr)).filter((x) => !x.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis)); const { p, t } = pickBch(u, need); if (t < need) throw new Error(`low BCH ${t}<${need}`); return { p, t }; }
async function eligible(w) {
let u = await w.getUtxos(); let e = u.filter((x) => x.vout === 0 && !x.token); if (e.length) return e[0];
const bad = u.filter((x) => !(x.vout === 0 && !x.token));
await w.send([{ cashaddr: w.cashaddr, value: 2000n, unit: 'sat' }], bad.length ? { utxoIds: bad } : undefined); await sleep(12000);
u = await w.getUtxos(); e = u.filter((x) => x.vout === 0 && !x.token); if (!e.length) throw new Error('splitter failed'); return e[0];
}
console.log(`Alice TRON ${ALICE_TRON} | BCH ${ALICE.cashaddr}`);
step('Setup — deploy TronTRXVault on Nile + genesis τTRX reservoir on chipnet');
const { abi, bytecode } = compileVault();
const vaultInst = await tronWeb.contract().new({ abi, bytecode, feeLimit: DEPLOY_FEE, callValue: 0, parameters: [ALICE_TRON] });
const VAULT = tronWeb.address.fromHex(vaultInst.address);
const vault = await tronWeb.contract(abi, VAULT);
console.log(` vault ${VAULT} (native TRX, operator ${ALICE_TRON})`);
const u0 = await eligible(C); const cat = u0.txid; const w = wrap(cat);
{ const need = DUST + MF + 1000n, uSat = BigInt(u0.satoshis);
const all = (await provider.getUtxos(C.cashaddr)).filter((x) => !x.token && !(x.txid === u0.txid && x.vout === u0.vout)).sort((a, b) => Number(b.satoshis) - Number(a.satoshis));
const { p, t } = pickBch(all, need > uSat ? need - uSat : 0n); const total = uSat + t;
const tb = new TransactionBuilder({ provider });
tb.addInput(u0, sigC.unlockP2PKH()); for (const x of p) tb.addInput(x, sigC.unlockP2PKH());
tb.addOutput({ to: w.tokenAddress, amount: DUST, token: { amount: SUPPLY, category: cat, nft: { capability: 'minting', commitment: '' } } });
tb.addOutput({ to: C.cashaddr, amount: total - DUST - MF });
console.log(` reservoir ${txidOf(await tb.send())} · category ${cat.slice(0, 16)}…`); await sleep(9000); }
const reservoir = async () => (await provider.getUtxos(w.tokenAddress)).find((x) => x.token?.nft?.capability === 'minting' && x.token?.category === cat);
step(`B-native — Alice locks ${LOCK / 1e6} TRX (native) → mint ${LOCK} τTRX on BCH (floating 1:1)`);
await sendTx(await vault.lock('0x' + '00'.repeat(32)).send({ feeLimit: CALL_FEE, callValue: LOCK }), 'lock 10 TRX (payable)');
const locked1 = Number(await vault.lockedBalance().call());
console.log(` vault holds ${locked1 / 1e6} TRX (native, on-chain)`);
let oSig, msg;
{ const res = await reservoir(); if (!res) throw new Error('reservoir missing');
msg = mintMessage(p2pkh(ALICE.publicKeyHash), LOCK, res.txid, res.vout); oSig = oracleSign(E.privateKey, msg);
const { p, t } = await pick(C, DUST + MF + 1000n); const tb = new TransactionBuilder({ provider });
tb.addInput(res, w.unlock.mint(oSig, msg)); for (const x of p) tb.addInput(x, sigC.unlockP2PKH());
tb.addOutput({ to: w.tokenAddress, amount: BigInt(res.satoshis), token: { amount: SUPPLY - BigInt(LOCK), category: cat, nft: { capability: 'minting', commitment: '' } } });
tb.addOutput({ to: ALICE.tokenaddr, amount: DUST, token: { amount: BigInt(LOCK), category: cat } });
tb.addOutput({ to: C.cashaddr, amount: t - DUST - MF });
console.log(` minted ${txidOf(await tb.send())} — Alice holds ${LOCK} τTRX`); await sleep(9000); }
const aliceTtrx = await ftBal(ALICE, cat);
const res1 = await reservoir(); const circ1 = (SUPPLY - BigInt(res1.token.amount)) - await ftBal(C, cat);
console.log(` reconcile #1: locked ${locked1} == circulating ${circ1} τTRX → ${BigInt(locked1) === circ1 ? 'OK' : 'MISMATCH'}`);
step('Over-mint — replay the same attestation → expect REJECTED');
let replayRejected = false;
try { const res = await reservoir(); const { p, t } = await pick(C, DUST + MF + 1000n); const tb = new TransactionBuilder({ provider });
tb.addInput(res, w.unlock.mint(oSig, msg)); for (const x of p) tb.addInput(x, sigC.unlockP2PKH());
tb.addOutput({ to: w.tokenAddress, amount: BigInt(res.satoshis), token: { amount: SUPPLY - BigInt(LOCK) * 2n, category: cat, nft: { capability: 'minting', commitment: '' } } });
tb.addOutput({ to: ALICE.tokenaddr, amount: DUST, token: { amount: BigInt(LOCK), category: cat } });
tb.addOutput({ to: C.cashaddr, amount: t - DUST - MF }); await tb.send();
} catch { replayRejected = true; }
console.log(` replay: ${replayRejected ? 'REJECTED' : 'NOT rejected'}`);
step(`Redeem — return ${LOCK} τTRX on BCH → release ${LOCK / 1e6} TRX on Tron`);
{ const ft = (await provider.getUtxos(ALICE.cashaddr)).find((x) => x.token?.category === cat && !x.token?.nft && BigInt(x.token.amount) >= BigInt(LOCK));
const { p, t } = await pick(ALICE, DUST + MF); const tb = new TransactionBuilder({ provider });
tb.addInput(ft, sigA.unlockP2PKH()); for (const x of p) tb.addInput(x, sigA.unlockP2PKH());
tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: BigInt(LOCK), category: cat } });
tb.addOutput({ to: ALICE.cashaddr, amount: t + BigInt(ft.satoshis) - DUST - MF });
console.log(` τTRX returned to bridge ${txidOf(await tb.send())}`); await sleep(9000); }
await sendTx(await vault.release(1, ALICE_TRON, LOCK).send({ feeLimit: CALL_FEE, callValue: 0 }), 'release TRX to Alice');
const locked2 = Number(await vault.lockedBalance().call());
const res2 = await reservoir(); const circ2 = (SUPPLY - BigInt(res2.token.amount)) - await ftBal(C, cat);
console.log(` reconcile #2: locked ${locked2} == circulating ${circ2} τTRX → ${BigInt(locked2) === circ2 ? 'OK' : 'MISMATCH'}`);
step('RESULT — native-coin lock-and-mint τTRX (List B2)');
const checks = [
[`lock: vault holds ${LOCK} native TRX (sun)`, locked1 === LOCK],
[`mint: Alice holds ${LOCK} τTRX (floating 1:1 to TRX)`, aliceTtrx === BigInt(LOCK)],
['reconcile #1: τTRX circulating == TRX locked', BigInt(locked1) === circ1 && circ1 === BigInt(LOCK)],
['over-mint replay rejected', replayRejected],
['redeem: vault released TRX (locked back to 0)', locked2 === 0],
['reconcile #2: τTRX circulating == TRX locked (0:0)', BigInt(locked2) === circ2 && circ2 === 0n],
];
let ok = true; for (const [l, p] of checks) { console.log(` ${p ? 'PASS' : 'FAIL'} ${l}`); if (!p) ok = false; }
divider();
console.log(ok
? `LIST B2 VALIDATED — native-coin lock-and-mint. Alice locked real native TRX; the bridge\noracle attested it; exactly that many τTRX were minted on BCH (floating 1:1 to TRX, NOT a\ndollar); replay rejected; redeem released the TRX; Σ minted == Σ locked throughout. Proves\nthe hub holds volatile native L1 coins, not just stablecoins. Trust = oracle/operator (B6 = M-of-N).`
: 'SOME CHECKS FAILED.');
console.log(`\nvault @ ${VAULT} · τTRX category ${cat.slice(0, 16)}…`);
if (!ok) process.exitCode = 1;
}
main().catch((e) => { console.error('FAILED:', e?.stack ?? e?.message ?? e); process.exitCode = 1; });