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