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>
182 lines
8 KiB
JavaScript
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);
|
|
});
|