deviant/scripts/chipnet-mut-deploy.mjs
walerikus 518a3ce174 chore: initial project baseline — full Deviant codebase under version control
Brings the whole project into git (previously only the on-ramp feature and
a few parallel files were tracked). Includes: wallet core (src/), CashScript
contracts/, the Capacitor shell (shells/), scripts/, tests, and the docs
corpus (wiki/, snapshots/, Parameters/, Archive/, KnowledgeBase text).

Excluded via .gitignore: node_modules/, dist/, .env (secrets), testnet WIF
key files (.chipnet-*.json, devWallets.ts), KnowledgeBase/RAW media (~9.6 GB),
transcripts/ (session logs), and machine-local config (.claude/*.local,
*.lock). Secret-scanned before commit — no credentials in the tree.

Note: src/swapMath.ts, src/swapBuilder.ts, contracts/OpenMintToken.cash are
legacy (OP_RETURN era) but still imported; committed as-is, flagged for a
separate removal once de-referenced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:38:07 +02:00

166 lines
5.9 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-mut-deploy.mjs — deploy a canonical DeviantOpenMintFT contract
* at given fee/rate. Used by the mutability test to demonstrate that
* each "fee change" or "rate change" is in fact a fresh contract.
*
* Env:
* LABEL (required) — local nickname for the deployment (e.g., "mba-1pc")
* FEE_BPS (required) — founder fee in basis points (0..500, integer)
* RATE (default 1000) — backing rate in sats/unit (must be in ALLOWED_RATE_TIERS)
* FOUNDER (default c) — wallet a|b|c that funds genesis & receives fees
*
* State: appends/updates `.chipnet-mut-state.json` under contracts[LABEL].
* Re-runs with the same LABEL are no-ops (idempotent).
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { TestNetWallet } from 'mainnet-js';
import { Contract, ElectrumNetworkProvider } from 'cashscript';
const ROOT = join(import.meta.dirname, '..');
const STATE_FILE = join(ROOT, '.chipnet-mut-state.json');
const ARTIFACT_PATH = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
const LABEL = process.env.LABEL;
const FEE_BPS = process.env.FEE_BPS;
const RATE = BigInt(process.env.RATE ?? '1000');
const FOUNDER = (process.env.FOUNDER ?? 'c').toLowerCase();
if (!LABEL) throw new Error('LABEL env var is required');
if (!FEE_BPS) throw new Error('FEE_BPS env var is required');
const FEE_BPS_BIG = BigInt(FEE_BPS);
if (FEE_BPS_BIG < 0n) throw new Error('FEE_BPS must be >= 0');
const KEYS = {
a: join(ROOT, '.chipnet-keys.json'),
b: join(ROOT, '.chipnet-keys-b.json'),
c: join(ROOT, '.chipnet-keys-c.json'),
};
if (!KEYS[FOUNDER]) throw new Error(`FOUNDER must be a/b/c, got ${FOUNDER}`);
const RESERVOIR_FT_SUPPLY = 1_000_000_000_000_000_000n;
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 binToHex(bin) {
return Array.from(bin, (b) => b.toString(16).padStart(2, '0')).join('');
}
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;
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function loadState() {
if (!existsSync(STATE_FILE)) return { contracts: {}, events: [] };
return JSON.parse(readFileSync(STATE_FILE, 'utf8'));
}
function saveState(state) {
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
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];
console.log(' (no vout=0 BCH UTXO; splitting...)');
const ineligible = utxos.filter((u) => !(u.vout === 0 && !u.token));
const result = await wallet.send(
[{ cashaddr: wallet.cashaddr, value: 2000n, unit: 'sat' }],
ineligible.length > 0 ? { utxoIds: ineligible } : undefined,
);
console.log(` splitter TXID: ${result?.txId ?? result?.txid}`);
await sleep(12_000);
utxos = await wallet.getUtxos();
eligible = utxos.filter((u) => u.vout === 0 && !u.token);
if (eligible.length === 0) throw new Error('splitter did not produce eligible UTXO');
return eligible[0];
}
async function main() {
const state = loadState();
if (state.contracts[LABEL]) {
console.log(`[mut-deploy] ${LABEL} already deployed — skipping.`);
console.log(` category: ${state.contracts[LABEL].categoryId}`);
console.log(` contract: ${state.contracts[LABEL].contractTokenAddress}`);
console.log(` genesis txid: ${state.contracts[LABEL].genesisTxId}`);
return;
}
const { wif } = JSON.parse(readFileSync(KEYS[FOUNDER], 'utf8'));
const founder = await TestNetWallet.fromWIF(wif);
const artifact = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8'));
const provider = new ElectrumNetworkProvider('chipnet');
const utxo = await ensureEligibleUtxo(founder);
const categoryId = utxo.txid;
const tokenCategory = txidToConsensusBytes(categoryId);
const founderLockingBytecode = p2pkhLockingBytecode(founder.publicKeyHash);
const contract = new Contract(
artifact,
[tokenCategory, RATE, FEE_BPS_BIG, founderLockingBytecode],
{ provider },
);
console.log(`[mut-deploy] ${LABEL}: rate=${RATE}, feeBps=${FEE_BPS_BIG}, founder=${FOUNDER}`);
console.log(` contract address: ${contract.tokenAddress}`);
console.log(` category (TXID): ${categoryId}`);
const result = await founder.tokenGenesis(
{
cashaddr: contract.tokenAddress,
amount: RESERVOIR_FT_SUPPLY,
value: 1000n,
nft: { capability: 'minting', commitment: '' },
},
[],
{ utxoIds: [utxo] },
);
const txId = result?.txId ?? result?.txid;
console.log(` genesis TXID: ${txId}`);
state.contracts[LABEL] = {
label: LABEL,
founderWallet: FOUNDER.toUpperCase(),
founderAddress: founder.cashaddr,
founderLockingBytecodeHex: binToHex(founderLockingBytecode),
rateSats: RATE.toString(),
feeBps: FEE_BPS_BIG.toString(),
categoryId,
contractAddress: contract.address,
contractTokenAddress: contract.tokenAddress,
genesisInputUtxo: { txid: utxo.txid, vout: utxo.vout, satoshis: utxo.satoshis.toString() },
genesisTxId: txId,
createdAt: new Date().toISOString(),
};
state.events.push({
action: 'deploy',
label: LABEL,
feeBps: FEE_BPS_BIG.toString(),
rate: RATE.toString(),
txid: txId,
});
saveState(state);
console.log(` state saved to ${STATE_FILE}`);
}
main().catch((err) => {
console.error('FAILED:', err?.message ?? err);
if (err?.stack) console.error(err.stack);
process.exit(1);
});