#!/usr/bin/env node /** * deviant-web — local web UI for the Deviant wallet. * * Starts a localhost HTTP server that serves the SPA in `public/` and a * small JSON API backed by a real DeviantWallet (same core the CLI uses). * * Run: node --import tsx scripts/deviant-web.mjs * npm: npm run deviant:web * * Flags (same defaults as the CLI): * --keys WIF keys file (default .chipnet-keys.json) * --network chipnet | testnet4 | mainnet (default chipnet) * --tokens token JSON files (default asm + asm2) * --port listen port (default 8088) * * Binds to 127.0.0.1 only — never expose this off the machine; it can sign * and broadcast with the wallet's key. */ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync } 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 { startWebServer } from '../src/web/server.js'; import { parseArgs } from '../src/cli/wallet-cli.js'; const ROOT = join(import.meta.dirname, '..'); const TOKENS_DIR = join(ROOT, '.deviant', 'tokens'); // Load `.env` from the project root if present, so on-ramp keys/referral IDs // (KYCNOT_API_KEY, TROCADOR_API_KEY, SIDESHIFT_AFFILIATE, …) reach process.env. // Uses Node's built-in loader (v20.12+/v21.7+); silently skipped if absent. const ENV_FILE = join(ROOT, '.env'); if (existsSync(ENV_FILE) && typeof process.loadEnvFile === 'function') { try { process.loadEnvFile(ENV_FILE); } catch { /* malformed .env — ignore */ } } async function main() { // deviant-web takes only flags (no command). Prepend a placeholder so the // flag parser doesn't consume the first `--flag` as the command slot. const { flags } = parseArgs(['web', ...process.argv.slice(2)]); const network = typeof flags.network === 'string' ? flags.network : 'chipnet'; const port = typeof flags.port === 'string' ? Number(flags.port) : 8088; const keysPath = typeof flags.keys === 'string' ? flags.keys : join(ROOT, '.chipnet-keys.json'); const tokenPaths = typeof flags.tokens === 'string' ? flags.tokens.split(',').map((p) => p.trim()) : ['ChipnetTests/.chipnet-asm-token.json', 'ChipnetTests/.chipnet-asm2-token.json'].map((f) => join(ROOT, f)); const registry = new InMemoryTokenRegistry(); for (const p of tokenPaths) { const abs = p.startsWith('/') || /^[A-Za-z]:/.test(p) ? p : join(ROOT, p); if (existsSync(abs)) registry.add(tokenFileToTierA(JSON.parse(readFileSync(abs, 'utf8'))), 'replace'); } // Persistent registered tokens (same dir the CLI's `register` writes to). 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}`); } } } // Persist newly-registered tokens as portable JSON so they reload next run. const persistToken = (token) => { mkdirSync(TOKENS_DIR, { recursive: true }); writeFileSync(join(TOKENS_DIR, `${token.id}.json`), JSON.stringify(tokenToPortable(token), null, 2)); }; // Drop a forgotten token's persisted file ("Convert to BCH" for Tier B). const forgetToken = (id) => { const f = join(TOKENS_DIR, `${id}.json`); if (existsSync(f)) rmSync(f); }; 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) }); startWebServer( { ctx: { wallet, registry, ownTokenAddress: w.tokenaddr }, publicDir: join(ROOT, 'public'), walletInfo: { network, cashaddr: w.cashaddr, tokenAddress: w.tokenaddr }, persistToken, forgetToken, }, port, ); console.log(`Deviant web UI → http://127.0.0.1:${port} (${network}, ${w.cashaddr})`); console.log('Ctrl-C to stop.'); } main().catch((err) => { console.error('FAILED:', err?.message ?? err); if (err?.stack) console.error(err.stack); process.exit(1); });