deviant/scripts/chipnet-wrapmint-iou.mjs
walerikus f32c28ed84 feat(chipnet): USDm/PUSD AMM + real cross-chain wrap USDt→τUSDt
Build A — DEX, validated on chipnet:
- scripts/chipnet-amm-usdm-pusd.mjs: USDm/PUSD constant-product swap on the
  first-party SharePairAMM (ADR-0030); price discovery 1.000→1.440 PUSD/USDm,
  constant product held. No Cauldron needed (it's mainnet-only).

Build B — cross-chain wrap, validated end-to-end across Tron Nile + chipnet:
- contracts/DeviantWrapMintIOU.cash: mint-on-attestation IOU covenant —
  checkDataSig oracle gate, NO BCH backing (dust only; value is the source
  lock, ADR-0035), replay-bound to the reservoir outpoint.
- scripts/chipnet-wrapmint-iou.mjs: BCH leg — genesis reservoir into the
  covenant, oracle-attested mint of τUSDt, replay rejected by the covenant.
- contracts/tron/UsdtLockNile.sol + scripts/tron-deploy-lock.cjs: Nile lock
  contract (lock/markConsumed/refund), deployed at TXmMRnau9Ct…ZintH.
- scripts/tron-bch-wrap.mjs: the oracle watcher — locked 0.5 real USDt on
  Nile → minted 500000 τUSDt on chipnet → markConsumed. Genuine USDt→τUSDt.

Docs (Parameters/ASM DEX/ASM-Multichain.md): which source chains qualify,
reverse unwrap, three-tier trust model, source-chain prefix marker, and the
cross-chain-redemption design decision (universal IOU = non-goal; per-chain
IOUs + BCH AMM swap = the answer; CCTP does not cover BCH).

Deps: tronweb ^6.3.0, solc ^0.8.35. Keys/state gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:07:58 +02:00

168 lines
11 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-wrapmint-iou.mjs — Build B, step 1: the BCH mint side of the
* cross-chain wrap, on chipnet, with a LOCALLY-signed oracle (no Tron/Sepolia
* yet). Proves DeviantWrapMintIOU end-to-end.
*
* Scenario: a USDt lock on Tron is attested by the oracle; the BCH covenant
* mints the IOU τUSDt to the recipient. The IOU is NOT BCH-backed — it carries
* only dust; its value is the Tron-locked USDt, claimable back on Tron.
*
* Roles: C = operator/funder (deploys the IOU reservoir); E = bridge oracle;
* B = recipient (the party who locked on the source chain). Reuses funded
* chipnet keys (.chipnet-keys-{b,c,e}.json). Resumable via state JSON.
*
* SEQUENCE:
* 1. genesis a minting-NFT + FT reservoir for τUSDt (C). category = genesis txid.
* 2. relocate the reservoir into the DeviantWrapMintIOU(category, oraclePk=E) covenant.
* 3. oracle (E) signs {recipientLB ‖ amount ‖ reservoirOutpoint}; covenant mints
* `MINT` τUSDt to B at DUST value only (checkDataSig-gated, no BCH backing).
* 4. VERIFY B holds MINT τUSDt, the IOU UTXO is dust-valued (not amount-backed),
* and the reservoir decremented by exactly MINT.
* 5. REPLAY — re-submit the SAME attestation against the new reservoir; the
* outpoint binding must REJECT it (single-use signature).
*
* Run: node --import tsx scripts/chipnet-wrapmint-iou.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';
const ROOT = join(import.meta.dirname, '..');
const STATE = join(ROOT, 'ChipnetTests', '.chipnet-wrapmint-iou.json');
const ARTIFACT = 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 TICK = 'τUSDt'; // display only — Tron-sourced USDt IOU
const SUPPLY = 1000000n; // IOU reservoir FT supply
const MINT = 10000n; // mint 10000 atomic units (= 100.00 USDt @ 2dp)
const DUST = 1000n;
const MF = 2500n;
function 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;}
function 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;}
function 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;}
function u32le(n){const b=new Uint8Array(4);new DataView(b.buffer).setUint32(0,Number(n),true);return b;}
function u64le(n){const b=new Uint8Array(8);new DataView(b.buffer).setBigUint64(0,BigInt(n),true);return b;}
function oracleSign(priv,msg){const s=secp256k1.signMessageHashSchnorr(priv,laSha256.hash(msg));if(typeof s==='string')throw new Error(s);return s;}
function mintMessage(recipientLB,amount,resTxid,resVout){const m=new Uint8Array(69);m.set(recipientLB,0);m.set(u64le(amount),25);m.set(txidLE(resTxid),33);m.set(u32le(resVout),65);return m;}
const sleep=(ms)=>new Promise(r=>setTimeout(r,ms));
function divider(c='='){console.log(c.repeat(80));}
function step(t){console.log('');divider();console.log(t);divider();}
function load(){return existsSync(STATE)?JSON.parse(readFileSync(STATE,'utf8')):{};}
function save(s){writeFileSync(STATE,JSON.stringify(s,(k,v)=>typeof v==='bigint'?v.toString():v,2));}
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};}
function txidOf(s){return typeof s==='string'?s:s?.txid??s?.txId;}
async function main(){
if(!process.argv.includes('--yes')){console.log('Re-run with --yes to broadcast on chipnet.');return;}
const art=JSON.parse(readFileSync(ARTIFACT,'utf8'));
for(const [k,f] of Object.entries(KEYS)) if(!existsSync(f)) throw new Error(`missing key ${k} (${f})`);
const B=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 oraclePk=E.publicKey; // 33-byte compressed
let st=load();
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));
const r=await w.send([{cashaddr:w.cashaddr,value:2000n,unit:'sat'}],bad.length?{utxoIds:bad}:undefined);
console.log(` splitter ${txidOf(r)}`);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];
}
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 ftBal(w,cat){return (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 ftUtxoOf(w,cat){return (await provider.getUtxos(w.cashaddr)).find(x=>x.token?.category===cat&&!x.token?.nft);}
// ---- 1. Genesis the minting-NFT + FT reservoir DIRECTLY into the covenant ----
// One tx: input[0] is a vout-0 UTXO → its txid is the new category; the output
// carries the minting NFT + full FT supply straight to the covenant address.
step(`Genesis ${TICK} minting reservoir into the covenant — supply ${SUPPLY}`);
let wrap;
if(!st.cat){
const u=await eligible(C);const cat=u.txid;
wrap=new Contract(art,[txidLE(cat),oraclePk],{provider});
const want=DUST+MF+1000n, uSat=BigInt(u.satoshis), extra=want>uSat?want-uSat:0n;
const all=(await provider.getUtxos(C.cashaddr)).filter(x=>!x.token&&!(x.txid===u.txid&&x.vout===u.vout)).sort((a,b)=>Number(b.satoshis)-Number(a.satoshis));
const {p,t}=pickBch(all,extra);const total=uSat+t;
if(total<DUST+MF+546n)throw new Error(`low BCH ${total}<${DUST+MF+546n}`);
const tb=new TransactionBuilder({provider});
tb.addInput(u,sigC.unlockP2PKH());
for(const x of p)tb.addInput(x,sigC.unlockP2PKH());
tb.addOutput({to:wrap.tokenAddress,amount:DUST,token:{amount:SUPPLY,category:cat,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:C.cashaddr,amount:total-DUST-MF});
st.cat=cat;st.deploy=txidOf(await tb.send());save(st);
console.log(` genesis+deploy ${st.deploy} — category ${cat}`);console.log(` covenant ${wrap.tokenAddress}`);await sleep(10000);
} else {
wrap=new Contract(art,[txidLE(st.cat),oraclePk],{provider});
console.log(` ${TICK} ${st.cat} · covenant ${wrap.tokenAddress}`);
}
const cat=st.cat;
async function reservoir(){return (await provider.getUtxos(wrap.tokenAddress)).find(x=>x.token?.nft?.capability==='minting'&&x.token?.category===cat);}
// ---- 3. Oracle attests a (simulated) Tron lock → covenant mints τUSDt to B ----
step(`Mint ${MINT} ${TICK} to B on oracle attestation (no BCH backing)`);
if(!st.mint){
const res=await reservoir();if(!res)throw new Error('reservoir missing at covenant');
const recipientLB=p2pkh(B.publicKeyHash);
const msg=mintMessage(recipientLB,MINT,res.txid,res.vout);
const oSig=oracleSign(E.privateKey,msg);
console.log(` attest: recipient ${B.tokenaddr.slice(0,22)}… amount ${MINT} · bound to reservoir ${res.txid.slice(0,12)}…:${res.vout}`);
const {p,t}=await pick(C,DUST+MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(res,wrap.unlock.mint(oSig,msg));
for(const x of p)tb.addInput(x,sigC.unlockP2PKH());
tb.addOutput({to:wrap.tokenAddress,amount:BigInt(res.satoshis),token:{amount:SUPPLY-MINT,category:cat,nft:{capability:'minting',commitment:''}}}); // [0] reservoir back
tb.addOutput({to:B.tokenaddr,amount:DUST,token:{amount:MINT,category:cat}}); // [1] IOU — dust only
tb.addOutput({to:C.cashaddr,amount:t-DUST-MF}); // [2] BCH change
st.mint=txidOf(await tb.send());st.msg=Buffer.from(msg).toString('hex');st.sig=Buffer.from(oSig).toString('hex');save(st);
console.log(` minted — ${st.mint}`);await sleep(10000);
} else console.log(` (minted ${st.mint})`);
// ---- 4. Verify ----
step('VERIFY');
const bBal=await ftBal(B,cat);
const iou=await ftUtxoOf(B,cat);
const res2=await reservoir();
const iouVal=iou?BigInt(iou.satoshis):0n;
const backedWouldBe=MINT*1000n; // a BCH-backed token (e.g. rate 1000) would lock this many sats
console.log(` B holds ${bBal} ${TICK}; IOU UTXO value ${iouVal} sats (a ${MINT}-unit BACKED token would lock ${backedWouldBe} sats)`);
// ---- 5. Replay — same attestation must be rejected ----
step('REPLAY — re-submit the same attestation (must be REJECTED)');
let replayRejected=false;
if(res2){
try{
const msg=hexToBin(st.msg), oSig=hexToBin(st.sig);
const {p,t}=await pick(C,DUST+MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(res2,wrap.unlock.mint(oSig,msg)); // old message → bound to the OLD reservoir outpoint
for(const x of p)tb.addInput(x,sigC.unlockP2PKH());
tb.addOutput({to:wrap.tokenAddress,amount:BigInt(res2.satoshis),token:{amount:SUPPLY-MINT-MINT,category:cat,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:B.tokenaddr,amount:DUST,token:{amount:MINT,category:cat}});
tb.addOutput({to:C.cashaddr,amount:t-DUST-MF}); // well-formed change → tx reaches covenant eval; must fail on the outpoint binding
await tb.send();
console.log(' !! replay SUCCEEDED — this is a BUG');
}catch(e){replayRejected=true;console.log(` replay rejected as expected: ${String(e?.message??e).split('\n')[0].slice(0,90)}`);}
}
step('RESULT');
const checks=[
['reservoir relocated into covenant', !!st.deploy],
['IOU minted on oracle attestation (checkDataSig)', !!st.mint],
[`B received ${MINT} ${TICK}`, bBal>=MINT],
[`IOU is NOT BCH-backed (dust ${iouVal} sats ≪ backed ${backedWouldBe})`, iouVal>0n && iouVal<backedWouldBe],
[`reservoir decremented to ${SUPPLY-MINT}`, !!res2 && BigInt(res2.token.amount)===SUPPLY-MINT],
['replayed attestation rejected (outpoint-bound, single-use)', replayRejected],
];
let ok=true;for(const [label,pass] of checks){console.log(` ${pass?'PASS':'FAIL'} ${label}`);if(!pass)ok=false;}
divider();console.log(ok?`ALL CHECKS PASSED — ${TICK} cross-chain IOU mint validated on chipnet (locally-signed oracle).`:'SOME CHECKS FAILED.');
if(!ok)process.exitCode=1;
}
main().catch(e=>{console.error(e);process.exitCode=1;});