@profullstack/coinpay 0.3.10 → 0.4.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/bin/coinpay.js CHANGED
@@ -3,19 +3,28 @@
3
3
  /**
4
4
  * CoinPay CLI
5
5
  * Command-line interface for CoinPay cryptocurrency payments
6
+ * with persistent GPG-encrypted wallet storage
6
7
  */
7
8
 
8
9
  import { CoinPayClient } from '../src/client.js';
9
10
  import { PaymentStatus, Blockchain, FiatCurrency } from '../src/payments.js';
10
- import { readFileSync, writeFileSync, existsSync } from 'fs';
11
+ import {
12
+ WalletClient,
13
+ generateMnemonic,
14
+ validateMnemonic,
15
+ DEFAULT_CHAINS,
16
+ } from '../src/wallet.js';
17
+ import { SwapClient, SwapCoins } from '../src/swap.js';
18
+ import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
11
19
  import { execSync } from 'child_process';
12
20
  import { createInterface } from 'readline';
13
21
  import { homedir } from 'os';
14
22
  import { join } from 'path';
15
23
  import { tmpdir } from 'os';
16
24
 
17
- const VERSION = '0.3.3';
25
+ const VERSION = '0.4.0';
18
26
  const CONFIG_FILE = join(homedir(), '.coinpay.json');
27
+ const DEFAULT_WALLET_FILE = join(homedir(), '.coinpay-wallet.gpg');
19
28
 
20
29
  // ANSI colors
21
30
  const colors = {
@@ -57,7 +66,29 @@ function loadConfig() {
57
66
  * Save configuration
58
67
  */
59
68
  function saveConfig(config) {
60
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
69
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
70
+ }
71
+
72
+ /**
73
+ * Get wallet file path from config or default
74
+ */
75
+ function getWalletFilePath(flags = {}) {
76
+ if (flags['wallet-file']) {
77
+ return flags['wallet-file'].replace(/^~/, homedir());
78
+ }
79
+ const config = loadConfig();
80
+ if (config.walletFile) {
81
+ return config.walletFile.replace(/^~/, homedir());
82
+ }
83
+ return DEFAULT_WALLET_FILE;
84
+ }
85
+
86
+ /**
87
+ * Check if encrypted wallet exists
88
+ */
89
+ function hasEncryptedWallet(flags = {}) {
90
+ const walletFile = getWalletFilePath(flags);
91
+ return existsSync(walletFile);
61
92
  }
62
93
 
63
94
  /**
@@ -102,7 +133,6 @@ function parseArgs(args) {
102
133
  if (eqIdx !== -1) {
103
134
  result.flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
104
135
  } else {
105
- // Peek ahead: if next arg doesn't start with '-', it's the value
106
136
  const next = args[i + 1];
107
137
  if (next && !next.startsWith('-')) {
108
138
  result.flags[arg.slice(2)] = next;
@@ -158,8 +188,25 @@ ${colors.cyan}Commands:${colors.reset}
158
188
  list Get all exchange rates
159
189
 
160
190
  ${colors.bright}wallet${colors.reset}
161
- backup-seed Encrypt seed phrase to GPG file
162
- decrypt-backup <file> Decrypt a GPG backup file
191
+ create Create new wallet (saves encrypted)
192
+ import <mnemonic> Import wallet (saves encrypted)
193
+ unlock Decrypt wallet and show info
194
+ info Show wallet info
195
+ addresses List all addresses
196
+ derive <chain> Derive new address
197
+ derive-missing Derive addresses for missing chains
198
+ balance [chain] Get balance(s)
199
+ send Send a transaction
200
+ history Transaction history
201
+ backup Export encrypted backup
202
+ delete Delete local wallet file
203
+
204
+ ${colors.bright}swap${colors.reset}
205
+ coins List supported coins
206
+ quote Get swap quote
207
+ create Create swap transaction
208
+ status <swap-id> Check swap status
209
+ history Swap history
163
210
 
164
211
  ${colors.bright}escrow${colors.reset}
165
212
  create Create a new escrow
@@ -174,60 +221,222 @@ ${colors.cyan}Commands:${colors.reset}
174
221
  logs <business-id> Get webhook logs
175
222
  test <business-id> Send test webhook
176
223
 
177
- ${colors.cyan}Options:${colors.reset}
178
- --help, -h Show help
179
- --version, -v Show version
180
- --json Output as JSON
181
- --business-id <id> Business ID for operations
182
- --amount <amount> Payment amount in fiat currency
183
- --currency <code> Fiat currency (USD, EUR, etc.) - default: USD
184
- --blockchain <code> Blockchain (BTC, ETH, SOL, POL, BCH, USDC_ETH, USDC_POL, USDC_SOL)
185
- --description <text> Payment description
186
- --seed <phrase> Seed phrase (or reads from stdin)
187
- --password <pass> GPG passphrase (or prompts interactively)
188
- --wallet-id <id> Wallet ID for backup filename
189
- --output <path> Output file path (default: wallet_<id>_seedphrase.txt.gpg)
224
+ ${colors.cyan}Wallet Options:${colors.reset}
225
+ --words <12|24> Number of mnemonic words (default: 12)
226
+ --chains <BTC,ETH,...> Chains to derive (default: BTC,ETH,SOL,POL,BCH)
227
+ --chain <chain> Single chain for operations
228
+ --to <address> Recipient address
229
+ --amount <amount> Amount to send
230
+ --password <pass> Wallet encryption password
231
+ --wallet-file <path> Custom wallet file (default: ~/.coinpay-wallet.gpg)
232
+ --no-save Don't save wallet locally after create/import
233
+
234
+ ${colors.cyan}Swap Options:${colors.reset}
235
+ --from <coin> Source coin (e.g., BTC)
236
+ --to <coin> Destination coin (e.g., ETH)
237
+ --amount <amount> Amount to swap
238
+ --refund <address> Refund address (recommended)
190
239
 
191
240
  ${colors.cyan}Examples:${colors.reset}
192
- # Configure your API key (get it from your CoinPay dashboard)
193
- coinpay config set-key cp_live_xxxxx
241
+ # Create a new wallet (auto-saves encrypted)
242
+ coinpay wallet create --words 12
194
243
 
195
- # Create a $100 Bitcoin payment
196
- coinpay payment create --business-id biz_123 --amount 100 --blockchain BTC
244
+ # Import existing wallet
245
+ coinpay wallet import "word1 word2 ... word12"
197
246
 
198
- # Create a $50 Ethereum payment with description
199
- coinpay payment create --business-id biz_123 --amount 50 --blockchain ETH --description "Order #12345"
247
+ # Get wallet balance (auto-decrypts)
248
+ coinpay wallet balance
200
249
 
201
- # Create a USDC payment on Polygon
202
- coinpay payment create --business-id biz_123 --amount 25 --blockchain USDC_POL
250
+ # Send transaction (auto-decrypts for signing)
251
+ coinpay wallet send --chain ETH --to 0x123... --amount 0.1
252
+
253
+ # Swap BTC to ETH
254
+ coinpay swap quote --from BTC --to ETH --amount 0.1
255
+ coinpay swap create --from BTC --to ETH --amount 0.1 --settle 0x...
256
+
257
+ ${colors.cyan}Environment Variables:${colors.reset}
258
+ COINPAY_API_KEY API key (overrides config)
259
+ COINPAY_BASE_URL Custom API URL
260
+ `);
261
+ }
203
262
 
204
- # Get payment status
205
- coinpay payment get pay_abc123
263
+ /**
264
+ * Prompt for user input
265
+ */
266
+ function prompt(question) {
267
+ return new Promise((resolve) => {
268
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
269
+ rl.question(question, (answer) => {
270
+ rl.close();
271
+ resolve(answer.trim());
272
+ });
273
+ });
274
+ }
206
275
 
207
- # Get exchange rates
208
- coinpay rates get BTC
276
+ /**
277
+ * Prompt for password (hidden input)
278
+ */
279
+ function promptPassword(promptText = 'Password: ') {
280
+ return new Promise((resolve) => {
281
+ process.stdout.write(promptText);
282
+
283
+ if (process.stdin.isTTY) {
284
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
285
+ const origWrite = process.stdout.write.bind(process.stdout);
286
+ process.stdout.write = (chunk) => {
287
+ if (typeof chunk === 'string' && chunk !== promptText && chunk !== '\n' && chunk !== '\r\n') {
288
+ return true;
289
+ }
290
+ return origWrite(chunk);
291
+ };
292
+
293
+ rl.question('', (answer) => {
294
+ process.stdout.write = origWrite;
295
+ process.stdout.write('\n');
296
+ rl.close();
297
+ resolve(answer);
298
+ });
299
+ } else {
300
+ const chunks = [];
301
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
302
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString().trim()));
303
+ process.stdin.resume();
304
+ }
305
+ });
306
+ }
209
307
 
210
- # List your businesses
211
- coinpay business list
308
+ /**
309
+ * Prompt yes/no confirmation
310
+ */
311
+ async function promptYesNo(question, defaultYes = true) {
312
+ const suffix = defaultYes ? '[Y/n]' : '[y/N]';
313
+ const answer = await prompt(`${question} ${suffix} `);
314
+
315
+ if (!answer) return defaultYes;
316
+ return answer.toLowerCase().startsWith('y');
317
+ }
212
318
 
213
- # Encrypt seed phrase to GPG backup file
214
- coinpay wallet backup-seed --seed "word1 word2 ..." --password "mypass" --wallet-id "wid-abc"
319
+ /**
320
+ * Check if gpg is available
321
+ */
322
+ function hasGpg() {
323
+ try {
324
+ execSync('gpg --version', { stdio: 'pipe' });
325
+ return true;
326
+ } catch {
327
+ return false;
328
+ }
329
+ }
215
330
 
216
- # Encrypt seed phrase (interactive password prompt)
217
- coinpay wallet backup-seed --seed "word1 word2 ..." --wallet-id "wid-abc"
331
+ /**
332
+ * Encrypt mnemonic with GPG and save to file
333
+ */
334
+ async function saveEncryptedWallet(mnemonic, walletId, password, walletFile) {
335
+ if (!hasGpg()) {
336
+ throw new Error('GPG is required for wallet encryption. Install: apt install gnupg');
337
+ }
338
+
339
+ const content = JSON.stringify({
340
+ version: 1,
341
+ walletId,
342
+ mnemonic,
343
+ createdAt: new Date().toISOString(),
344
+ });
345
+
346
+ const tmpFile = join(tmpdir(), `coinpay-wallet-${Date.now()}.json`);
347
+ const passFile = join(tmpdir(), `coinpay-pass-${Date.now()}`);
348
+
349
+ try {
350
+ writeFileSync(tmpFile, content, { mode: 0o600 });
351
+ writeFileSync(passFile, password, { mode: 0o600 });
352
+
353
+ execSync(
354
+ `gpg --batch --yes --passphrase-file "${passFile}" --pinentry-mode loopback --symmetric --cipher-algo AES256 --output "${walletFile}" "${tmpFile}"`,
355
+ { stdio: ['pipe', 'pipe', 'pipe'] }
356
+ );
357
+
358
+ return true;
359
+ } finally {
360
+ // Secure cleanup
361
+ try { writeFileSync(tmpFile, Buffer.alloc(content.length, 0)); unlinkSync(tmpFile); } catch {}
362
+ try { writeFileSync(passFile, Buffer.alloc(password.length, 0)); unlinkSync(passFile); } catch {}
363
+ }
364
+ }
218
365
 
219
- # Pipe seed phrase from stdin
220
- echo "word1 word2 ..." | coinpay wallet backup-seed --wallet-id "wid-abc" --password "mypass"
366
+ /**
367
+ * Decrypt wallet file and return contents
368
+ */
369
+ async function loadEncryptedWallet(password, walletFile) {
370
+ if (!hasGpg()) {
371
+ throw new Error('GPG is required for wallet decryption');
372
+ }
373
+
374
+ if (!existsSync(walletFile)) {
375
+ throw new Error(`Wallet file not found: ${walletFile}`);
376
+ }
377
+
378
+ const passFile = join(tmpdir(), `coinpay-pass-${Date.now()}`);
379
+
380
+ try {
381
+ writeFileSync(passFile, password, { mode: 0o600 });
382
+
383
+ const result = execSync(
384
+ `gpg --batch --yes --passphrase-file "${passFile}" --pinentry-mode loopback --decrypt "${walletFile}"`,
385
+ { stdio: ['pipe', 'pipe', 'pipe'] }
386
+ );
387
+
388
+ const data = JSON.parse(result.toString());
389
+ return data;
390
+ } finally {
391
+ try { writeFileSync(passFile, Buffer.alloc(password.length, 0)); unlinkSync(passFile); } catch {}
392
+ }
393
+ }
221
394
 
222
- # Decrypt a backup file
223
- coinpay wallet decrypt-backup wallet_wid-abc_seedphrase.txt.gpg --password "mypass"
395
+ /**
396
+ * Get decrypted mnemonic from wallet file
397
+ * Prompts for password if needed
398
+ */
399
+ async function getDecryptedMnemonic(flags) {
400
+ const walletFile = getWalletFilePath(flags);
401
+
402
+ if (!existsSync(walletFile)) {
403
+ return null;
404
+ }
405
+
406
+ let password = flags.password;
407
+ if (!password) {
408
+ if (!process.stdin.isTTY) {
409
+ throw new Error('Password required. Use --password or run interactively.');
410
+ }
411
+ password = await promptPassword('Wallet password: ');
412
+ }
413
+
414
+ try {
415
+ const data = await loadEncryptedWallet(password, walletFile);
416
+ return data.mnemonic;
417
+ } catch (error) {
418
+ if (error.message.includes('decrypt')) {
419
+ throw new Error('Wrong password or corrupted wallet file');
420
+ }
421
+ throw error;
422
+ }
423
+ }
224
424
 
225
- ${colors.cyan}Environment Variables:${colors.reset}
226
- COINPAY_API_KEY API key (overrides config)
227
- COINPAY_BASE_URL Custom API URL
228
- `);
425
+ /**
426
+ * Securely clear a string from memory
427
+ */
428
+ function clearString(str) {
429
+ if (str && typeof str === 'string') {
430
+ // Can't truly clear in JS, but we can try to minimize exposure
431
+ return '';
432
+ }
433
+ return str;
229
434
  }
230
435
 
436
+ // ═══════════════════════════════════════════════════════════════
437
+ // COMMAND HANDLERS
438
+ // ═══════════════════════════════════════════════════════════════
439
+
231
440
  /**
232
441
  * Config commands
233
442
  */
@@ -260,6 +469,8 @@ async function handleConfig(subcommand, args) {
260
469
  print.json({
261
470
  apiKey: config.apiKey ? `${config.apiKey.slice(0, 10)}...` : '(not set)',
262
471
  baseUrl: config.baseUrl || '(default)',
472
+ walletId: config.walletId || '(none)',
473
+ walletFile: config.walletFile || DEFAULT_WALLET_FILE,
263
474
  });
264
475
  break;
265
476
 
@@ -299,7 +510,7 @@ async function handlePayment(subcommand, args, flags) {
299
510
  print.info(`Amount: ${payment.payment.crypto_amount} ${payment.payment.blockchain}`);
300
511
  print.info(`Expires: ${payment.payment.expires_at}`);
301
512
  }
302
- if (!flags.json) {
513
+ if (flags.json) {
303
514
  print.json(payment);
304
515
  }
305
516
  break;
@@ -337,15 +548,14 @@ async function handlePayment(subcommand, args, flags) {
337
548
 
338
549
  case 'qr': {
339
550
  const paymentId = args[0];
340
- const { format } = flags;
341
551
 
342
552
  if (!paymentId) {
343
553
  print.error('Payment ID required');
344
554
  return;
345
555
  }
346
556
 
347
- const qr = await client.getPaymentQR(paymentId, format);
348
- print.json(qr);
557
+ const url = client.getPaymentQRUrl(paymentId);
558
+ print.info(`QR Code URL: ${url}`);
349
559
  break;
350
560
  }
351
561
 
@@ -496,254 +706,661 @@ async function handleWebhook(subcommand, args, flags) {
496
706
  }
497
707
  }
498
708
 
499
- /**
500
- * Prompt for password interactively (hides input)
501
- */
502
- function promptPassword(prompt = 'Password: ') {
503
- return new Promise((resolve) => {
504
- process.stdout.write(prompt);
505
-
506
- // If stdin is a TTY, read with hidden input
507
- if (process.stdin.isTTY) {
508
- const rl = createInterface({ input: process.stdin, output: process.stdout });
509
- // Temporarily override output to hide password chars
510
- const origWrite = process.stdout.write.bind(process.stdout);
511
- process.stdout.write = (chunk) => {
512
- // Only suppress characters that are the user's input
513
- if (typeof chunk === 'string' && chunk !== prompt && chunk !== '\n' && chunk !== '\r\n') {
514
- return true;
515
- }
516
- return origWrite(chunk);
517
- };
518
-
519
- rl.question('', (answer) => {
520
- process.stdout.write = origWrite;
521
- process.stdout.write('\n');
522
- rl.close();
523
- resolve(answer);
524
- });
525
- } else {
526
- // Pipe mode — read from stdin
527
- const chunks = [];
528
- process.stdin.on('data', (chunk) => chunks.push(chunk));
529
- process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString().trim()));
530
- process.stdin.resume();
531
- }
532
- });
533
- }
534
-
535
- /**
536
- * Check if gpg is available
537
- */
538
- function hasGpg() {
539
- try {
540
- execSync('gpg --version', { stdio: 'pipe' });
541
- return true;
542
- } catch {
543
- return false;
544
- }
545
- }
546
-
547
709
  /**
548
710
  * Wallet commands
549
711
  */
550
712
  async function handleWallet(subcommand, args, flags) {
713
+ const baseUrl = getBaseUrl();
714
+ const config = loadConfig();
715
+ const walletFile = getWalletFilePath(flags);
716
+
551
717
  switch (subcommand) {
552
- case 'backup-seed': {
553
- if (!hasGpg()) {
554
- print.error('gpg is required but not found. Install it with:');
555
- print.info(' Ubuntu/Debian: sudo apt install gnupg');
556
- print.info(' macOS: brew install gnupg');
557
- print.info(' Windows: https://www.gnupg.org/download/');
558
- process.exit(1);
559
- }
560
-
561
- const walletId = flags['wallet-id'];
562
- if (!walletId) {
563
- print.error('Required: --wallet-id <id>');
718
+ case 'create': {
719
+ const words = parseInt(flags.words || '12', 10);
720
+ const chainsStr = flags.chains || DEFAULT_CHAINS.join(',');
721
+ const chains = chainsStr.split(',').map(c => c.trim().toUpperCase());
722
+ const noSave = flags['no-save'] === true;
723
+
724
+ if (words !== 12 && words !== 24) {
725
+ print.error('Words must be 12 or 24');
564
726
  return;
565
727
  }
566
-
567
- // Get seed phrase from --seed flag or stdin
568
- let seed = flags.seed;
569
- if (!seed) {
570
- if (process.stdin.isTTY) {
571
- print.error('Required: --seed <phrase> (or pipe via stdin)');
572
- print.info('Example: coinpay wallet backup-seed --seed "word1 word2 ..." --wallet-id "wid-abc"');
728
+
729
+ // Check if wallet already exists
730
+ if (hasEncryptedWallet(flags)) {
731
+ const overwrite = await promptYesNo('Wallet already exists. Overwrite?', false);
732
+ if (!overwrite) {
733
+ print.info('Aborted');
573
734
  return;
574
735
  }
575
- // Read from stdin
576
- const chunks = [];
577
- for await (const chunk of process.stdin) {
578
- chunks.push(chunk);
736
+ }
737
+
738
+ print.info(`Creating wallet with ${words} words...`);
739
+
740
+ // Generate mnemonic locally
741
+ const mnemonic = generateMnemonic(words);
742
+
743
+ try {
744
+ // Register with server
745
+ const wallet = await WalletClient.create({
746
+ words,
747
+ chains,
748
+ baseUrl,
749
+ });
750
+
751
+ const walletId = wallet.getWalletId();
752
+
753
+ // Show mnemonic to user
754
+ console.log(`\n${colors.bright}${colors.yellow}⚠️ BACKUP YOUR SEED PHRASE:${colors.reset}`);
755
+ console.log(`${colors.yellow}${wallet.getMnemonic()}${colors.reset}\n`);
756
+ print.warn('Write this down and store it safely. It CANNOT be recovered!');
757
+
758
+ // Save encrypted locally (unless --no-save)
759
+ if (!noSave) {
760
+ let shouldSave = true;
761
+ if (process.stdin.isTTY) {
762
+ shouldSave = await promptYesNo('\nSave encrypted wallet locally?', true);
763
+ }
764
+
765
+ if (shouldSave) {
766
+ let password = flags.password;
767
+ if (!password) {
768
+ password = await promptPassword('Create wallet password: ');
769
+ const confirm = await promptPassword('Confirm password: ');
770
+ if (password !== confirm) {
771
+ print.error('Passwords do not match. Wallet not saved locally.');
772
+ print.warn('Your wallet is registered but NOT saved locally. Save your seed phrase!');
773
+ } else {
774
+ await saveEncryptedWallet(wallet.getMnemonic(), walletId, password, walletFile);
775
+ print.success(`Encrypted wallet saved to: ${walletFile}`);
776
+ }
777
+ } else {
778
+ await saveEncryptedWallet(wallet.getMnemonic(), walletId, password, walletFile);
779
+ print.success(`Encrypted wallet saved to: ${walletFile}`);
780
+ }
781
+ }
579
782
  }
580
- seed = Buffer.concat(chunks).toString().trim();
783
+
784
+ // Update config
785
+ config.walletId = walletId;
786
+ config.walletFile = walletFile;
787
+ saveConfig(config);
788
+
789
+ print.success(`Wallet created: ${walletId}`);
790
+ } catch (error) {
791
+ print.error(error.message);
581
792
  }
582
-
583
- if (!seed) {
584
- print.error('Seed phrase is empty');
793
+ break;
794
+ }
795
+
796
+ case 'import': {
797
+ const mnemonic = args.join(' ') || flags.mnemonic;
798
+ const noSave = flags['no-save'] === true;
799
+
800
+ if (!mnemonic) {
801
+ print.error('Mnemonic required');
802
+ print.info('Usage: coinpay wallet import "word1 word2 ... word12"');
585
803
  return;
586
804
  }
587
-
588
- // Get password from --password flag or prompt
589
- let password = flags.password;
590
- if (!password) {
591
- if (!process.stdin.isTTY) {
592
- print.error('Required: --password <pass> (cannot prompt in pipe mode)');
805
+
806
+ if (!validateMnemonic(mnemonic)) {
807
+ print.error('Invalid mnemonic phrase');
808
+ return;
809
+ }
810
+
811
+ // Check if wallet already exists
812
+ if (hasEncryptedWallet(flags)) {
813
+ const overwrite = await promptYesNo('Wallet already exists. Overwrite?', false);
814
+ if (!overwrite) {
815
+ print.info('Aborted');
593
816
  return;
594
817
  }
595
- password = await promptPassword('Encryption password: ');
596
- const confirm = await promptPassword('Confirm password: ');
597
- if (password !== confirm) {
598
- print.error('Passwords do not match');
599
- return;
818
+ }
819
+
820
+ const chainsStr = flags.chains || DEFAULT_CHAINS.join(',');
821
+ const chains = chainsStr.split(',').map(c => c.trim().toUpperCase());
822
+
823
+ print.info('Importing wallet...');
824
+
825
+ try {
826
+ const wallet = await WalletClient.fromSeed(mnemonic, {
827
+ chains,
828
+ baseUrl,
829
+ });
830
+
831
+ const walletId = wallet.getWalletId();
832
+
833
+ // Save encrypted locally (unless --no-save)
834
+ if (!noSave) {
835
+ let shouldSave = true;
836
+ if (process.stdin.isTTY) {
837
+ shouldSave = await promptYesNo('Save encrypted wallet locally?', true);
838
+ }
839
+
840
+ if (shouldSave) {
841
+ let password = flags.password;
842
+ if (!password) {
843
+ password = await promptPassword('Create wallet password: ');
844
+ const confirm = await promptPassword('Confirm password: ');
845
+ if (password !== confirm) {
846
+ print.error('Passwords do not match. Wallet not saved locally.');
847
+ } else {
848
+ await saveEncryptedWallet(mnemonic, walletId, password, walletFile);
849
+ print.success(`Encrypted wallet saved to: ${walletFile}`);
850
+ }
851
+ } else {
852
+ await saveEncryptedWallet(mnemonic, walletId, password, walletFile);
853
+ print.success(`Encrypted wallet saved to: ${walletFile}`);
854
+ }
855
+ }
600
856
  }
857
+
858
+ // Update config
859
+ config.walletId = walletId;
860
+ config.walletFile = walletFile;
861
+ saveConfig(config);
862
+
863
+ print.success(`Wallet imported: ${walletId}`);
864
+ } catch (error) {
865
+ print.error(error.message);
601
866
  }
602
-
603
- if (!password) {
604
- print.error('Password is empty');
867
+ break;
868
+ }
869
+
870
+ case 'unlock': {
871
+ if (!hasEncryptedWallet(flags)) {
872
+ print.error(`No wallet file found at: ${walletFile}`);
873
+ print.info('Create a wallet with: coinpay wallet create');
605
874
  return;
606
875
  }
607
-
608
- // Build the plaintext content
609
- const filename = `wallet_${walletId}_seedphrase.txt`;
610
- const content = [
611
- '# CoinPayPortal Wallet Seed Phrase Backup',
612
- `# Wallet ID: ${walletId}`,
613
- `# Created: ${new Date().toISOString()}`,
614
- '#',
615
- '# KEEP THIS FILE SAFE. Anyone with this phrase can access your funds.',
616
- `# Decrypt with: gpg --decrypt ${filename}.gpg`,
617
- '',
618
- seed,
619
- '',
620
- ].join('\n');
621
-
622
- // Determine output path
623
- const outputPath = flags.output || `${filename}.gpg`;
624
-
625
- // Write plaintext to temp file, encrypt with gpg, remove temp
626
- const tmpFile = join(tmpdir(), `coinpay-backup-${Date.now()}.txt`);
876
+
627
877
  try {
628
- writeFileSync(tmpFile, content, { mode: 0o600 });
629
-
630
- // Write passphrase to a temp file for gpg (avoids shell escaping issues)
631
- const passFile = join(tmpdir(), `coinpay-pass-${Date.now()}`);
632
- writeFileSync(passFile, password, { mode: 0o600 });
633
- try {
634
- execSync(
635
- `gpg --batch --yes --passphrase-file "${passFile}" --pinentry-mode loopback --symmetric --cipher-algo AES256 --output "${outputPath}" "${tmpFile}"`,
636
- { stdio: ['pipe', 'pipe', 'pipe'] }
637
- );
638
- } finally {
639
- try { writeFileSync(passFile, Buffer.alloc(password.length, 0)); } catch {}
640
- try { const { unlinkSync: u } = await import('fs'); u(passFile); } catch {}
878
+ const mnemonic = await getDecryptedMnemonic(flags);
879
+
880
+ print.success('Wallet unlocked');
881
+ print.info(`Wallet ID: ${config.walletId || 'unknown'}`);
882
+ print.info(`Wallet file: ${walletFile}`);
883
+
884
+ if (flags.show) {
885
+ console.log(`\n${colors.bright}Seed Phrase:${colors.reset}`);
886
+ console.log(`${colors.yellow}${mnemonic}${colors.reset}\n`);
887
+ print.warn('This is sensitive data — do not share it.');
641
888
  }
642
-
643
- print.success(`Encrypted backup saved to: ${outputPath}`);
644
- print.info(`Decrypt with: gpg --decrypt ${outputPath}`);
645
- } finally {
646
- // Securely delete temp file
647
- try {
648
- writeFileSync(tmpFile, Buffer.alloc(content.length, 0));
649
- const { unlinkSync } = await import('fs');
650
- unlinkSync(tmpFile);
651
- } catch {
652
- // Best effort cleanup
889
+
890
+ // Clear from memory
891
+ clearString(mnemonic);
892
+ } catch (error) {
893
+ print.error(error.message);
894
+ }
895
+ break;
896
+ }
897
+
898
+ case 'info': {
899
+ if (!config.walletId && !hasEncryptedWallet(flags)) {
900
+ print.error('No wallet configured. Run: coinpay wallet create');
901
+ return;
902
+ }
903
+
904
+ print.info(`Wallet ID: ${config.walletId || '(unknown)'}`);
905
+ print.info(`Wallet file: ${walletFile}`);
906
+ print.info(`File exists: ${existsSync(walletFile) ? 'yes' : 'no'}`);
907
+
908
+ if (flags.json) {
909
+ print.json({
910
+ walletId: config.walletId,
911
+ walletFile,
912
+ exists: existsSync(walletFile),
913
+ });
914
+ }
915
+ break;
916
+ }
917
+
918
+ case 'addresses': {
919
+ if (!config.walletId) {
920
+ print.error('No wallet configured. Run: coinpay wallet create');
921
+ return;
922
+ }
923
+
924
+ print.info(`Wallet: ${config.walletId}`);
925
+ print.info('Note: Full address list requires API authentication.');
926
+ break;
927
+ }
928
+
929
+ case 'derive': {
930
+ const chain = (args[0] || flags.chain || '').toUpperCase();
931
+ const index = parseInt(flags.index || '0', 10);
932
+
933
+ if (!chain) {
934
+ print.error('Chain required');
935
+ print.info('Usage: coinpay wallet derive ETH --index 0');
936
+ return;
937
+ }
938
+
939
+ if (!config.walletId) {
940
+ print.error('No wallet configured. Run: coinpay wallet create');
941
+ return;
942
+ }
943
+
944
+ // Need mnemonic for derivation
945
+ if (!hasEncryptedWallet(flags)) {
946
+ print.error('No encrypted wallet found. Cannot derive addresses.');
947
+ return;
948
+ }
949
+
950
+ try {
951
+ const mnemonic = await getDecryptedMnemonic(flags);
952
+
953
+ // Create wallet client with mnemonic
954
+ const wallet = await WalletClient.fromSeed(mnemonic, { baseUrl });
955
+ const result = await wallet.deriveAddress(chain, index);
956
+
957
+ print.success(`Derived ${chain} address at index ${index}`);
958
+ if (flags.json) {
959
+ print.json(result);
653
960
  }
961
+
962
+ clearString(mnemonic);
963
+ } catch (error) {
964
+ print.error(error.message);
654
965
  }
655
966
  break;
656
967
  }
657
-
658
- case 'decrypt-backup': {
659
- if (!hasGpg()) {
660
- print.error('gpg is required but not found.');
661
- process.exit(1);
968
+
969
+ case 'derive-missing': {
970
+ const chainsStr = flags.chains;
971
+
972
+ if (!config.walletId) {
973
+ print.error('No wallet configured. Run: coinpay wallet create');
974
+ return;
662
975
  }
663
-
664
- const filePath = args[0];
665
- if (!filePath) {
666
- print.error('Backup file path required');
667
- print.info('Example: coinpay wallet decrypt-backup wallet_wid-abc_seedphrase.txt.gpg');
976
+
977
+ if (!hasEncryptedWallet(flags)) {
978
+ print.error('No encrypted wallet found.');
668
979
  return;
669
980
  }
670
-
671
- if (!existsSync(filePath)) {
672
- print.error(`File not found: ${filePath}`);
981
+
982
+ try {
983
+ const mnemonic = await getDecryptedMnemonic(flags);
984
+ const wallet = await WalletClient.fromSeed(mnemonic, { baseUrl });
985
+
986
+ const chains = chainsStr ? chainsStr.split(',').map(c => c.trim().toUpperCase()) : undefined;
987
+ const results = await wallet.deriveMissingChains(chains);
988
+
989
+ print.success(`Derived ${results.length} new addresses`);
990
+ if (flags.json) {
991
+ print.json(results);
992
+ }
993
+
994
+ clearString(mnemonic);
995
+ } catch (error) {
996
+ print.error(error.message);
997
+ }
998
+ break;
999
+ }
1000
+
1001
+ case 'balance': {
1002
+ const chain = (args[0] || flags.chain || '').toUpperCase();
1003
+
1004
+ if (!config.walletId) {
1005
+ print.error('No wallet configured. Run: coinpay wallet create');
673
1006
  return;
674
1007
  }
675
-
676
- // Get password
677
- let password = flags.password;
678
- if (!password) {
679
- if (!process.stdin.isTTY) {
680
- print.error('Required: --password <pass> (cannot prompt in pipe mode)');
681
- return;
1008
+
1009
+ // For balance, we need the wallet client with mnemonic for authenticated requests
1010
+ if (!hasEncryptedWallet(flags)) {
1011
+ print.info(`Wallet: ${config.walletId}`);
1012
+ if (chain) {
1013
+ print.info(`Chain: ${chain}`);
682
1014
  }
683
- password = await promptPassword('Decryption password: ');
1015
+ print.info('Note: Balance check requires encrypted wallet file.');
1016
+ return;
684
1017
  }
685
-
1018
+
686
1019
  try {
687
- const passFile = join(tmpdir(), `coinpay-pass-${Date.now()}`);
688
- writeFileSync(passFile, password, { mode: 0o600 });
689
- let result;
690
- try {
691
- result = execSync(
692
- `gpg --batch --yes --passphrase-file "${passFile}" --pinentry-mode loopback --decrypt "${filePath}"`,
693
- { stdio: ['pipe', 'pipe', 'pipe'] }
694
- );
695
- } finally {
696
- try { writeFileSync(passFile, Buffer.alloc(password.length, 0)); } catch {}
697
- try { const { unlinkSync: u } = await import('fs'); u(passFile); } catch {}
1020
+ const mnemonic = await getDecryptedMnemonic(flags);
1021
+ const wallet = await WalletClient.fromSeed(mnemonic, { baseUrl });
1022
+
1023
+ const result = chain ? await wallet.getBalance(chain) : await wallet.getBalances();
1024
+
1025
+ if (flags.json) {
1026
+ print.json(result);
1027
+ } else {
1028
+ print.success('Balances:');
1029
+ for (const bal of result.balances || []) {
1030
+ console.log(` ${colors.bright}${bal.chain}${colors.reset}: ${bal.balance}`);
1031
+ }
698
1032
  }
699
-
700
- const output = result.toString();
701
- // Extract just the mnemonic (skip comments)
702
- const lines = output.split('\n');
703
- const mnemonic = lines
704
- .filter((l) => !l.startsWith('#') && l.trim().length > 0)
705
- .join(' ')
706
- .trim();
707
-
1033
+
1034
+ clearString(mnemonic);
1035
+ } catch (error) {
1036
+ print.error(error.message);
1037
+ }
1038
+ break;
1039
+ }
1040
+
1041
+ case 'send': {
1042
+ const chain = (flags.chain || '').toUpperCase();
1043
+ const to = flags.to;
1044
+ const amount = flags.amount;
1045
+ const priority = flags.priority || 'medium';
1046
+
1047
+ if (!chain || !to || !amount) {
1048
+ print.error('Required: --chain, --to, --amount');
1049
+ print.info('Usage: coinpay wallet send --chain ETH --to 0x123... --amount 0.1');
1050
+ return;
1051
+ }
1052
+
1053
+ if (!config.walletId) {
1054
+ print.error('No wallet configured. Run: coinpay wallet create');
1055
+ return;
1056
+ }
1057
+
1058
+ if (!hasEncryptedWallet(flags)) {
1059
+ print.error('No encrypted wallet found. Cannot sign transactions.');
1060
+ return;
1061
+ }
1062
+
1063
+ try {
1064
+ const mnemonic = await getDecryptedMnemonic(flags);
1065
+ const wallet = await WalletClient.fromSeed(mnemonic, { baseUrl });
1066
+
1067
+ print.info(`Sending ${amount} ${chain} to ${to}...`);
1068
+
1069
+ const result = await wallet.send({ chain, to, amount, priority });
1070
+
1071
+ print.success('Transaction sent!');
1072
+ if (result.tx_hash) {
1073
+ print.info(`TX Hash: ${result.tx_hash}`);
1074
+ }
1075
+
1076
+ if (flags.json) {
1077
+ print.json(result);
1078
+ }
1079
+
1080
+ clearString(mnemonic);
1081
+ } catch (error) {
1082
+ print.error(error.message);
1083
+ }
1084
+ break;
1085
+ }
1086
+
1087
+ case 'history': {
1088
+ const chain = (flags.chain || '').toUpperCase();
1089
+ const limit = parseInt(flags.limit || '20', 10);
1090
+
1091
+ if (!config.walletId) {
1092
+ print.error('No wallet configured. Run: coinpay wallet create');
1093
+ return;
1094
+ }
1095
+
1096
+ if (!hasEncryptedWallet(flags)) {
1097
+ print.info(`Wallet: ${config.walletId}`);
1098
+ print.info('Note: Transaction history requires encrypted wallet file.');
1099
+ return;
1100
+ }
1101
+
1102
+ try {
1103
+ const mnemonic = await getDecryptedMnemonic(flags);
1104
+ const wallet = await WalletClient.fromSeed(mnemonic, { baseUrl });
1105
+
1106
+ const result = await wallet.getHistory({ chain: chain || undefined, limit });
1107
+
708
1108
  if (flags.json) {
709
- print.json({ mnemonic, raw: output });
1109
+ print.json(result);
710
1110
  } else {
711
- print.success('Backup decrypted successfully');
712
- console.log(`\n${colors.bright}Seed Phrase:${colors.reset}`);
713
- console.log(`${colors.yellow}${mnemonic}${colors.reset}\n`);
714
- print.warn('This is sensitive data do not share it with anyone.');
1111
+ print.info(`Transactions (${result.total || 0} total):`);
1112
+ for (const tx of result.transactions || []) {
1113
+ const dir = tx.direction === 'incoming' ? colors.green + '←' : colors.red + '→';
1114
+ console.log(` ${dir}${colors.reset} ${tx.amount} ${tx.chain} | ${tx.status} | ${tx.created_at}`);
1115
+ }
715
1116
  }
716
- } catch (err) {
717
- print.error('Decryption failed — wrong password or corrupted file');
1117
+
1118
+ clearString(mnemonic);
1119
+ } catch (error) {
1120
+ print.error(error.message);
1121
+ }
1122
+ break;
1123
+ }
1124
+
1125
+ case 'backup': {
1126
+ if (!hasEncryptedWallet(flags)) {
1127
+ print.error(`No wallet file found at: ${walletFile}`);
1128
+ return;
1129
+ }
1130
+
1131
+ const outputPath = flags.output || `coinpay-wallet-backup-${Date.now()}.gpg`;
1132
+
1133
+ try {
1134
+ // Just copy the encrypted file
1135
+ const content = readFileSync(walletFile);
1136
+ writeFileSync(outputPath, content, { mode: 0o600 });
1137
+
1138
+ print.success(`Backup saved to: ${outputPath}`);
1139
+ print.info('This file is GPG encrypted. Keep your password safe!');
1140
+ } catch (error) {
1141
+ print.error(error.message);
1142
+ }
1143
+ break;
1144
+ }
1145
+
1146
+ case 'delete': {
1147
+ if (!hasEncryptedWallet(flags)) {
1148
+ print.info('No wallet file to delete');
1149
+ return;
1150
+ }
1151
+
1152
+ const confirm = await promptYesNo('Are you sure you want to delete the local wallet?', false);
1153
+ if (!confirm) {
1154
+ print.info('Aborted');
1155
+ return;
1156
+ }
1157
+
1158
+ try {
1159
+ unlinkSync(walletFile);
1160
+
1161
+ // Clear from config
1162
+ delete config.walletFile;
1163
+ saveConfig(config);
1164
+
1165
+ print.success('Wallet file deleted');
1166
+ print.warn('Your wallet is still registered on the server. Keep your seed phrase safe!');
1167
+ } catch (error) {
1168
+ print.error(error.message);
718
1169
  }
719
1170
  break;
720
1171
  }
721
1172
 
722
1173
  default:
723
1174
  print.error(`Unknown wallet command: ${subcommand}`);
724
- print.info('Available: backup-seed, decrypt-backup');
1175
+ print.info('Available: create, import, unlock, info, addresses, derive, derive-missing, balance, send, history, backup, delete');
1176
+ process.exit(1);
725
1177
  }
726
1178
  }
727
1179
 
728
1180
  /**
729
- * Main entry point
1181
+ * Swap commands
730
1182
  */
731
- async function main() {
732
- const { command, subcommand, args, flags } = parseArgs(process.argv.slice(2));
733
-
734
- // Handle global flags
735
- if (flags.version || flags.v) {
736
- console.log(VERSION);
737
- return;
738
- }
1183
+ async function handleSwap(subcommand, args, flags) {
1184
+ const baseUrl = getBaseUrl();
1185
+ const config = loadConfig();
1186
+ const swap = new SwapClient({ baseUrl, walletId: config.walletId });
739
1187
 
740
- if (flags.help || flags.h || !command) {
741
- showHelp();
742
- return;
1188
+ switch (subcommand) {
1189
+ case 'coins': {
1190
+ const result = await swap.getSwapCoins({ search: flags.search });
1191
+
1192
+ if (flags.json) {
1193
+ print.json(result);
1194
+ } else {
1195
+ print.info(`Supported coins (${result.count}):`);
1196
+ for (const coin of result.coins) {
1197
+ console.log(` ${colors.bright}${coin.symbol}${colors.reset} - ${coin.name} (${coin.network})`);
1198
+ }
1199
+ }
1200
+ break;
1201
+ }
1202
+
1203
+ case 'quote': {
1204
+ const from = (flags.from || '').toUpperCase();
1205
+ const to = (flags.to || '').toUpperCase();
1206
+ const amount = flags.amount;
1207
+
1208
+ if (!from || !to || !amount) {
1209
+ print.error('Required: --from, --to, --amount');
1210
+ print.info('Example: coinpay swap quote --from BTC --to ETH --amount 0.1');
1211
+ return;
1212
+ }
1213
+
1214
+ try {
1215
+ const result = await swap.getSwapQuote(from, to, amount);
1216
+
1217
+ if (flags.json) {
1218
+ print.json(result);
1219
+ } else {
1220
+ const q = result.quote;
1221
+ print.success('Swap Quote:');
1222
+ console.log(` ${colors.bright}${q.from}${colors.reset} → ${colors.bright}${q.to}${colors.reset}`);
1223
+ console.log(` You send: ${colors.yellow}${q.depositAmount} ${q.from}${colors.reset}`);
1224
+ console.log(` You receive: ${colors.green}~${q.settleAmount} ${q.to}${colors.reset}`);
1225
+ console.log(` Rate: 1 ${q.from} = ${q.rate} ${q.to}`);
1226
+ console.log(` Min amount: ${q.minAmount} ${q.from}`);
1227
+ }
1228
+ } catch (error) {
1229
+ print.error(error.message);
1230
+ }
1231
+ break;
1232
+ }
1233
+
1234
+ case 'create': {
1235
+ const from = (flags.from || '').toUpperCase();
1236
+ const to = (flags.to || '').toUpperCase();
1237
+ const amount = flags.amount;
1238
+ const settleAddress = flags.settle;
1239
+ const refundAddress = flags.refund;
1240
+
1241
+ if (!from || !to || !amount) {
1242
+ print.error('Required: --from, --to, --amount');
1243
+ print.info('Example: coinpay swap create --from BTC --to ETH --amount 0.1 --settle 0x...');
1244
+ return;
1245
+ }
1246
+
1247
+ if (!config.walletId) {
1248
+ print.error('No wallet configured. Run: coinpay wallet create');
1249
+ return;
1250
+ }
1251
+
1252
+ if (!settleAddress) {
1253
+ print.error('Required: --settle <address>');
1254
+ return;
1255
+ }
1256
+
1257
+ print.info(`Creating swap: ${amount} ${from} → ${to}`);
1258
+
1259
+ try {
1260
+ const result = await swap.createSwap({
1261
+ from,
1262
+ to,
1263
+ amount,
1264
+ settleAddress,
1265
+ refundAddress,
1266
+ });
1267
+
1268
+ if (flags.json) {
1269
+ print.json(result);
1270
+ } else {
1271
+ const s = result.swap;
1272
+ print.success('Swap created!');
1273
+ console.log(`\n ${colors.bright}Swap ID:${colors.reset} ${s.id}`);
1274
+ console.log(` ${colors.bright}Status:${colors.reset} ${s.status}`);
1275
+ console.log(`\n ${colors.yellow}⚠️ Send exactly ${s.depositAmount} ${from} to:${colors.reset}`);
1276
+ console.log(` ${colors.bright}${s.depositAddress}${colors.reset}\n`);
1277
+ }
1278
+ } catch (error) {
1279
+ print.error(error.message);
1280
+ }
1281
+ break;
1282
+ }
1283
+
1284
+ case 'status': {
1285
+ const swapId = args[0] || flags.id;
1286
+
1287
+ if (!swapId) {
1288
+ print.error('Swap ID required');
1289
+ print.info('Usage: coinpay swap status <swap-id>');
1290
+ return;
1291
+ }
1292
+
1293
+ try {
1294
+ const result = await swap.getSwapStatus(swapId);
1295
+
1296
+ if (flags.json) {
1297
+ print.json(result);
1298
+ } else {
1299
+ const s = result.swap;
1300
+ const statusColor = s.status === 'settled' ? colors.green :
1301
+ s.status === 'failed' ? colors.red : colors.yellow;
1302
+
1303
+ print.info(`Swap ${s.id}:`);
1304
+ console.log(` Status: ${statusColor}${s.status}${colors.reset}`);
1305
+ console.log(` ${s.depositCoin || s.from} → ${s.settleCoin || s.to}`);
1306
+ console.log(` Deposit: ${s.depositAmount}`);
1307
+ if (s.settleAmount) {
1308
+ console.log(` Settled: ${s.settleAmount}`);
1309
+ }
1310
+ }
1311
+ } catch (error) {
1312
+ print.error(error.message);
1313
+ }
1314
+ break;
1315
+ }
1316
+
1317
+ case 'history': {
1318
+ const limit = parseInt(flags.limit || '20', 10);
1319
+
1320
+ if (!config.walletId) {
1321
+ print.error('No wallet configured. Run: coinpay wallet create');
1322
+ return;
1323
+ }
1324
+
1325
+ try {
1326
+ const result = await swap.getSwapHistory(config.walletId, {
1327
+ status: flags.status,
1328
+ limit,
1329
+ });
1330
+
1331
+ if (flags.json) {
1332
+ print.json(result);
1333
+ } else {
1334
+ print.info(`Swap history (${result.pagination?.total || 0} total):`);
1335
+
1336
+ if (!result.swaps || result.swaps.length === 0) {
1337
+ console.log(' No swaps found.');
1338
+ } else {
1339
+ for (const s of result.swaps) {
1340
+ const statusColor = s.status === 'settled' ? colors.green :
1341
+ s.status === 'failed' ? colors.red : colors.yellow;
1342
+ console.log(` ${s.id} | ${s.from_coin} → ${s.to_coin} | ${statusColor}${s.status}${colors.reset}`);
1343
+ }
1344
+ }
1345
+ }
1346
+ } catch (error) {
1347
+ print.error(error.message);
1348
+ }
1349
+ break;
1350
+ }
1351
+
1352
+ default:
1353
+ print.error(`Unknown swap command: ${subcommand}`);
1354
+ print.info('Available: coins, quote, create, status, history');
1355
+ process.exit(1);
743
1356
  }
1357
+ }
744
1358
 
1359
+ /**
1360
+ * Escrow commands
1361
+ */
745
1362
  async function handleEscrow(subcommand, args, flags) {
746
- const client = getClient();
1363
+ const client = createClient();
747
1364
 
748
1365
  switch (subcommand) {
749
1366
  case 'create': {
@@ -758,8 +1375,6 @@ async function handleEscrow(subcommand, args, flags) {
758
1375
  }
759
1376
 
760
1377
  print.info(`Creating escrow: ${amount} ${chain}`);
761
- print.info(` Depositor: ${depositor}`);
762
- print.info(` Beneficiary: ${beneficiary}`);
763
1378
 
764
1379
  const escrow = await client.createEscrow({
765
1380
  chain,
@@ -773,12 +1388,10 @@ async function handleEscrow(subcommand, args, flags) {
773
1388
  print.success(`Escrow created: ${escrow.id}`);
774
1389
  print.info(` Deposit to: ${escrow.escrowAddress}`);
775
1390
  print.info(` Status: ${escrow.status}`);
776
- print.info(` Expires: ${escrow.expiresAt}`);
777
1391
  print.warn(` Release Token: ${escrow.releaseToken}`);
778
- print.warn(` Beneficiary Token: ${escrow.beneficiaryToken}`);
779
- print.warn(' ⚠️ Save these tokens! They cannot be recovered.');
1392
+ print.warn(' ⚠️ Save these tokens!');
780
1393
 
781
- if (flags.json) console.log(JSON.stringify(escrow, null, 2));
1394
+ if (flags.json) print.json(escrow);
782
1395
  break;
783
1396
  }
784
1397
 
@@ -791,57 +1404,49 @@ async function handleEscrow(subcommand, args, flags) {
791
1404
  print.info(` Status: ${escrow.status}`);
792
1405
  print.info(` Chain: ${escrow.chain}`);
793
1406
  print.info(` Amount: ${escrow.amount}`);
794
- print.info(` Escrow Address: ${escrow.escrowAddress}`);
795
- print.info(` Depositor: ${escrow.depositorAddress}`);
796
- print.info(` Beneficiary: ${escrow.beneficiaryAddress}`);
797
- if (escrow.depositedAmount) print.info(` Deposited: ${escrow.depositedAmount}`);
798
- if (escrow.depositTxHash) print.info(` Deposit TX: ${escrow.depositTxHash}`);
799
- if (escrow.settlementTxHash) print.info(` Settlement TX: ${escrow.settlementTxHash}`);
800
1407
 
801
- if (flags.json) console.log(JSON.stringify(escrow, null, 2));
1408
+ if (flags.json) print.json(escrow);
802
1409
  break;
803
1410
  }
804
1411
 
805
1412
  case 'list': {
806
1413
  const result = await client.listEscrows({
807
1414
  status: flags.status,
808
- depositor: flags.depositor,
809
- beneficiary: flags.beneficiary,
810
1415
  limit: flags.limit ? parseInt(flags.limit) : 20,
811
1416
  });
812
1417
 
813
1418
  print.info(`Escrows (${result.total} total):`);
814
1419
  for (const e of result.escrows) {
815
- console.log(` ${e.id} | ${e.status} | ${e.amount} ${e.chain} | ${e.createdAt}`);
1420
+ console.log(` ${e.id} | ${e.status} | ${e.amount} ${e.chain}`);
816
1421
  }
817
1422
 
818
- if (flags.json) console.log(JSON.stringify(result, null, 2));
1423
+ if (flags.json) print.json(result);
819
1424
  break;
820
1425
  }
821
1426
 
822
1427
  case 'release': {
823
1428
  const id = args[0];
824
- const token = flags.token || flags['release-token'];
825
- if (!id || !token) { print.error('Required: <id> --token <release_token>'); process.exit(1); }
1429
+ const token = flags.token;
1430
+ if (!id || !token) { print.error('Required: <id> --token <token>'); process.exit(1); }
826
1431
 
827
1432
  const escrow = await client.releaseEscrow(id, token);
828
- print.success(`Escrow ${id} released → ${escrow.beneficiaryAddress}`);
1433
+ print.success(`Escrow ${id} released`);
829
1434
  break;
830
1435
  }
831
1436
 
832
1437
  case 'refund': {
833
1438
  const id = args[0];
834
- const token = flags.token || flags['release-token'];
835
- if (!id || !token) { print.error('Required: <id> --token <release_token>'); process.exit(1); }
1439
+ const token = flags.token;
1440
+ if (!id || !token) { print.error('Required: <id> --token <token>'); process.exit(1); }
836
1441
 
837
1442
  const escrow = await client.refundEscrow(id, token);
838
- print.success(`Escrow ${id} refunded → ${escrow.depositorAddress}`);
1443
+ print.success(`Escrow ${id} refunded`);
839
1444
  break;
840
1445
  }
841
1446
 
842
1447
  case 'dispute': {
843
1448
  const id = args[0];
844
- const token = flags.token || flags['release-token'] || flags['beneficiary-token'];
1449
+ const token = flags.token;
845
1450
  const reason = flags.reason;
846
1451
  if (!id || !token || !reason) {
847
1452
  print.error('Required: <id> --token <token> --reason "description"');
@@ -850,7 +1455,6 @@ async function handleEscrow(subcommand, args, flags) {
850
1455
 
851
1456
  const escrow = await client.disputeEscrow(id, token, reason);
852
1457
  print.success(`Escrow ${id} disputed`);
853
- print.info(` Reason: ${escrow.disputeReason}`);
854
1458
  break;
855
1459
  }
856
1460
 
@@ -861,10 +1465,10 @@ async function handleEscrow(subcommand, args, flags) {
861
1465
  const events = await client.getEscrowEvents(id);
862
1466
  print.info(`Events for escrow ${id}:`);
863
1467
  for (const e of events) {
864
- console.log(` ${e.createdAt} | ${e.eventType} | ${e.actor || 'system'}`);
1468
+ console.log(` ${e.createdAt} | ${e.eventType}`);
865
1469
  }
866
1470
 
867
- if (flags.json) console.log(JSON.stringify(events, null, 2));
1471
+ if (flags.json) print.json(events);
868
1472
  break;
869
1473
  }
870
1474
 
@@ -874,6 +1478,22 @@ async function handleEscrow(subcommand, args, flags) {
874
1478
  process.exit(1);
875
1479
  }
876
1480
  }
1481
+
1482
+ /**
1483
+ * Main entry point
1484
+ */
1485
+ async function main() {
1486
+ const { command, subcommand, args, flags } = parseArgs(process.argv.slice(2));
1487
+
1488
+ if (flags.version || flags.v) {
1489
+ console.log(VERSION);
1490
+ return;
1491
+ }
1492
+
1493
+ if (flags.help || flags.h || !command) {
1494
+ showHelp();
1495
+ return;
1496
+ }
877
1497
 
878
1498
  try {
879
1499
  switch (command) {
@@ -893,6 +1513,14 @@ async function handleEscrow(subcommand, args, flags) {
893
1513
  await handleRates(subcommand, args, flags);
894
1514
  break;
895
1515
 
1516
+ case 'wallet':
1517
+ await handleWallet(subcommand, args, flags);
1518
+ break;
1519
+
1520
+ case 'swap':
1521
+ await handleSwap(subcommand, args, flags);
1522
+ break;
1523
+
896
1524
  case 'escrow':
897
1525
  await handleEscrow(subcommand, args, flags);
898
1526
  break;
@@ -900,10 +1528,6 @@ async function handleEscrow(subcommand, args, flags) {
900
1528
  case 'webhook':
901
1529
  await handleWebhook(subcommand, args, flags);
902
1530
  break;
903
-
904
- case 'wallet':
905
- await handleWallet(subcommand, args, flags);
906
- break;
907
1531
 
908
1532
  default:
909
1533
  print.error(`Unknown command: ${command}`);
@@ -919,4 +1543,4 @@ async function handleEscrow(subcommand, args, flags) {
919
1543
  }
920
1544
  }
921
1545
 
922
- main();
1546
+ main();