deviant/scripts/deviant.mjs
walerikus 599202a721 chore: checkpoint working tree — shell Tier-B retirement + accumulated WIP
Snapshot of accumulated work-in-progress on top of the baseline commit.

This session — retire Tier B from the wallet shell (single-tier model, glossary
"Tiers & trust"):
- SendFlow.tsx: BCH-only; removed the non-functional Tier-B send path, the D-4/D-5
  raw-BCH co-display, and the ADR-0013 friction ladder.
- ArtemisionScreen.tsx: Create wizard collapsed to a single 5-step on-chain genesis
  flow; tier-selection step + Tier-B branch (prepareTierBImport) deleted.
- QuickActions.tsx / SettingsScreen.tsx: receive note reworded; "Send friction
  thresholds" + "Tier explainer" rows removed; registry-trust note reworded.
- Verified: tsc clean, vitest 74/74, full preview walkthrough, no console errors.

Pre-existing uncommitted work also folded in:
- USDm -> FIATm family rename/reorg (Parameters/FIATm/, design records)
- ~130 chipnet + sim-fiatm scripts (catalytic-converter, hyperinflation backtests)
- snapshots 0153-0168; wiki updates (glossary, decisions, index); ADR-0044/0045
- mockups + additional shell screens (Agora, Apps, MainScreen, AssetDetail, ...)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 01:01:37 +02:00

158 lines
6.2 KiB
JavaScript

#!/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 <command> [...]
* npm: npm run deviant -- <command> [...]
*
* Config (env or flags):
* --keys <path> WIF keys file (default .chipnet-keys.json). JSON: { wif }
* --network <net> chipnet | testnet4 | mainnet (default chipnet)
* --tokens <a,b,..> 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 <file>` 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 <file>` — 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 <file.json>');
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);
});