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>
112 lines
7.2 KiB
JavaScript
112 lines
7.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-distribution-payout-test.mjs — ADR-0042 payout side.
|
|
*
|
|
* The consensus-locked DistributionVault pays each holder their oracle-attested
|
|
* entitlement; the company can't take or redirect it, and a signature can't be
|
|
* replayed (it's bound to the vault UTXO's outpoint).
|
|
*
|
|
* Pool 31,000 sat. Oracle = E attests: holder C → 18,000 (60%), holder D → 12,000
|
|
* (40%); 1,000 residual stays in the vault. B funds the vault + miner fees.
|
|
*
|
|
* Run: node --import tsx scripts/chipnet-distribution-payout-test.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-distribution-payout.json');
|
|
const ARTIFACT = join(ROOT, 'contracts/dist/DistributionVault.json');
|
|
|
|
const POOL = 31000n;
|
|
const PAYOUTS = [{ who: 'C', amount: 18000n }, { who: 'D', amount: 12000n }];
|
|
|
|
const 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; };
|
|
const txidToConsensusBytes = (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; };
|
|
const 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; };
|
|
const u32le = (n) => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, Number(n), true); return b; };
|
|
const u64le = (n) => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(n), true); return b; };
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const pick = (cash, need) => { const p = []; let t = 0n; for (const u of cash) { p.push(u); t += BigInt(u.satoshis); if (t >= need) break; } if (t < need) throw new Error(`insufficient ${t}<${need}`); return { p, t }; };
|
|
function oracleSign(priv, msg) { const s = secp256k1.signMessageHashSchnorr(priv, laSha256.hash(msg)); if (typeof s === 'string') throw new Error('sign: ' + s); return s; }
|
|
// payout message: recipientLB(25) ‖ amount(8 LE) ‖ outpointTxHash(32) ‖ outpointIndex(4 LE)
|
|
function payoutMessage(recipientLB, amount, vaultUtxo) {
|
|
const m = new Uint8Array(69);
|
|
m.set(recipientLB, 0);
|
|
m.set(u64le(amount), 25);
|
|
m.set(txidToConsensusBytes(vaultUtxo.txid), 33);
|
|
m.set(u32le(vaultUtxo.vout), 65);
|
|
return m;
|
|
}
|
|
|
|
async function main() {
|
|
if (!process.argv.includes('--yes')) { console.log('Re-run with --yes.'); return; }
|
|
const artifact = JSON.parse(readFileSync(ARTIFACT, 'utf8'));
|
|
const provider = new ElectrumNetworkProvider('chipnet');
|
|
const B = await TestNetWallet.fromWIF(JSON.parse(readFileSync(join(ROOT, '.chipnet-keys-b.json'), 'utf8')).wif);
|
|
const C = await TestNetWallet.fromWIF(JSON.parse(readFileSync(join(ROOT, '.chipnet-keys-c.json'), 'utf8')).wif);
|
|
const D = await TestNetWallet.fromWIF(JSON.parse(readFileSync(join(ROOT, '.chipnet-keys-d.json'), 'utf8')).wif);
|
|
const E = await TestNetWallet.fromWIF(JSON.parse(readFileSync(join(ROOT, '.chipnet-keys-e.json'), 'utf8')).wif);
|
|
const sigB = new SignatureTemplate(B.privateKey);
|
|
const W = { C, D };
|
|
const contract = new Contract(artifact, [E.publicKey], { provider });
|
|
const st = existsSync(STATE) ? JSON.parse(readFileSync(STATE, 'utf8')) : { payouts: [] };
|
|
const save = () => writeFileSync(STATE, JSON.stringify(st, null, 2));
|
|
const recip = async (addr) => (await provider.getUtxos(addr)).filter((u) => !u.token).reduce((a, u) => a + BigInt(u.satoshis), 0n);
|
|
const vaultUtxo = async () => { const u = await provider.getUtxos(contract.address); const v = u.filter((x) => !x.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis))); return v[0]; };
|
|
|
|
console.log(`DistributionVault ${contract.address}\noracle=E · pool ${POOL} · payouts ${PAYOUTS.map((p) => p.who + ':' + p.amount).join(' ')}\n`);
|
|
|
|
// fund the vault
|
|
if (!st.funded) {
|
|
const r = await B.send([{ cashaddr: contract.address, value: Number(POOL), unit: 'sat' }]);
|
|
console.log(`fund vault ${POOL} sat · ${r?.txId ?? r?.txid}`); st.funded = true; save(); await sleep(10_000);
|
|
}
|
|
|
|
const cBefore = await recip(C.cashaddr), dBefore = await recip(D.cashaddr);
|
|
console.log(`balances before — C ${cBefore} · D ${dBefore}\n`);
|
|
|
|
for (let i = st.payouts.length; i < PAYOUTS.length; i++) {
|
|
const { who, amount } = PAYOUTS[i];
|
|
const recipient = W[who];
|
|
const recipientLB = p2pkh(recipient.publicKeyHash);
|
|
const vu = await vaultUtxo();
|
|
if (!vu) throw new Error('vault UTXO not found');
|
|
const remainder = BigInt(vu.satoshis) - amount;
|
|
const msg = payoutMessage(recipientLB, amount, vu);
|
|
const sig = oracleSign(E.privateKey, msg);
|
|
|
|
const minerFee = 1500n;
|
|
const cash = (await provider.getUtxos(B.cashaddr)).filter((u) => !u.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis)));
|
|
const { p, t } = pick(cash, minerFee + 1000n);
|
|
const change = t - minerFee;
|
|
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(vu, contract.unlock.payout(recipientLB, sig, msg)); // input 0 = vault (activeInputIndex)
|
|
for (const u of p) tb.addInput(u, sigB.unlockP2PKH()); // miner-fee funding
|
|
tb.addOutput({ to: recipient.cashaddr, amount }); // [0] recipient
|
|
tb.addOutput({ to: contract.address, amount: remainder }); // [1] vault remainder
|
|
if (change >= 546n) tb.addOutput({ to: B.cashaddr, amount: change });
|
|
const sent = await tb.send(); const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
|
|
console.log(`PAYOUT ${i + 1}: ${amount} sat → ${who} · vault ${vu.satoshis}→${remainder} · ${txid}`);
|
|
st.payouts.push({ who, amount: amount.toString(), remainder: remainder.toString(), txid }); save(); await sleep(9_000);
|
|
}
|
|
|
|
const cAfter = await recip(C.cashaddr), dAfter = await recip(D.cashaddr);
|
|
const vu = await vaultUtxo();
|
|
const expC = PAYOUTS.filter((p) => p.who === 'C').reduce((a, p) => a + p.amount, 0n);
|
|
const expD = PAYOUTS.filter((p) => p.who === 'D').reduce((a, p) => a + p.amount, 0n);
|
|
|
|
console.log(`\n=== RESULT — ADR-0042 distribution payout ===`);
|
|
console.log(` vault ${contract.address}`);
|
|
for (const p of st.payouts) console.log(` payout ${p.amount} → ${p.who} · vault→${p.remainder} · ${p.txid.slice(0, 16)}…`);
|
|
console.log(` C Δ ${cAfter - cBefore} (expected ${expC}) ${cAfter - cBefore === expC ? '✓' : '✗'}`);
|
|
console.log(` D Δ ${dAfter - dBefore} (expected ${expD}) ${dAfter - dBefore === expD ? '✓' : '✗'}`);
|
|
console.log(` vault residual now: ${vu ? vu.satoshis : 0} sat (expected ${POOL - expC - expD})`);
|
|
console.log(` BEHAVIOR: each payout = oracle-attested recipient+amount, paid from the consensus-locked`);
|
|
console.log(` vault; signatures bound to the vault outpoint (no replay); remainder preserved.`);
|
|
}
|
|
main().then(() => process.exit(0)).catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });
|