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>
129 lines
7.5 KiB
JavaScript
129 lines
7.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-escalating-dual-test.mjs — FEASIBILITY PROOF for the COMBINED model:
|
|
* price x10 escalation + founder-share ramp (10% -> 50% -> 100%), bound to an NFT.
|
|
*
|
|
* EscalatingDualNFT.cash. Position UTXO = mutable NFT (commitment = next price) +
|
|
* FT reservoir. Each buy(): the covenant charges `price`, splits it into
|
|
* founderCut = price*share/10000 (share by price tier) and redeemable backing for
|
|
* the buyer's unit (dust floor at 100% share), and rewrites the commitment x10.
|
|
*
|
|
* start 10,000 · baseShare 10% · midShare 50%. Buyer = B, founder sink = C.
|
|
* Writes results to .chipnet-escalating-dual-test.json.
|
|
*
|
|
* Run: node --import tsx scripts/chipnet-escalating-dual-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_FILE = join(ROOT, 'ChipnetTests', '.chipnet-escalating-dual-test.json');
|
|
const ARTIFACT_PATH = join(ROOT, 'contracts/dist/EscalatingDualNFT.json');
|
|
const BUYER_KEYS = join(ROOT, '.chipnet-keys-b.json');
|
|
const FOUNDER_KEYS = join(ROOT, '.chipnet-keys-c.json');
|
|
|
|
const START = 10_000n, BASE_BPS = 1000n, MID_BPS = 5000n; // 10% -> 50% -> 100%
|
|
const SUPPLY = 10n, POSITION_VALUE = 1000n, DUST_FLOOR = 1000n, BUYS = 3;
|
|
|
|
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 binToHex = (b) => Buffer.from(b).toString('hex');
|
|
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));
|
|
|
|
function scriptNum(n) {
|
|
if (n === 0n) return new Uint8Array([]);
|
|
let v = n; const out = [];
|
|
while (v > 0n) { out.push(Number(v & 0xffn)); v >>= 8n; }
|
|
if (out[out.length - 1] & 0x80) out.push(0x00);
|
|
return Uint8Array.from(out);
|
|
}
|
|
const priceHex = (n) => binToHex(scriptNum(n));
|
|
function decodeNum(hex) { const b = hexToBin(hex); let p = 0n; for (let k = b.length - 1; k >= 0; k--) p = (p << 8n) | BigInt(b[k] & (k === b.length - 1 ? 0x7f : 0xff)); return p; }
|
|
|
|
// Mirror the covenant's split exactly.
|
|
function split(price) {
|
|
let bps = 10000n;
|
|
if (price <= START) bps = BASE_BPS;
|
|
else if (price <= START * 10n) bps = MID_BPS;
|
|
let unitValue = price - (price * bps) / 10000n;
|
|
if (unitValue < DUST_FLOOR) unitValue = DUST_FLOOR;
|
|
return { bps, unitValue, founderCut: price - unitValue };
|
|
}
|
|
|
|
async function ensureEligibleUtxo(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 vout-0'); return e[0];
|
|
}
|
|
|
|
async function main() {
|
|
if (!process.argv.includes('--yes')) { console.log('Re-run with --yes.'); return; }
|
|
const artifact = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8'));
|
|
const buyer = await TestNetWallet.fromWIF(JSON.parse(readFileSync(BUYER_KEYS, 'utf8')).wif);
|
|
const founder = await TestNetWallet.fromWIF(JSON.parse(readFileSync(FOUNDER_KEYS, 'utf8')).wif);
|
|
const provider = new ElectrumNetworkProvider('chipnet');
|
|
const sig = new SignatureTemplate(buyer.privateKey);
|
|
const founderLB = p2pkh(founder.publicKeyHash);
|
|
const recipientLB = p2pkh(buyer.publicKeyHash);
|
|
const ctor = (cat) => [txidToConsensusBytes(cat), founderLB, START, BASE_BPS, MID_BPS];
|
|
const state = existsSync(STATE_FILE) ? JSON.parse(readFileSync(STATE_FILE, 'utf8')) : { buys: [] };
|
|
|
|
if (!state.categoryId) {
|
|
const utxo = await ensureEligibleUtxo(buyer);
|
|
const cat = utxo.txid;
|
|
const contract = new Contract(artifact, ctor(cat), { provider });
|
|
console.log(`contract ${contract.tokenAddress}\ncategory ${cat}`);
|
|
const r = await buyer.tokenGenesis(
|
|
{ cashaddr: contract.tokenAddress, amount: SUPPLY, value: POSITION_VALUE,
|
|
nft: { capability: 'mutable', commitment: priceHex(START) } }, [], { utxoIds: [utxo] });
|
|
console.log(`genesis ${r?.txId ?? r?.txid} (commitment ${priceHex(START)} = price ${START})`);
|
|
state.categoryId = cat; state.contractTokenAddress = contract.tokenAddress;
|
|
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); await sleep(10_000);
|
|
}
|
|
const contract = new Contract(artifact, ctor(state.categoryId), { provider });
|
|
|
|
for (let i = state.buys.length; i < BUYS; i++) {
|
|
const ru = await provider.getUtxos(contract.tokenAddress);
|
|
const pos = ru.find((u) => u.token?.nft && u.token?.category === state.categoryId && u.token?.amount > 0n);
|
|
if (!pos) throw new Error('position not found');
|
|
const price = decodeNum(pos.token.nft.commitment);
|
|
const { bps, unitValue, founderCut } = split(price);
|
|
console.log(`\nbuy #${i + 1}: price ${price} · share ${Number(bps) / 100}% → founder ${founderCut}, buyer-backing ${unitValue}`);
|
|
|
|
const cash = (await provider.getUtxos(buyer.cashaddr)).filter((u) => !u.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis)));
|
|
const minerFee = 2000n;
|
|
const need = unitValue + founderCut + minerFee + 1000n;
|
|
const picked = []; let tot = 0n;
|
|
for (const u of cash) { picked.push(u); tot += BigInt(u.satoshis); if (tot >= need) break; }
|
|
if (tot < need) throw new Error(`insufficient BCH ${tot} < ${need}`);
|
|
const change = BigInt(pos.satoshis) + tot - POSITION_VALUE - unitValue - founderCut - minerFee;
|
|
|
|
const tb = new TransactionBuilder({ provider });
|
|
tb.addInput(pos, contract.unlock.buy(recipientLB));
|
|
for (const u of picked) tb.addInput(u, sig.unlockP2PKH());
|
|
tb.addOutput({ to: contract.tokenAddress, amount: POSITION_VALUE,
|
|
token: { amount: BigInt(pos.token.amount) - 1n, category: state.categoryId, nft: { capability: 'mutable', commitment: priceHex(price * 10n) } } });
|
|
tb.addOutput({ to: buyer.tokenaddr, amount: unitValue, token: { amount: 1n, category: state.categoryId } });
|
|
tb.addOutput({ to: founder.cashaddr, amount: founderCut });
|
|
if (change >= 546n) tb.addOutput({ to: buyer.cashaddr, amount: change });
|
|
const sent = await tb.send();
|
|
const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
|
|
console.log(` ✓ founder +${founderCut} · buyer keeps ${unitValue} (redeemable) · next price ${price * 10n} · ${txid}`);
|
|
state.buys.push({ n: i + 1, price: price.toString(), shareBps: bps.toString(), founderCut: founderCut.toString(), buyerBacking: unitValue.toString(), txid });
|
|
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); await sleep(10_000);
|
|
}
|
|
|
|
console.log(`\n=== RESULT — combined x10 price + share ramp (NFT-bound) ===`);
|
|
let totFounder = 0n;
|
|
for (const b of state.buys) { totFounder += BigInt(b.founderCut); console.log(` #${b.n}: price ${b.price} · ${Number(b.shareBps) / 100}% → founder ${b.founderCut}, backing ${b.buyerBacking}`); }
|
|
console.log(` founder total: ${totFounder} sats`);
|
|
}
|
|
|
|
main().catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });
|