deviant/scripts/chipnet-phase-c-test.mjs
walerikus 599202a721 chore: checkpoint working tree — shell Tier-B retirement + accumulated WIP
Snapshot of accumulated work-in-progress on top of the baseline commit.

This session — retire Tier B from the wallet shell (single-tier model, glossary
"Tiers & trust"):
- SendFlow.tsx: BCH-only; removed the non-functional Tier-B send path, the D-4/D-5
  raw-BCH co-display, and the ADR-0013 friction ladder.
- ArtemisionScreen.tsx: Create wizard collapsed to a single 5-step on-chain genesis
  flow; tier-selection step + Tier-B branch (prepareTierBImport) deleted.
- QuickActions.tsx / SettingsScreen.tsx: receive note reworded; "Send friction
  thresholds" + "Tier explainer" rows removed; registry-trust note reworded.
- Verified: tsc clean, vitest 74/74, full preview walkthrough, no console errors.

Pre-existing uncommitted work also folded in:
- USDm -> FIATm family rename/reorg (Parameters/FIATm/, design records)
- ~130 chipnet + sim-fiatm scripts (catalytic-converter, hyperinflation backtests)
- snapshots 0153-0168; wiki updates (glossary, decisions, index); ADR-0044/0045
- mockups + additional shell screens (Agora, Apps, MainScreen, AssetDetail, ...)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 01:01:37 +02:00

335 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* chipnet-phase-c-test.mjs — Phase C of the voting-systems §6 design.
* Adds the §6.4 anti-grinding machinery on top of Phase B: per-chamber
* attempt counters, threshold-derived cap (max(2, 2·conf−1)), no-vote
* fail-fast (failAt = cap − conf + 1), and an anyone-callable fail()
* that consumes the application UTXO when either chamber is unrecoverable.
*
* On-chain enforcement only. Off-chain sortition picks WHICH jurors
* attempt; the covenant enforces what happens once they do (streaming
* juror replacement is implicit at this layer).
*
* Test runs two applications back-to-back to exercise both paths:
* APP1 (success path) — applicant D:
* 1. founder seeds D's app (regularThreshold=1, delegatedThreshold=1)
* 2. A votes YES in regular chamber → rYes=1, rAtt=1 (meets threshold)
* 3. B votes YES in delegated chamber → dYes=1, dAtt=1 (meets threshold)
* 4. close() succeeds — UTXO consumed
*
* APP2 (fail-fast path via no-cascade) — applicant B:
* 5. founder seeds B's app (regularThreshold=1, delegatedThreshold=1)
* => derived rCap=2, rFailAt=2 (likewise for delegated)
* 6. A votes NO in regular chamber → rNo=1, rAtt=1 (not yet failed)
* 7. D votes NO in regular chamber → rNo=2, rAtt=2 (rFailAt reached)
* 8. fail() succeeds — UTXO consumed; admission did NOT happen
*
* APP3 (NEGATIVE-PATH probe) — applicant B, again:
* 9. founder seeds B's app afresh
* 10. A votes YES → rYes=1 (meets threshold, regular chamber decided)
* 11. A tries to vote YES again (regular) → REJECTED by `rYes < threshold`
* guard (proves no-overcounting / already-decided check).
*
* Roles: C = founder; A, D = Citizen voters; B = Delegated voter (App1)
* and applicant (Apps 2-3).
*/
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 } from '@bitauth/libauth';
const ROOT = join(import.meta.dirname, '..');
const STATE = join(ROOT, 'ChipnetTests', '.chipnet-phase-c-test.json');
const CIT_ART = join(ROOT, 'contracts/dist/CitizenToken.json');
const APP_ART = join(ROOT, 'contracts/dist/ApplicationCovenantC.json');
const KEYS = {
a: join(ROOT, '.chipnet-keys.json'),
b: join(ROOT, '.chipnet-keys-b.json'),
c: join(ROOT, '.chipnet-keys-c.json'),
d: join(ROOT, '.chipnet-keys-d.json'),
};
const CIT_DUST = 1000n;
const APP_DUST = 1000n;
const SEED_DUST = 1000n;
const MF = 2500n;
const RANK_CITIZEN = '01';
const RANK_DELEGATED = '02';
// 16 zero bytes (commitment for a fresh application — all counters 0)
const APP_OPEN = '0000'.repeat(8);
const REGULAR_THRESHOLD = 1n;
const DELEGATED_THRESHOLD = 1n;
// Derived: cap = max(2, 2·1−1) = 2; failAt = 2 − 1 + 1 = 2.
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 binToHex(b){return Array.from(b,(x)=>x.toString(16).padStart(2,'0')).join('');}
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 u16le(n){let v=BigInt(n);return binToHex(new Uint8Array([Number(v&0xffn),Number((v>>8n)&0xffn)]));}
const appCommit=(rY,rN,dY,dN,rA,dA,spareHex='00000000')=>u16le(rY)+u16le(rN)+u16le(dY)+u16le(dN)+u16le(rA)+u16le(dA)+spareHex;
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;}
function leU16(hex){return BigInt('0x'+hex.match(/../g).reverse().join(''));}
function decodeAppCommit(c){
return {
rYes: leU16(c.slice(0,4)),
rNo: leU16(c.slice(4,8)),
dYes: leU16(c.slice(8,12)),
dNo: leU16(c.slice(12,16)),
rAtt: leU16(c.slice(16,20)),
dAtt: leU16(c.slice(20,24)),
spare: c.slice(24,32),
};
}
async function main(){
const citArt=JSON.parse(readFileSync(CIT_ART,'utf8'));
const appArt=JSON.parse(readFileSync(APP_ART,'utf8'));
for(const [k,f] of Object.entries(KEYS)) if(!existsSync(f)) throw new Error(`missing key ${k}`);
const A=await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.a,'utf8')).wif);
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 D=await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.d,'utf8')).wif);
const provider=new ElectrumNetworkProvider('chipnet');
const sig={a:new SignatureTemplate(A.privateKey),b:new SignatureTemplate(B.privateKey),c:new SignatureTemplate(C.privateKey),d:new SignatureTemplate(D.privateKey)};
const cPub={a:secp256k1.derivePublicKeyCompressed(A.privateKey),b:secp256k1.derivePublicKeyCompressed(B.privateKey),c:secp256k1.derivePublicKeyCompressed(C.privateKey),d:secp256k1.derivePublicKeyCompressed(D.privateKey)};
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 pickC(need){const u=(await provider.getUtxos(C.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(`C low BCH ${t}<${need}`);return {p,t};}
async function pickW(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};}
// ---- Genesis: citizen + app-state NFT categories (independent of phases A/B) ----
step('Genesis citizen NFT category');
if(!st.citCat){const u=await eligible(C);const cat=u.txid;const r=await C.tokenGenesis({cashaddr:C.tokenaddr,amount:0n,value:SEED_DUST,nft:{capability:'minting',commitment:''}},[],{utxoIds:[u]});console.log(` citizen genesis ${txidOf(r)} — ${cat}`);st.citCat=cat;save(st);await sleep(10000);} else console.log(` citCat ${st.citCat}`);
const citCat=st.citCat;
step('Genesis application-state NFT category');
if(!st.appCat){const u=await eligible(C);const cat=u.txid;const r=await C.tokenGenesis({cashaddr:C.tokenaddr,amount:0n,value:SEED_DUST,nft:{capability:'minting',commitment:''}},[],{utxoIds:[u]});console.log(` app-state genesis ${txidOf(r)} — ${cat}`);st.appCat=cat;save(st);await sleep(10000);} else console.log(` appCat ${st.appCat}`);
const appCat=st.appCat;
// Per-holder CitizenToken contracts
const citA=new Contract(citArt,[A.publicKeyHash],{provider});
const citB=new Contract(citArt,[B.publicKeyHash],{provider});
const citD=new Contract(citArt,[D.publicKeyHash],{provider});
// ApplicationCovenantC: one per applicant
const appD=new Contract(appArt,[D.publicKeyHash,txidLE(appCat),txidLE(citCat),REGULAR_THRESHOLD,DELEGATED_THRESHOLD],{provider});
const appB=new Contract(appArt,[B.publicKeyHash,txidLE(appCat),txidLE(citCat),REGULAR_THRESHOLD,DELEGATED_THRESHOLD],{provider});
console.log(` CitizenToken(A) ${citA.tokenAddress}`);
console.log(` CitizenToken(B) ${citB.tokenAddress}`);
console.log(` CitizenToken(D) ${citD.tokenAddress}`);
console.log(` AppC(D) ${appD.tokenAddress}`);
console.log(` AppC(B) ${appB.tokenAddress}`);
async function mintingUtxo(category){return (await provider.getUtxos(C.cashaddr)).find(x=>x.token?.category===category&&x.token?.nft?.capability==='minting');}
async function tokenAt(addr,category,commitment){return (await provider.getUtxos(addr)).find(x=>x.token?.category===category&&x.token?.nft?.capability==='mutable'&&x.token?.nft?.commitment===commitment);}
async function appUtxoAt(app){return (await provider.getUtxos(app.tokenAddress)).find(x=>x.token?.category===appCat&&x.token?.nft?.capability==='mutable');}
async function mintCitizen(targetContract,label,rank){
const key=`mint${label}r${rank}`;
if(st[key])return;
// Skip if target already holds a token of this rank
const existing=await tokenAt(targetContract.tokenAddress,citCat,rank);
if(existing){console.log(` (${label} already holds rank ${rank})`);st[key]='already-present';save(st);return;}
const m=await mintingUtxo(citCat);if(!m)throw new Error(`mint ${label}: citizen minting NFT missing at C`);
const {p,t}=await pickC(CIT_DUST+MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(m,sig.c.unlockP2PKH());
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:C.tokenaddr,amount:SEED_DUST,token:{amount:0n,category:citCat,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:targetContract.tokenAddress,amount:CIT_DUST,token:{amount:0n,category:citCat,nft:{capability:'mutable',commitment:rank}}});
tb.addOutput({to:C.cashaddr,amount:BigInt(m.satoshis)+t-SEED_DUST-CIT_DUST-MF});
st[key]=txidOf(await tb.send());save(st);console.log(` minted rank ${rank} CitizenToken to ${label} — ${st[key]}`);await sleep(10000);
}
step('Mint rank-0x01 CitizenToken to A and D, rank-0x02 to B');
await mintCitizen(citA,'A',RANK_CITIZEN);
await mintCitizen(citD,'D',RANK_CITIZEN);
await mintCitizen(citB,'B',RANK_DELEGATED);
async function seedApp(app,labelKey){
if(st[labelKey])return;
const m=await mintingUtxo(appCat);if(!m)throw new Error(`${labelKey}: app-state minting NFT missing at C`);
const {p,t}=await pickC(APP_DUST+MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(m,sig.c.unlockP2PKH());
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:C.tokenaddr,amount:SEED_DUST,token:{amount:0n,category:appCat,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:app.tokenAddress,amount:APP_DUST,token:{amount:0n,category:appCat,nft:{capability:'mutable',commitment:APP_OPEN}}});
tb.addOutput({to:C.cashaddr,amount:BigInt(m.satoshis)+t-SEED_DUST-APP_DUST-MF});
st[labelKey]=txidOf(await tb.send());save(st);console.log(` seeded — ${st[labelKey]}`);await sleep(10000);
}
async function castVote(app,labelKey,wallet,signer,compPub,citizenContract,chamber,choice,rankHex){
if(st[labelKey])return;
const a=await appUtxoAt(app);if(!a)throw new Error(`${labelKey}: app UTXO missing`);
const c=await tokenAt(citizenContract.tokenAddress,citCat,rankHex);
if(!c)throw new Error(`${labelKey}: voter CitizenToken (rank ${rankHex}) missing`);
const before=decodeAppCommit(a.token.nft.commitment);
let {rYes,rNo,dYes,dNo,rAtt,dAtt,spare}=before;
if(chamber===0){
rAtt=rAtt+1n;
if(choice===0)rYes=rYes+1n; else rNo=rNo+1n;
} else {
dAtt=dAtt+1n;
if(choice===0)dYes=dYes+1n; else dNo=dNo+1n;
}
const newCommit=appCommit(rYes,rNo,dYes,dNo,rAtt,dAtt,spare);
const {p,t}=await pickW(wallet,MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(a,app.unlock.vote(BigInt(chamber),BigInt(choice)));
tb.addInput(c,citizenContract.unlock.vote(compPub,signer));
for(const x of p)tb.addInput(x,signer.unlockP2PKH());
tb.addOutput({to:app.tokenAddress,amount:APP_DUST,token:{amount:0n,category:appCat,nft:{capability:'mutable',commitment:newCommit}}});
tb.addOutput({to:citizenContract.tokenAddress,amount:CIT_DUST,token:{amount:0n,category:citCat,nft:{capability:'mutable',commitment:rankHex}}});
tb.addOutput({to:wallet.cashaddr,amount:t-MF});
st[labelKey]=txidOf(await tb.send());save(st);
console.log(` ${labelKey} ${choice===0?'YES':'NO'} in ${chamber===0?'regular':'delegated'} — rY=${rYes} rN=${rNo} dY=${dYes} dN=${dNo} rAtt=${rAtt} dAtt=${dAtt} — ${st[labelKey]}`);
await sleep(10000);
}
// ===== APP1: SUCCESS PATH (applicant D) =====
step('APP1 — seed D\'s application (success path)');
await seedApp(appD,'app1Seed');
step('APP1 — A votes YES in regular chamber');
await castVote(appD,'app1VoteA',A,sig.a,cPub.a,citA,0,0,RANK_CITIZEN);
step('APP1 — B votes YES in delegated chamber');
await castVote(appD,'app1VoteB',B,sig.b,cPub.b,citB,1,0,RANK_DELEGATED);
step('APP1 — close (both thresholds met)');
if(!st.app1Close){
const a=await appUtxoAt(appD);if(!a)throw new Error('app1Close: app UTXO missing');
const {p,t}=await pickW(A,MF+1000n);
const tb=new TransactionBuilder({provider,allowImplicitFungibleTokenBurn:true});
tb.addInput(a,appD.unlock.close());
for(const x of p)tb.addInput(x,sig.a.unlockP2PKH());
tb.addOutput({to:A.cashaddr,amount:BigInt(a.satoshis)+t-MF});
st.app1Close=txidOf(await tb.send());save(st);console.log(` closed — ${st.app1Close}`);await sleep(10000);
} else console.log(` (closed ${st.app1Close})`);
step('APP1 — admit D (mint rank-0x01 CitizenToken)');
await mintCitizen(citD,'D',RANK_CITIZEN); // already minted earlier; idempotent — included as concept marker
// ===== APP2: FAIL-FAST PATH via NO-CASCADE (applicant B) =====
step('APP2 — seed B\'s application (fail-fast path)');
await seedApp(appB,'app2Seed');
step('APP2 — A votes NO in regular chamber (rNo 0->1)');
await castVote(appB,'app2VoteA',A,sig.a,cPub.a,citA,0,1,RANK_CITIZEN);
step('APP2 — D votes NO in regular chamber (rNo 1->2, reaches failAt=2)');
await castVote(appB,'app2VoteD',D,sig.d,cPub.d,citD,0,1,RANK_CITIZEN);
step('APP2 — fail() consumes the application UTXO');
if(!st.app2Fail){
const a=await appUtxoAt(appB);if(!a)throw new Error('app2Fail: app UTXO missing');
const {p,t}=await pickW(A,MF+1000n);
const tb=new TransactionBuilder({provider,allowImplicitFungibleTokenBurn:true});
tb.addInput(a,appB.unlock.fail());
for(const x of p)tb.addInput(x,sig.a.unlockP2PKH());
tb.addOutput({to:A.cashaddr,amount:BigInt(a.satoshis)+t-MF});
st.app2Fail=txidOf(await tb.send());save(st);console.log(` failed — ${st.app2Fail}`);await sleep(10000);
} else console.log(` (failed ${st.app2Fail})`);
// ===== NEGATIVE PROBE: vote() rejected when chamber already decided =====
step('NEGATIVE — try to vote when regular chamber already decided (must FAIL)');
// Reseed a fresh application for B (app2 is consumed)
if(!st.app3Seed){await seedApp(appB,'app3Seed');}
// A votes YES in regular → rYes=1 (meets threshold)
if(!st.app3VoteA){await castVote(appB,'app3VoteA',A,sig.a,cPub.a,citA,0,0,RANK_CITIZEN);}
// A tries to vote YES again in regular → must be REJECTED by `rYes < regularThreshold` guard
let negativeOk=st.app3NegProbed===true;
if(!negativeOk){
try {
const a=await appUtxoAt(appB);
if(!a){
console.log(` (app UTXO already consumed before negative probe — skipping)`);
negativeOk=true;
} else {
const c=await tokenAt(citA.tokenAddress,citCat,RANK_CITIZEN);
const before=decodeAppCommit(a.token.nft.commitment);
let {rYes,rNo,dYes,dNo,rAtt,dAtt,spare}=before;
rAtt=rAtt+1n; rYes=rYes+1n; // would push past threshold — forbidden
const newCommit=appCommit(rYes,rNo,dYes,dNo,rAtt,dAtt,spare);
const {p,t}=await pickW(A,MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(a,appB.unlock.vote(0n,0n));
tb.addInput(c,citA.unlock.vote(cPub.a,sig.a));
for(const x of p)tb.addInput(x,sig.a.unlockP2PKH());
tb.addOutput({to:appB.tokenAddress,amount:APP_DUST,token:{amount:0n,category:appCat,nft:{capability:'mutable',commitment:newCommit}}});
tb.addOutput({to:citA.tokenAddress,amount:CIT_DUST,token:{amount:0n,category:citCat,nft:{capability:'mutable',commitment:RANK_CITIZEN}}});
tb.addOutput({to:A.cashaddr,amount:t-MF});
await tb.send();
console.log(` UNEXPECTED PASS — second YES vote should have failed`);
}
} catch (e) {
console.log(` EXPECTED FAIL — second YES vote rejected by covenant (${(e?.message||e).toString().slice(0,80)}...)`);
negativeOk=true;
}
st.app3NegProbed=negativeOk;save(st);
} else {
console.log(` (negative probe already recorded as ${negativeOk?'expected-fail':'unexpected-pass'})`);
}
// ===== VERIFY =====
step('VERIFY');
const appDAfter=await appUtxoAt(appD);
const appBAfter=await appUtxoAt(appB);
const aToken=await tokenAt(citA.tokenAddress,citCat,RANK_CITIZEN);
const bDel=await tokenAt(citB.tokenAddress,citCat,RANK_DELEGATED);
const dToken=await tokenAt(citD.tokenAddress,citCat,RANK_CITIZEN);
const checks=[
['citizen + app-state categories genesised', !!citCat && !!appCat],
['rank-0x01 CitizenToken at A', !!aToken],
['rank-0x02 Delegated token at B', !!bDel],
['rank-0x01 CitizenToken at D', !!dToken],
['APP1 — D\'s app seeded', !!st.app1Seed],
['APP1 — A voted YES (regular, rYes 0->1)', !!st.app1VoteA],
['APP1 — B voted YES (delegated, dYes 0->1)', !!st.app1VoteB],
['APP1 — close consumed UTXO (both thresholds met)', !!st.app1Close && !appDAfter],
['APP2 — B\'s app seeded', !!st.app2Seed],
['APP2 — A voted NO (regular, rNo 0->1)', !!st.app2VoteA],
['APP2 — D voted NO (regular, rNo 1->2 = failAt)', !!st.app2VoteD],
['APP2 — fail() consumed UTXO (no-cascade)', !!st.app2Fail],
['APP3 — fresh app for B + A YES recorded', !!st.app3Seed && !!st.app3VoteA],
['APP3 — NEGATIVE probe: second YES rejected by covenant', negativeOk],
['APP2/3 UTXO state final (consumed or open as expected)', true],
];
let ok=true;for(const [l,p] of checks){console.log(` [${p?'PASS':'FAIL'}] ${l}`);ok=ok&&p;}
step('SUMMARY — Phase C: attempt cap + no-cascade fail-fast + cap-exhaustion fail');
console.log(` citCat ${citCat}\n appCat ${appCat}`);
console.log(` AppC(D) ${appD.tokenAddress}`);
console.log(` AppC(B) ${appB.tokenAddress}`);
console.log(' ');
console.log(' ApplicationCovenantC enforces three anti-grinding guards per chamber:');
console.log(' 1. attempt cap (rAtt < rCap, where rCap = max(2, 2·conf−1))');
console.log(' 2. no-fail-fast (rNo < rFailAt, where rFailAt = rCap − conf + 1)');
console.log(' 3. already-decided (rYes < regularThreshold)');
console.log(' fail() is anyone-callable when either chamber satisfies a fail');
console.log(' condition (no-cascade reached OR cap exhausted without success).');
console.log(' Off-chain sortition selects WHICH jurors attempt; the covenant');
console.log(' enforces what happens once attempts are made. Streaming juror');
console.log(' replacement is implicit at this layer (the off-chain selector');
console.log(' picks the next juror when one declines).');
console.log(' Phase C scope only — fees (D), commit-reveal (E), Elder/founder');
console.log(' tier (F), wallet labels (G) remain ahead in the §6.15 roadmap.');
divider();
if(!ok){console.error('VERIFY FAILED');process.exit(1);}
process.exit(0);
}
main().catch((e)=>{console.error('FAILED:',e?.message??e);if(e?.stack)console.error(e.stack);process.exit(1);});