@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 +1005 -235
- package/package.json +21 -4
- package/src/client.js +87 -0
- package/src/escrow.js +245 -0
- package/src/index.d.ts +65 -2
- package/src/index.js +82 -1
- package/src/swap.d.ts +254 -0
- package/src/swap.js +360 -0
- package/src/wallet.d.ts +259 -0
- package/src/wallet.js +757 -0
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 {
|
|
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.
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
--
|
|
170
|
-
--
|
|
171
|
-
--
|
|
172
|
-
--
|
|
173
|
-
--amount <amount>
|
|
174
|
-
--
|
|
175
|
-
--
|
|
176
|
-
--
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
--
|
|
180
|
-
--
|
|
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
|
-
#
|
|
184
|
-
coinpay
|
|
241
|
+
# Create a new wallet (auto-saves encrypted)
|
|
242
|
+
coinpay wallet create --words 12
|
|
185
243
|
|
|
186
|
-
#
|
|
187
|
-
coinpay
|
|
244
|
+
# Import existing wallet
|
|
245
|
+
coinpay wallet import "word1 word2 ... word12"
|
|
188
246
|
|
|
189
|
-
#
|
|
190
|
-
coinpay
|
|
247
|
+
# Get wallet balance (auto-decrypts)
|
|
248
|
+
coinpay wallet balance
|
|
191
249
|
|
|
192
|
-
#
|
|
193
|
-
coinpay
|
|
250
|
+
# Send transaction (auto-decrypts for signing)
|
|
251
|
+
coinpay wallet send --chain ETH --to 0x123... --amount 0.1
|
|
194
252
|
|
|
195
|
-
#
|
|
196
|
-
coinpay
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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 (
|
|
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
|
|
339
|
-
print.
|
|
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
|
-
*
|
|
710
|
+
* Wallet commands
|
|
492
711
|
*/
|
|
493
|
-
function
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
508
|
-
};
|
|
818
|
+
}
|
|
509
819
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
*
|
|
1181
|
+
* Swap commands
|
|
540
1182
|
*/
|
|
541
|
-
async function
|
|
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 '
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
print.
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
562
|
-
print.
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
print.error(error.message);
|
|
572
1230
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
595
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
|
715
|
-
print.info('Available:
|
|
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();
|