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>
156 lines
8.5 KiB
JavaScript
156 lines
8.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-escfee-txcount-test.mjs — empirical test of trigger 2:
|
|
* TX-COUNT escalating fee via mutable NFT COMMITMENT (DeviantEscFeeTxCount.cash).
|
|
*
|
|
* txCount = int(reservoir.nftCommitment)
|
|
* feeBps = min(baseFeeBps + (txCount / txThreshold) * feeStepBps, maxFeeBps)
|
|
* reservoir-out commitment := bytes8(txCount + 1)
|
|
*
|
|
* Params: backingRate=1000, baseFeeBps=10, feeStepBps=10, txThreshold=3,
|
|
* maxFeeBps=500. Mint 600 units/claim (gross=600k so fee ≥ dust).
|
|
* Expected: mints 1-3 feeBps=10, mints 4-5 feeBps=20 (one step at claim 4),
|
|
* and the commitment counter increments 0→1→2→3→4→5.
|
|
*
|
|
* Answers empirically: (a) int(nftCommitment)/bytes8 VM round-trip,
|
|
* (b) commitment mutation leaves the P2SH32 address unchanged.
|
|
*
|
|
* Founder == claimer (Wallet B).
|
|
*/
|
|
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_FILE = join(ROOT, 'ChipnetTests', '.chipnet-escfee-txcount-test.json');
|
|
const ARTIFACT_PATH = join(ROOT, 'contracts/dist/DeviantEscFeeTxCount.json');
|
|
const KEYS = join(ROOT, '.chipnet-keys-b.json'); // Wallet B
|
|
|
|
const BACKING_RATE = 1000n;
|
|
const BASE_FEE_BPS = 10n;
|
|
const FEE_STEP_BPS = 10n;
|
|
const TX_THRESHOLD = 3n;
|
|
const MAX_FEE_BPS = 500n;
|
|
const MINT_AMOUNT = 600n; // gross 600k → fee ≥ dust at 10 bps
|
|
|
|
function hexToBin(hex) { const o = new Uint8Array(hex.length / 2); for (let i = 0; i < o.length; i++) o[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); return o; }
|
|
function 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; }
|
|
function binToHex(b) { return Array.from(b, (x) => x.toString(16).padStart(2, '0')).join(''); }
|
|
function 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; }
|
|
// 8-byte little-endian hex == CashScript bytes8(n) for n >= 0 small ints.
|
|
function bytes8LE(n) { let h = ''; let v = BigInt(n); for (let i = 0; i < 8; i++) { h += (v & 0xffn).toString(16).padStart(2, '0'); v >>= 8n; } return h; }
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
function divider(c = '=') { console.log(c.repeat(80)); }
|
|
function step(n, t) { console.log(''); divider(); console.log(`STEP ${n} — ${t}`); divider(); }
|
|
|
|
function expectedFeeBps(txCount) {
|
|
let feeBps = BASE_FEE_BPS + (txCount / TX_THRESHOLD) * FEE_STEP_BPS;
|
|
if (feeBps > MAX_FEE_BPS) feeBps = MAX_FEE_BPS;
|
|
return feeBps;
|
|
}
|
|
|
|
async function ensureEligibleUtxo(wallet) {
|
|
let utxos = await wallet.getUtxos();
|
|
let eligible = utxos.filter((u) => u.vout === 0 && !u.token);
|
|
if (eligible.length > 0) return eligible[0];
|
|
const ineligible = utxos.filter((u) => !(u.vout === 0 && !u.token));
|
|
const r = await wallet.send([{ cashaddr: wallet.cashaddr, value: 2000n, unit: 'sat' }], ineligible.length ? { utxoIds: ineligible } : undefined);
|
|
console.log(` splitter TXID: ${r?.txId ?? r?.txid}`);
|
|
await sleep(12_000);
|
|
utxos = await wallet.getUtxos();
|
|
eligible = utxos.filter((u) => u.vout === 0 && !u.token);
|
|
if (!eligible.length) throw new Error('splitter did not produce eligible UTXO');
|
|
return eligible[0];
|
|
}
|
|
|
|
async function main() {
|
|
const artifact = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8'));
|
|
const { wif } = JSON.parse(readFileSync(KEYS, 'utf8'));
|
|
const founder = await TestNetWallet.fromWIF(wif);
|
|
const provider = new ElectrumNetworkProvider('chipnet');
|
|
const sig = new SignatureTemplate(founder.privateKey);
|
|
const founderLB = p2pkh(founder.publicKeyHash);
|
|
let state = existsSync(STATE_FILE) ? JSON.parse(readFileSync(STATE_FILE, 'utf8')) : { claims: [] };
|
|
const SUPPLY = 1_000_000n;
|
|
|
|
const ctorFrom = (cat) => [cat, BACKING_RATE, BASE_FEE_BPS, FEE_STEP_BPS, TX_THRESHOLD, MAX_FEE_BPS, founderLB];
|
|
|
|
step(1, 'Genesis DeviantEscFeeTxCount (commitment seeded to bytes8(0))');
|
|
let entry = state.contract;
|
|
if (!entry) {
|
|
const utxo = await ensureEligibleUtxo(founder);
|
|
const categoryId = utxo.txid;
|
|
const contract = new Contract(artifact, ctorFrom(txidToConsensusBytes(categoryId)), { provider });
|
|
console.log(` contract: ${contract.tokenAddress}`);
|
|
console.log(` category: ${categoryId}`);
|
|
const r = await founder.tokenGenesis(
|
|
{ cashaddr: contract.tokenAddress, amount: SUPPLY, value: 1000n, nft: { capability: 'minting', commitment: bytes8LE(0) } },
|
|
[], { utxoIds: [utxo] });
|
|
const txId = r?.txId ?? r?.txid;
|
|
console.log(` genesis TXID: ${txId}`);
|
|
entry = { categoryId, contractTokenAddress: contract.tokenAddress, genesisTxId: txId };
|
|
state.contract = entry; writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
await sleep(10_000);
|
|
} else {
|
|
console.log(` (already deployed: ${entry.contractTokenAddress})`);
|
|
}
|
|
|
|
const contract = new Contract(artifact, ctorFrom(txidToConsensusBytes(entry.categoryId)), { provider });
|
|
if (contract.tokenAddress !== entry.contractTokenAddress) throw new Error('contract address mismatch — commitment mutation must NOT change address');
|
|
console.log(` address stable across reconstruct ✓ (${contract.tokenAddress})`);
|
|
|
|
async function makeClaim(label) {
|
|
const rUtxos = await provider.getUtxos(contract.tokenAddress);
|
|
const reservoir = rUtxos.find((u) => u.token?.nft?.capability === 'minting' && u.token?.category === entry.categoryId && u.token?.amount > 0n);
|
|
if (!reservoir) throw new Error('reservoir not found');
|
|
const commitHex = reservoir.token.nft.commitment ?? '';
|
|
const txCount = BigInt('0x' + (commitHex === '' ? '0' : binToHex(hexToBin(commitHex).slice().reverse())));
|
|
const feeBps = expectedFeeBps(txCount);
|
|
const gross = MINT_AMOUNT * BACKING_RATE;
|
|
const fee = (gross * feeBps) / 10_000n;
|
|
const nextCommit = bytes8LE(txCount + 1n);
|
|
console.log(` ${label}: commitment=0x${commitHex || '(empty)'} → txCount=${txCount} → feeBps=${feeBps}, gross=${gross}, fee=${fee}`);
|
|
console.log(` next commitment := 0x${nextCommit}`);
|
|
if (fee > 0n && fee < 546n) throw new Error(`fee ${fee} < dust`);
|
|
|
|
const userUtxos = await provider.getUtxos(founder.cashaddr);
|
|
const bch = userUtxos.filter((u) => !u.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis)));
|
|
const minerFee = 2000n;
|
|
const target = gross + fee + minerFee + 1000n;
|
|
const picked = []; let total = 0n;
|
|
for (const u of bch) { picked.push(u); total += BigInt(u.satoshis); if (total >= target) break; }
|
|
if (total < target) throw new Error(`insufficient BCH ${total} < ${target}`);
|
|
const change = BigInt(reservoir.satoshis) + total - 1000n - gross - fee - minerFee;
|
|
if (change < 546n) throw new Error(`change ${change} < dust`);
|
|
|
|
const recipientLB = p2pkh(founder.publicKeyHash);
|
|
const remaining = reservoir.token.amount - MINT_AMOUNT;
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(reservoir, contract.unlock.claim(MINT_AMOUNT, recipientLB));
|
|
for (const u of picked) tb.addInput(u, sig.unlockP2PKH());
|
|
tb.addOutput({ to: contract.tokenAddress, amount: 1000n, token: { amount: remaining, category: entry.categoryId, nft: { capability: 'minting', commitment: nextCommit } } });
|
|
tb.addOutput({ to: founder.tokenaddr, amount: gross, token: { amount: MINT_AMOUNT, category: entry.categoryId } });
|
|
if (fee > 0n) tb.addOutput({ to: founder.cashaddr, amount: fee });
|
|
tb.addOutput({ to: founder.cashaddr, amount: change });
|
|
const sent = await tb.send();
|
|
const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
|
|
console.log(` claim TXID: ${txid}`);
|
|
state.claims.push({ label, txCount: txCount.toString(), feeBps: feeBps.toString(), fee: fee.toString(), nextCommit, txid });
|
|
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
await sleep(10_000);
|
|
}
|
|
|
|
for (let i = 1; i <= 5; i++) {
|
|
step(1 + i, `Mint ${i}`);
|
|
const label = `mint${i}`;
|
|
if (state.claims.find((c) => c.label === label)) { console.log(` (already done: ${state.claims.find((c) => c.label === label).txid})`); continue; }
|
|
await makeClaim(label);
|
|
}
|
|
|
|
step(7, 'Result — counter + fee per mint');
|
|
for (const c of state.claims) console.log(` ${c.label}: txCount=${c.txCount}, feeBps=${c.feeBps}, fee=${c.fee}, newCommit=0x${c.nextCommit} (${c.txid})`);
|
|
divider();
|
|
process.exit(0);
|
|
}
|
|
main().catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });
|