chore: initial project baseline — full Deviant codebase under version control
Brings the whole project into git (previously only the on-ramp feature and
a few parallel files were tracked). Includes: wallet core (src/), CashScript
contracts/, the Capacitor shell (shells/), scripts/, tests, and the docs
corpus (wiki/, snapshots/, Parameters/, Archive/, KnowledgeBase text).
Excluded via .gitignore: node_modules/, dist/, .env (secrets), testnet WIF
key files (.chipnet-*.json, devWallets.ts), KnowledgeBase/RAW media (~9.6 GB),
transcripts/ (session logs), and machine-local config (.claude/*.local,
*.lock). Secret-scanned before commit — no credentials in the tree.
Note: src/swapMath.ts, src/swapBuilder.ts, contracts/OpenMintToken.cash are
legacy (OP_RETURN era) but still imported; committed as-is, flagged for a
separate removal once de-referenced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:38:07 +02:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
/**
|
|
|
|
|
* chipnet-phase-a-test.mjs — Phase A of the voting-systems §6 bicameral-sortition
|
|
|
|
|
* design (§6.15 roadmap). Proves the minimal end-to-end mechanic on chipnet:
|
|
|
|
|
* per-rank CitizenToken + a single-chamber ApplicationCovenant + admission flow.
|
|
|
|
|
*
|
|
|
|
|
* 1. genesis citizen category + application-state category (founder C)
|
|
|
|
|
* 2. founder mints CitizenTokens (rank=0x01) for A and B
|
|
|
|
|
* 3. founder mints an application-state NFT at D's ApplicationCovenant address
|
|
|
|
|
* (commit = 16 zero bytes; threshold = 2)
|
|
|
|
|
* 4. A votes yes — co-spend application + A's CitizenToken; counter 0 -> 1
|
|
|
|
|
* 5. B votes yes — counter 1 -> 2 (reaches threshold)
|
|
|
|
|
* 6. anyone calls close() on D's application — UTXO consumed
|
|
|
|
|
* 7. founder mints CitizenToken for D — admission complete
|
|
|
|
|
*
|
|
|
|
|
* Phase A scope: ONE chamber, fixed threshold, no fees, no sortition, no
|
|
|
|
|
* double-vote enforcement. Later phases layer the rest.
|
|
|
|
|
*
|
|
|
|
|
* Roles: C = founder/issuer; A, B = existing Citizens (vote); D = applicant.
|
|
|
|
|
*/
|
|
|
|
|
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, '..');
|
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
|
|
|
const STATE = join(ROOT, 'ChipnetTests', '.chipnet-phase-a-test.json');
|
chore: initial project baseline — full Deviant codebase under version control
Brings the whole project into git (previously only the on-ramp feature and
a few parallel files were tracked). Includes: wallet core (src/), CashScript
contracts/, the Capacitor shell (shells/), scripts/, tests, and the docs
corpus (wiki/, snapshots/, Parameters/, Archive/, KnowledgeBase text).
Excluded via .gitignore: node_modules/, dist/, .env (secrets), testnet WIF
key files (.chipnet-*.json, devWallets.ts), KnowledgeBase/RAW media (~9.6 GB),
transcripts/ (session logs), and machine-local config (.claude/*.local,
*.lock). Secret-scanned before commit — no credentials in the tree.
Note: src/swapMath.ts, src/swapBuilder.ts, contracts/OpenMintToken.cash are
legacy (OP_RETURN era) but still imported; committed as-is, flagged for a
separate removal once de-referenced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:38:07 +02:00
|
|
|
const CIT_ART = join(ROOT, 'contracts/dist/CitizenToken.json');
|
|
|
|
|
const APP_ART = join(ROOT, 'contracts/dist/ApplicationCovenant.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; // CitizenToken UTXO sats
|
|
|
|
|
const APP_DUST = 1000n; // ApplicationState UTXO sats
|
|
|
|
|
const SEED_DUST = 1000n; // minting-NFT dust
|
|
|
|
|
const MF = 2500n;
|
|
|
|
|
const THRESHOLD = 2n;
|
|
|
|
|
|
|
|
|
|
const RANK_CITIZEN = '01';
|
|
|
|
|
const APP_OPEN = '0000000000000000' + '0000000000000000'; // 16 zero bytes
|
|
|
|
|
|
|
|
|
|
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 u64le(n){let v=BigInt(n);const o=new Uint8Array(8);for(let i=0;i<8;i++){o[i]=Number(v&0xffn);v>>=8n;}return binToHex(o);}
|
|
|
|
|
const appCommit=(yes,no)=>u64le(yes)+u64le(no);
|
|
|
|
|
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(){
|
|
|
|
|
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 NFT category (minting NFT to C) ----
|
|
|
|
|
step('Genesis citizen NFT category (minting NFT to founder C)');
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// ---- Genesis: application-state NFT category (minting NFT to C) ----
|
|
|
|
|
step('Genesis application-state NFT category (minting NFT to founder C)');
|
|
|
|
|
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});
|
|
|
|
|
console.log(` CitizenToken(A) ${citA.tokenAddress}`);
|
|
|
|
|
console.log(` CitizenToken(B) ${citB.tokenAddress}`);
|
|
|
|
|
console.log(` CitizenToken(D) ${citD.tokenAddress}`);
|
|
|
|
|
|
|
|
|
|
// D's ApplicationCovenant (per-applicant address)
|
|
|
|
|
const app=new Contract(appArt,[D.publicKeyHash,txidLE(appCat),txidLE(citCat),THRESHOLD],{provider});
|
|
|
|
|
console.log(` ApplicationCovenant(D) ${app.tokenAddress}`);
|
|
|
|
|
|
|
|
|
|
async function mintingUtxo(category){return (await provider.getUtxos(C.cashaddr)).find(x=>x.token?.category===category&&x.token?.nft?.capability==='minting');}
|
|
|
|
|
async function citUtxo(c){return (await provider.getUtxos(c.tokenAddress)).find(x=>x.token?.category===citCat&&x.token?.nft?.capability==='mutable'&&x.token?.nft?.commitment===RANK_CITIZEN);}
|
|
|
|
|
async function appUtxo(){return (await provider.getUtxos(app.tokenAddress)).find(x=>x.token?.category===appCat&&x.token?.nft?.capability==='mutable');}
|
|
|
|
|
|
|
|
|
|
// ---- Mint CitizenTokens to A and B ----
|
|
|
|
|
async function mintCitizen(target,label){
|
|
|
|
|
if(st[`mint${label}`])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());
|
|
|
|
|
// [0] minting NFT back to C (preserve minting authority)
|
|
|
|
|
tb.addOutput({to:C.tokenaddr,amount:SEED_DUST,token:{amount:0n,category:citCat,nft:{capability:'minting',commitment:''}}});
|
|
|
|
|
// [1] new mutable CitizenToken (rank 0x01) at target's CitizenToken address
|
|
|
|
|
tb.addOutput({to:target.tokenAddress,amount:CIT_DUST,token:{amount:0n,category:citCat,nft:{capability:'mutable',commitment:RANK_CITIZEN}}});
|
|
|
|
|
// [2] C change
|
|
|
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(m.satoshis)+t-SEED_DUST-CIT_DUST-MF});
|
|
|
|
|
st[`mint${label}`]=txidOf(await tb.send());save(st);console.log(` minted CitizenToken to ${label} — ${st[`mint${label}`]}`);await sleep(10000);
|
|
|
|
|
}
|
|
|
|
|
step('Mint CitizenTokens (rank 0x01) to A and B');
|
|
|
|
|
await mintCitizen(citA,'A');
|
|
|
|
|
await mintCitizen(citB,'B');
|
|
|
|
|
|
|
|
|
|
// ---- Mint ApplicationState NFT for D (commit = 16 zero bytes) ----
|
|
|
|
|
step(`Mint ApplicationState for D (threshold=${THRESHOLD})`);
|
|
|
|
|
if(!st.appMint){
|
|
|
|
|
const m=await mintingUtxo(appCat);if(!m)throw new Error('appMint: 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.appMint=txidOf(await tb.send());save(st);console.log(` application state seeded at D's app addr — ${st.appMint}`);await sleep(10000);
|
|
|
|
|
} else console.log(` (app state seeded ${st.appMint})`);
|
|
|
|
|
|
|
|
|
|
// ---- VOTE helper: voter co-spends app[0] + their CitizenToken[1] ----
|
|
|
|
|
async function castVote(voterLabel,wallet,signer,compPub,citizenContract,choice){
|
|
|
|
|
if(st[`vote${voterLabel}`])return;
|
|
|
|
|
const a=await appUtxo();if(!a)throw new Error(`vote ${voterLabel}: app UTXO missing`);
|
|
|
|
|
const c=await citUtxo(citizenContract);if(!c)throw new Error(`vote ${voterLabel}: CitizenToken missing`);
|
|
|
|
|
// Read current counts
|
|
|
|
|
const commit=a.token.nft.commitment;
|
|
|
|
|
const yes=BigInt('0x'+commit.slice(0,16).match(/../g).reverse().join(''));
|
|
|
|
|
const no=BigInt('0x'+commit.slice(16,32).match(/../g).reverse().join(''));
|
|
|
|
|
const newYes=choice===0?yes+1n:yes;
|
|
|
|
|
const newNo=choice===1?no+1n:no;
|
|
|
|
|
const newCommit=appCommit(newYes,newNo);
|
|
|
|
|
const {p,t}=await pickW(wallet,MF+1000n);
|
|
|
|
|
const tb=new TransactionBuilder({provider});
|
|
|
|
|
tb.addInput(a,app.unlock.vote(BigInt(choice))); // [0] application
|
|
|
|
|
tb.addInput(c,citizenContract.unlock.vote(compPub,signer));// [1] voter's CitizenToken
|
|
|
|
|
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:RANK_CITIZEN}}});
|
|
|
|
|
tb.addOutput({to:wallet.cashaddr,amount:t-MF});
|
|
|
|
|
st[`vote${voterLabel}`]=txidOf(await tb.send());save(st);console.log(` ${voterLabel} voted ${choice===0?'YES':'NO'} (counter -> yes=${newYes}, no=${newNo}) — ${st[`vote${voterLabel}`]}`);await sleep(10000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
step('VOTE — A casts YES');
|
|
|
|
|
await castVote('A',A,sig.a,cPub.a,citA,0);
|
|
|
|
|
step('VOTE — B casts YES (reaches threshold)');
|
|
|
|
|
await castVote('B',B,sig.b,cPub.b,citB,0);
|
|
|
|
|
|
|
|
|
|
// ---- CLOSE the application (anyone can after threshold) ----
|
|
|
|
|
step('CLOSE — A (third party) consumes the application UTXO');
|
|
|
|
|
if(!st.close){
|
|
|
|
|
const a=await appUtxo();if(!a)throw new Error('close: app UTXO missing');
|
|
|
|
|
const {p,t}=await pickW(A,MF+1000n);
|
|
|
|
|
const tb=new TransactionBuilder({provider,allowImplicitFungibleTokenBurn:true});
|
|
|
|
|
tb.addInput(a,app.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.close=txidOf(await tb.send());save(st);console.log(` closed — ${st.close}`);await sleep(10000);
|
|
|
|
|
} else console.log(` (closed ${st.close})`);
|
|
|
|
|
|
|
|
|
|
// ---- Founder mints CitizenToken for D (admission complete) ----
|
|
|
|
|
step('MINT — founder mints CitizenToken for D (admission complete)');
|
|
|
|
|
await mintCitizen(citD,'D');
|
|
|
|
|
|
|
|
|
|
step('VERIFY');
|
|
|
|
|
const dToken=await citUtxo(citD);
|
|
|
|
|
const aToken=await citUtxo(citA);
|
|
|
|
|
const bToken=await citUtxo(citB);
|
|
|
|
|
const appAfter=await appUtxo();
|
|
|
|
|
const checks=[
|
|
|
|
|
['citizen + app-state categories genesised', !!citCat && !!appCat],
|
|
|
|
|
['CitizenTokens minted for A and B (rank 0x01)', !!aToken && !!bToken],
|
|
|
|
|
['application state seeded for D', !!st.appMint],
|
|
|
|
|
['A voted YES (counter 0 -> 1)', !!st.voteA],
|
|
|
|
|
['B voted YES (counter 1 -> 2 = threshold)', !!st.voteB],
|
|
|
|
|
['application closed (UTXO consumed)', !!st.close && !appAfter],
|
|
|
|
|
['D admitted — CitizenToken minted (rank 0x01)', !!dToken],
|
|
|
|
|
];
|
|
|
|
|
let ok=true;for(const [l,p] of checks){console.log(` [${p?'PASS':'FAIL'}] ${l}`);ok=ok&&p;}
|
|
|
|
|
|
|
|
|
|
step('SUMMARY — Phase A: per-rank CitizenToken + single-chamber ApplicationCovenant');
|
|
|
|
|
console.log(` citCat ${citCat}\n appCat ${appCat}`);
|
|
|
|
|
console.log(` CitizenToken(A) ${citA.tokenAddress}`);
|
|
|
|
|
console.log(` CitizenToken(B) ${citB.tokenAddress}`);
|
|
|
|
|
console.log(` CitizenToken(D) ${citD.tokenAddress}`);
|
|
|
|
|
console.log(` App(D) ${app.tokenAddress}`);
|
|
|
|
|
console.log(` mintA/mintB/appMint: ${st.mintA} / ${st.mintB} / ${st.appMint}`);
|
|
|
|
|
console.log(` voteA/voteB/close/mintD: ${st.voteA} / ${st.voteB} / ${st.close} / ${st.mintD}`);
|
|
|
|
|
console.log(' ');
|
|
|
|
|
console.log(' Citizen tokens are per-voter mutable NFTs (rank in commitment); voters');
|
|
|
|
|
console.log(' authorise their participation by co-spending their token (read-only) with');
|
|
|
|
|
console.log(' the ApplicationCovenant\'s vote() — both contracts run in the same tx.');
|
|
|
|
|
console.log(' Threshold reached -> close() consumes the application; founder mints the');
|
|
|
|
|
console.log(' new CitizenToken to complete admission. Phase A scope only — sortition,');
|
|
|
|
|
console.log(' fees, second chamber, and Elder/Founder tiers are later phases.');
|
|
|
|
|
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);});
|