deviant/scripts/chipnet-create-divisible-metals.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

133 lines
7.6 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-create-divisible-metals.mjs — founder (Wallet C) genesis's DIVISIBLE
* variants of the metals and registers them into the wallet in one shot.
*
* Divisibility is bounded by the CashTokens dust floor (CT_DUST_MIN_SATS = 675):
* a single atomic FT output must clear it, so the atomic backing rate is pinned
* at 1000 sats (ADR-0006 floor, > dust). Decimals then express the whole-unit
* value:
* GLDd atomic 1000 · decimals 2 → 1 GLDd = 100,000 sats, min 0.01 GLDd
* SLVd atomic 1000 · decimals 1 → 1 SLVd = 10,000 sats, min 0.1 SLVd
* CUP is omitted — its whole unit (1000 sats) already IS the atomic floor, so a
* fractional CUP would be a sub-dust (unrelayable) output.
*
* Same immutable decreasing-fee schedule as the originals (5.0% → 0.25%). The
* supply checkpoint is expressed in ATOMIC units, scaled so it still fires every
* 100,000 whole GLDd (=10,000,000 atomic) / 1,000,000 whole SLVd (=10,000,000
* atomic). Decimals is a display-only registry field (not a contract param), so
* genesis is identical to the whole-unit metals apart from rate + checkpoint.
*
* Run (plan): node --import tsx scripts/chipnet-create-divisible-metals.mjs
* Run (broadcast): node --import tsx scripts/chipnet-create-divisible-metals.mjs --yes
*/
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { TestNetWallet } from 'mainnet-js';
import { Contract, ElectrumNetworkProvider } from 'cashscript';
import { DESC_FEE_SUPPLY_BYTECODE_HASH } from '../src/verify/canonical.js';
const ROOT = join(import.meta.dirname, '..');
const STATE_FILE = join(ROOT, '.chipnet-divisible-metals.json');
const TOKENS_DIR = join(ROOT, '.deviant', 'tokens');
const ARTIFACT_PATH = join(ROOT, 'contracts/dist/DeviantDescFeeSupply.json');
const KEYS = join(ROOT, '.chipnet-keys-c.json'); // Wallet C — founder
const BASE_FEE_BPS = 500n, FEE_STEP_BPS = 50n, MIN_FEE_BPS = 25n;
const ATOMIC_RATE = 1000n; // pinned at the dust-safe floor for max divisibility
const MAX_FT_SUPPLY = 9_223_372_036_854_775_807n;
const ASSETS = [
{ ticker: 'GLDd', name: 'Gold (divisible)', decimals: 2, step: 10_000_000n }, // 100k whole
{ ticker: 'SLVd', name: 'Silver (divisible)', decimals: 1, step: 10_000_000n }, // 1M whole
];
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));
const NOW = Date.now();
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 produced no 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: {} };
mkdirSync(TOKENS_DIR, { recursive: true });
console.log(`\nCREATE DIVISIBLE METAL VARIANTS — founder ${founder.cashaddr} (Wallet C)`);
console.log(` atomic rate ${ATOMIC_RATE} sats/FT (dust-safe floor) · fee 5.0%→0.25% · reservoir 2^63-1\n`);
for (const a of ASSETS) {
const wholeValue = ATOMIC_RATE * (10n ** BigInt(a.decimals));
console.log(` [${a.ticker}] ${a.name}: decimals ${a.decimals} → 1 ${a.ticker} = ${wholeValue} sats · min 0.${'0'.repeat(a.decimals - 1)}1 ${a.ticker} (= ${ATOMIC_RATE} sats) · checkpoint every ${a.step} atomic FT`);
}
console.log('');
if (!yes) { console.log('(preview only — re-run with --yes to broadcast 2 immutable genesis txs + register)\n'); return; }
for (const a of ASSETS) {
console.log(`\n=== ${a.ticker} ===`);
let entry = state.assets[a.ticker];
if (!entry) {
const utxo = await ensureEligibleUtxo(founder);
const categoryId = utxo.txid;
const contract = new Contract(artifact, [
txidToConsensusBytes(categoryId), ATOMIC_RATE, MAX_FT_SUPPLY,
BASE_FEE_BPS, FEE_STEP_BPS, a.step, MIN_FEE_BPS, founderLB,
], { provider });
const r = await founder.tokenGenesis(
{ cashaddr: contract.tokenAddress, amount: MAX_FT_SUPPLY, value: 1000n, nft: { capability: 'minting', commitment: '' } },
[], { utxoIds: [utxo] });
const genesisTxId = r?.txId ?? r?.txid;
console.log(` genesis TXID: ${genesisTxId} category ${categoryId}`);
entry = {
ticker: a.ticker, name: a.name, decimals: a.decimals, categoryId,
contractAddress: contract.address, contractTokenAddress: contract.tokenAddress, genesisTxId,
backingRateSats: ATOMIC_RATE.toString(), initialReservoirFtSupply: MAX_FT_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,
};
state.assets[a.ticker] = entry;
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
await sleep(10_000);
} else {
console.log(` (already deployed: ${entry.contractTokenAddress})`);
}
// Register: portable v1 with decimals + feeSchedule.
const portable = {
version: 1, id: entry.categoryId, tier: 'A', name: entry.name, ticker: entry.ticker,
rateSats: entry.backingRateSats, decimals: entry.decimals,
createdAt: NOW, lastSeen: NOW, userVerified: false, pinned: false, hidden: false,
categoryId: entry.categoryId, founderFeeBps: entry.baseFeeBps,
founderLockingBytecodeHex: entry.founderLockingBytecodeHex, founderAddress: entry.founderAddress,
contractAddress: entry.contractAddress, contractTokenAddress: entry.contractTokenAddress,
contractBytecodeHash: DESC_FEE_SUPPLY_BYTECODE_HASH,
initialReservoirSupply: entry.initialReservoirFtSupply, genesisTxId: entry.genesisTxId,
feeSchedule: { direction: 'decreasing', baseFeeBps: entry.baseFeeBps, feeStepBps: entry.feeStepBps, supplyStepSize: entry.supplyStepSize, boundFeeBps: entry.minFeeBps },
};
writeFileSync(join(TOKENS_DIR, `${entry.categoryId}.json`), JSON.stringify(portable, null, 2));
console.log(` registered (decimals ${entry.decimals}) → .deviant/tokens/${entry.categoryId.slice(0, 12)}….json`);
}
console.log(`\n✓ Done. GLDd/SLVd are divisible, claimable, swap-all targets.`);
}
main().catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });