deviant/scripts/chipnet-mut-analyze.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

182 lines
8 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-mut-analyze.mjs — pure-local analysis of the mutability test.
*
* Reports:
* 1. The canonical contract ABI — proving there is no admin function.
* 2. For each "attempted change" in the user scenario, what would
* mechanically happen: validateMetadata verdict + would-be P2SH32
* address. Demonstrates that every "change" is a fresh asset.
* 3. Final accounting of deployed contracts in the state file —
* proving they coexist independently.
*
* No broadcasts. Safe to run any time.
*/
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { TestNetWallet } from 'mainnet-js';
import { Contract, ElectrumNetworkProvider, MockNetworkProvider } from 'cashscript';
import { validateMetadata, ALLOWED_RATE_TIERS, InvalidMetadataError } from '../src/tokenMetadata.ts';
const ROOT = join(import.meta.dirname, '..');
const STATE_FILE = join(ROOT, 'ChipnetTests', '.chipnet-mut-state.json');
const ARTIFACT_PATH = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
const KEYS_C = join(ROOT, '.chipnet-keys-c.json');
function hexToBin(hex) {
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
return out;
}
function txidToConsensusBytes(txidHex) {
const display = hexToBin(txidHex);
const out = new Uint8Array(display.length);
for (let i = 0; i < display.length; i++) out[i] = display[display.length - 1 - i];
return out;
}
function p2pkhLockingBytecode(pkh) {
const out = new Uint8Array(25);
out[0] = 0x76; out[1] = 0xa9; out[2] = 0x14;
out.set(pkh, 3);
out[23] = 0x88; out[24] = 0xac;
return out;
}
function divider(ch = '=') { console.log(ch.repeat(80)); }
function step(label) { console.log(''); divider(); console.log(label); divider(); }
const artifact = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8'));
const provider = new MockNetworkProvider();
function deriveAddress(categoryHex, rateSats, feeBps, founderLockingBytecode) {
const tokenCategory = txidToConsensusBytes(categoryHex);
const contract = new Contract(
artifact,
[tokenCategory, rateSats, feeBps, founderLockingBytecode],
{ provider },
);
return contract.tokenAddress;
}
async function main() {
// -----------------------------------------------------------------
step('1. Canonical contract ABI');
// -----------------------------------------------------------------
console.log(`contractName: ${artifact.contractName}`);
console.log(`ABI functions: ${JSON.stringify(artifact.abi.map((f) => f.name))}`);
console.log(`Constructor: ${JSON.stringify(artifact.constructorInputs.map((p) => `${p.name}:${p.type}`))}`);
console.log('');
if (artifact.abi.length === 1 && artifact.abi[0].name === 'claim') {
console.log('VERDICT: contract has exactly one function — `claim`.');
console.log(' No updateFee(), no updateRate(), no admin path. Founder fee + backing rate');
console.log(' are CONSTRUCTOR params → baked into bytecode → baked into the P2SH32 address.');
} else {
console.log(`UNEXPECTED ABI shape (${artifact.abi.length} functions) — investigate.`);
}
// -----------------------------------------------------------------
step('2. ATTEMPTED fee/rate changes — what each one would actually do');
// -----------------------------------------------------------------
// We need a founder pubkey hash to derive realistic addresses.
// Use wallet C — same one used as the default founder in the deploy script.
const { wif } = JSON.parse(readFileSync(KEYS_C, 'utf8'));
const founder = await TestNetWallet.fromWIF(wif);
const founderLockingBytecode = p2pkhLockingBytecode(founder.publicKeyHash);
const dummyMd = (feeBps, rateSats = 1000n) => ({
name: 'MBA',
ticker: 'MBA',
backingRateSats: rateSats,
openMint: true,
stablecoin: false,
founderFeeBps: Number(feeBps),
founderPkh: founder.publicKeyHash,
genesisHeight: 1,
});
// Use a placeholder category for would-be-address calculations.
// (A real fresh genesis would have a fresh categoryId — the point of
// these derivations is to show that the address depends on params,
// not to predict the actual address of a future deployment.)
const placeholderCat = '00'.repeat(32);
// Baseline: derive MBA@1% address with placeholder category.
const baselineAddr = deriveAddress(placeholderCat, 1000n, 100n, founderLockingBytecode);
console.log(`Baseline (rate=1000, feeBps=100, placeholder category):`);
console.log(` ${baselineAddr}`);
console.log('');
const scenarios = [
{ label: 'fee → 2%', feeBps: 200n, rateSats: 1000n },
{ label: 'fee → 3%', feeBps: 300n, rateSats: 1000n },
{ label: 'fee → 4%', feeBps: 400n, rateSats: 1000n },
{ label: 'fee → 50% (5000 bps)', feeBps: 5000n, rateSats: 1000n },
{ label: 'fee → back to 1%', feeBps: 100n, rateSats: 1000n, note: 'same params, but a fresh genesis = fresh categoryId; can\'t recreate the original' },
{ label: 'rate → 100 (sub-floor)', feeBps: 100n, rateSats: 100n },
{ label: 'rate → 10,000', feeBps: 100n, rateSats: 10_000n },
];
for (const s of scenarios) {
console.log(`Attempted: ${s.label}`);
// 1) wallet-layer gate
try {
validateMetadata(dummyMd(s.feeBps, s.rateSats));
console.log(' validateMetadata: ACCEPTED at wallet layer');
} catch (err) {
if (err instanceof InvalidMetadataError) {
console.log(` validateMetadata: REJECTED — ${err.message}`);
} else throw err;
}
// 2) would-be canonical address
const addr = deriveAddress(placeholderCat, s.rateSats, s.feeBps, founderLockingBytecode);
console.log(` would-be address: ${addr}`);
console.log(` same as baseline? ${addr === baselineAddr}`);
if (s.note) console.log(` note: ${s.note}`);
console.log('');
}
console.log(`Current ALLOWED_RATE_TIERS: ${ALLOWED_RATE_TIERS.join(', ')}`);
console.log('');
console.log('VERDICT: each "change" produces a different P2SH32 address.');
console.log(' Even "back to 1%" cannot recreate the original — the categoryId is the genesis-input');
console.log(' TXID, which is fresh for every deployment. There is no path to a "same contract');
console.log(' with new rules" — only to a "new contract with the desired rules" = different asset.');
// -----------------------------------------------------------------
step('3. Final accounting — contracts currently deployed in state');
// -----------------------------------------------------------------
if (!existsSync(STATE_FILE)) {
console.log(`(no state file at ${STATE_FILE} — no on-chain deployments to report)`);
return;
}
const state = JSON.parse(readFileSync(STATE_FILE, 'utf8'));
const contracts = state.contracts ?? {};
const labels = Object.keys(contracts);
if (labels.length === 0) {
console.log('(state file present but no contracts recorded)');
return;
}
console.log(`${labels.length} contract(s) deployed on chipnet:`);
console.log('');
for (const label of labels) {
const c = contracts[label];
console.log(` ${label}:`);
console.log(` rate=${c.rateSats} feeBps=${c.feeBps} founder=${c.founderWallet}`);
console.log(` category: ${c.categoryId}`);
console.log(` contract: ${c.contractTokenAddress}`);
console.log(` genesis txid: ${c.genesisTxId}`);
const claimCount = (c.claims ?? []).length;
const redeemCount = (c.redemptions ?? []).length;
if (claimCount > 0 || redeemCount > 0) {
console.log(` interactions: ${claimCount} claim(s), ${redeemCount} redemption(s)`);
}
}
console.log('');
console.log('Each contract has a distinct address, a distinct category, and an independent');
console.log('reservoir. None can replace another. Spec V1 Rule 5 / V2 §3.2 confirmed:');
console.log(' "The contract is ossified at deployment. No upgrade path, no selfdestruct,');
console.log(' no admin function."');
}
main().catch((err) => {
console.error('FAILED:', err?.message ?? err);
if (err?.stack) console.error(err.stack);
process.exit(1);
});