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>
168 lines
11 KiB
JavaScript
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;});
|