@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 +415 -2
- package/package.json +1 -1
- package/src/client.js +87 -0
- package/src/escrow.js +245 -0
- package/src/index.js +21 -0
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
|
|
99
|
-
|
|
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
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
|