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>
111 lines
7.9 KiB
JavaScript
111 lines
7.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-share-collection-test.mjs — ADR-0042 collection side.
|
|
*
|
|
* Proves the per-claim fee SPLITS on-chain into two consensus-enforced streams:
|
|
* - company treasury (direct) → C
|
|
* - shareholder profit-share → D (stands in for the distribution covenant)
|
|
* and that the shareholder stream ACCRUES across claims (D's balance grows),
|
|
* never touching the company key.
|
|
*
|
|
* Asset: DeviantShareMBA · rate 100,000 sat/unit · feeBps 500 (5%) ·
|
|
* companySplitBps 4000 (company gets 40% of the fee → 2% of gross;
|
|
* shareholders get 60% of the fee → 3% of gross).
|
|
*
|
|
* Funder/claimer = B · company treasury = C · distribution pool = D.
|
|
* Run: node --import tsx scripts/chipnet-share-collection-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';
|
|
|
|
const ROOT = join(import.meta.dirname, '..');
|
|
const STATE = join(ROOT, 'ChipnetTests', '.chipnet-share-collection.json');
|
|
const ARTIFACT = join(ROOT, 'contracts/dist/DeviantShareMBA.json');
|
|
|
|
const RATE = 100000n, FEE_BPS = 500n, COMPANY_SPLIT_BPS = 4000n;
|
|
const SUPPLY = 1000000n, POSITION_VALUE = 1000n;
|
|
const CLAIMS = [2n, 3n]; // unit counts → gross 200k/300k, fee 10k/15k, co 4k/6k, sh 6k/9k
|
|
|
|
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 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 }; };
|
|
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 r = await w.send([{ cashaddr: w.cashaddr, value: 5000n, unit: 'sat' }]); console.log(` splitter ${r?.txId ?? r?.txid}`); await sleep(12_000); u = await w.getUtxos(); e = u.filter((x) => x.vout === 0 && !x.token); if (!e.length) throw new Error('no eligible'); return e[0]; }
|
|
const bchAt = async (provider, addr) => (await provider.getUtxos(addr)).filter((u) => !u.token).reduce((a, u) => a + BigInt(u.satoshis), 0n);
|
|
|
|
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 sigB = new SignatureTemplate(B.privateKey);
|
|
const companyLB = p2pkh(C.publicKeyHash), distributionLB = p2pkh(D.publicKeyHash);
|
|
const st = existsSync(STATE) ? JSON.parse(readFileSync(STATE, 'utf8')) : { claims: [] };
|
|
const save = () => writeFileSync(STATE, JSON.stringify(st, null, 2));
|
|
const ctor = (cat) => [txidToConsensusBytes(cat), RATE, FEE_BPS, COMPANY_SPLIT_BPS, companyLB, distributionLB];
|
|
|
|
console.log(`DeviantShareMBA · rate ${RATE} · fee ${FEE_BPS}bps · company split ${COMPANY_SPLIT_BPS}bps`);
|
|
console.log(`company treasury=C ${C.cashaddr.slice(0, 18)}… · distribution pool=D ${D.cashaddr.slice(0, 18)}…\n`);
|
|
|
|
// genesis reservoir (B funds)
|
|
if (!st.categoryId) {
|
|
const utxo = await eligible(B); const cat = utxo.txid;
|
|
const ct = new Contract(artifact, ctor(cat), { provider });
|
|
const r = await B.tokenGenesis({ cashaddr: ct.tokenAddress, amount: SUPPLY, value: POSITION_VALUE }, [], { utxoIds: [utxo] });
|
|
console.log(`contract ${ct.tokenAddress}\ngenesis ${r?.txId ?? r?.txid} · category ${cat}\n`);
|
|
st.categoryId = cat; save(); await sleep(10_000);
|
|
}
|
|
const contract = new Contract(artifact, ctor(st.categoryId), { provider });
|
|
|
|
const coBefore = await bchAt(provider, C.cashaddr), dBefore = await bchAt(provider, D.cashaddr);
|
|
console.log(`balances before — company(C) ${coBefore} · distribution(D) ${dBefore}\n`);
|
|
|
|
for (let i = st.claims.length; i < CLAIMS.length; i++) {
|
|
const tokenAmount = CLAIMS[i];
|
|
const gross = tokenAmount * RATE;
|
|
const fee = gross * FEE_BPS / 10000n;
|
|
const company = fee * COMPANY_SPLIT_BPS / 10000n;
|
|
const shareholder = fee - company;
|
|
|
|
const ru = await provider.getUtxos(contract.tokenAddress);
|
|
const pos = ru.find((u) => u.token && u.token.category === st.categoryId && !u.token.nft && BigInt(u.token.amount) > 0n);
|
|
if (!pos) throw new Error('reservoir not found');
|
|
const reservoir = BigInt(pos.token.amount);
|
|
|
|
const cash = (await provider.getUtxos(B.cashaddr)).filter((u) => !u.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis)));
|
|
const minerFee = 2000n;
|
|
const { p, t } = pick(cash, gross + fee + minerFee + 1000n);
|
|
const change = BigInt(pos.satoshis) + t - POSITION_VALUE - gross - company - shareholder - minerFee;
|
|
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(pos, contract.unlock.claim(tokenAmount, p2pkh(B.publicKeyHash)));
|
|
for (const u of p) tb.addInput(u, sigB.unlockP2PKH());
|
|
tb.addOutput({ to: contract.tokenAddress, amount: POSITION_VALUE, token: { amount: reservoir - tokenAmount, category: st.categoryId } });
|
|
tb.addOutput({ to: B.tokenaddr, amount: gross, token: { amount: tokenAmount, category: st.categoryId } });
|
|
tb.addOutput({ to: C.cashaddr, amount: company }); // [2] company treasury (direct)
|
|
tb.addOutput({ to: D.cashaddr, amount: shareholder }); // [3] distribution covenant (shareholder share)
|
|
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(`CLAIM ${i + 1}: ${tokenAmount} units · gross ${gross} · fee ${fee} → company ${company} (C) + shareholder ${shareholder} (D) · ${txid}`);
|
|
st.claims.push({ tokenAmount: tokenAmount.toString(), gross: gross.toString(), fee: fee.toString(), company: company.toString(), shareholder: shareholder.toString(), txid });
|
|
save(); await sleep(9_000);
|
|
}
|
|
|
|
const coAfter = await bchAt(provider, C.cashaddr), dAfter = await bchAt(provider, D.cashaddr);
|
|
const expCo = st.claims.reduce((a, c) => a + BigInt(c.company), 0n);
|
|
const expSh = st.claims.reduce((a, c) => a + BigInt(c.shareholder), 0n);
|
|
|
|
console.log(`\n=== RESULT — ADR-0042 collection-side fee split ===`);
|
|
console.log(` category ${st.categoryId}`);
|
|
for (const c of st.claims) console.log(` claim ${c.tokenAmount}u: fee ${c.fee} = company ${c.company} + shareholder ${c.shareholder} · ${c.txid.slice(0, 16)}…`);
|
|
console.log(` company(C) Δ ${coAfter - coBefore} sat (expected ${expCo}) ${coAfter - coBefore === expCo ? '✓' : '✗'}`);
|
|
console.log(` distribution(D) Δ ${dAfter - dBefore} sat (expected ${expSh}) ${dAfter - dBefore === expSh ? '✓' : '✗'}`);
|
|
console.log(` BEHAVIOR: every claim's fee split consensus-enforced into company(direct) + shareholder(routed);`);
|
|
console.log(` the shareholder stream ACCRUED in D across ${st.claims.length} claims, never touching the company key.`);
|
|
}
|
|
main().then(() => process.exit(0)).catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });
|