@veil-cash/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/balance.ts ADDED
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Balance functions for Veil SDK
3
+ * Query queue and private balances directly from blockchain
4
+ */
5
+
6
+ import { createPublicClient, http, formatEther } from 'viem';
7
+ import { base } from 'viem/chains';
8
+ import { getAddresses } from './addresses.js';
9
+ import { QUEUE_ABI, POOL_ABI } from './abi.js';
10
+ import { Keypair } from './keypair.js';
11
+ import { Utxo } from './utxo.js';
12
+ import { toFixedHex } from './utils.js';
13
+ import type {
14
+ QueueBalanceResult,
15
+ PendingDeposit,
16
+ PrivateBalanceResult,
17
+ UtxoInfo,
18
+ } from './types.js';
19
+
20
+ // Deposit status enum from Queue contract
21
+ const DEPOSIT_STATUS_MAP = {
22
+ 0: 'pending',
23
+ 1: 'accepted',
24
+ 2: 'rejected',
25
+ 3: 'refunded',
26
+ } as const;
27
+
28
+ /**
29
+ * Progress callback type
30
+ */
31
+ export type ProgressCallback = (stage: string, detail?: string) => void;
32
+
33
+ /**
34
+ * Get queue balance and pending deposits for an address
35
+ * Queries the Queue contract directly (no API dependency)
36
+ *
37
+ * @param options - Query options
38
+ * @param options.address - Address to check
39
+ * @param options.rpcUrl - Optional RPC URL (uses default if not provided)
40
+ * @param options.onProgress - Optional progress callback
41
+ * @returns Queue balance and pending deposits
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const result = await getQueueBalance({
46
+ * address: '0x...',
47
+ * onProgress: (stage, detail) => console.log(stage, detail),
48
+ * });
49
+ *
50
+ * console.log(`Queue balance: ${result.queueBalance} ETH`);
51
+ * console.log(`Pending deposits: ${result.pendingCount}`);
52
+ * ```
53
+ */
54
+ export async function getQueueBalance(options: {
55
+ address: `0x${string}`;
56
+ rpcUrl?: string;
57
+ onProgress?: ProgressCallback;
58
+ }): Promise<QueueBalanceResult> {
59
+ const { address, rpcUrl, onProgress } = options;
60
+ const addresses = getAddresses();
61
+
62
+ // Create public client
63
+ const publicClient = createPublicClient({
64
+ chain: base,
65
+ transport: http(rpcUrl),
66
+ });
67
+
68
+ // Get all pending deposit nonces
69
+ onProgress?.('Fetching pending deposits...');
70
+ const pendingNonces = await publicClient.readContract({
71
+ address: addresses.ethQueue,
72
+ abi: QUEUE_ABI,
73
+ functionName: 'getPendingDeposits',
74
+ }) as bigint[];
75
+ onProgress?.('Queue status', `${pendingNonces.length} pending deposits in queue`);
76
+
77
+ // Fetch deposit details for each pending nonce
78
+ const pendingDeposits: PendingDeposit[] = [];
79
+ let totalQueueBalance = 0n;
80
+
81
+ for (let i = 0; i < pendingNonces.length; i++) {
82
+ const nonce = pendingNonces[i];
83
+ onProgress?.('Checking deposit', `${i + 1}/${pendingNonces.length}`);
84
+
85
+ const deposit = await publicClient.readContract({
86
+ address: addresses.ethQueue,
87
+ abi: QUEUE_ABI,
88
+ functionName: 'getDeposit',
89
+ args: [nonce],
90
+ }) as {
91
+ fallbackReceiver: `0x${string}`;
92
+ amountIn: bigint;
93
+ fee: bigint;
94
+ shieldAmount: bigint;
95
+ timestamp: bigint;
96
+ status: number;
97
+ depositKey: `0x${string}`;
98
+ };
99
+
100
+ // Check if this deposit belongs to the user
101
+ if (deposit.fallbackReceiver.toLowerCase() === address.toLowerCase()) {
102
+ totalQueueBalance += deposit.amountIn;
103
+ pendingDeposits.push({
104
+ nonce: nonce.toString(),
105
+ status: DEPOSIT_STATUS_MAP[deposit.status as keyof typeof DEPOSIT_STATUS_MAP] || 'pending',
106
+ amount: formatEther(deposit.amountIn),
107
+ amountWei: deposit.amountIn.toString(),
108
+ timestamp: new Date(Number(deposit.timestamp) * 1000).toISOString(),
109
+ });
110
+ }
111
+ }
112
+
113
+ if (pendingDeposits.length > 0) {
114
+ onProgress?.('Found', `${pendingDeposits.length} deposits for your address`);
115
+ }
116
+
117
+ return {
118
+ address,
119
+ queueBalance: formatEther(totalQueueBalance),
120
+ queueBalanceWei: totalQueueBalance.toString(),
121
+ pendingDeposits,
122
+ pendingCount: pendingDeposits.length,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Get private balance from the Pool contract
128
+ * Decrypts all encrypted outputs, calculates nullifiers, and checks spent status
129
+ *
130
+ * @param options - Query options
131
+ * @param options.keypair - Keypair to decrypt UTXOs with
132
+ * @param options.rpcUrl - Optional RPC URL (uses default if not provided)
133
+ * @param options.onProgress - Optional progress callback
134
+ * @returns Private balance and UTXO details
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const keypair = new Keypair(process.env.VEIL_KEY);
139
+ * const result = await getPrivateBalance({
140
+ * keypair,
141
+ * onProgress: (stage, detail) => console.log(stage, detail),
142
+ * });
143
+ *
144
+ * console.log(`Private balance: ${result.privateBalance} ETH`);
145
+ * console.log(`Unspent UTXOs: ${result.unspentCount}`);
146
+ * ```
147
+ */
148
+ export async function getPrivateBalance(options: {
149
+ keypair: Keypair;
150
+ rpcUrl?: string;
151
+ onProgress?: ProgressCallback;
152
+ }): Promise<PrivateBalanceResult> {
153
+ const { keypair, rpcUrl, onProgress } = options;
154
+ const addresses = getAddresses();
155
+
156
+ if (!keypair.privkey) {
157
+ throw new Error('Keypair must have a private key to calculate private balance');
158
+ }
159
+
160
+ // Create public client
161
+ const publicClient = createPublicClient({
162
+ chain: base,
163
+ transport: http(rpcUrl),
164
+ });
165
+
166
+ // 1. Get total count of encrypted outputs
167
+ onProgress?.('Fetching pool index...');
168
+ const nextIndex = await publicClient.readContract({
169
+ address: addresses.ethPool,
170
+ abi: POOL_ABI,
171
+ functionName: 'nextIndex',
172
+ }) as number;
173
+ onProgress?.('Pool index', `${nextIndex} commitments`);
174
+
175
+ if (nextIndex === 0) {
176
+ return {
177
+ privateBalance: '0',
178
+ privateBalanceWei: '0',
179
+ utxoCount: 0,
180
+ spentCount: 0,
181
+ unspentCount: 0,
182
+ utxos: [],
183
+ };
184
+ }
185
+
186
+ // 2. Fetch encrypted outputs in batches of 5000
187
+ const BATCH_SIZE = 5000;
188
+ const allEncryptedOutputs: string[] = [];
189
+ const totalBatches = Math.ceil(nextIndex / BATCH_SIZE);
190
+
191
+ for (let start = 0; start < nextIndex; start += BATCH_SIZE) {
192
+ const end = Math.min(start + BATCH_SIZE, nextIndex);
193
+ const batchNum = Math.floor(start / BATCH_SIZE) + 1;
194
+ onProgress?.('Fetching encrypted outputs', `batch ${batchNum}/${totalBatches} (${start}-${end})`);
195
+
196
+ const batch = await publicClient.readContract({
197
+ address: addresses.ethPool,
198
+ abi: POOL_ABI,
199
+ functionName: 'getEncryptedOutputs',
200
+ args: [BigInt(start), BigInt(end)],
201
+ }) as string[];
202
+ allEncryptedOutputs.push(...batch);
203
+ }
204
+
205
+ // 3. Decrypt outputs - only user's UTXOs will decrypt successfully
206
+ onProgress?.('Decrypting outputs', `scanning ${allEncryptedOutputs.length} outputs...`);
207
+ const decryptedUtxos: { utxo: Utxo; index: number }[] = [];
208
+
209
+ for (let i = 0; i < allEncryptedOutputs.length; i++) {
210
+ try {
211
+ const utxo = Utxo.decrypt(allEncryptedOutputs[i], keypair);
212
+ utxo.index = i;
213
+ // Only include UTXOs with non-zero amounts
214
+ if (utxo.amount > 0n) {
215
+ decryptedUtxos.push({ utxo, index: i });
216
+ }
217
+ } catch {
218
+ // Not our UTXO - decryption fails, skip
219
+ }
220
+ }
221
+ onProgress?.('Found UTXOs', `${decryptedUtxos.length} belonging to you`);
222
+
223
+ // 4. Check spent status for each UTXO
224
+ const utxoInfos: UtxoInfo[] = [];
225
+ let totalBalance = 0n;
226
+ let spentCount = 0;
227
+ let unspentCount = 0;
228
+
229
+ for (let i = 0; i < decryptedUtxos.length; i++) {
230
+ const { utxo, index } = decryptedUtxos[i];
231
+ onProgress?.('Checking spent status', `UTXO ${i + 1}/${decryptedUtxos.length}`);
232
+
233
+ const nullifier = utxo.getNullifier();
234
+ const nullifierHex = toFixedHex(nullifier) as `0x${string}`;
235
+
236
+ const isSpent = await publicClient.readContract({
237
+ address: addresses.ethPool,
238
+ abi: POOL_ABI,
239
+ functionName: 'isSpent',
240
+ args: [nullifierHex],
241
+ }) as boolean;
242
+
243
+ utxoInfos.push({
244
+ index,
245
+ amount: formatEther(utxo.amount),
246
+ amountWei: utxo.amount.toString(),
247
+ isSpent,
248
+ });
249
+
250
+ if (isSpent) {
251
+ spentCount++;
252
+ } else {
253
+ unspentCount++;
254
+ totalBalance += utxo.amount;
255
+ }
256
+ }
257
+
258
+ return {
259
+ privateBalance: formatEther(totalBalance),
260
+ privateBalanceWei: totalBalance.toString(),
261
+ utxoCount: decryptedUtxos.length,
262
+ spentCount,
263
+ unspentCount,
264
+ utxos: utxoInfos,
265
+ };
266
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Balance CLI command - Show both queue and private balances
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { getQueueBalance, getPrivateBalance } from '../../balance.js';
7
+ import { Keypair } from '../../keypair.js';
8
+ import { getAddress } from '../wallet.js';
9
+ import { formatEther } from 'viem';
10
+ import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
11
+
12
+ export function createBalanceCommand(): Command {
13
+ const balance = new Command('balance')
14
+ .description('Show queue and private balances')
15
+ .option('--wallet-key <key>', 'Ethereum wallet key (or set WALLET_KEY env)')
16
+ .option('--address <address>', 'Address to check (or derived from wallet key)')
17
+ .option('--veil-key <key>', 'Veil private key (or set VEIL_KEY env)')
18
+ .option('--rpc-url <url>', 'RPC URL (or set RPC_URL env)')
19
+ .option('--quiet', 'Suppress progress output')
20
+ .action(async (options) => {
21
+ try {
22
+ // Get address
23
+ let address: `0x${string}`;
24
+ if (options.address) {
25
+ address = options.address as `0x${string}`;
26
+ } else {
27
+ const walletKey = options.walletKey || process.env.WALLET_KEY;
28
+ if (!walletKey) {
29
+ throw new CLIError(ErrorCode.WALLET_KEY_MISSING, 'Must provide --address or --wallet-key (or set WALLET_KEY env)');
30
+ }
31
+ address = getAddress(walletKey as `0x${string}`);
32
+ }
33
+
34
+ // Get keypair for private balance
35
+ const veilKey = options.veilKey || process.env.VEIL_KEY;
36
+ const keypair = veilKey ? new Keypair(veilKey) : null;
37
+
38
+ const rpcUrl = options.rpcUrl || process.env.RPC_URL;
39
+
40
+ // Progress callback
41
+ const onProgress = options.quiet
42
+ ? undefined
43
+ : (stage: string, detail?: string) => {
44
+ const msg = detail ? `${stage}: ${detail}` : stage;
45
+ process.stderr.write(`\r\x1b[K${msg}`);
46
+ };
47
+
48
+ // Get queue balance
49
+ const queueResult = await getQueueBalance({ address, rpcUrl, onProgress });
50
+
51
+ // Get private balance if keypair available
52
+ let privateResult = null;
53
+ if (keypair) {
54
+ privateResult = await getPrivateBalance({ keypair, rpcUrl, onProgress });
55
+ }
56
+
57
+ // Clear progress line
58
+ if (!options.quiet) {
59
+ process.stderr.write('\r\x1b[K');
60
+ }
61
+
62
+ // Calculate total balance
63
+ const queueBalanceWei = BigInt(queueResult.queueBalanceWei);
64
+ const privateBalanceWei = privateResult ? BigInt(privateResult.privateBalanceWei) : 0n;
65
+ const totalBalanceWei = queueBalanceWei + privateBalanceWei;
66
+
67
+ // Get deposit key if available
68
+ const depositKey = process.env.DEPOSIT_KEY || (keypair ? keypair.depositKey() : null);
69
+
70
+ // Build output structure
71
+ const output: Record<string, unknown> = {
72
+ address,
73
+ depositKey: depositKey || null,
74
+ totalBalance: formatEther(totalBalanceWei),
75
+ totalBalanceWei: totalBalanceWei.toString(),
76
+ };
77
+
78
+ // Private balance first
79
+ if (privateResult) {
80
+ const unspentUtxos = privateResult.utxos.filter(u => !u.isSpent);
81
+ output.private = {
82
+ balance: privateResult.privateBalance,
83
+ balanceWei: privateResult.privateBalanceWei,
84
+ utxoCount: privateResult.unspentCount,
85
+ utxos: unspentUtxos.map(u => ({
86
+ index: u.index,
87
+ amount: u.amount,
88
+ })),
89
+ };
90
+ } else {
91
+ output.private = {
92
+ balance: null,
93
+ note: 'Set VEIL_KEY to see private balance',
94
+ };
95
+ }
96
+
97
+ // Queue details second
98
+ output.queue = {
99
+ balance: queueResult.queueBalance,
100
+ balanceWei: queueResult.queueBalanceWei,
101
+ count: queueResult.pendingCount,
102
+ deposits: queueResult.pendingDeposits.map(d => ({
103
+ nonce: d.nonce,
104
+ amount: d.amount,
105
+ status: d.status,
106
+ timestamp: d.timestamp,
107
+ })),
108
+ };
109
+
110
+ console.log(JSON.stringify(output, null, 2));
111
+ } catch (error) {
112
+ process.stderr.write('\r\x1b[K');
113
+ handleCLIError(error);
114
+ }
115
+ });
116
+
117
+ return balance;
118
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Deposit CLI command
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { buildDepositETHTx } from '../../deposit.js';
7
+ import { sendTransaction, getAddress, getBalance } from '../wallet.js';
8
+ import { getConfig } from '../config.js';
9
+ import { parseEther, formatEther } from 'viem';
10
+ import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
11
+
12
+ // Minimum deposit: 0.01 ETH (net after 0.3% fee)
13
+ // To deposit 0.01 ETH net, you need to send: 0.01 / (1 - 0.003) ≈ 0.01003 ETH
14
+ const MINIMUM_DEPOSIT_ETH = 0.01;
15
+ const DEPOSIT_FEE_PERCENT = 0.3;
16
+ const MINIMUM_DEPOSIT_WITH_FEE = MINIMUM_DEPOSIT_ETH / (1 - DEPOSIT_FEE_PERCENT / 100);
17
+
18
+ // Progress helper - writes to stderr so JSON output stays clean
19
+ function progress(msg: string, quiet?: boolean) {
20
+ if (!quiet) {
21
+ process.stderr.write(`\r\x1b[K${msg}`);
22
+ }
23
+ }
24
+
25
+ export function createDepositCommand(): Command {
26
+ const deposit = new Command('deposit')
27
+ .description('Deposit ETH into Veil')
28
+ .argument('<amount>', 'Amount to deposit (e.g., 0.1)')
29
+ .option('--deposit-key <key>', 'Your Veil deposit key (or set DEPOSIT_KEY env)')
30
+ .option('--wallet-key <key>', 'Ethereum wallet key for signing (or set WALLET_KEY env)')
31
+ .option('--rpc-url <url>', 'RPC URL (or set RPC_URL env)')
32
+ .option('--unsigned', 'Output unsigned transaction payload (Bankr-compatible format)')
33
+ .option('--quiet', 'Suppress progress output')
34
+ .action(async (amount: string, options) => {
35
+ try {
36
+ const amountNum = parseFloat(amount);
37
+
38
+ // Check minimum deposit
39
+ if (amountNum < MINIMUM_DEPOSIT_WITH_FEE) {
40
+ throw new CLIError(
41
+ ErrorCode.INVALID_AMOUNT,
42
+ `Minimum deposit is ${MINIMUM_DEPOSIT_ETH} ETH (net). ` +
43
+ `With ${DEPOSIT_FEE_PERCENT}% fee, send at least ${MINIMUM_DEPOSIT_WITH_FEE.toFixed(5)} ETH.`
44
+ );
45
+ }
46
+
47
+ // Get deposit key from option or env
48
+ const depositKey = options.depositKey || process.env.DEPOSIT_KEY;
49
+ if (!depositKey) {
50
+ throw new CLIError(ErrorCode.DEPOSIT_KEY_MISSING, 'Deposit key required. Use --deposit-key or set DEPOSIT_KEY in .env (run: veil init)');
51
+ }
52
+
53
+ progress('Building transaction...', options.quiet);
54
+
55
+ // Build the transaction
56
+ const tx = buildDepositETHTx({
57
+ depositKey,
58
+ amount,
59
+ });
60
+
61
+ // Handle --unsigned mode (no wallet required, just build payload)
62
+ if (options.unsigned) {
63
+ progress('', options.quiet); // Clear line
64
+ // Output Bankr-compatible format
65
+ const payload = {
66
+ to: tx.to,
67
+ data: tx.data,
68
+ value: tx.value ? tx.value.toString() : '0',
69
+ chainId: 8453, // Base mainnet
70
+ };
71
+
72
+ console.log(JSON.stringify(payload, null, 2));
73
+ return;
74
+ }
75
+
76
+ // Regular mode: sign and send
77
+ const config = getConfig(options);
78
+ const address = getAddress(config.privateKey);
79
+
80
+ progress('Checking balance...', options.quiet);
81
+
82
+ // Check balance
83
+ const balance = await getBalance(address, config.rpcUrl);
84
+ const amountWei = parseEther(amount);
85
+
86
+ if (balance < amountWei) {
87
+ progress('', options.quiet);
88
+ throw new CLIError(ErrorCode.INSUFFICIENT_BALANCE, `Insufficient ETH balance. Have: ${formatEther(balance)} ETH, Need: ${amount} ETH`);
89
+ }
90
+
91
+ progress('Sending transaction...', options.quiet);
92
+
93
+ // Send the transaction
94
+ const result = await sendTransaction(config, tx);
95
+
96
+ progress('Confirming...', options.quiet);
97
+
98
+ // Clear progress line
99
+ progress('', options.quiet);
100
+
101
+ console.log(JSON.stringify({
102
+ success: result.receipt.status === 'success',
103
+ hash: result.hash,
104
+ amount,
105
+ blockNumber: result.receipt.blockNumber.toString(),
106
+ gasUsed: result.receipt.gasUsed.toString(),
107
+ }, null, 2));
108
+ } catch (error) {
109
+ progress('', options.quiet); // Clear progress line
110
+ handleCLIError(error);
111
+ }
112
+ });
113
+
114
+ return deposit;
115
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Init CLI command - Generate and save a Veil keypair
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { createInterface } from 'readline';
7
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
8
+ import { dirname } from 'path';
9
+ import { mkdirSync } from 'fs';
10
+ import { Keypair } from '../../keypair.js';
11
+
12
+ /**
13
+ * Prompt user for yes/no confirmation
14
+ */
15
+ async function confirm(question: string): Promise<boolean> {
16
+ const rl = createInterface({
17
+ input: process.stdin,
18
+ output: process.stdout,
19
+ });
20
+
21
+ return new Promise((resolve) => {
22
+ rl.question(`${question} (y/n): `, (answer) => {
23
+ rl.close();
24
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
25
+ });
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Get the default path to .env.veil file (current directory)
31
+ */
32
+ function getDefaultEnvPath(): string {
33
+ return '.env.veil';
34
+ }
35
+
36
+ /**
37
+ * Check if VEIL_KEY exists in an env file
38
+ */
39
+ function veilKeyExistsAt(envPath: string): boolean {
40
+ if (!existsSync(envPath)) return false;
41
+
42
+ const content = readFileSync(envPath, 'utf-8');
43
+ return /^VEIL_KEY=/m.test(content);
44
+ }
45
+
46
+ /**
47
+ * Update or add a key in .env content
48
+ */
49
+ function updateEnvVar(content: string, key: string, value: string): string {
50
+ const regex = new RegExp(`^${key}=.*`, 'm');
51
+ if (regex.test(content)) {
52
+ return content.replace(regex, `${key}=${value}`);
53
+ } else {
54
+ if (content && !content.endsWith('\n')) {
55
+ content += '\n';
56
+ }
57
+ return content + `${key}=${value}\n`;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Save Veil keypair to a file
63
+ */
64
+ function saveVeilKeypair(veilKey: string, depositKey: string, envPath: string): void {
65
+ // Ensure directory exists
66
+ const dir = dirname(envPath);
67
+ if (dir && dir !== '.' && !existsSync(dir)) {
68
+ mkdirSync(dir, { recursive: true });
69
+ }
70
+
71
+ let content = '';
72
+
73
+ if (existsSync(envPath)) {
74
+ content = readFileSync(envPath, 'utf-8');
75
+ } else {
76
+ content = '# Veil Cash Configuration\n# Generated by: veil init\n\n';
77
+ }
78
+
79
+ content = updateEnvVar(content, 'VEIL_KEY', veilKey);
80
+ content = updateEnvVar(content, 'DEPOSIT_KEY', depositKey);
81
+
82
+ writeFileSync(envPath, content);
83
+ }
84
+
85
+ export function createInitCommand(): Command {
86
+ const init = new Command('init')
87
+ .description('Generate a new Veil keypair')
88
+ .option('--force', 'Overwrite existing keypair without prompting')
89
+ .option('--json', 'Output as JSON (no prompts, no file save)')
90
+ .option('--out <path>', 'Save to custom path instead of .env.veil')
91
+ .option('--no-save', 'Print keypair without saving to file')
92
+ .action(async (options) => {
93
+ const envPath = options.out || getDefaultEnvPath();
94
+
95
+ // JSON mode: no prompts, no save, just output JSON
96
+ if (options.json) {
97
+ const kp = new Keypair();
98
+ console.log(JSON.stringify({
99
+ veilKey: kp.privkey,
100
+ depositKey: kp.depositKey(),
101
+ }, null, 2));
102
+ process.exit(0);
103
+ return;
104
+ }
105
+
106
+ // No-save mode: print but don't save
107
+ if (!options.save) {
108
+ const kp = new Keypair();
109
+ console.log('\nGenerated new Veil keypair:\n');
110
+ console.log('Veil Private Key:');
111
+ console.log(` ${kp.privkey}\n`);
112
+ console.log('Deposit Key (register this on-chain):');
113
+ console.log(` ${kp.depositKey()}\n`);
114
+ console.log('(Not saved - use --out <path> to save to a file)');
115
+ process.exit(0);
116
+ return;
117
+ }
118
+
119
+ // Check if VEIL_KEY already exists (unless --force)
120
+ const keyExists = veilKeyExistsAt(envPath);
121
+
122
+ if (keyExists && !options.force) {
123
+ console.log(`\nWARNING: A Veil key already exists in ${envPath}`);
124
+ const proceed = await confirm('Create a new key? This will overwrite the existing one');
125
+ if (!proceed) {
126
+ console.log('Aborted. Existing key preserved.');
127
+ process.exit(0);
128
+ return;
129
+ }
130
+ }
131
+
132
+ const kp = new Keypair();
133
+
134
+ console.log('\nGenerated new Veil keypair:\n');
135
+ console.log('Veil Private Key:');
136
+ console.log(` ${kp.privkey}\n`);
137
+ console.log('Deposit Key (register this on-chain):');
138
+ console.log(` ${kp.depositKey()}\n`);
139
+
140
+ saveVeilKeypair(kp.privkey!, kp.depositKey(), envPath);
141
+ console.log(`Saved to ${envPath}`);
142
+ console.log('\nNext step: veil register');
143
+ process.exit(0);
144
+ });
145
+
146
+ return init;
147
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Keypair CLI command - Show current keypair as JSON
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { Keypair } from '../../keypair.js';
7
+
8
+ export function createKeypairCommand(): Command {
9
+ const keypair = new Command('keypair')
10
+ .description('Show current Veil keypair as JSON')
11
+ .action(() => {
12
+ const veilKey = process.env.VEIL_KEY;
13
+
14
+ if (!veilKey) {
15
+ console.log(JSON.stringify({
16
+ success: false,
17
+ error: 'No keypair found. Run "veil init" first.'
18
+ }, null, 2));
19
+ process.exit(1);
20
+ }
21
+
22
+ const kp = new Keypair(veilKey);
23
+
24
+ console.log(JSON.stringify({
25
+ veilPrivateKey: kp.privkey,
26
+ depositKey: kp.depositKey(),
27
+ }, null, 2));
28
+ });
29
+
30
+ return keypair;
31
+ }