deviant/scripts/chipnet-escalating-nft-test.mjs
walerikus 599202a721 chore: checkpoint working tree — shell Tier-B retirement + accumulated WIP
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>
2026-06-17 01:01:37 +02:00

127 lines
7.4 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-escalating-nft-test.mjs — FEASIBILITY PROOF for NFT-bound x10
* escalating price (EscalatingPriceNFT.cash).
*
* Genesis a position UTXO {mutable NFT, commitment = price 1000, + small FT
* reservoir} at the contract, then run 3 sequential buy() txs and show the
* price the covenant forces each time escalates 1000 → 10000 → 100000, the NFT
* commitment advances x10 (and is recreated at the contract address, so it is
* un-resettable), and the escalating payment lands at the founder.
*
* Funder/buyer = Wallet B; founder (payment sink) = Wallet C.
* Writes results to .chipnet-escalating-nft-test.json.
*
* Run: node --import tsx scripts/chipnet-escalating-nft-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-nft-test.json');
const ARTIFACT_PATH = join(ROOT, 'contracts/dist/EscalatingPriceNFT.json');
const BUYER_KEYS = join(ROOT, '.chipnet-keys-b.json'); // funds genesis + buys
const FOUNDER_KEYS = join(ROOT, '.chipnet-keys-c.json'); // escalating payment sink
const SUPPLY = 10n; // FT units available through this position
const POSITION_VALUE = 1000n; // sats carried on the position UTXO
const 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));
/** CScriptNum (VM number) encoding — must match CashScript's bytes(int)/int(bytes). */
function scriptNum(n) {
if (n === 0n) return new Uint8Array([]);
const neg = n < 0n; let v = neg ? -n : n; const out = [];
while (v > 0n) { out.push(Number(v & 0xffn)); v >>= 8n; }
if (out[out.length - 1] & 0x80) out.push(neg ? 0x80 : 0x00);
else if (neg) out[out.length - 1] |= 0x80;
return Uint8Array.from(out);
}
const priceHex = (n) => binToHex(scriptNum(n));
async function ensureEligibleUtxo(wallet) {
let utxos = await wallet.getUtxos();
let e = utxos.filter((u) => u.vout === 0 && !u.token);
if (e.length) return e[0];
const bad = utxos.filter((u) => !(u.vout === 0 && !u.token));
const r = await wallet.send([{ cashaddr: wallet.cashaddr, value: 5000n, unit: 'sat' }], bad.length ? { utxoIds: bad } : undefined);
console.log(` splitter: ${r?.txId ?? r?.txid}`); await sleep(12_000);
utxos = await wallet.getUtxos(); e = utxos.filter((u) => u.vout === 0 && !u.token);
if (!e.length) throw new Error('no eligible vout-0 UTXO'); return e[0];
}
async function main() {
if (!process.argv.includes('--yes')) { console.log('Re-run with --yes to broadcast genesis + 3 buys.'); 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 state = existsSync(STATE_FILE) ? JSON.parse(readFileSync(STATE_FILE, 'utf8')) : { buys: [] };
// ---- genesis the position ----
if (!state.categoryId) {
const utxo = await ensureEligibleUtxo(buyer);
const categoryId = utxo.txid;
const contract = new Contract(artifact, [txidToConsensusBytes(categoryId), founderLB], { provider });
console.log(`contract: ${contract.tokenAddress}\ncategory: ${categoryId}`);
const r = await buyer.tokenGenesis(
{ cashaddr: contract.tokenAddress, amount: SUPPLY, value: POSITION_VALUE,
nft: { capability: 'mutable', commitment: priceHex(1000n) } },
[], { utxoIds: [utxo] });
console.log(`genesis: ${r?.txId ?? r?.txid} (commitment = ${priceHex(1000n)} = price 1000)`);
state.categoryId = categoryId; state.contractTokenAddress = contract.tokenAddress;
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); await sleep(10_000);
}
const contract = new Contract(artifact, [txidToConsensusBytes(state.categoryId), founderLB], { provider });
// ---- sequential buys ----
for (let i = state.buys.length; i < BUYS; i++) {
const rUtxos = await provider.getUtxos(contract.tokenAddress);
const pos = rUtxos.find((u) => u.token?.nft && u.token?.category === state.categoryId && u.token?.amount > 0n);
if (!pos) throw new Error('position UTXO not found');
// decode price from commitment (LE CScriptNum)
const commit = pos.token.nft.commitment; // hex
let p = 0n; const cb = hexToBin(commit); for (let k = cb.length - 1; k >= 0; k--) p = (p << 8n) | BigInt(cb[k] & (k === cb.length - 1 ? 0x7f : 0xff));
const nextPrice = p;
console.log(`\nbuy #${i + 1}: position commitment ${commit} → price ${nextPrice} sats`);
const cash = (await provider.getUtxos(buyer.cashaddr)).filter((u) => !u.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis)));
const UNIT_VALUE = 1000n; // token output must clear CT dust (~651)
const minerFee = 2000n;
const need = nextPrice + UNIT_VALUE + 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 - UNIT_VALUE - nextPrice - 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(nextPrice * 10n) } } });
tb.addOutput({ to: buyer.tokenaddr, amount: UNIT_VALUE, token: { amount: 1n, category: state.categoryId } });
tb.addOutput({ to: founder.cashaddr, amount: nextPrice });
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(` paid ${nextPrice} → founder C · new commitment ${priceHex(nextPrice * 10n)} (price ${nextPrice * 10n}) · txid ${txid}`);
state.buys.push({ n: i + 1, price: nextPrice.toString(), newPrice: (nextPrice * 10n).toString(), txid });
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); await sleep(10_000);
}
console.log(`\n=== RESULT — escalating price enforced by covenant via NFT commitment ===`);
for (const b of state.buys) console.log(` buy #${b.n}: paid ${b.price} sats → next price ${b.newPrice} (${b.txid})`);
}
main().catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });