@profullstack/coinpay 0.3.9 → 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,67 +188,255 @@ ${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
210
+
211
+ ${colors.bright}escrow${colors.reset}
212
+ create Create a new escrow
213
+ get <id> Get escrow status
214
+ list List escrows
215
+ release <id> Release funds to beneficiary
216
+ refund <id> Refund funds to depositor
217
+ dispute <id> Open a dispute
218
+ events <id> Get escrow audit log
163
219
 
164
220
  ${colors.bright}webhook${colors.reset}
165
221
  logs <business-id> Get webhook logs
166
222
  test <business-id> Send test webhook
167
223
 
168
- ${colors.cyan}Options:${colors.reset}
169
- --help, -h Show help
170
- --version, -v Show version
171
- --json Output as JSON
172
- --business-id <id> Business ID for operations
173
- --amount <amount> Payment amount in fiat currency
174
- --currency <code> Fiat currency (USD, EUR, etc.) - default: USD
175
- --blockchain <code> Blockchain (BTC, ETH, SOL, POL, BCH, USDC_ETH, USDC_POL, USDC_SOL)
176
- --description <text> Payment description
177
- --seed <phrase> Seed phrase (or reads from stdin)
178
- --password <pass> GPG passphrase (or prompts interactively)
179
- --wallet-id <id> Wallet ID for backup filename
180
- --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)
181
239
 
182
240
  ${colors.cyan}Examples:${colors.reset}
183
- # Configure your API key (get it from your CoinPay dashboard)
184
- coinpay config set-key cp_live_xxxxx
241
+ # Create a new wallet (auto-saves encrypted)
242
+ coinpay wallet create --words 12
185
243
 
186
- # Create a $100 Bitcoin payment
187
- coinpay payment create --business-id biz_123 --amount 100 --blockchain BTC
244
+ # Import existing wallet
245
+ coinpay wallet import "word1 word2 ... word12"
188
246
 
189
- # Create a $50 Ethereum payment with description
190
- coinpay payment create --business-id biz_123 --amount 50 --blockchain ETH --description "Order #12345"
247
+ # Get wallet balance (auto-decrypts)
248
+ coinpay wallet balance
191
249
 
192
- # Create a USDC payment on Polygon
193
- 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
194
252
 
195
- # Get payment status
196
- coinpay payment get pay_abc123
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...
197
256
 
198
- # Get exchange rates
199
- coinpay rates get BTC
257
+ ${colors.cyan}Environment Variables:${colors.reset}
258
+ COINPAY_API_KEY API key (overrides config)
259
+ COINPAY_BASE_URL Custom API URL
260
+ `);
261
+ }
200
262
 
201
- # List your businesses
202
- coinpay business list
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
+ }
203
275
 
204
- # Encrypt seed phrase to GPG backup file
205
- coinpay wallet backup-seed --seed "word1 word2 ..." --password "mypass" --wallet-id "wid-abc"
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
+ }
206
307
 
207
- # Encrypt seed phrase (interactive password prompt)
208
- coinpay wallet backup-seed --seed "word1 word2 ..." --wallet-id "wid-abc"
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
+ }
209
318
 
210
- # Pipe seed phrase from stdin
211
- echo "word1 word2 ..." | coinpay wallet backup-seed --wallet-id "wid-abc" --password "mypass"
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
+ }
212
330
 
213
- # Decrypt a backup file
214
- coinpay wallet decrypt-backup wallet_wid-abc_seedphrase.txt.gpg --password "mypass"
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
+ }
215
365
 
216
- ${colors.cyan}Environment Variables:${colors.reset}
217
- COINPAY_API_KEY API key (overrides config)
218
- COINPAY_BASE_URL Custom API URL
219
- `);
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
+ }
220
393
  }
221
394
 
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
+ }
424
+
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;
434
+ }
435
+
436
+ // ═══════════════════════════════════════════════════════════════
437
+ // COMMAND HANDLERS
438
+ // ═══════════════════════════════════════════════════════════════
439
+
222
440
  /**
223
441
  * Config commands
224
442
  */
@@ -251,6 +469,8 @@ async function handleConfig(subcommand, args) {
251
469
  print.json({
252
470
  apiKey: config.apiKey ? `${config.apiKey.slice(0, 10)}...` : '(not set)',
253
471
  baseUrl: config.baseUrl || '(default)',
472
+ walletId: config.walletId || '(none)',
473
+ walletFile: config.walletFile || DEFAULT_WALLET_FILE,
254
474
  });
255
475
  break;
256
476
 
@@ -290,7 +510,7 @@ async function handlePayment(subcommand, args, flags) {
290
510
  print.info(`Amount: ${payment.payment.crypto_amount} ${payment.payment.blockchain}`);
291
511
  print.info(`Expires: ${payment.payment.expires_at}`);
292
512
  }
293
- if (!flags.json) {
513
+ if (flags.json) {
294
514
  print.json(payment);
295
515
  }
296
516
  break;
@@ -328,15 +548,14 @@ async function handlePayment(subcommand, args, flags) {
328
548
 
329
549
  case 'qr': {
330
550
  const paymentId = args[0];
331
- const { format } = flags;
332
551
 
333
552
  if (!paymentId) {
334
553
  print.error('Payment ID required');
335
554
  return;
336
555
  }
337
556
 
338
- const qr = await client.getPaymentQR(paymentId, format);
339
- print.json(qr);
557
+ const url = client.getPaymentQRUrl(paymentId);
558
+ print.info(`QR Code URL: ${url}`);
340
559
  break;
341
560
  }
342
561
 
@@ -488,231 +707,775 @@ async function handleWebhook(subcommand, args, flags) {
488
707
  }
489
708
 
490
709
  /**
491
- * Prompt for password interactively (hides input)
710
+ * Wallet commands
492
711
  */
493
- function promptPassword(prompt = 'Password: ') {
494
- return new Promise((resolve) => {
495
- process.stdout.write(prompt);
712
+ async function handleWallet(subcommand, args, flags) {
713
+ const baseUrl = getBaseUrl();
714
+ const config = loadConfig();
715
+ const walletFile = getWalletFilePath(flags);
716
+
717
+ switch (subcommand) {
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');
726
+ return;
727
+ }
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');
734
+ return;
735
+ }
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
+ }
782
+ }
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);
792
+ }
793
+ break;
794
+ }
496
795
 
497
- // If stdin is a TTY, read with hidden input
498
- if (process.stdin.isTTY) {
499
- const rl = createInterface({ input: process.stdin, output: process.stdout });
500
- // Temporarily override output to hide password chars
501
- const origWrite = process.stdout.write.bind(process.stdout);
502
- process.stdout.write = (chunk) => {
503
- // Only suppress characters that are the user's input
504
- if (typeof chunk === 'string' && chunk !== prompt && chunk !== '\n' && chunk !== '\r\n') {
505
- return true;
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"');
803
+ return;
804
+ }
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');
816
+ return;
506
817
  }
507
- return origWrite(chunk);
508
- };
818
+ }
509
819
 
510
- rl.question('', (answer) => {
511
- process.stdout.write = origWrite;
512
- process.stdout.write('\n');
513
- rl.close();
514
- resolve(answer);
515
- });
516
- } else {
517
- // Pipe mode — read from stdin
518
- const chunks = [];
519
- process.stdin.on('data', (chunk) => chunks.push(chunk));
520
- process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString().trim()));
521
- process.stdin.resume();
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
+ }
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);
866
+ }
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');
874
+ return;
875
+ }
876
+
877
+ try {
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.');
888
+ }
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);
960
+ }
961
+
962
+ clearString(mnemonic);
963
+ } catch (error) {
964
+ print.error(error.message);
965
+ }
966
+ break;
967
+ }
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;
975
+ }
976
+
977
+ if (!hasEncryptedWallet(flags)) {
978
+ print.error('No encrypted wallet found.');
979
+ return;
980
+ }
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');
1006
+ return;
1007
+ }
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}`);
1014
+ }
1015
+ print.info('Note: Balance check requires encrypted wallet file.');
1016
+ return;
1017
+ }
1018
+
1019
+ try {
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
+ }
1032
+ }
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
+
1108
+ if (flags.json) {
1109
+ print.json(result);
1110
+ } else {
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
+ }
1116
+ }
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);
1169
+ }
1170
+ break;
522
1171
  }
523
- });
524
- }
525
1172
 
526
- /**
527
- * Check if gpg is available
528
- */
529
- function hasGpg() {
530
- try {
531
- execSync('gpg --version', { stdio: 'pipe' });
532
- return true;
533
- } catch {
534
- return false;
1173
+ default:
1174
+ print.error(`Unknown wallet command: ${subcommand}`);
1175
+ print.info('Available: create, import, unlock, info, addresses, derive, derive-missing, balance, send, history, backup, delete');
1176
+ process.exit(1);
535
1177
  }
536
1178
  }
537
1179
 
538
1180
  /**
539
- * Wallet commands
1181
+ * Swap commands
540
1182
  */
541
- async function handleWallet(subcommand, args, flags) {
1183
+ async function handleSwap(subcommand, args, flags) {
1184
+ const baseUrl = getBaseUrl();
1185
+ const config = loadConfig();
1186
+ const swap = new SwapClient({ baseUrl, walletId: config.walletId });
1187
+
542
1188
  switch (subcommand) {
543
- case 'backup-seed': {
544
- if (!hasGpg()) {
545
- print.error('gpg is required but not found. Install it with:');
546
- print.info(' Ubuntu/Debian: sudo apt install gnupg');
547
- print.info(' macOS: brew install gnupg');
548
- print.info(' Windows: https://www.gnupg.org/download/');
549
- process.exit(1);
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
+ }
550
1199
  }
551
-
552
- const walletId = flags['wallet-id'];
553
- if (!walletId) {
554
- print.error('Required: --wallet-id <id>');
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');
555
1211
  return;
556
1212
  }
557
-
558
- // Get seed phrase from --seed flag or stdin
559
- let seed = flags.seed;
560
- if (!seed) {
561
- if (process.stdin.isTTY) {
562
- print.error('Required: --seed <phrase> (or pipe via stdin)');
563
- print.info('Example: coinpay wallet backup-seed --seed "word1 word2 ..." --wallet-id "wid-abc"');
564
- return;
565
- }
566
- // Read from stdin
567
- const chunks = [];
568
- for await (const chunk of process.stdin) {
569
- chunks.push(chunk);
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}`);
570
1227
  }
571
- seed = Buffer.concat(chunks).toString().trim();
1228
+ } catch (error) {
1229
+ print.error(error.message);
572
1230
  }
573
-
574
- if (!seed) {
575
- print.error('Seed phrase is empty');
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...');
576
1244
  return;
577
1245
  }
578
-
579
- // Get password from --password flag or prompt
580
- let password = flags.password;
581
- if (!password) {
582
- if (!process.stdin.isTTY) {
583
- print.error('Required: --password <pass> (cannot prompt in pipe mode)');
584
- return;
585
- }
586
- password = await promptPassword('Encryption password: ');
587
- const confirm = await promptPassword('Confirm password: ');
588
- if (password !== confirm) {
589
- print.error('Passwords do not match');
590
- return;
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`);
591
1277
  }
1278
+ } catch (error) {
1279
+ print.error(error.message);
592
1280
  }
593
-
594
- if (!password) {
595
- print.error('Password is empty');
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>');
596
1290
  return;
597
1291
  }
598
-
599
- // Build the plaintext content
600
- const filename = `wallet_${walletId}_seedphrase.txt`;
601
- const content = [
602
- '# CoinPayPortal Wallet Seed Phrase Backup',
603
- `# Wallet ID: ${walletId}`,
604
- `# Created: ${new Date().toISOString()}`,
605
- '#',
606
- '# KEEP THIS FILE SAFE. Anyone with this phrase can access your funds.',
607
- `# Decrypt with: gpg --decrypt ${filename}.gpg`,
608
- '',
609
- seed,
610
- '',
611
- ].join('\n');
612
-
613
- // Determine output path
614
- const outputPath = flags.output || `${filename}.gpg`;
615
-
616
- // Write plaintext to temp file, encrypt with gpg, remove temp
617
- const tmpFile = join(tmpdir(), `coinpay-backup-${Date.now()}.txt`);
1292
+
618
1293
  try {
619
- writeFileSync(tmpFile, content, { mode: 0o600 });
620
-
621
- // Write passphrase to a temp file for gpg (avoids shell escaping issues)
622
- const passFile = join(tmpdir(), `coinpay-pass-${Date.now()}`);
623
- writeFileSync(passFile, password, { mode: 0o600 });
624
- try {
625
- execSync(
626
- `gpg --batch --yes --passphrase-file "${passFile}" --pinentry-mode loopback --symmetric --cipher-algo AES256 --output "${outputPath}" "${tmpFile}"`,
627
- { stdio: ['pipe', 'pipe', 'pipe'] }
628
- );
629
- } finally {
630
- try { writeFileSync(passFile, Buffer.alloc(password.length, 0)); } catch {}
631
- try { const { unlinkSync: u } = await import('fs'); u(passFile); } catch {}
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
+ }
632
1310
  }
633
-
634
- print.success(`Encrypted backup saved to: ${outputPath}`);
635
- print.info(`Decrypt with: gpg --decrypt ${outputPath}`);
636
- } finally {
637
- // Securely delete temp file
638
- try {
639
- writeFileSync(tmpFile, Buffer.alloc(content.length, 0));
640
- const { unlinkSync } = await import('fs');
641
- unlinkSync(tmpFile);
642
- } catch {
643
- // Best effort cleanup
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
+ }
644
1345
  }
1346
+ } catch (error) {
1347
+ print.error(error.message);
645
1348
  }
646
1349
  break;
647
1350
  }
1351
+
1352
+ default:
1353
+ print.error(`Unknown swap command: ${subcommand}`);
1354
+ print.info('Available: coins, quote, create, status, history');
1355
+ process.exit(1);
1356
+ }
1357
+ }
1358
+
1359
+ /**
1360
+ * Escrow commands
1361
+ */
1362
+ async function handleEscrow(subcommand, args, flags) {
1363
+ const client = createClient();
1364
+
1365
+ switch (subcommand) {
1366
+ case 'create': {
1367
+ const chain = flags.chain || flags.blockchain;
1368
+ const amount = parseFloat(flags.amount);
1369
+ const depositor = flags.depositor || flags['depositor-address'];
1370
+ const beneficiary = flags.beneficiary || flags['beneficiary-address'];
648
1371
 
649
- case 'decrypt-backup': {
650
- if (!hasGpg()) {
651
- print.error('gpg is required but not found.');
1372
+ if (!chain || !amount || !depositor || !beneficiary) {
1373
+ print.error('Required: --chain, --amount, --depositor, --beneficiary');
652
1374
  process.exit(1);
653
1375
  }
654
1376
 
655
- const filePath = args[0];
656
- if (!filePath) {
657
- print.error('Backup file path required');
658
- print.info('Example: coinpay wallet decrypt-backup wallet_wid-abc_seedphrase.txt.gpg');
659
- return;
660
- }
1377
+ print.info(`Creating escrow: ${amount} ${chain}`);
661
1378
 
662
- if (!existsSync(filePath)) {
663
- print.error(`File not found: ${filePath}`);
664
- return;
1379
+ const escrow = await client.createEscrow({
1380
+ chain,
1381
+ amount,
1382
+ depositorAddress: depositor,
1383
+ beneficiaryAddress: beneficiary,
1384
+ metadata: flags.metadata ? JSON.parse(flags.metadata) : undefined,
1385
+ expiresInHours: flags['expires-in'] ? parseFloat(flags['expires-in']) : undefined,
1386
+ });
1387
+
1388
+ print.success(`Escrow created: ${escrow.id}`);
1389
+ print.info(` Deposit to: ${escrow.escrowAddress}`);
1390
+ print.info(` Status: ${escrow.status}`);
1391
+ print.warn(` Release Token: ${escrow.releaseToken}`);
1392
+ print.warn(' ⚠️ Save these tokens!');
1393
+
1394
+ if (flags.json) print.json(escrow);
1395
+ break;
1396
+ }
1397
+
1398
+ case 'get': {
1399
+ const id = args[0];
1400
+ if (!id) { print.error('Escrow ID required'); process.exit(1); }
1401
+
1402
+ const escrow = await client.getEscrow(id);
1403
+ print.success(`Escrow ${escrow.id}`);
1404
+ print.info(` Status: ${escrow.status}`);
1405
+ print.info(` Chain: ${escrow.chain}`);
1406
+ print.info(` Amount: ${escrow.amount}`);
1407
+
1408
+ if (flags.json) print.json(escrow);
1409
+ break;
1410
+ }
1411
+
1412
+ case 'list': {
1413
+ const result = await client.listEscrows({
1414
+ status: flags.status,
1415
+ limit: flags.limit ? parseInt(flags.limit) : 20,
1416
+ });
1417
+
1418
+ print.info(`Escrows (${result.total} total):`);
1419
+ for (const e of result.escrows) {
1420
+ console.log(` ${e.id} | ${e.status} | ${e.amount} ${e.chain}`);
665
1421
  }
666
1422
 
667
- // Get password
668
- let password = flags.password;
669
- if (!password) {
670
- if (!process.stdin.isTTY) {
671
- print.error('Required: --password <pass> (cannot prompt in pipe mode)');
672
- return;
673
- }
674
- password = await promptPassword('Decryption password: ');
1423
+ if (flags.json) print.json(result);
1424
+ break;
1425
+ }
1426
+
1427
+ case 'release': {
1428
+ const id = args[0];
1429
+ const token = flags.token;
1430
+ if (!id || !token) { print.error('Required: <id> --token <token>'); process.exit(1); }
1431
+
1432
+ const escrow = await client.releaseEscrow(id, token);
1433
+ print.success(`Escrow ${id} released`);
1434
+ break;
1435
+ }
1436
+
1437
+ case 'refund': {
1438
+ const id = args[0];
1439
+ const token = flags.token;
1440
+ if (!id || !token) { print.error('Required: <id> --token <token>'); process.exit(1); }
1441
+
1442
+ const escrow = await client.refundEscrow(id, token);
1443
+ print.success(`Escrow ${id} refunded`);
1444
+ break;
1445
+ }
1446
+
1447
+ case 'dispute': {
1448
+ const id = args[0];
1449
+ const token = flags.token;
1450
+ const reason = flags.reason;
1451
+ if (!id || !token || !reason) {
1452
+ print.error('Required: <id> --token <token> --reason "description"');
1453
+ process.exit(1);
675
1454
  }
676
1455
 
677
- try {
678
- const passFile = join(tmpdir(), `coinpay-pass-${Date.now()}`);
679
- writeFileSync(passFile, password, { mode: 0o600 });
680
- let result;
681
- try {
682
- result = execSync(
683
- `gpg --batch --yes --passphrase-file "${passFile}" --pinentry-mode loopback --decrypt "${filePath}"`,
684
- { stdio: ['pipe', 'pipe', 'pipe'] }
685
- );
686
- } finally {
687
- try { writeFileSync(passFile, Buffer.alloc(password.length, 0)); } catch {}
688
- try { const { unlinkSync: u } = await import('fs'); u(passFile); } catch {}
689
- }
1456
+ const escrow = await client.disputeEscrow(id, token, reason);
1457
+ print.success(`Escrow ${id} disputed`);
1458
+ break;
1459
+ }
690
1460
 
691
- const output = result.toString();
692
- // Extract just the mnemonic (skip comments)
693
- const lines = output.split('\n');
694
- const mnemonic = lines
695
- .filter((l) => !l.startsWith('#') && l.trim().length > 0)
696
- .join(' ')
697
- .trim();
1461
+ case 'events': {
1462
+ const id = args[0];
1463
+ if (!id) { print.error('Escrow ID required'); process.exit(1); }
698
1464
 
699
- if (flags.json) {
700
- print.json({ mnemonic, raw: output });
701
- } else {
702
- print.success('Backup decrypted successfully');
703
- console.log(`\n${colors.bright}Seed Phrase:${colors.reset}`);
704
- console.log(`${colors.yellow}${mnemonic}${colors.reset}\n`);
705
- print.warn('This is sensitive data — do not share it with anyone.');
706
- }
707
- } catch (err) {
708
- print.error('Decryption failed — wrong password or corrupted file');
1465
+ const events = await client.getEscrowEvents(id);
1466
+ print.info(`Events for escrow ${id}:`);
1467
+ for (const e of events) {
1468
+ console.log(` ${e.createdAt} | ${e.eventType}`);
709
1469
  }
1470
+
1471
+ if (flags.json) print.json(events);
710
1472
  break;
711
1473
  }
712
1474
 
713
1475
  default:
714
- print.error(`Unknown wallet command: ${subcommand}`);
715
- print.info('Available: backup-seed, decrypt-backup');
1476
+ print.error(`Unknown escrow command: ${subcommand}`);
1477
+ print.info('Available: create, get, list, release, refund, dispute, events');
1478
+ process.exit(1);
716
1479
  }
717
1480
  }
718
1481
 
@@ -722,7 +1485,6 @@ async function handleWallet(subcommand, args, flags) {
722
1485
  async function main() {
723
1486
  const { command, subcommand, args, flags } = parseArgs(process.argv.slice(2));
724
1487
 
725
- // Handle global flags
726
1488
  if (flags.version || flags.v) {
727
1489
  console.log(VERSION);
728
1490
  return;
@@ -751,14 +1513,22 @@ async function main() {
751
1513
  await handleRates(subcommand, args, flags);
752
1514
  break;
753
1515
 
754
- case 'webhook':
755
- await handleWebhook(subcommand, args, flags);
756
- break;
757
-
758
1516
  case 'wallet':
759
1517
  await handleWallet(subcommand, args, flags);
760
1518
  break;
761
1519
 
1520
+ case 'swap':
1521
+ await handleSwap(subcommand, args, flags);
1522
+ break;
1523
+
1524
+ case 'escrow':
1525
+ await handleEscrow(subcommand, args, flags);
1526
+ break;
1527
+
1528
+ case 'webhook':
1529
+ await handleWebhook(subcommand, args, flags);
1530
+ break;
1531
+
762
1532
  default:
763
1533
  print.error(`Unknown command: ${command}`);
764
1534
  showHelp();
@@ -773,4 +1543,4 @@ async function main() {
773
1543
  }
774
1544
  }
775
1545
 
776
- main();
1546
+ main();