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-d-test.mjs — Phase D of the voting-systems §6 design (§6.15
|
|
|
|
|
* roadmap). Extends Phase C with the §6.5 pay-to-launch + paid-regardless-of-vote
|
|
|
|
|
* mechanism: the application UTXO carries a sats escrow that drains by a
|
|
|
|
|
* per-chamber reward on each vote. Citizens earn 40%, Delegated 60% (per vote).
|
|
|
|
|
* On close/fail, the residual flows to the caller (cleanup incentive).
|
|
|
|
|
*
|
|
|
|
|
* Sizing:
|
|
|
|
|
* citizenReward = 4000 sats (40% per Citizen vote)
|
|
|
|
|
* delegatedReward = 6000 sats (60% per Delegated vote)
|
|
|
|
|
* threshold = 1 per chamber → cap=2, failAt=2
|
|
|
|
|
* max-fee = cap · (citizenReward + delegatedReward) = 2·10000 = 20000 sats
|
|
|
|
|
*
|
|
|
|
|
* Tests:
|
|
|
|
|
* APP1 (success path) — applicant D:
|
|
|
|
|
* 1. seed at 20000 sats
|
|
|
|
|
* 2. A votes YES (regular) — value 20000 → 16000, A pockets 4000
|
|
|
|
|
* 3. B votes YES (delegated) — value 16000 → 10000, B pockets 6000
|
|
|
|
|
* 4. close() — A consumes the 10000 sat residual
|
|
|
|
|
*
|
|
|
|
|
* APP2 (NO voters STILL paid — anti-bribery property) — applicant B:
|
|
|
|
|
* 5. seed at 20000 sats
|
|
|
|
|
* 6. A votes NO (regular) — value 20000 → 16000, A still pockets 4000
|
|
|
|
|
* 7. D votes NO (regular) — value 16000 → 12000, D still pockets 4000
|
|
|
|
|
* 8. fail() — A consumes 12000 sat residual
|
|
|
|
|
*
|
|
|
|
|
* NEGATIVE — try to drain MORE than the reward (steal escrow):
|
|
|
|
|
* 9. seed APP3 at 20000 sats
|
|
|
|
|
* 10. A casts YES — try output[0] value 15000 (i.e., 5000 sats drained,
|
|
|
|
|
* 1000 more than the 4000 reward). Covenant MUST REJECT via
|
|
|
|
|
* `output[0].value == input[0].value - reward`.
|
|
|
|
|
*/
|
|
|
|
|
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-d-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/ApplicationCovenantD.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 SEED_DUST = 1000n;
|
|
|
|
|
const MF = 2500n;
|
|
|
|
|
|
|
|
|
|
const RANK_CITIZEN = '01';
|
|
|
|
|
const RANK_DELEGATED = '02';
|
|
|
|
|
const APP_OPEN = '0000'.repeat(8);
|
|
|
|
|
|
|
|
|
|
const REGULAR_THRESHOLD = 1n;
|
|
|
|
|
const DELEGATED_THRESHOLD = 1n;
|
|
|
|
|
const CITIZEN_REWARD = 4000n; // 40% per vote
|
|
|
|
|
const DELEGATED_REWARD = 6000n; // 60% per vote
|
|
|
|
|
// cap = 2 per chamber → initial app value = 2·(4000+6000) = 20000 sats
|
|
|
|
|
const APP_INITIAL_VALUE = 2n * (CITIZEN_REWARD + DELEGATED_REWARD);
|
|
|
|
|
|
|
|
|
|
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(`${w.cashaddr.slice(-8)} low BCH ${t}<${need}`);return {p,t};}
|
|
|
|
|
|
|
|
|
|
// ---- Genesis citizen + app-state NFT categories ----
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
const citA=new Contract(citArt,[A.publicKeyHash],{provider});
|
|
|
|
|
const citB=new Contract(citArt,[B.publicKeyHash],{provider});
|
|
|
|
|
const citD=new Contract(citArt,[D.publicKeyHash],{provider});
|
|
|
|
|
|
|
|
|
|
const appD=new Contract(appArt,[D.publicKeyHash,txidLE(appCat),txidLE(citCat),REGULAR_THRESHOLD,DELEGATED_THRESHOLD,CITIZEN_REWARD,DELEGATED_REWARD],{provider});
|
|
|
|
|
const appB=new Contract(appArt,[B.publicKeyHash,txidLE(appCat),txidLE(citCat),REGULAR_THRESHOLD,DELEGATED_THRESHOLD,CITIZEN_REWARD,DELEGATED_REWARD],{provider});
|
|
|
|
|
console.log(` AppD(D) ${appD.tokenAddress}`);
|
|
|
|
|
console.log(` AppD(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;
|
|
|
|
|
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 CitizenTokens — A and D rank-0x01, B rank-0x02');
|
|
|
|
|
await mintCitizen(citA,'A',RANK_CITIZEN);
|
|
|
|
|
await mintCitizen(citD,'D',RANK_CITIZEN);
|
|
|
|
|
await mintCitizen(citB,'B',RANK_DELEGATED);
|
|
|
|
|
|
|
|
|
|
// ---- seedApp funds the application UTXO with APP_INITIAL_VALUE sats of escrow ----
|
|
|
|
|
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_INITIAL_VALUE+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:''}}});
|
|
|
|
|
// App UTXO holds the escrow: APP_INITIAL_VALUE sats
|
|
|
|
|
tb.addOutput({to:app.tokenAddress,amount:APP_INITIAL_VALUE,token:{amount:0n,category:appCat,nft:{capability:'mutable',commitment:APP_OPEN}}});
|
|
|
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(m.satoshis)+t-SEED_DUST-APP_INITIAL_VALUE-MF});
|
|
|
|
|
st[labelKey]=txidOf(await tb.send());save(st);console.log(` seeded with ${APP_INITIAL_VALUE} sats escrow — ${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 reward=chamber===0?CITIZEN_REWARD:DELEGATED_REWARD;
|
|
|
|
|
const appBefore=BigInt(a.satoshis);
|
|
|
|
|
const appAfter=appBefore-reward;
|
|
|
|
|
// Voter funds the MF (and gets the reward via the app's value drop)
|
|
|
|
|
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());
|
|
|
|
|
// [0] app: smaller value (escrow drained by `reward`)
|
|
|
|
|
tb.addOutput({to:app.tokenAddress,amount:appAfter,token:{amount:0n,category:appCat,nft:{capability:'mutable',commitment:newCommit}}});
|
|
|
|
|
// [1] voter's CitizenToken preserved
|
|
|
|
|
tb.addOutput({to:citizenContract.tokenAddress,amount:CIT_DUST,token:{amount:0n,category:citCat,nft:{capability:'mutable',commitment:rankHex}}});
|
|
|
|
|
// [2] voter payout: funding-back + reward (reward materialises because input>output by `reward` on the app side)
|
|
|
|
|
tb.addOutput({to:wallet.cashaddr,amount:t+reward-MF});
|
|
|
|
|
st[labelKey]=txidOf(await tb.send());save(st);
|
|
|
|
|
console.log(` ${labelKey} ${choice===0?'YES':'NO'} in ${chamber===0?'regular':'delegated'} — app ${appBefore}→${appAfter} (voter +${reward}) — ${st[labelKey]}`);
|
|
|
|
|
await sleep(10000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== APP1: SUCCESS PATH =====
|
|
|
|
|
step(`APP1 — seed D's application (escrow=${APP_INITIAL_VALUE} sats)`);
|
|
|
|
|
await seedApp(appD,'app1Seed');
|
|
|
|
|
|
|
|
|
|
step(`APP1 — A YES (regular) — A receives ${CITIZEN_REWARD} sats`);
|
|
|
|
|
await castVote(appD,'app1VoteA',A,sig.a,cPub.a,citA,0,0,RANK_CITIZEN);
|
|
|
|
|
step(`APP1 — B YES (delegated) — B receives ${DELEGATED_REWARD} sats`);
|
|
|
|
|
await castVote(appD,'app1VoteB',B,sig.b,cPub.b,citB,1,0,RANK_DELEGATED);
|
|
|
|
|
|
|
|
|
|
step('APP1 — close (residual escrow flows to closer A)');
|
|
|
|
|
if(!st.app1Close){
|
|
|
|
|
const a=await appUtxoAt(appD);if(!a)throw new Error('app1Close: app UTXO missing');
|
|
|
|
|
const residual=BigInt(a.satoshis);
|
|
|
|
|
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:residual+t-MF});
|
|
|
|
|
st.app1Close=txidOf(await tb.send());save(st);console.log(` closed — A claimed ${residual} sat residual — ${st.app1Close}`);await sleep(10000);
|
|
|
|
|
} else console.log(` (closed ${st.app1Close})`);
|
|
|
|
|
|
|
|
|
|
// ===== APP2: NO VOTERS STILL PAID =====
|
|
|
|
|
step(`APP2 — seed B's application (fail-fast path, escrow=${APP_INITIAL_VALUE} sats)`);
|
|
|
|
|
await seedApp(appB,'app2Seed');
|
|
|
|
|
|
|
|
|
|
step(`APP2 — A NO (regular) — A receives ${CITIZEN_REWARD} sats REGARDLESS of vote direction`);
|
|
|
|
|
await castVote(appB,'app2VoteA',A,sig.a,cPub.a,citA,0,1,RANK_CITIZEN);
|
|
|
|
|
step(`APP2 — D NO (regular) — D receives ${CITIZEN_REWARD} sats REGARDLESS of vote direction`);
|
|
|
|
|
await castVote(appB,'app2VoteD',D,sig.d,cPub.d,citD,0,1,RANK_CITIZEN);
|
|
|
|
|
|
|
|
|
|
step('APP2 — fail() (residual flows to failer A)');
|
|
|
|
|
if(!st.app2Fail){
|
|
|
|
|
const a=await appUtxoAt(appB);if(!a)throw new Error('app2Fail: app UTXO missing');
|
|
|
|
|
const residual=BigInt(a.satoshis);
|
|
|
|
|
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:residual+t-MF});
|
|
|
|
|
st.app2Fail=txidOf(await tb.send());save(st);console.log(` failed — A claimed ${residual} sat residual — ${st.app2Fail}`);await sleep(10000);
|
|
|
|
|
} else console.log(` (failed ${st.app2Fail})`);
|
|
|
|
|
|
|
|
|
|
// ===== NEGATIVE: try to drain MORE than the reward =====
|
|
|
|
|
step('NEGATIVE — try to drain 5000 sats when reward is 4000 (must FAIL)');
|
|
|
|
|
// Re-seed APP-B (which was just consumed by fail()) for a fresh probe.
|
|
|
|
|
// We do NOT touch appD — close() consumed it; that consumption is the
|
|
|
|
|
// outcome the verify check confirms below.
|
|
|
|
|
if(!st.app3Reseed){
|
|
|
|
|
// Reseed at appB address with a fresh UTXO (after fail consumed previous)
|
|
|
|
|
const m=await mintingUtxo(appCat);if(!m)throw new Error('app3Reseed: app-state minting NFT missing');
|
|
|
|
|
const {p,t}=await pickC(APP_INITIAL_VALUE+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:appB.tokenAddress,amount:APP_INITIAL_VALUE,token:{amount:0n,category:appCat,nft:{capability:'mutable',commitment:APP_OPEN}}});
|
|
|
|
|
tb.addOutput({to:C.cashaddr,amount:BigInt(m.satoshis)+t-SEED_DUST-APP_INITIAL_VALUE-MF});
|
|
|
|
|
st.app3Reseed=txidOf(await tb.send());save(st);console.log(` reseeded appB for negative probe — ${st.app3Reseed}`);await sleep(10000);
|
|
|
|
|
} else console.log(` (reseeded ${st.app3Reseed})`);
|
|
|
|
|
|
|
|
|
|
let negativeOk=st.app3NegProbed===true;
|
|
|
|
|
if(!negativeOk){
|
|
|
|
|
try {
|
|
|
|
|
const a=await appUtxoAt(appB);
|
|
|
|
|
if(!a){console.log(` (app UTXO missing — skipping probe)`);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;
|
|
|
|
|
const newCommit=appCommit(rYes,rNo,dYes,dNo,rAtt,dAtt,spare);
|
|
|
|
|
const appBefore=BigInt(a.satoshis);
|
|
|
|
|
// OVER-DRAIN: take 5000 sats instead of the 4000 reward
|
|
|
|
|
const appAfter=appBefore - 5000n;
|
|
|
|
|
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:appAfter,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+5000n-MF});
|
|
|
|
|
await tb.send();
|
|
|
|
|
console.log(` UNEXPECTED PASS — over-drain should have failed`);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log(` EXPECTED FAIL — over-drain rejected by covenant (${(e?.message||e).toString().slice(0,90)}...)`);
|
|
|
|
|
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 and D', !!aToken && !!dToken],
|
|
|
|
|
['rank-0x02 Delegated token at B', !!bDel],
|
|
|
|
|
['APP1 — seeded with 20000 sats escrow', !!st.app1Seed],
|
|
|
|
|
[`APP1 — A YES paid ${CITIZEN_REWARD} sats (regular)`, !!st.app1VoteA],
|
|
|
|
|
[`APP1 — B YES paid ${DELEGATED_REWARD} sats (delegated)`, !!st.app1VoteB],
|
|
|
|
|
['APP1 — close txn executed, A took 10000 sat residual', !!st.app1Close],
|
|
|
|
|
['APP2 — seeded with 20000 sats escrow', !!st.app2Seed],
|
|
|
|
|
[`APP2 — A NO paid ${CITIZEN_REWARD} sats anyway (anti-bribery)`, !!st.app2VoteA],
|
|
|
|
|
[`APP2 — D NO paid ${CITIZEN_REWARD} sats anyway (anti-bribery)`, !!st.app2VoteD],
|
|
|
|
|
['APP2 — fail() consumed UTXO, A took 12000 sat residual', !!st.app2Fail],
|
|
|
|
|
['APP3 — reseeded for negative probe', !!st.app3Reseed],
|
|
|
|
|
['APP3 — NEGATIVE probe: over-drain (5000 vs 4000 reward) rejected', negativeOk],
|
|
|
|
|
['final state: all three txns (close, fail, negative-probe rejection) executed', !!st.app1Close && !!st.app2Fail && negativeOk],
|
|
|
|
|
];
|
|
|
|
|
let ok=true;for(const [l,p] of checks){console.log(` [${p?'PASS':'FAIL'}] ${l}`);ok=ok&&p;}
|
|
|
|
|
|
|
|
|
|
step('SUMMARY — Phase D: pay-to-launch + paid-regardless-of-vote (40/60 split)');
|
|
|
|
|
console.log(` citCat ${citCat}\n appCat ${appCat}`);
|
|
|
|
|
console.log(` AppD(D) ${appD.tokenAddress}`);
|
|
|
|
|
console.log(` AppD(B) ${appB.tokenAddress}`);
|
|
|
|
|
console.log(' ');
|
|
|
|
|
console.log(' ApplicationCovenantD embeds the §6.5 fee escrow IN the application');
|
|
|
|
|
console.log(' UTXO\'s BCH value — no separate FeeEscrow covenant. Each vote drains');
|
|
|
|
|
console.log(' the chamber\'s per-vote reward (citizenReward 4000 sats / delegatedReward');
|
|
|
|
|
console.log(' 6000 sats; 40 % / 60 % split per voter type). REWARDS PAY THE SAME');
|
|
|
|
|
console.log(' REGARDLESS OF VOTE DIRECTION — the §6.5 anti-bribery property is the');
|
|
|
|
|
console.log(' load-bearing primitive (decision/participation separation), not the');
|
|
|
|
|
console.log(' reward magnitude. close()/fail() cleanup-incentive: residual escrow');
|
|
|
|
|
console.log(' flows to the caller. Treasury-routing of residuals is post-Phase-G');
|
|
|
|
|
console.log(' (addenda 4/5, snapshot 0115).');
|
|
|
|
|
console.log(' Phase D scope only — commit-reveal (E), Elder/founder tier (F),');
|
|
|
|
|
console.log(' 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);});
|