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>
151 lines
7.1 KiB
JavaScript
151 lines
7.1 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* chipnet-create-metal-mbas.mjs — genesis 3 decreasing-fee MBAs (GLD/SLV/CUP)
|
||
* on the DeviantDescFeeSupply covenant (ADR-0029), founder = least-BCH wallet.
|
||
*
|
||
* Fee schedule (immutable, identical across all three):
|
||
* feeBps = max(500 - (issuedSoFar / supplyStepSize) * 50, 25)
|
||
* → 5.0% at genesis, -0.5% per checkpoint, floored at 0.25%.
|
||
* Sequence: 500,450,400,350,300,250,200,150,100,50,25(floor).
|
||
*
|
||
* The only per-asset difference is the checkpoint size (zero-ladder) and the
|
||
* flat backing rate:
|
||
* GLD rate 100000 sats/u checkpoint every 100,000 units
|
||
* SLV rate 10000 sats/u checkpoint every 1,000,000 units
|
||
* CUP rate 1000 sats/u checkpoint every 10,000,000 units
|
||
*
|
||
* Two-phase: prints the immutable plan and STOPS unless --yes is passed.
|
||
* Idempotent: a genesis already recorded in state is skipped on re-run.
|
||
*
|
||
* Run (plan): node --import tsx scripts/chipnet-create-metal-mbas.mjs
|
||
* Run (broadcast): node --import tsx scripts/chipnet-create-metal-mbas.mjs --yes
|
||
*/
|
||
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-metal-mbas.json');
|
||
const ARTIFACT_PATH = join(ROOT, 'contracts/dist/DeviantDescFeeSupply.json');
|
||
const KEYS = join(ROOT, '.chipnet-keys-c.json'); // Wallet C — least BCH (founder)
|
||
|
||
// Immutable fee schedule shared by all three.
|
||
const BASE_FEE_BPS = 500n;
|
||
const FEE_STEP_BPS = 50n;
|
||
const MIN_FEE_BPS = 25n;
|
||
|
||
// CashTokens consensus ceiling for a category's fungible supply (signed int64
|
||
// max). Metals are non-consumable and circulate indefinitely, so the reservoir
|
||
// is seeded at the ceiling — "depletion" becomes a non-event. The fee ladder is
|
||
// unaffected: it steps off issuedSoFar (cumulative claims), not reservoir size.
|
||
const MAX_FT_SUPPLY = 9_223_372_036_854_775_807n; // 2^63 - 1
|
||
|
||
const ASSETS = [
|
||
{ ticker: 'GLD', name: 'Gold', rate: 100_000n, step: 100_000n, supply: MAX_FT_SUPPLY },
|
||
{ ticker: 'SLV', name: 'Silver', rate: 10_000n, step: 1_000_000n, supply: MAX_FT_SUPPLY },
|
||
{ ticker: 'CUP', name: 'Copper', rate: 1_000n, step: 10_000_000n, supply: MAX_FT_SUPPLY },
|
||
];
|
||
|
||
const yes = process.argv.includes('--yes');
|
||
|
||
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 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));
|
||
|
||
/** The full immutable fee ladder for an asset, as a display table. */
|
||
function feeLadder(step) {
|
||
const rows = [];
|
||
for (let n = 0; n <= 10; n++) {
|
||
let bps = BASE_FEE_BPS - BigInt(n) * FEE_STEP_BPS;
|
||
if (bps < MIN_FEE_BPS) bps = MIN_FEE_BPS;
|
||
rows.push(` ${String(n * Number(step)).padStart(12)} units issued → ${(Number(bps) / 100).toFixed(2)}%`);
|
||
}
|
||
return rows.join('\n');
|
||
}
|
||
|
||
/** Ensure a genesis-eligible (vout 0, plain BCH) UTXO exists, minting one if not. */
|
||
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];
|
||
const ineligible = utxos.filter((u) => !(u.vout === 0 && !u.token));
|
||
const r = await wallet.send([{ cashaddr: wallet.cashaddr, value: 3000n, unit: 'sat' }], ineligible.length ? { utxoIds: ineligible } : undefined);
|
||
console.log(` splitter TXID: ${r?.txId ?? r?.txid}`);
|
||
await sleep(12_000);
|
||
utxos = await wallet.getUtxos();
|
||
eligible = utxos.filter((u) => u.vout === 0 && !u.token);
|
||
if (!eligible.length) throw new Error('splitter did not produce an eligible vout-0 UTXO');
|
||
return eligible[0];
|
||
}
|
||
|
||
async function main() {
|
||
const artifact = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8'));
|
||
const { wif } = JSON.parse(readFileSync(KEYS, 'utf8'));
|
||
const founder = await TestNetWallet.fromWIF(wif);
|
||
const provider = new ElectrumNetworkProvider('chipnet');
|
||
const founderLB = p2pkh(founder.publicKeyHash);
|
||
const founderLBHex = Buffer.from(founderLB).toString('hex');
|
||
const state = existsSync(STATE_FILE) ? JSON.parse(readFileSync(STATE_FILE, 'utf8')) : { founder: founder.cashaddr, assets: {} };
|
||
|
||
const ctor = (a, catBytes) => [catBytes, a.rate, a.supply, BASE_FEE_BPS, FEE_STEP_BPS, a.step, MIN_FEE_BPS, founderLB];
|
||
|
||
console.log(`\nCREATE DECREASING-FEE MBAs — GLD / SLV / CUP (DeviantDescFeeSupply, ADR-0029)`);
|
||
console.log(` Founder (fee recipient): ${founder.cashaddr} [least-BCH wallet C]`);
|
||
console.log(` Fee schedule (all 3): 5.0% → −0.5%/checkpoint → 0.25% floor`);
|
||
console.log(` baseFeeBps=${BASE_FEE_BPS} feeStepBps=${FEE_STEP_BPS} minFeeBps=${MIN_FEE_BPS}\n`);
|
||
for (const a of ASSETS) {
|
||
console.log(` [${a.ticker}] ${a.name}: rate ${a.rate} sats/u · checkpoint every ${a.step} units · reservoir ${a.supply}`);
|
||
console.log(feeLadder(a.step));
|
||
console.log('');
|
||
}
|
||
|
||
if (!yes) {
|
||
console.log(`(preview only — re-run with --yes to broadcast 3 immutable genesis transactions)\n`);
|
||
return;
|
||
}
|
||
|
||
for (const a of ASSETS) {
|
||
console.log(`\n=== Genesis ${a.ticker} (${a.name}) ===`);
|
||
if (state.assets[a.ticker]) {
|
||
console.log(` (already deployed: ${state.assets[a.ticker].contractTokenAddress})`);
|
||
continue;
|
||
}
|
||
const utxo = await ensureEligibleUtxo(founder);
|
||
const categoryId = utxo.txid;
|
||
const contract = new Contract(artifact, ctor(a, txidToConsensusBytes(categoryId)), { provider });
|
||
console.log(` contract: ${contract.tokenAddress}`);
|
||
console.log(` category: ${categoryId}`);
|
||
const r = await founder.tokenGenesis(
|
||
{ cashaddr: contract.tokenAddress, amount: a.supply, value: 1000n, nft: { capability: 'minting', commitment: '' } },
|
||
[], { utxoIds: [utxo] });
|
||
const genesisTxId = r?.txId ?? r?.txid;
|
||
console.log(` genesis TXID: ${genesisTxId}`);
|
||
state.assets[a.ticker] = {
|
||
ticker: a.ticker,
|
||
name: a.name,
|
||
categoryId,
|
||
contractTokenAddress: contract.tokenAddress,
|
||
genesisTxId,
|
||
backingRateSats: a.rate.toString(),
|
||
initialReservoirFtSupply: a.supply.toString(),
|
||
baseFeeBps: Number(BASE_FEE_BPS),
|
||
feeStepBps: Number(FEE_STEP_BPS),
|
||
supplyStepSize: a.step.toString(),
|
||
minFeeBps: Number(MIN_FEE_BPS),
|
||
founderLockingBytecodeHex: founderLBHex,
|
||
founderAddress: founder.cashaddr,
|
||
};
|
||
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
||
await sleep(10_000);
|
||
}
|
||
|
||
console.log(`\n✓ Done. State → ${STATE_FILE}`);
|
||
for (const t of Object.keys(state.assets)) {
|
||
const e = state.assets[t];
|
||
console.log(` ${t}: category ${e.categoryId} @ ${e.contractTokenAddress}`);
|
||
}
|
||
}
|
||
|
||
main().catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });
|