#!/usr/bin/env node /** * deviant — Deviant wallet CLI entry point. * * Wires real keys + network + token registry to the testable CLI core * (`src/cli/wallet-cli.ts`), then dispatches the parsed command. * * Run: node --import tsx scripts/deviant.mjs [...] * npm: npm run deviant -- [...] * * Config (env or flags): * --keys WIF keys file (default .chipnet-keys.json). JSON: { wif } * --network chipnet | testnet4 | mainnet (default chipnet) * --tokens comma-separated token JSON files to load into the * registry (default: the chipnet asm + asm2 files if present) * * Persistence: every token file under `.deviant/tokens/*.json` is loaded into * the registry on every run, and `register ` writes new entries there — * so a token you register stays named across sessions. (Lightweight * file-backed persistence; a SQLite backend can replace it behind the same * TokenRegistry interface.) * * Examples: * node --import tsx scripts/deviant.mjs balance * node --import tsx scripts/deviant.mjs register ./some-token.json * node --import tsx scripts/deviant.mjs claim ASM 1000 * node --import tsx scripts/deviant.mjs swap ASM ASM2 100 --yes * node --import tsx scripts/deviant.mjs send ASM 100 --to bchtest:z... --yes */ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { TestNetWallet, Wallet } from 'mainnet-js'; import { InMemoryTokenRegistry } from '../src/token/registry.js'; import { tokenToPortable } from '../src/token/portable.js'; import { connectWallet, tokenFileToTierA } from '../src/wallet/runtime.js'; import { registerToken } from '../src/wallet/register.js'; import { parseArgs, runCommand, CliError } from '../src/cli/wallet-cli.js'; const ROOT = join(import.meta.dirname, '..'); const TOKENS_DIR = join(ROOT, '.deviant', 'tokens'); /** Absolute-path resolver that respects already-absolute paths (POSIX + Windows). */ function abs(p) { return p.startsWith('/') || /^[A-Za-z]:/.test(p) ? p : join(ROOT, p); } /** Build the registry: chipnet defaults (or --tokens) + the persistent dir. */ function buildRegistry(parsed) { const tokenPaths = typeof parsed.flags.tokens === 'string' ? parsed.flags.tokens.split(',').map((p) => p.trim()) : ['ChipnetTests/.chipnet-asm-token.json', 'ChipnetTests/.chipnet-asm2-token.json']; const registry = new InMemoryTokenRegistry(); // 1) Seed files (legacy chipnet genesis artifacts) — tier-A only. for (const p of tokenPaths) { const a = abs(p); if (existsSync(a)) registry.add(tokenFileToTierA(JSON.parse(readFileSync(a, 'utf8'))), 'replace'); } // 2) Persistent registered tokens — portable or chipnet shape, either tier. if (existsSync(TOKENS_DIR)) { for (const f of readdirSync(TOKENS_DIR)) { if (!f.endsWith('.json')) continue; try { registerToken(JSON.parse(readFileSync(join(TOKENS_DIR, f), 'utf8')), registry, { policy: 'replace' }); } catch (err) { console.error(`[warn] skipping ${f}: ${err?.message ?? err}`); } } } return registry; } /** `register ` — local-only: parse, add, persist. No chain needed. */ function handleRegister(parsed, registry) { const file = parsed.positionals[0]; if (!file) throw new CliError('Usage: deviant register '); const a = abs(file); if (!existsSync(a)) throw new CliError(`File not found: ${a}`); const raw = JSON.parse(readFileSync(a, 'utf8')); const result = registerToken(raw, registry, { policy: 'replace' }); // Persist as portable JSON keyed by token id, so it reloads next run. mkdirSync(TOKENS_DIR, { recursive: true }); const dest = join(TOKENS_DIR, `${result.token.id}.json`); writeFileSync(dest, JSON.stringify(tokenToPortable(result.token), null, 2)); const lines = [result.summary, `Saved → ${dest}`]; if (result.confirmation) { lines.push('', result.confirmation.headline, '', result.confirmation.body); for (const w of result.confirmation.warnings) lines.push(` [${w.severity}] ${w.message}`); } console.log(lines.join('\n')); } async function main() { const argv = process.argv.slice(2); const parsed = parseArgs(argv); const registry = buildRegistry(parsed); // register is a local registry operation — no keys / network / wallet. if (parsed.command === 'register') { handleRegister(parsed, registry); return; } // Resolve config from flags (with sensible chipnet defaults). const network = typeof parsed.flags.network === 'string' ? parsed.flags.network : 'chipnet'; const keysPath = typeof parsed.flags.keys === 'string' ? parsed.flags.keys : join(ROOT, '.chipnet-keys.json'); const { wif } = JSON.parse(readFileSync(keysPath, 'utf8')); const w = network === 'mainnet' ? await Wallet.fromWIF(wif) : await TestNetWallet.fromWIF(wif); const wallet = connectWallet({ registry, network, privateKey: w.privateKey, cashaddr: w.cashaddr, tokenAddress: w.tokenaddr, mainnetWallet: w, // enables asset creation (genesis) }); const result = await runCommand( { wallet, registry, ownTokenAddress: w.tokenaddr }, parsed, ); // Auto-register a freshly created asset so it is immediately usable // (claim/balance/redeem-all/…) without a separate `register` step. if (result.createdToken) { mkdirSync(TOKENS_DIR, { recursive: true }); const dest = join(TOKENS_DIR, `${result.createdToken.id}.json`); writeFileSync(dest, JSON.stringify(tokenToPortable(result.createdToken), null, 2)); console.log(`\nRegistered ${result.createdToken.ticker} → ${dest}`); } console.log(result.output); } main().catch((err) => { // CliError and the typed plan errors (SwapPlanError, RedeemPlanError, // RegisterError, …) all carry a human-readable message and, for plan // errors, a string `.code`. These are expected validation failures — show // the message, not a stack trace. Only truly unexpected errors get a dump. if (err instanceof CliError || typeof err?.code === 'string') { console.error(`Error: ${err.message}`); } else { console.error('FAILED:', err?.message ?? err); if (err?.stack) console.error(err.stack); } process.exit(1); });