/** * Init CLI command - Generate and save a Veil keypair */ import { Command } from 'commander'; import { createInterface } from 'readline'; import { existsSync, readFileSync, writeFileSync } from 'fs'; import { dirname } from 'path'; import { mkdirSync } from 'fs'; import { Keypair } from '../../keypair.js'; import { handleCLIError, CLIError, ErrorCode } from '../errors.js'; import { printFields, printHeader, printJson, printLine } from '../output.js'; /** * Prompt user for yes/no confirmation */ async function confirm(question: string): Promise { const rl = createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { rl.question(`${question} (y/n): `, (answer) => { rl.close(); resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); }); }); } /** * Get the default path to .env.veil file (current directory) */ function getDefaultEnvPath(): string { return '.env.veil'; } /** * Enforce mutually exclusive wallet env modes. */ function ensureAddressEnvConsistency(): void { if (process.env.WALLET_KEY && process.env.SIGNER_ADDRESS) { throw new CLIError( ErrorCode.CONFIG_CONFLICT, 'WALLET_KEY and SIGNER_ADDRESS are mutually exclusive. Set only one.', ); } } /** * Check if VEIL_KEY exists in an env file */ function veilKeyExistsAt(envPath: string): boolean { if (!existsSync(envPath)) return false; const content = readFileSync(envPath, 'utf-8'); return /^VEIL_KEY=/m.test(content); } /** * Update or add a key in .env content */ function updateEnvVar(content: string, key: string, value: string): string { const regex = new RegExp(`^${key}=.*`, 'm'); if (regex.test(content)) { return content.replace(regex, `${key}=${value}`); } else { if (content && !content.endsWith('\n')) { content += '\n'; } return content + `${key}=${value}\n`; } } /** * Save Veil keypair to a file */ function saveVeilKeypair(veilKey: string, depositKey: string, envPath: string): void { const dir = dirname(envPath); if (dir && dir !== '.' && !existsSync(dir)) { mkdirSync(dir, { recursive: true }); } let content = ''; if (existsSync(envPath)) { content = readFileSync(envPath, 'utf-8'); } else { content = '# Veil Cash Configuration\n# Generated by: veil init\n\n'; } content = updateEnvVar(content, 'VEIL_KEY', veilKey); content = updateEnvVar(content, 'DEPOSIT_KEY', depositKey); writeFileSync(envPath, content); } /** * Resolve wallet key from CLI flag or WALLET_KEY env var */ function resolveWalletKey(): `0x${string}` { const raw = process.env.WALLET_KEY; if (!raw) { const hasExternalSigner = Boolean(process.env.SIGNER_ADDRESS); throw new CLIError( ErrorCode.WALLET_KEY_MISSING, hasExternalSigner ? 'WALLET_KEY env var required for wallet-derived init. If you are using an external signer, use "veil init --signature 0x..." or use --generate for a random keypair.' : 'WALLET_KEY env var required. Set it or use --generate for a random keypair.', ); } const key = raw.startsWith('0x') ? raw : `0x${raw}`; if (key.length !== 66) { throw new CLIError(ErrorCode.WALLET_KEY_MISSING, 'Invalid WALLET_KEY format. Must be a 0x-prefixed 64-character hex string.'); } return key as `0x${string}`; } export function createInitCommand(): Command { const init = new Command('init') .description('Derive a Veil keypair from your wallet (or generate a random one)') .option('--generate', 'Generate a random keypair instead of deriving from wallet') .option('--signature ', 'Derive keypair from a pre-computed EIP-191 personal_sign signature') .option('--force', 'Overwrite existing keypair without prompting') .option('--json', 'Output as JSON (no prompts, no file save)') .option('--no-save', 'Print keypair without saving to file') .addHelpText('after', ` Examples: veil init Derive from WALLET_KEY (default) veil init --generate Generate a random keypair veil init --signature 0x... Derive from a pre-computed signature veil init --json Output keypair as JSON `) .action(async (options) => { try { ensureAddressEnvConsistency(); const envPath = getDefaultEnvPath(); const useRandom = options.generate; const useSignature = options.signature; async function createKp(): Promise { if (useSignature) { return Keypair.fromSignature(options.signature); } if (useRandom) { return new Keypair(); } const walletKey = resolveWalletKey(); return Keypair.fromWalletKey(walletKey); } const derivation: 'wallet-signature' | 'provided-signature' | 'random' = useSignature ? 'provided-signature' : useRandom ? 'random' : 'wallet-signature'; const derivationLabel = useSignature ? 'Derived keypair from provided signature' : useRandom ? 'Generated random keypair' : 'Derived keypair from wallet signature'; const kp = await createKp(); const result = { veilKey: kp.privkey, veilPrivateKey: kp.privkey, depositKey: kp.depositKey(), derivation, }; if (options.json) { printJson(result); return; } if (!options.save) { printHeader(derivationLabel); printFields([ { label: 'Veil private key', value: kp.privkey }, { label: 'Deposit key', value: kp.depositKey() }, { label: 'Saved', value: 'no' }, ]); printLine(); printLine('Run `veil init` without `--no-save` to persist these keys to .env.veil.'); printLine(); return; } const keyExists = veilKeyExistsAt(envPath); if (keyExists && !options.force) { printLine(`WARNING: A Veil key already exists in ${envPath}`); const proceed = await confirm('Create a new key? This will overwrite the existing one'); if (!proceed) { printLine('Aborted. Existing key preserved.'); return; } } saveVeilKeypair(kp.privkey!, kp.depositKey(), envPath); printHeader(derivationLabel); printFields([ { label: 'Veil private key', value: kp.privkey }, { label: 'Deposit key', value: kp.depositKey() }, { label: 'Saved to', value: envPath }, ]); printLine(); printLine('Next step: veil register'); printLine(); } catch (error) { handleCLIError(error); } }); return init; }