@profullstack/coinpay 0.3.7 → 0.3.10

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
@@ -8,8 +8,11 @@
8
8
  import { CoinPayClient } from '../src/client.js';
9
9
  import { PaymentStatus, Blockchain, FiatCurrency } from '../src/payments.js';
10
10
  import { readFileSync, writeFileSync, existsSync } from 'fs';
11
+ import { execSync } from 'child_process';
12
+ import { createInterface } from 'readline';
11
13
  import { homedir } from 'os';
12
14
  import { join } from 'path';
15
+ import { tmpdir } from 'os';
13
16
 
14
17
  const VERSION = '0.3.3';
15
18
  const CONFIG_FILE = join(homedir(), '.coinpay.json');
@@ -95,8 +98,19 @@ function parseArgs(args) {
95
98
  const arg = args[i];
96
99
 
97
100
  if (arg.startsWith('--')) {
98
- const [key, value] = arg.slice(2).split('=');
99
- result.flags[key] = value ?? true;
101
+ const eqIdx = arg.indexOf('=');
102
+ if (eqIdx !== -1) {
103
+ result.flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
104
+ } else {
105
+ // Peek ahead: if next arg doesn't start with '-', it's the value
106
+ const next = args[i + 1];
107
+ if (next && !next.startsWith('-')) {
108
+ result.flags[arg.slice(2)] = next;
109
+ i++;
110
+ } else {
111
+ result.flags[arg.slice(2)] = true;
112
+ }
113
+ }
100
114
  } else if (arg.startsWith('-')) {
101
115
  result.flags[arg.slice(1)] = args[++i] ?? true;
102
116
  } else if (!result.command) {
@@ -143,6 +157,19 @@ ${colors.cyan}Commands:${colors.reset}
143
157
  get <crypto> Get exchange rate
144
158
  list Get all exchange rates
145
159
 
160
+ ${colors.bright}wallet${colors.reset}
161
+ backup-seed Encrypt seed phrase to GPG file
162
+ decrypt-backup <file> Decrypt a GPG backup file
163
+
164
+ ${colors.bright}escrow${colors.reset}
165
+ create Create a new escrow
166
+ get <id> Get escrow status
167
+ list List escrows
168
+ release <id> Release funds to beneficiary
169
+ refund <id> Refund funds to depositor
170
+ dispute <id> Open a dispute
171
+ events <id> Get escrow audit log
172
+
146
173
  ${colors.bright}webhook${colors.reset}
147
174
  logs <business-id> Get webhook logs
148
175
  test <business-id> Send test webhook
@@ -156,6 +183,10 @@ ${colors.cyan}Options:${colors.reset}
156
183
  --currency <code> Fiat currency (USD, EUR, etc.) - default: USD
157
184
  --blockchain <code> Blockchain (BTC, ETH, SOL, POL, BCH, USDC_ETH, USDC_POL, USDC_SOL)
158
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)
159
190
 
160
191
  ${colors.cyan}Examples:${colors.reset}
161
192
  # Configure your API key (get it from your CoinPay dashboard)
@@ -179,6 +210,18 @@ ${colors.cyan}Examples:${colors.reset}
179
210
  # List your businesses
180
211
  coinpay business list
181
212
 
213
+ # Encrypt seed phrase to GPG backup file
214
+ coinpay wallet backup-seed --seed "word1 word2 ..." --password "mypass" --wallet-id "wid-abc"
215
+
216
+ # Encrypt seed phrase (interactive password prompt)
217
+ coinpay wallet backup-seed --seed "word1 word2 ..." --wallet-id "wid-abc"
218
+
219
+ # Pipe seed phrase from stdin
220
+ echo "word1 word2 ..." | coinpay wallet backup-seed --wallet-id "wid-abc" --password "mypass"
221
+
222
+ # Decrypt a backup file
223
+ coinpay wallet decrypt-backup wallet_wid-abc_seedphrase.txt.gpg --password "mypass"
224
+
182
225
  ${colors.cyan}Environment Variables:${colors.reset}
183
226
  COINPAY_API_KEY API key (overrides config)
184
227
  COINPAY_BASE_URL Custom API URL
@@ -453,6 +496,235 @@ async function handleWebhook(subcommand, args, flags) {
453
496
  }
454
497
  }
455
498
 
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
+ /**
548
+ * Wallet commands
549
+ */
550
+ async function handleWallet(subcommand, args, flags) {
551
+ 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>');
564
+ return;
565
+ }
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"');
573
+ return;
574
+ }
575
+ // Read from stdin
576
+ const chunks = [];
577
+ for await (const chunk of process.stdin) {
578
+ chunks.push(chunk);
579
+ }
580
+ seed = Buffer.concat(chunks).toString().trim();
581
+ }
582
+
583
+ if (!seed) {
584
+ print.error('Seed phrase is empty');
585
+ return;
586
+ }
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)');
593
+ return;
594
+ }
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;
600
+ }
601
+ }
602
+
603
+ if (!password) {
604
+ print.error('Password is empty');
605
+ return;
606
+ }
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`);
627
+ 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 {}
641
+ }
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
653
+ }
654
+ }
655
+ break;
656
+ }
657
+
658
+ case 'decrypt-backup': {
659
+ if (!hasGpg()) {
660
+ print.error('gpg is required but not found.');
661
+ process.exit(1);
662
+ }
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');
668
+ return;
669
+ }
670
+
671
+ if (!existsSync(filePath)) {
672
+ print.error(`File not found: ${filePath}`);
673
+ return;
674
+ }
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;
682
+ }
683
+ password = await promptPassword('Decryption password: ');
684
+ }
685
+
686
+ 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 {}
698
+ }
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
+
708
+ if (flags.json) {
709
+ print.json({ mnemonic, raw: output });
710
+ } 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.');
715
+ }
716
+ } catch (err) {
717
+ print.error('Decryption failed — wrong password or corrupted file');
718
+ }
719
+ break;
720
+ }
721
+
722
+ default:
723
+ print.error(`Unknown wallet command: ${subcommand}`);
724
+ print.info('Available: backup-seed, decrypt-backup');
725
+ }
726
+ }
727
+
456
728
  /**
457
729
  * Main entry point
458
730
  */
@@ -469,6 +741,139 @@ async function main() {
469
741
  showHelp();
470
742
  return;
471
743
  }
744
+
745
+ async function handleEscrow(subcommand, args, flags) {
746
+ const client = getClient();
747
+
748
+ switch (subcommand) {
749
+ case 'create': {
750
+ const chain = flags.chain || flags.blockchain;
751
+ const amount = parseFloat(flags.amount);
752
+ const depositor = flags.depositor || flags['depositor-address'];
753
+ const beneficiary = flags.beneficiary || flags['beneficiary-address'];
754
+
755
+ if (!chain || !amount || !depositor || !beneficiary) {
756
+ print.error('Required: --chain, --amount, --depositor, --beneficiary');
757
+ process.exit(1);
758
+ }
759
+
760
+ print.info(`Creating escrow: ${amount} ${chain}`);
761
+ print.info(` Depositor: ${depositor}`);
762
+ print.info(` Beneficiary: ${beneficiary}`);
763
+
764
+ const escrow = await client.createEscrow({
765
+ chain,
766
+ amount,
767
+ depositorAddress: depositor,
768
+ beneficiaryAddress: beneficiary,
769
+ metadata: flags.metadata ? JSON.parse(flags.metadata) : undefined,
770
+ expiresInHours: flags['expires-in'] ? parseFloat(flags['expires-in']) : undefined,
771
+ });
772
+
773
+ print.success(`Escrow created: ${escrow.id}`);
774
+ print.info(` Deposit to: ${escrow.escrowAddress}`);
775
+ print.info(` Status: ${escrow.status}`);
776
+ print.info(` Expires: ${escrow.expiresAt}`);
777
+ print.warn(` Release Token: ${escrow.releaseToken}`);
778
+ print.warn(` Beneficiary Token: ${escrow.beneficiaryToken}`);
779
+ print.warn(' ⚠️ Save these tokens! They cannot be recovered.');
780
+
781
+ if (flags.json) console.log(JSON.stringify(escrow, null, 2));
782
+ break;
783
+ }
784
+
785
+ case 'get': {
786
+ const id = args[0];
787
+ if (!id) { print.error('Escrow ID required'); process.exit(1); }
788
+
789
+ const escrow = await client.getEscrow(id);
790
+ print.success(`Escrow ${escrow.id}`);
791
+ print.info(` Status: ${escrow.status}`);
792
+ print.info(` Chain: ${escrow.chain}`);
793
+ 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
+
801
+ if (flags.json) console.log(JSON.stringify(escrow, null, 2));
802
+ break;
803
+ }
804
+
805
+ case 'list': {
806
+ const result = await client.listEscrows({
807
+ status: flags.status,
808
+ depositor: flags.depositor,
809
+ beneficiary: flags.beneficiary,
810
+ limit: flags.limit ? parseInt(flags.limit) : 20,
811
+ });
812
+
813
+ print.info(`Escrows (${result.total} total):`);
814
+ for (const e of result.escrows) {
815
+ console.log(` ${e.id} | ${e.status} | ${e.amount} ${e.chain} | ${e.createdAt}`);
816
+ }
817
+
818
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
819
+ break;
820
+ }
821
+
822
+ case 'release': {
823
+ 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); }
826
+
827
+ const escrow = await client.releaseEscrow(id, token);
828
+ print.success(`Escrow ${id} released → ${escrow.beneficiaryAddress}`);
829
+ break;
830
+ }
831
+
832
+ case 'refund': {
833
+ 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); }
836
+
837
+ const escrow = await client.refundEscrow(id, token);
838
+ print.success(`Escrow ${id} refunded → ${escrow.depositorAddress}`);
839
+ break;
840
+ }
841
+
842
+ case 'dispute': {
843
+ const id = args[0];
844
+ const token = flags.token || flags['release-token'] || flags['beneficiary-token'];
845
+ const reason = flags.reason;
846
+ if (!id || !token || !reason) {
847
+ print.error('Required: <id> --token <token> --reason "description"');
848
+ process.exit(1);
849
+ }
850
+
851
+ const escrow = await client.disputeEscrow(id, token, reason);
852
+ print.success(`Escrow ${id} disputed`);
853
+ print.info(` Reason: ${escrow.disputeReason}`);
854
+ break;
855
+ }
856
+
857
+ case 'events': {
858
+ const id = args[0];
859
+ if (!id) { print.error('Escrow ID required'); process.exit(1); }
860
+
861
+ const events = await client.getEscrowEvents(id);
862
+ print.info(`Events for escrow ${id}:`);
863
+ for (const e of events) {
864
+ console.log(` ${e.createdAt} | ${e.eventType} | ${e.actor || 'system'}`);
865
+ }
866
+
867
+ if (flags.json) console.log(JSON.stringify(events, null, 2));
868
+ break;
869
+ }
870
+
871
+ default:
872
+ print.error(`Unknown escrow command: ${subcommand}`);
873
+ print.info('Available: create, get, list, release, refund, dispute, events');
874
+ process.exit(1);
875
+ }
876
+ }
472
877
 
473
878
  try {
474
879
  switch (command) {
@@ -488,9 +893,17 @@ async function main() {
488
893
  await handleRates(subcommand, args, flags);
489
894
  break;
490
895
 
896
+ case 'escrow':
897
+ await handleEscrow(subcommand, args, flags);
898
+ break;
899
+
491
900
  case 'webhook':
492
901
  await handleWebhook(subcommand, args, flags);
493
902
  break;
903
+
904
+ case 'wallet':
905
+ await handleWallet(subcommand, args, flags);
906
+ break;
494
907
 
495
908
  default:
496
909
  print.error(`Unknown command: ${command}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/coinpay",
3
- "version": "0.3.7",
3
+ "version": "0.3.10",
4
4
  "description": "CoinPay SDK & CLI — Accept cryptocurrency payments (BTC, ETH, SOL, POL, BCH, USDC) in your Node.js application",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/client.js CHANGED
@@ -396,6 +396,93 @@ export class CoinPayClient {
396
396
  }),
397
397
  });
398
398
  }
399
+
400
+ // ── Escrow Methods ──────────────────────────────────────
401
+
402
+ /**
403
+ * Create a new escrow
404
+ * @param {Object} params - See escrow.js createEscrow for full params
405
+ * @returns {Promise<Object>} Created escrow with releaseToken
406
+ */
407
+ async createEscrow(params) {
408
+ const { createEscrow } = await import('./escrow.js');
409
+ return createEscrow(this, params);
410
+ }
411
+
412
+ /**
413
+ * Get escrow status
414
+ * @param {string} escrowId
415
+ * @returns {Promise<Object>}
416
+ */
417
+ async getEscrow(escrowId) {
418
+ const { getEscrow } = await import('./escrow.js');
419
+ return getEscrow(this, escrowId);
420
+ }
421
+
422
+ /**
423
+ * List escrows with filters
424
+ * @param {Object} [filters]
425
+ * @returns {Promise<Object>}
426
+ */
427
+ async listEscrows(filters) {
428
+ const { listEscrows } = await import('./escrow.js');
429
+ return listEscrows(this, filters);
430
+ }
431
+
432
+ /**
433
+ * Release escrow funds to beneficiary
434
+ * @param {string} escrowId
435
+ * @param {string} releaseToken
436
+ * @returns {Promise<Object>}
437
+ */
438
+ async releaseEscrow(escrowId, releaseToken) {
439
+ const { releaseEscrow } = await import('./escrow.js');
440
+ return releaseEscrow(this, escrowId, releaseToken);
441
+ }
442
+
443
+ /**
444
+ * Refund escrow to depositor
445
+ * @param {string} escrowId
446
+ * @param {string} releaseToken
447
+ * @returns {Promise<Object>}
448
+ */
449
+ async refundEscrow(escrowId, releaseToken) {
450
+ const { refundEscrow } = await import('./escrow.js');
451
+ return refundEscrow(this, escrowId, releaseToken);
452
+ }
453
+
454
+ /**
455
+ * Dispute an escrow
456
+ * @param {string} escrowId
457
+ * @param {string} token - release_token or beneficiary_token
458
+ * @param {string} reason - At least 10 characters
459
+ * @returns {Promise<Object>}
460
+ */
461
+ async disputeEscrow(escrowId, token, reason) {
462
+ const { disputeEscrow } = await import('./escrow.js');
463
+ return disputeEscrow(this, escrowId, token, reason);
464
+ }
465
+
466
+ /**
467
+ * Get escrow audit log
468
+ * @param {string} escrowId
469
+ * @returns {Promise<Array>}
470
+ */
471
+ async getEscrowEvents(escrowId) {
472
+ const { getEscrowEvents } = await import('./escrow.js');
473
+ return getEscrowEvents(this, escrowId);
474
+ }
475
+
476
+ /**
477
+ * Poll escrow until target status
478
+ * @param {string} escrowId
479
+ * @param {Object} [options] - { targetStatus, intervalMs, timeoutMs }
480
+ * @returns {Promise<Object>}
481
+ */
482
+ async waitForEscrow(escrowId, options) {
483
+ const { waitForEscrow } = await import('./escrow.js');
484
+ return waitForEscrow(this, escrowId, options);
485
+ }
399
486
  }
400
487
 
401
488
  export default CoinPayClient;
package/src/escrow.js ADDED
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Escrow SDK Module
3
+ *
4
+ * Anonymous, non-custodial escrow for crypto payments.
5
+ * Both humans and AI agents can create/fund/release/dispute escrows.
6
+ *
7
+ * @example
8
+ * import { CoinPayClient } from '@profullstack/coinpay';
9
+ *
10
+ * const client = new CoinPayClient({ apiKey: 'your-key' });
11
+ *
12
+ * // Create escrow
13
+ * const escrow = await client.createEscrow({
14
+ * chain: 'SOL',
15
+ * amount: 0.5,
16
+ * depositorAddress: 'depositor-wallet',
17
+ * beneficiaryAddress: 'worker-wallet',
18
+ * metadata: { job: 'Code review', deadline: '2026-02-10' }
19
+ * });
20
+ * // Save escrow.releaseToken — needed to release/refund
21
+ *
22
+ * // Check status
23
+ * const status = await client.getEscrow(escrow.id);
24
+ *
25
+ * // Release funds to worker
26
+ * await client.releaseEscrow(escrow.id, escrow.releaseToken);
27
+ */
28
+
29
+ /**
30
+ * Create a new escrow
31
+ * @param {CoinPayClient} client - API client instance
32
+ * @param {Object} params - Escrow parameters
33
+ * @param {string} params.chain - Blockchain (BTC, ETH, SOL, POL, etc.)
34
+ * @param {number} params.amount - Crypto amount to escrow
35
+ * @param {string} params.depositorAddress - Wallet address for refunds
36
+ * @param {string} params.beneficiaryAddress - Wallet address for releases
37
+ * @param {string} [params.arbiterAddress] - Optional dispute resolver address
38
+ * @param {Object} [params.metadata] - Job details, milestones, etc.
39
+ * @param {number} [params.expiresInHours] - Deposit window (default: 24h)
40
+ * @returns {Promise<Object>} Created escrow with releaseToken and beneficiaryToken
41
+ */
42
+ export async function createEscrow(client, {
43
+ chain,
44
+ amount,
45
+ depositorAddress,
46
+ beneficiaryAddress,
47
+ arbiterAddress,
48
+ metadata,
49
+ expiresInHours,
50
+ }) {
51
+ const body = {
52
+ chain,
53
+ amount,
54
+ depositor_address: depositorAddress,
55
+ beneficiary_address: beneficiaryAddress,
56
+ };
57
+ if (arbiterAddress) body.arbiter_address = arbiterAddress;
58
+ if (metadata) body.metadata = metadata;
59
+ if (expiresInHours) body.expires_in_hours = expiresInHours;
60
+
61
+ const data = await client.request('/escrow', {
62
+ method: 'POST',
63
+ body: JSON.stringify(body),
64
+ });
65
+
66
+ return {
67
+ id: data.id,
68
+ escrowAddress: data.escrow_address,
69
+ chain: data.chain,
70
+ amount: data.amount,
71
+ amountUsd: data.amount_usd,
72
+ status: data.status,
73
+ depositorAddress: data.depositor_address,
74
+ beneficiaryAddress: data.beneficiary_address,
75
+ releaseToken: data.release_token,
76
+ beneficiaryToken: data.beneficiary_token,
77
+ metadata: data.metadata,
78
+ expiresAt: data.expires_at,
79
+ createdAt: data.created_at,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Get escrow status
85
+ * @param {CoinPayClient} client
86
+ * @param {string} escrowId
87
+ * @returns {Promise<Object>} Escrow status (public view, no tokens)
88
+ */
89
+ export async function getEscrow(client, escrowId) {
90
+ const data = await client.request(`/escrow/${escrowId}`);
91
+ return normalizeEscrow(data);
92
+ }
93
+
94
+ /**
95
+ * List escrows with filters
96
+ * @param {CoinPayClient} client
97
+ * @param {Object} [filters]
98
+ * @param {string} [filters.status] - Filter by status
99
+ * @param {string} [filters.depositor] - Filter by depositor address
100
+ * @param {string} [filters.beneficiary] - Filter by beneficiary address
101
+ * @param {number} [filters.limit] - Results per page (default: 20)
102
+ * @param {number} [filters.offset] - Offset for pagination
103
+ * @returns {Promise<Object>} { escrows, total, limit, offset }
104
+ */
105
+ export async function listEscrows(client, filters = {}) {
106
+ const params = new URLSearchParams();
107
+ if (filters.status) params.set('status', filters.status);
108
+ if (filters.depositor) params.set('depositor', filters.depositor);
109
+ if (filters.beneficiary) params.set('beneficiary', filters.beneficiary);
110
+ if (filters.limit) params.set('limit', String(filters.limit));
111
+ if (filters.offset) params.set('offset', String(filters.offset));
112
+
113
+ const data = await client.request(`/escrow?${params.toString()}`);
114
+ return {
115
+ escrows: (data.escrows || []).map(normalizeEscrow),
116
+ total: data.total,
117
+ limit: data.limit,
118
+ offset: data.offset,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Release escrow funds to beneficiary
124
+ * @param {CoinPayClient} client
125
+ * @param {string} escrowId
126
+ * @param {string} releaseToken - Secret token from escrow creation
127
+ * @returns {Promise<Object>} Updated escrow
128
+ */
129
+ export async function releaseEscrow(client, escrowId, releaseToken) {
130
+ const data = await client.request(`/escrow/${escrowId}/release`, {
131
+ method: 'POST',
132
+ body: JSON.stringify({ release_token: releaseToken }),
133
+ });
134
+ return normalizeEscrow(data);
135
+ }
136
+
137
+ /**
138
+ * Refund escrow to depositor
139
+ * @param {CoinPayClient} client
140
+ * @param {string} escrowId
141
+ * @param {string} releaseToken
142
+ * @returns {Promise<Object>} Updated escrow
143
+ */
144
+ export async function refundEscrow(client, escrowId, releaseToken) {
145
+ const data = await client.request(`/escrow/${escrowId}/refund`, {
146
+ method: 'POST',
147
+ body: JSON.stringify({ release_token: releaseToken }),
148
+ });
149
+ return normalizeEscrow(data);
150
+ }
151
+
152
+ /**
153
+ * Dispute an escrow
154
+ * @param {CoinPayClient} client
155
+ * @param {string} escrowId
156
+ * @param {string} token - release_token or beneficiary_token
157
+ * @param {string} reason - Dispute reason (min 10 chars)
158
+ * @returns {Promise<Object>} Updated escrow
159
+ */
160
+ export async function disputeEscrow(client, escrowId, token, reason) {
161
+ const data = await client.request(`/escrow/${escrowId}/dispute`, {
162
+ method: 'POST',
163
+ body: JSON.stringify({ token, reason }),
164
+ });
165
+ return normalizeEscrow(data);
166
+ }
167
+
168
+ /**
169
+ * Get escrow event log
170
+ * @param {CoinPayClient} client
171
+ * @param {string} escrowId
172
+ * @returns {Promise<Array>} Array of events
173
+ */
174
+ export async function getEscrowEvents(client, escrowId) {
175
+ const data = await client.request(`/escrow/${escrowId}/events`);
176
+ return (data.events || []).map(e => ({
177
+ id: e.id,
178
+ escrowId: e.escrow_id,
179
+ eventType: e.event_type,
180
+ actor: e.actor,
181
+ details: e.details,
182
+ createdAt: e.created_at,
183
+ }));
184
+ }
185
+
186
+ /**
187
+ * Poll escrow until it reaches a target status
188
+ * @param {CoinPayClient} client
189
+ * @param {string} escrowId
190
+ * @param {Object} [options]
191
+ * @param {string} [options.targetStatus] - Status to wait for (default: 'funded')
192
+ * @param {number} [options.intervalMs] - Poll interval (default: 10000)
193
+ * @param {number} [options.timeoutMs] - Max wait time (default: 3600000 = 1h)
194
+ * @returns {Promise<Object>} Escrow when target status reached
195
+ */
196
+ export async function waitForEscrow(client, escrowId, options = {}) {
197
+ const {
198
+ targetStatus = 'funded',
199
+ intervalMs = 10000,
200
+ timeoutMs = 3600000,
201
+ } = options;
202
+
203
+ const start = Date.now();
204
+
205
+ while (Date.now() - start < timeoutMs) {
206
+ const escrow = await getEscrow(client, escrowId);
207
+
208
+ if (escrow.status === targetStatus) return escrow;
209
+ if (['settled', 'refunded', 'expired'].includes(escrow.status)) return escrow;
210
+
211
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
212
+ }
213
+
214
+ throw new Error(`Escrow ${escrowId} did not reach status '${targetStatus}' within ${timeoutMs}ms`);
215
+ }
216
+
217
+ // ── Helpers ──
218
+
219
+ function normalizeEscrow(data) {
220
+ return {
221
+ id: data.id,
222
+ escrowAddress: data.escrow_address,
223
+ chain: data.chain,
224
+ amount: data.amount,
225
+ amountUsd: data.amount_usd,
226
+ feeAmount: data.fee_amount,
227
+ depositedAmount: data.deposited_amount,
228
+ status: data.status,
229
+ depositorAddress: data.depositor_address,
230
+ beneficiaryAddress: data.beneficiary_address,
231
+ arbiterAddress: data.arbiter_address,
232
+ depositTxHash: data.deposit_tx_hash,
233
+ settlementTxHash: data.settlement_tx_hash,
234
+ metadata: data.metadata,
235
+ disputeReason: data.dispute_reason,
236
+ disputeResolution: data.dispute_resolution,
237
+ createdAt: data.created_at,
238
+ fundedAt: data.funded_at,
239
+ releasedAt: data.released_at,
240
+ settledAt: data.settled_at,
241
+ disputedAt: data.disputed_at,
242
+ refundedAt: data.refunded_at,
243
+ expiresAt: data.expires_at,
244
+ };
245
+ }
package/src/index.js CHANGED
@@ -36,6 +36,17 @@ import {
36
36
  WebhookEvent,
37
37
  } from './webhooks.js';
38
38
 
39
+ import {
40
+ createEscrow,
41
+ getEscrow,
42
+ listEscrows,
43
+ releaseEscrow,
44
+ refundEscrow,
45
+ disputeEscrow,
46
+ getEscrowEvents,
47
+ waitForEscrow,
48
+ } from './escrow.js';
49
+
39
50
  export {
40
51
  // Client
41
52
  CoinPayClient,
@@ -45,6 +56,16 @@ export {
45
56
  getPayment,
46
57
  listPayments,
47
58
 
59
+ // Escrow functions
60
+ createEscrow,
61
+ getEscrow,
62
+ listEscrows,
63
+ releaseEscrow,
64
+ refundEscrow,
65
+ disputeEscrow,
66
+ getEscrowEvents,
67
+ waitForEscrow,
68
+
48
69
  // Constants
49
70
  Blockchain,
50
71
  Cryptocurrency, // Deprecated, use Blockchain