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>
166 lines
5.9 KiB
JavaScript
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);
|
|
});
|