deviant/scripts/chipnet-product2-shares-dividends.mjs
Local Dev debd6a6fbb chore: sweep in-flight work across all domains
Snapshot commit rolling up ~255 files of coherent-but-multi-domain in-flight
work that had accumulated on this branch. Split by area:

Contracts + covenants
- New: AsmFxSwap, DeviantOpenMintNBA, DeviantTieredMintOracleFT,
  TermCouponVault, UniformReserve, UniformReserveAggregate.
- Verify-artifact refresh (src/verify/artifacts/*.json + canonical.ts).

Wallet + shell (shells/app + src/wallet)
- App.tsx, MainScreen, ArtemisionScreen, AssetDetail, AgoraScreen,
  SettingsScreen, Unlock, TokenImportModal, UniformFiat/DepositPanel,
  KatalogosPanel (renamed from PinaxPanel), shell.css.
- src/wallet: wallet.ts, runtime.ts, index.ts. Core registry/chipnetFile.
- Deleted stale hook: useAllDevBalances.ts.

Cross-chain + correspondent (services/correspondent + scripts)
- Correspondent server/bch/config updates for PYUSD Solana, EURC Sepolia,
  aggregate reserve flows.
- New scripts: PYUSD roundtrip, EURC onboarding, GMX game loop /
  genesis / mint sanity, tiered mint fee, aggregate reserve, product2
  shares dividends, product3 t-bond, uniform fiat cell tests.

Telegram order-book (P2PExchange bot)
- db.ts, index.ts, en/ru locales — updates upstream from the extracted
  standalone repo at D:/Dev/P2PExchange.

Docs — Parameters (specs), wiki, snapshots, design mockups
- Parameters: FIATm/MBA_Shares/Potidaea/Anthemus refinements +
  ASM_CrossChain/DebitCredit + UniformFiat + Anthemus DEX MVP prompt +
  Artemision + Athenae Politeia + Philosophy + SSHkeyServer runbook +
  new dApp/{Artemision,Katalogos,Pinax,Pari_escrow,SURFACES}.
- wiki: cross-chain, games-tote, glossary, governance/voting-systems,
  infra, wallet, wallet-ui-design, decisions (ADR log).
- snapshots: catch-up entries and INDEX regeneration.
- design: mockups refreshed (agora, asset-detail, assets-tab, potidaea)
  + Potidaea assets + tote deploy notes.

Housekeeping
- .gitignore: exclude the new root test/session state files
  (.sol-vault-keys.json, .football-tote-state.json,
  .tote-platform-state.json, .redeem-roundtrip-state.json,
  .eurc-onboard-state.json — some contain oracle privkeys / vault
  secretKeyBase58; NEVER commit) + the .usdm-historical-chart.html
  chart output + MS Office `~$*` lock files + scripts/bundle-surfaces
  `build/` output. Also add scripts/bundle-surfaces.mjs.
- CLAUDE.md, WALLET-COORDINATION.md, snapshots/INDEX.md, design/README.md
  refreshed.
- package.json / package-lock.json bumps.

Verified: sensitive files at repo root confirmed ignored (git check-ignore
each). No `.env`, no raw WIF/xprv/mnemonic/secretKey/oraclePriv strings in
the staged diff. MS Word autosave file (`Parameters/Philosophy/~$*.docx`)
excluded and gitignored.

The WizardConnect responder work landed separately in the previous commit
(6987591); this is everything else that had accumulated alongside it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-29 01:14:33 +02:00

158 lines
12 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-product2-shares-dividends.mjs — Product ② (shares + dividends), end-to-end.
*
* Fixed cap table + dividend ∝ shares + a REDEEMABLE FLOOR (the new bit vs ①).
* "Floored equity": a shareholder can take income (dividend) OR recover principal (floor).
*
* - `DeviantShareMBA` share · rate 100k · fee 500bps · split 4000bps · distributionLB = vault.
* 1. B claims 10 shares (the issued cap table); fee 50k → company 20k(B) + pool 30k(vault).
* 2. distribute: 6 → C, 4 → D.
* 3. DIVIDEND: oracle pays C its pro-rata (6/10 = 18,000) from the vault — C keeps its shares.
* 4. FLOOR: D redeems its 4 shares → recovers the BCH backing (4×100k = 400,000 − miner),
* shares burned. Downside protection exercised.
*
* Run: node --import tsx scripts/chipnet-product2-shares-dividends.mjs --yes
*/
import { existsSync, readFileSync, writeFileSync, mkdirSync } 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, cashAddressToLockingBytecode } from '@bitauth/libauth';
const ROOT = join(import.meta.dirname, '..');
const STATE = join(ROOT, 'ChipnetTests', '.chipnet-product2-shares.json');
const SHARE_ART = JSON.parse(readFileSync(join(ROOT, 'contracts/dist/DeviantShareMBA.json'), 'utf8'));
const VAULT_ART = JSON.parse(readFileSync(join(ROOT, 'contracts/dist/DistributionVault.json'), 'utf8'));
const RATE = 100000n, FEE_BPS = 500n, COMPANY_SPLIT_BPS = 4000n;
const SUPPLY = 1000n, POSITION_VALUE = 1000n;
const CAP = 10n; // issued cap table
const SPLIT = [{ who: 'C', units: 6n }, { who: 'D', units: 4n }];
const DIVIDEND_TO = 'C'; // C takes income
const REDEEM_BY = 'D'; // D takes the floor
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(s); return s; }
function lockOf(addr) { const r = cashAddressToLockingBytecode(addr); if (typeof r === 'string') throw new Error(r); return r.bytecode; }
function payoutMessage(recipientLB, amount, vu) { const m = new Uint8Array(69); m.set(recipientLB, 0); m.set(u64le(amount), 25); m.set(txidToConsensusBytes(vu.txid), 33); m.set(u32le(vu.vout), 65); return m; }
async function main() {
if (!process.argv.includes('--yes')) { console.log('Re-run with --yes.'); return; }
mkdirSync(join(ROOT, 'ChipnetTests'), { recursive: true });
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), sigD = new SignatureTemplate(D.privateKey);
const W = { C, D }; const SIG = { C: new SignatureTemplate(C.privateKey), D: sigD };
const vault = new Contract(VAULT_ART, [E.publicKey], { provider });
const companyLB = p2pkh(B.publicKeyHash), distributionLB = lockOf(vault.address);
const st = existsSync(STATE) ? JSON.parse(readFileSync(STATE, 'utf8')) : {};
const save = () => writeFileSync(STATE, JSON.stringify(st, null, 2));
const ctor = (cat) => [txidToConsensusBytes(cat), RATE, FEE_BPS, COMPANY_SPLIT_BPS, companyLB, distributionLB];
const cashOf = async (w) => (await provider.getUtxos(w.cashaddr)).filter((u) => !u.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis)));
const coinBal = async (w) => (await provider.getUtxos(w.tokenaddr)).filter((u) => u.token && u.token.category === st.categoryId && !u.token.nft).reduce((a, u) => a + BigInt(u.token.amount), 0n);
const bchOf = async (w) => (await provider.getUtxos(w.cashaddr)).filter((u) => !u.token).reduce((a, u) => a + BigInt(u.satoshis), 0n);
const vaultUtxo = async () => (await provider.getUtxos(vault.address)).filter((x) => !x.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis)))[0];
console.log(`Product ② shares+dividends · cap table ${CAP} shares · vault ${vault.address.slice(0, 22)}…`);
if (!st.categoryId) {
let u = await provider.getUtxos(B.cashaddr); let e = u.filter((x) => x.vout === 0 && !x.token);
if (!e.length) { await B.send([{ cashaddr: B.cashaddr, value: 5000n, unit: 'sat' }]); await sleep(12000); u = await provider.getUtxos(B.cashaddr); e = u.filter((x) => x.vout === 0 && !x.token); }
const utxo = e[0]; const cat = utxo.txid;
const ct = new Contract(SHARE_ART, ctor(cat), { provider });
const r = await B.tokenGenesis({ cashaddr: ct.tokenAddress, amount: SUPPLY, value: POSITION_VALUE }, [], { utxoIds: [utxo] });
console.log(`share contract ${ct.tokenAddress}\ngenesis ${r?.txId ?? r?.txid} · category ${cat}`);
st.categoryId = cat; save(); await sleep(10000);
}
const share = new Contract(SHARE_ART, ctor(st.categoryId), { provider });
// 1. claim the cap table (10 shares) → B
if (!st.claim) {
const gross = CAP * RATE, fee = gross * FEE_BPS / 10000n, company = fee * COMPANY_SPLIT_BPS / 10000n, shareholder = fee - company;
const ru = await provider.getUtxos(share.tokenAddress);
const pos = ru.find((u) => u.token && u.token.category === st.categoryId && !u.token.nft && BigInt(u.token.amount) > 0n);
const reservoir = BigInt(pos.token.amount);
const minerFee = 2500n; const { p, t } = pick(await cashOf(B), gross + fee + minerFee + 1000n);
const change = BigInt(pos.satoshis) + t - POSITION_VALUE - gross - company - shareholder - minerFee;
const tb = new TransactionBuilder({ provider });
tb.addInput(pos, share.unlock.claim(CAP, p2pkh(B.publicKeyHash)));
for (const u of p) tb.addInput(u, sigB.unlockP2PKH());
tb.addOutput({ to: share.tokenAddress, amount: POSITION_VALUE, token: { amount: reservoir - CAP, category: st.categoryId } });
tb.addOutput({ to: B.tokenaddr, amount: gross, token: { amount: CAP, category: st.categoryId } });
tb.addOutput({ to: B.cashaddr, amount: company });
tb.addOutput({ to: vault.address, amount: shareholder });
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(`\n1. issue ${CAP} shares · fee ${fee} → company ${company}(B) + dividend pool ${shareholder}(vault) · ${txid}`);
st.claim = { pool: shareholder.toString(), txid }; save(); await sleep(9000);
}
// 2. distribute the cap table: 6 → C, 4 → D
if (!st.distributed) {
const bu = (await provider.getUtxos(B.tokenaddr)).find((u) => u.token && u.token.category === st.categoryId && !u.token.nft && BigInt(u.token.amount) === CAP);
const minerFee = 2000n; const { p, t } = pick(await cashOf(B), minerFee + 1000n);
const tb = new TransactionBuilder({ provider });
tb.addInput(bu, sigB.unlockP2PKH()); for (const u of p) tb.addInput(u, sigB.unlockP2PKH());
for (const s of SPLIT) tb.addOutput({ to: W[s.who].tokenaddr, amount: s.units * RATE, token: { amount: s.units, category: st.categoryId } });
const change = BigInt(bu.satoshis) + t - SPLIT.reduce((a, s) => a + s.units * RATE, 0n) - minerFee;
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(`2. distribute cap table: ${SPLIT.map((s) => s.units + '→' + s.who).join(', ')} · ${txid}`);
st.distributed = { txid }; save(); await sleep(9000);
}
// 3. DIVIDEND to C (∝ shares), from the vault
if (!st.dividend) {
const cShares = await coinBal(C); const pool = BigInt(st.claim.pool);
const amount = pool * cShares / CAP; // 6/10 of 30,000 = 18,000
const recipientLB = p2pkh(C.publicKeyHash);
const vu = await vaultUtxo(); const remainder = BigInt(vu.satoshis) - amount;
const msg = payoutMessage(recipientLB, amount, vu); const sig = oracleSign(E.privateKey, msg);
const minerFee = 1500n; const { p, t } = pick(await cashOf(B), minerFee + 1000n); const change = t - minerFee;
const cBefore = await bchOf(C);
const tb = new TransactionBuilder({ provider });
tb.addInput(vu, vault.unlock.payout(recipientLB, sig, msg));
for (const u of p) tb.addInput(u, sigB.unlockP2PKH());
tb.addOutput({ to: C.cashaddr, amount });
tb.addOutput({ to: vault.address, amount: 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(`3. DIVIDEND: C (${cShares}/${CAP} shares) → ${amount} sat · vault→${remainder} · ${txid}`);
st.dividend = { cShares: cShares.toString(), amount: amount.toString(), cBefore: cBefore.toString(), txid }; save(); await sleep(9000);
}
// 4. FLOOR: D redeems its shares → recovers backing (self-backed P2PKH burn, no oracle)
if (!st.redeem) {
const du = (await provider.getUtxos(D.tokenaddr)).find((u) => u.token && u.token.category === st.categoryId && !u.token.nft && BigInt(u.token.amount) > 0n);
if (!du) throw new Error('D shares not found');
const dShares = BigInt(du.token.amount); const backing = BigInt(du.satoshis);
const minerFee = 1000n; const recovered = backing - minerFee;
const dBefore = await bchOf(D);
const tb = new TransactionBuilder({ provider, allowImplicitFungibleTokenBurn: true });
tb.addInput(du, sigD.unlockP2PKH());
tb.addOutput({ to: D.cashaddr, amount: recovered });
const sent = await tb.send(); const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
console.log(`4. FLOOR: D redeems ${dShares} shares → recovered ${recovered} sat (backing ${backing}) · ${txid}`);
st.redeem = { dShares: dShares.toString(), recovered: recovered.toString(), dBefore: dBefore.toString(), txid }; save(); await sleep(9000);
}
const cGot = (await bchOf(C)) - BigInt(st.dividend.cBefore);
const dGot = (await bchOf(D)) - BigInt(st.redeem.dBefore);
const dSharesNow = await coinBal(D);
console.log(`\n=== RESULT — Product ② shares + dividends (floored equity) ===`);
console.log(` cap table: ${CAP} shares issued (C ${st.dividend.cShares} · D ${st.redeem.dShares})`);
console.log(` C — INCOME: dividend ${cGot} sat on ${st.dividend.cShares} shares (∝ 6/10) ${cGot === BigInt(st.dividend.amount) ? '✓' : '✗'}; still holds its shares (floor intact)`);
console.log(` D — FLOOR: redeemed ${st.redeem.dShares} shares → recovered ${dGot} sat (principal) ${dGot === BigInt(st.redeem.recovered) ? '✓' : '✗'}; shares now ${dSharesNow} ${dSharesNow === 0n ? '(burned ✓)' : ''}`);
console.log(` → floored equity proven: income path (C) + redeemable-floor path (D), on a fixed cap table.`);
}
main().then(() => process.exit(0)).catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });