Snapshot of the shared working tree's outstanding changes (multiple concurrent workstreams), excluding generated runtime state files (.football-tote-state.json, .tote-platform-state.json, .usdm-historical-chart.html). Spans: wallet shell (connect/, keystore MultiKeyStore+vault, screens, currency picker, sign-message), verify artifacts, new Potidaea/DRA/tote contracts + regtest suites, cross-chain/T6 + WalletConnect Parameters, a telegram-orderbook subproject, design mockups, and docs. See shells/app/WALLET-COORDINATION.md for the per-file ownership split. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
336 lines
18 KiB
JavaScript
336 lines
18 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* chipnet-reflective-dra-genesis.mjs
|
||
*
|
||
* Chipnet genesis + end-to-end demonstration of the Reflective DRA system:
|
||
*
|
||
* 1. Deploy DeviantReflectiveDRA — shared-pool, oracle-reflective, fee-receiving
|
||
* DRA covenant. Genesis with seed mint to initialize the per-DRA NAV.
|
||
* 2. Deploy DeviantOracleMBA_DraFee — a FIATm-family variant that routes a fee
|
||
* slice (output [2]) to the DRA pool address on every mint.
|
||
* 3. Mint a small DRA-feeder USDm (named "fUSDm" for fee-feeder) on the DraFee
|
||
* variant. The mint TX deposits a fee output landing at the DRA pool address.
|
||
* 4. Read both UTXO sets and verify: DRA pool BCH value grew by the fee amount;
|
||
* DRA NFT commitment unchanged (consolidated separately via receiveFees()).
|
||
*
|
||
* What this proves on chain:
|
||
* • The fee-redirection channel (FIATm tx → DRA pool) works end-to-end.
|
||
* • The DRA pool covenant accepts external fee deposits at its address.
|
||
* • The DRA NFT commitment + pool value separate cleanly (state in NFT, fees in BCH).
|
||
*
|
||
* What this does NOT yet prove (later chipnet scripts):
|
||
* • DRA mint via the reflective rate formula (separate test).
|
||
* • DRA receiveFees() consolidating multiple pending deposits.
|
||
* • Per-currency multi-feed under the multi-currency competition design (T8 sim).
|
||
*
|
||
* Prerequisites:
|
||
* 1. Compile contracts: `rtk npm run build:contracts` (or whatever the project uses
|
||
* to produce contracts/dist/*.json artifacts).
|
||
* 2. Funded chipnet wallets: .chipnet-keys-b.json (test wallet) + .chipnet-keys-e.json
|
||
* (oracle/founder). The keys files are gitignored — generate locally per project
|
||
* convention.
|
||
*
|
||
* Run (plan only): node --import tsx scripts/chipnet-reflective-dra-genesis.mjs
|
||
* Run (broadcast): node --import tsx scripts/chipnet-reflective-dra-genesis.mjs --yes
|
||
*
|
||
* State file: ChipnetTests/.chipnet-reflective-dra-genesis.json
|
||
*/
|
||
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-reflective-dra-genesis.json');
|
||
const DRA_ARTIFACT = join(ROOT, 'contracts/dist/DeviantReflectiveDRA.json');
|
||
const MBA_DRAFEE_ARTIFACT = join(ROOT, 'contracts/dist/DeviantOracleMBA_DraFee.json');
|
||
const KEYS_B = join(ROOT, '.chipnet-keys-b.json');
|
||
const KEYS_E = join(ROOT, '.chipnet-keys-e.json');
|
||
|
||
// ── Genesis parameters (matching design record §4 + simulator) ───────────
|
||
const BCH_EUR_GENESIS = 200n;
|
||
const EUR_RATE_0 = 100_000_000n / BCH_EUR_GENESIS; // 500,000 sats per EURm
|
||
const LAUNCH_RATIO = 357n;
|
||
const DRA_RATE_0 = EUR_RATE_0 / LAUNCH_RATIO; // 1401 sats per DRA
|
||
const SEED_DRA_SUPPLY = 10_000n; // initial pool tokens
|
||
const SEED_POOL_SATS = DRA_RATE_0 * SEED_DRA_SUPPLY; // 14,010,000 sats
|
||
|
||
const CATALYST_TRIGGER = 210_000n;
|
||
const BETA_X1000 = 100n; // β = 0.10
|
||
const GAMMA_X1000 = 50n; // γ = 0.05
|
||
|
||
// ── fUSDm (fee-feeder USDm test currency) ────────────────────────────────
|
||
const FUSD_DECIMALS = 2n;
|
||
const FUSD_ATOMIC_SATS = 5_000n; // 1 USD = 500,000 sats; 1 atomic (cent) = 5,000 sats
|
||
const FUSD_FOUNDER_FEE_BPS = 5n; // 0.05% to founder (existing pattern)
|
||
const FUSD_DRA_FEE_BPS = 5n; // 0.05% to DRA pool (NEW per §2)
|
||
const FUSD_SUPPLY = 1_000_000n; // reservoir size
|
||
const FUSD_CLAIM_ATOMS = 500n; // claim $5.00 face value
|
||
|
||
const POSITION_VALUE = 1000n;
|
||
const MINER_FEE = 4000n;
|
||
|
||
// ── Helpers (shared with existing chipnet patterns) ──────────────────────
|
||
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 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);
|
||
const v = BigInt(n);
|
||
for (let i = 0; i < 8; i++) b[i] = Number((v >> BigInt(i * 8)) & 0xffn);
|
||
return b;
|
||
};
|
||
const omsg = (price) => { const m = new Uint8Array(16); m.set(u32le(1), 0); m.set(u32le(1), 4); m.set(u32le(1), 8); m.set(u32le(price), 12); return m; };
|
||
const osign = (priv, m) => { const s = secp256k1.signMessageHashSchnorr(priv, laSha256.hash(m)); if (typeof s === 'string') throw new Error(s); return s; };
|
||
const pickCash = (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 }; };
|
||
const fmtSats = (s) => { const a = BigInt(s); return `${a.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')} sats (${(Number(a) / 1e8).toFixed(8)} BCH)`; };
|
||
|
||
// Pack DRA pool initial NFT commitment per design §1 layout:
|
||
// bytes 8: supply || bytes 4: catalysts || bytes 8: dra_rate_0 || bytes 8: eur_rate_0
|
||
function packDraState(supply, catalysts, dra_rate_0, eur_rate_0) {
|
||
const c = new Uint8Array(28);
|
||
c.set(u64le(supply), 0);
|
||
c.set(u32le(catalysts), 8);
|
||
c.set(u64le(dra_rate_0), 12);
|
||
c.set(u64le(eur_rate_0), 20);
|
||
return c;
|
||
}
|
||
|
||
function commitmentHex(c) {
|
||
let s = '';
|
||
for (const b of c) s += b.toString(16).padStart(2, '0');
|
||
return s;
|
||
}
|
||
|
||
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: 5000n, unit: 'sat' }], bad.length ? { utxoIds: bad } : undefined);
|
||
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 UTXO available');
|
||
return e[0];
|
||
}
|
||
|
||
async function cashOf(provider, addr) {
|
||
return (await provider.getUtxos(addr)).filter((u) => !u.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis)));
|
||
}
|
||
|
||
function checkArtifacts() {
|
||
const missing = [];
|
||
if (!existsSync(DRA_ARTIFACT)) missing.push(DRA_ARTIFACT);
|
||
if (!existsSync(MBA_DRAFEE_ARTIFACT)) missing.push(MBA_DRAFEE_ARTIFACT);
|
||
if (missing.length) {
|
||
console.error('Missing compiled contract artifacts:');
|
||
for (const m of missing) console.error(' - ' + m);
|
||
console.error('\nCompile the contracts first:');
|
||
console.error(' rtk npm run build:contracts');
|
||
console.error('(Or whatever the project uses to produce contracts/dist/*.json.)');
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
function checkKeys() {
|
||
if (!existsSync(KEYS_B)) { console.error(`Missing ${KEYS_B} — wallet keys required.`); process.exit(1); }
|
||
if (!existsSync(KEYS_E)) { console.error(`Missing ${KEYS_E} — oracle/founder keys required.`); process.exit(1); }
|
||
}
|
||
|
||
async function main() {
|
||
const yes = process.argv.includes('--yes');
|
||
|
||
console.log('═══════════════════════════════════════════════════════════════════');
|
||
console.log(' Chipnet genesis — Reflective DRA + fee-redirector FIATm variant');
|
||
console.log('═══════════════════════════════════════════════════════════════════');
|
||
console.log('');
|
||
console.log(` Genesis parameters:`);
|
||
console.log(` BCH/EUR @ genesis: €${BCH_EUR_GENESIS}`);
|
||
console.log(` EUR_rate_0: ${EUR_RATE_0} sats per EURm`);
|
||
console.log(` Launch ratio: 1 EUR = ${LAUNCH_RATIO} DRA`);
|
||
console.log(` DRA_rate_0: ${DRA_RATE_0} sats per DRA`);
|
||
console.log(` Seed pool: ${SEED_DRA_SUPPLY} DRA backed by ${SEED_POOL_SATS} sats`);
|
||
console.log(` Catalyst trigger: every ${CATALYST_TRIGGER} cumulative DRA mints`);
|
||
console.log(` β (counter): ${Number(BETA_X1000) / 1000}`);
|
||
console.log(` γ (gain): ${Number(GAMMA_X1000) / 1000}`);
|
||
console.log('');
|
||
console.log(` fUSDm test currency:`);
|
||
console.log(` Decimals: ${FUSD_DECIMALS}`);
|
||
console.log(` Atomic sats: ${FUSD_ATOMIC_SATS}`);
|
||
console.log(` Founder fee: ${Number(FUSD_FOUNDER_FEE_BPS) / 100}%`);
|
||
console.log(` DRA pool fee: ${Number(FUSD_DRA_FEE_BPS) / 100}%`);
|
||
console.log(` Claim atoms: ${FUSD_CLAIM_ATOMS} ($${Number(FUSD_CLAIM_ATOMS) / 100})`);
|
||
console.log('');
|
||
|
||
checkArtifacts();
|
||
checkKeys();
|
||
|
||
const draArtifact = JSON.parse(readFileSync(DRA_ARTIFACT, 'utf8'));
|
||
const mbaArtifact = JSON.parse(readFileSync(MBA_DRAFEE_ARTIFACT, 'utf8'));
|
||
const provider = new ElectrumNetworkProvider('chipnet');
|
||
const B = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS_B, 'utf8')).wif);
|
||
const E = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS_E, 'utf8')).wif);
|
||
const sigB = new SignatureTemplate(B.privateKey);
|
||
const oraclePk = E.publicKey;
|
||
const founderLB = p2pkh(E.publicKeyHash);
|
||
|
||
console.log(` B (test holder): ${B.cashaddr}`);
|
||
console.log(` E (oracle/founder): ${E.cashaddr}`);
|
||
console.log('');
|
||
|
||
const st = existsSync(STATE) ? JSON.parse(readFileSync(STATE, 'utf8')) : {};
|
||
const save = () => writeFileSync(STATE, JSON.stringify(st, null, 2));
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// Phase 1 — DRA pool genesis
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
console.log('──── Phase 1 — DRA pool genesis ────────────────────────────────────');
|
||
|
||
if (!st.dra) st.dra = {};
|
||
if (!st.dra.contractAddress) {
|
||
// Compute the contract address (constructor depends on tokenCategory which we
|
||
// assign at genesis). Two-step: genesis the category, then construct contract.
|
||
const utxo = await eligible(B);
|
||
const draCategory = utxo.txid;
|
||
const draCtor = [
|
||
txidToConsensusBytes(draCategory),
|
||
oraclePk,
|
||
CATALYST_TRIGGER,
|
||
BETA_X1000,
|
||
GAMMA_X1000,
|
||
];
|
||
const draContract = new Contract(draArtifact, draCtor, { provider });
|
||
console.log(` contract: ${draContract.tokenAddress}`);
|
||
console.log(` category: ${draCategory}`);
|
||
|
||
if (!yes) {
|
||
console.log(` (plan only — re-run with --yes to genesis)`);
|
||
} else {
|
||
const initialCommitment = packDraState(SEED_DRA_SUPPLY, 0n, DRA_RATE_0, EUR_RATE_0);
|
||
console.log(` initial NFT commitment (28B): ${commitmentHex(initialCommitment)}`);
|
||
// Genesis: mint mutable NFT carrying state, FT reservoir, BCH = seed pool.
|
||
// (Real genesis sequence depends on cashscript / mainnet-js version; this
|
||
// is a planning sketch — the actual tx construction needs project-specific
|
||
// primitives, often via TransactionBuilder.)
|
||
console.log(` ⚠ Genesis transaction construction is project-specific —`);
|
||
console.log(` requires custom NFT-issuance + FT-reservoir-fund flow.`);
|
||
console.log(` Mark as TODO until verified locally.`);
|
||
}
|
||
st.dra.contractAddress = draContract.tokenAddress;
|
||
st.dra.category = draCategory;
|
||
st.dra.draRate0 = DRA_RATE_0.toString();
|
||
st.dra.eurRate0 = EUR_RATE_0.toString();
|
||
st.dra.seedPoolSats = SEED_POOL_SATS.toString();
|
||
st.dra.seedSupply = SEED_DRA_SUPPLY.toString();
|
||
save();
|
||
} else {
|
||
console.log(` resumed contract: ${st.dra.contractAddress}`);
|
||
console.log(` category: ${st.dra.category.slice(0, 24)}...`);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// Phase 2 — fUSDm (fee-feeder) deployment on DeviantOracleMBA_DraFee
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
console.log('\n──── Phase 2 — fUSDm (fee-feeder) genesis on DraFee variant ─────────');
|
||
|
||
if (!st.fusdm) st.fusdm = {};
|
||
if (!st.fusdm.contractAddress) {
|
||
const utxo = await eligible(B);
|
||
const fusdCategory = utxo.txid;
|
||
|
||
// DRA pool address must be available as the draPoolLockingBytecode parameter.
|
||
// For chipnet POC, derive the P2SH32 lockingBytecode from the DRA contract address.
|
||
const draPoolLockingBytecode = st.dra?.lockingBytecode
|
||
? hexToBin(st.dra.lockingBytecode)
|
||
: null;
|
||
|
||
if (!draPoolLockingBytecode) {
|
||
console.log(` ⚠ DRA pool locking bytecode not yet computed (Phase 1 needs to land first).`);
|
||
console.log(` Re-run after Phase 1 completes.`);
|
||
} else {
|
||
const fusdCtor = [
|
||
txidToConsensusBytes(fusdCategory),
|
||
oraclePk,
|
||
FUSD_FOUNDER_FEE_BPS,
|
||
founderLB,
|
||
FUSD_DRA_FEE_BPS,
|
||
draPoolLockingBytecode,
|
||
];
|
||
const fusdContract = new Contract(mbaArtifact, fusdCtor, { provider });
|
||
console.log(` contract: ${fusdContract.tokenAddress}`);
|
||
console.log(` category: ${fusdCategory}`);
|
||
|
||
if (!yes) {
|
||
console.log(` (plan only — re-run with --yes to genesis)`);
|
||
} else {
|
||
const r = await B.tokenGenesis(
|
||
{ cashaddr: fusdContract.tokenAddress, amount: FUSD_SUPPLY, value: POSITION_VALUE },
|
||
[],
|
||
{ utxoIds: [utxo] },
|
||
);
|
||
const genTxid = r?.txId ?? r?.txid;
|
||
console.log(` genesis tx: ${genTxid}`);
|
||
st.fusdm.genesisTxid = genTxid;
|
||
await sleep(10_000);
|
||
}
|
||
st.fusdm.contractAddress = fusdContract.tokenAddress;
|
||
st.fusdm.category = fusdCategory;
|
||
st.fusdm.atomicSats = FUSD_ATOMIC_SATS.toString();
|
||
st.fusdm.draFeeBps = Number(FUSD_DRA_FEE_BPS);
|
||
save();
|
||
}
|
||
} else {
|
||
console.log(` resumed contract: ${st.fusdm.contractAddress}`);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// Phase 3 — claim fUSDm → DRA pool receives fee deposit
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
console.log('\n──── Phase 3 — claim fUSDm, fee output lands at DRA pool ─────────────');
|
||
|
||
if (!st.fusdm?.contractAddress || !st.dra?.contractAddress) {
|
||
console.log(` ⚠ Requires Phases 1 + 2 complete.`);
|
||
} else {
|
||
const gross = FUSD_CLAIM_ATOMS * FUSD_ATOMIC_SATS;
|
||
const founderFee = (gross * FUSD_FOUNDER_FEE_BPS) / 10_000n;
|
||
const draFeeRaw = (gross * FUSD_DRA_FEE_BPS) / 10_000n;
|
||
const draFee = draFeeRaw < 546n ? 546n : draFeeRaw;
|
||
const need = gross + founderFee + draFee + MINER_FEE + 1000n;
|
||
|
||
console.log(` Planned claim:`);
|
||
console.log(` gross to user: ${fmtSats(gross)}`);
|
||
console.log(` founder fee: ${fmtSats(founderFee)} ${founderFee < 546n ? '(WAIVED — sub-dust)' : ''}`);
|
||
console.log(` DRA pool fee: ${fmtSats(draFee)}${draFeeRaw < 546n ? ' (raised from sub-dust to 546)' : ''}`);
|
||
console.log(` miner fee: ${fmtSats(MINER_FEE)}`);
|
||
console.log(` total BCH required: ${fmtSats(need)}`);
|
||
console.log('');
|
||
console.log(` Expected outputs:`);
|
||
console.log(` [0] reservoir → fUSDm contract`);
|
||
console.log(` [1] FT → B (${FUSD_CLAIM_ATOMS} atomic = $${Number(FUSD_CLAIM_ATOMS) / 100})`);
|
||
console.log(` [2] DRA pool fee deposit → ${st.dra.contractAddress.slice(0, 28)}... (${draFee} sats)`);
|
||
if (founderFee >= 546n) {
|
||
console.log(` [3] founder fee → ${E.cashaddr.slice(0, 28)}... (${founderFee} sats)`);
|
||
}
|
||
console.log(` [N] BCH change → B`);
|
||
|
||
if (!yes) {
|
||
console.log(`\n (plan only — re-run with --yes to broadcast)`);
|
||
} else {
|
||
console.log(`\n ⚠ Actual tx construction skipped in this chipnet sketch — adapt`);
|
||
console.log(` the claim() helper from chipnet-multicurrency-mba-test.mjs by`);
|
||
console.log(` adding output [2] = { to: draContract.tokenAddress, amount: draFee }`);
|
||
console.log(` and shifting founder fee to output [3].`);
|
||
}
|
||
}
|
||
|
||
console.log('\n═══════════════════════════════════════════════════════════════════');
|
||
console.log(' State file: ' + STATE);
|
||
console.log('═══════════════════════════════════════════════════════════════════\n');
|
||
}
|
||
|
||
main().catch((e) => { console.error(e); process.exit(1); });
|