nansen-cli 1.13.1 → 1.15.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/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/package.json +2 -1
- package/src/api.js +107 -64
- package/src/chain-ids.js +2 -3
- package/src/cli.js +31 -23
- package/src/keychain.js +229 -0
- package/src/privy.js +359 -0
- package/src/schema.json +42 -2
- package/src/trading.js +264 -118
- package/src/transfer.js +150 -25
- package/src/wallet.js +354 -70
- package/src/x402-svm.js +43 -24
- package/src/x402.js +2 -2
package/src/wallet.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from 'fs';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import * as readline from 'readline';
|
|
10
10
|
import { base58 } from '@scure/base';
|
|
11
|
+
import { storePassword, retrievePassword, deletePassword, deleteCredentialsFile } from './keychain.js';
|
|
11
12
|
|
|
12
13
|
// ============= Constants =============
|
|
13
14
|
|
|
@@ -238,8 +239,8 @@ function getWalletFile(name) {
|
|
|
238
239
|
* Verify the global password against stored hash.
|
|
239
240
|
*/
|
|
240
241
|
export function verifyPassword(password, config) {
|
|
242
|
+
if (password == null) return false;
|
|
241
243
|
if (!config.passwordHash) return true; // No password set yet
|
|
242
|
-
if (password === null || password === undefined) return false;
|
|
243
244
|
const { salt, hash } = config.passwordHash;
|
|
244
245
|
const derived = crypto.scryptSync(password, Buffer.from(salt, 'hex'), 32, {
|
|
245
246
|
N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: 256 * 1024 * 1024,
|
|
@@ -265,7 +266,7 @@ async function promptPassword(question, deps = {}) {
|
|
|
265
266
|
if (promptFn) {
|
|
266
267
|
return promptFn(question, true);
|
|
267
268
|
}
|
|
268
|
-
// Fallback to readline
|
|
269
|
+
// Fallback to readline (only available in --human mode)
|
|
269
270
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
270
271
|
return new Promise((resolve) => {
|
|
271
272
|
if (process.stdout.isTTY) {
|
|
@@ -298,6 +299,60 @@ async function promptPassword(question, deps = {}) {
|
|
|
298
299
|
});
|
|
299
300
|
}
|
|
300
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Resolve wallet password from all available sources (non-interactive).
|
|
304
|
+
* Order: NANSEN_WALLET_PASSWORD env var → OS keychain → .credentials file → null
|
|
305
|
+
* Emits a warning to stderr when using the insecure .credentials file.
|
|
306
|
+
* @returns {string|null}
|
|
307
|
+
*/
|
|
308
|
+
function resolveWalletPassword() {
|
|
309
|
+
const { password, source } = retrievePassword();
|
|
310
|
+
if (source === 'file') {
|
|
311
|
+
process.stderr.write(
|
|
312
|
+
'⚠️ Password loaded from ~/.nansen/wallets/.credentials (insecure — plaintext on disk).\n' +
|
|
313
|
+
' For better security, migrate to OS keychain: nansen wallet secure\n' +
|
|
314
|
+
' Or set NANSEN_WALLET_PASSWORD via a secrets manager.\n'
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return password;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Resolve wallet password for a command. If --human flag is set and no
|
|
322
|
+
* password found, falls back to interactive prompt. Otherwise returns
|
|
323
|
+
* structured error info for agents.
|
|
324
|
+
*
|
|
325
|
+
* @param {object} config - wallet config (needs config.passwordHash)
|
|
326
|
+
* @param {object} flags - CLI flags
|
|
327
|
+
* @param {object} deps - { promptFn, log, exit }
|
|
328
|
+
* @returns {{ password: string|null, error: string|null }}
|
|
329
|
+
*/
|
|
330
|
+
async function resolvePasswordForCommand(config, flags, deps) {
|
|
331
|
+
if (!config.passwordHash) {
|
|
332
|
+
return { password: null, error: null };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const password = resolveWalletPassword();
|
|
336
|
+
if (password) return { password, error: null };
|
|
337
|
+
|
|
338
|
+
if (flags.human && (process.stdin.isTTY || deps.promptFn)) {
|
|
339
|
+
const prompted = await promptPassword('Enter wallet password: ', deps);
|
|
340
|
+
if (prompted) return { password: prompted, error: null };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
password: null,
|
|
345
|
+
error: JSON.stringify({
|
|
346
|
+
error: 'PASSWORD_REQUIRED',
|
|
347
|
+
message: 'Wallet is encrypted and no password was found.',
|
|
348
|
+
resolution: [
|
|
349
|
+
'Set NANSEN_WALLET_PASSWORD environment variable',
|
|
350
|
+
'Or re-run wallet create with the password (it will be persisted for future use)',
|
|
351
|
+
],
|
|
352
|
+
}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
301
356
|
// ============= Public API =============
|
|
302
357
|
|
|
303
358
|
/**
|
|
@@ -312,6 +367,7 @@ export function listWallets() {
|
|
|
312
367
|
const data = JSON.parse(fs.readFileSync(path.join(getWalletsDir(), f), 'utf8'));
|
|
313
368
|
return {
|
|
314
369
|
name: data.name,
|
|
370
|
+
provider: data.provider || 'local',
|
|
315
371
|
evm: data.evm?.address || null,
|
|
316
372
|
solana: data.solana?.address || null,
|
|
317
373
|
createdAt: data.createdAt,
|
|
@@ -342,10 +398,18 @@ export function createWallet(name, password) {
|
|
|
342
398
|
} else {
|
|
343
399
|
// Encrypted mode
|
|
344
400
|
if (!config.passwordHash) {
|
|
345
|
-
// First encrypted wallet: reject if passwordless wallets exist
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
401
|
+
// First encrypted wallet: reject if passwordless local wallets exist
|
|
402
|
+
// (non-local wallets like Privy don't contain private keys, so skip them)
|
|
403
|
+
const walletsDir = getWalletsDir();
|
|
404
|
+
const existingLocalWallets = fs.readdirSync(walletsDir)
|
|
405
|
+
.filter(f => f.endsWith('.json') && f !== 'config.json')
|
|
406
|
+
.filter(f => {
|
|
407
|
+
try {
|
|
408
|
+
const data = JSON.parse(fs.readFileSync(path.join(walletsDir, f), 'utf8'));
|
|
409
|
+
return !data.provider || data.provider === 'local';
|
|
410
|
+
} catch { return true; }
|
|
411
|
+
});
|
|
412
|
+
if (existingLocalWallets.length > 0) {
|
|
349
413
|
throw new Error('Existing wallets are passwordless. Cannot mix encrypted and unencrypted wallets.');
|
|
350
414
|
}
|
|
351
415
|
config.passwordHash = hashPassword(password);
|
|
@@ -397,13 +461,21 @@ export function showWallet(name) {
|
|
|
397
461
|
|
|
398
462
|
const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
|
|
399
463
|
const config = getWalletConfig();
|
|
464
|
+
const isPrivy = data.provider === 'privy';
|
|
400
465
|
|
|
401
466
|
return {
|
|
402
467
|
name: data.name,
|
|
468
|
+
provider: data.provider || 'local',
|
|
403
469
|
evm: data.evm?.address || null,
|
|
404
470
|
solana: data.solana?.address || null,
|
|
405
471
|
createdAt: data.createdAt,
|
|
406
472
|
isDefault: data.name === config.defaultWallet,
|
|
473
|
+
...(isPrivy ? {
|
|
474
|
+
privyWalletIds: {
|
|
475
|
+
evm: data.evm?.privyWalletId,
|
|
476
|
+
solana: data.solana?.privyWalletId,
|
|
477
|
+
}
|
|
478
|
+
} : {}),
|
|
407
479
|
};
|
|
408
480
|
}
|
|
409
481
|
|
|
@@ -416,13 +488,16 @@ export function exportWallet(name, password) {
|
|
|
416
488
|
throw new Error(`Wallet "${name}" not found`);
|
|
417
489
|
}
|
|
418
490
|
|
|
491
|
+
const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
|
|
492
|
+
if (data.provider && data.provider !== 'local') {
|
|
493
|
+
throw new Error(`${data.provider} wallets don't support key export. Keys are managed by the provider.`);
|
|
494
|
+
}
|
|
495
|
+
|
|
419
496
|
const config = getWalletConfig();
|
|
420
497
|
if (config.passwordHash && !verifyPassword(password, config)) {
|
|
421
498
|
throw new Error('Incorrect password');
|
|
422
499
|
}
|
|
423
500
|
|
|
424
|
-
const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
|
|
425
|
-
|
|
426
501
|
return {
|
|
427
502
|
name: data.name,
|
|
428
503
|
evm: {
|
|
@@ -455,41 +530,37 @@ export function setDefaultWallet(name) {
|
|
|
455
530
|
/**
|
|
456
531
|
* Delete a wallet.
|
|
457
532
|
*/
|
|
458
|
-
export function deleteWallet(name, password) {
|
|
533
|
+
export async function deleteWallet(name, password) {
|
|
459
534
|
const walletFile = getWalletFile(name);
|
|
460
535
|
if (!fs.existsSync(walletFile)) {
|
|
461
536
|
throw new Error(`Wallet "${name}" not found`);
|
|
462
537
|
}
|
|
463
538
|
|
|
539
|
+
const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
|
|
464
540
|
const config = getWalletConfig();
|
|
465
|
-
|
|
466
|
-
|
|
541
|
+
|
|
542
|
+
if (data.provider && data.provider !== 'local') {
|
|
543
|
+
// Non-local wallets: just remove local reference, no password needed
|
|
544
|
+
} else {
|
|
545
|
+
if (config.passwordHash && !verifyPassword(password, config)) {
|
|
546
|
+
throw new Error('Incorrect password');
|
|
547
|
+
}
|
|
467
548
|
}
|
|
468
549
|
|
|
469
550
|
fs.unlinkSync(walletFile);
|
|
470
551
|
|
|
471
|
-
|
|
472
|
-
// Pick another wallet as default, or null
|
|
473
|
-
const remaining = fs.readdirSync(getWalletsDir()).filter(f => f.endsWith('.json') && f !== 'config.json');
|
|
474
|
-
config.defaultWallet = remaining.length > 0 ? remaining[0].replace('.json', '') : null;
|
|
475
|
-
saveWalletConfig(config);
|
|
476
|
-
}
|
|
552
|
+
const remaining = fs.readdirSync(getWalletsDir()).filter(f => f.endsWith('.json') && f !== 'config.json');
|
|
477
553
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
export function getDefaultAddress(chainType = 'evm') {
|
|
485
|
-
const config = getWalletConfig();
|
|
486
|
-
if (!config.defaultWallet) {
|
|
487
|
-
throw new Error('No default wallet set. Run: nansen wallet create');
|
|
554
|
+
if (remaining.length === 0) {
|
|
555
|
+
config.defaultWallet = null;
|
|
556
|
+
config.passwordHash = null;
|
|
557
|
+
deletePassword();
|
|
558
|
+
} else if (config.defaultWallet === name) {
|
|
559
|
+
config.defaultWallet = remaining[0].replace('.json', '');
|
|
488
560
|
}
|
|
561
|
+
saveWalletConfig(config);
|
|
489
562
|
|
|
490
|
-
|
|
491
|
-
const field = chainType === 'solana' ? 'solana' : 'evm';
|
|
492
|
-
return wallet[field];
|
|
563
|
+
return { deleted: name, newDefault: config.defaultWallet };
|
|
493
564
|
}
|
|
494
565
|
|
|
495
566
|
// ============= CLI Command Builder =============
|
|
@@ -504,6 +575,27 @@ export function buildWalletCommands(deps = {}) {
|
|
|
504
575
|
'wallet': async (args, apiInstance, flags, options) => {
|
|
505
576
|
const subcommand = args[0] || 'help';
|
|
506
577
|
|
|
578
|
+
// Privy-specific: only 'create' and policy commands need --provider privy
|
|
579
|
+
if (options.provider === 'privy' || process.env.NANSEN_WALLET_PROVIDER === 'privy') {
|
|
580
|
+
if (subcommand === 'create') {
|
|
581
|
+
const { createPrivyWalletPair } = await import('./privy.js');
|
|
582
|
+
const name = options.name || args[1] || 'default';
|
|
583
|
+
try {
|
|
584
|
+
const result = await createPrivyWalletPair(name);
|
|
585
|
+
log(`\n✓ Privy wallet "${result.name}" created\n`);
|
|
586
|
+
log(` EVM: ${result.evm.address}`);
|
|
587
|
+
log(` Solana: ${result.solana.address}`);
|
|
588
|
+
log('');
|
|
589
|
+
return;
|
|
590
|
+
} catch (err) {
|
|
591
|
+
log(`❌ ${err.message}`);
|
|
592
|
+
exit(1);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// All other subcommands fall through to unified handlers below
|
|
597
|
+
}
|
|
598
|
+
|
|
507
599
|
const handlers = {
|
|
508
600
|
'create': async () => {
|
|
509
601
|
const name = options.name || args[1] || 'default';
|
|
@@ -512,32 +604,74 @@ export function buildWalletCommands(deps = {}) {
|
|
|
512
604
|
if (flags['unsafe-no-password']) {
|
|
513
605
|
process.stderr.write('WARNING: --unsafe-no-password is set. Private keys will be stored UNENCRYPTED on disk.\nAnyone with access to this machine can steal your funds.\n');
|
|
514
606
|
password = null;
|
|
515
|
-
} else if (!process.env.NANSEN_WALLET_PASSWORD && !process.stdin.isTTY && !deps.promptFn) {
|
|
516
|
-
log('❌ No password provided. Either:');
|
|
517
|
-
log(' set NANSEN_WALLET_PASSWORD, or');
|
|
518
|
-
log(' use --unsafe-no-password (WARNING: Private keys will be stored UNENCRYPTED on disk. Anyone with access to this machine can steal your funds.)');
|
|
519
|
-
exit(1);
|
|
520
|
-
return;
|
|
521
607
|
} else {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
608
|
+
// Step 1: Check env var and keychain
|
|
609
|
+
password = resolveWalletPassword();
|
|
610
|
+
|
|
611
|
+
// Step 2: If --human flag, allow interactive prompt (requires TTY)
|
|
612
|
+
if (!password && flags.human && !process.stdin.isTTY && !deps.promptFn) {
|
|
613
|
+
log(JSON.stringify({
|
|
614
|
+
error: 'NOT_A_TTY',
|
|
615
|
+
message: '--human requires an interactive terminal. Set NANSEN_WALLET_PASSWORD env var instead.',
|
|
616
|
+
}));
|
|
525
617
|
exit(1);
|
|
526
618
|
return;
|
|
527
619
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const confirm = await promptPassword('Confirm password: ', deps);
|
|
533
|
-
if (password !== confirm) {
|
|
534
|
-
log('❌ Passwords do not match');
|
|
620
|
+
if (!password && flags.human && (process.stdin.isTTY || deps.promptFn)) {
|
|
621
|
+
password = await promptPassword('Enter wallet password: ', deps);
|
|
622
|
+
if (password && password.length < 12) {
|
|
623
|
+
log('❌ Password must be at least 12 characters');
|
|
535
624
|
exit(1);
|
|
536
625
|
return;
|
|
537
626
|
}
|
|
627
|
+
if (password) {
|
|
628
|
+
const config = getWalletConfig();
|
|
629
|
+
if (!config.passwordHash) {
|
|
630
|
+
const confirm = await promptPassword('Confirm password: ', deps);
|
|
631
|
+
if (password !== confirm) {
|
|
632
|
+
log('❌ Passwords do not match');
|
|
633
|
+
exit(1);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Step 3: No password available — return structured error for agents
|
|
641
|
+
if (!password) {
|
|
642
|
+
log(JSON.stringify({
|
|
643
|
+
error: 'PASSWORD_REQUIRED',
|
|
644
|
+
message: 'A wallet password is required. Ask the user to provide one.',
|
|
645
|
+
instructions: 'Re-run with: NANSEN_WALLET_PASSWORD=<password> nansen wallet create',
|
|
646
|
+
note: 'Password must be at least 12 characters. After creation, the password is saved to the OS keychain automatically — future operations will not require it.',
|
|
647
|
+
}));
|
|
648
|
+
exit(1);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (password.length < 12) {
|
|
653
|
+
log('❌ Password must be at least 12 characters');
|
|
654
|
+
exit(1);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Verify password matches existing wallets BEFORE touching keychain
|
|
660
|
+
if (password !== null) {
|
|
661
|
+
const config = getWalletConfig();
|
|
662
|
+
if (config.passwordHash && !verifyPassword(password, config)) {
|
|
663
|
+
log('❌ Incorrect password — does not match existing wallets.');
|
|
664
|
+
exit(1);
|
|
665
|
+
return;
|
|
538
666
|
}
|
|
539
667
|
}
|
|
540
668
|
|
|
669
|
+
// Persist password BEFORE creating wallet so we know the storage situation
|
|
670
|
+
let storageResult = { stored: false, method: 'none' };
|
|
671
|
+
if (password !== null) {
|
|
672
|
+
storageResult = storePassword(password);
|
|
673
|
+
}
|
|
674
|
+
|
|
541
675
|
try {
|
|
542
676
|
const result = createWallet(name, password);
|
|
543
677
|
log(`\n✓ Wallet "${result.name}" created\n`);
|
|
@@ -551,9 +685,24 @@ export function buildWalletCommands(deps = {}) {
|
|
|
551
685
|
log('');
|
|
552
686
|
if (password === null) {
|
|
553
687
|
log(' ⚠️ This is an UNENCRYPTED hot wallet — private keys are stored in plaintext on disk.');
|
|
688
|
+
} else if (storageResult.stored && storageResult.method === 'keychain') {
|
|
689
|
+
log(' ✓ Password saved to system keychain (secure).');
|
|
690
|
+
log(' Future wallet operations will retrieve the password automatically.');
|
|
691
|
+
} else if (storageResult.stored && storageResult.method === 'file') {
|
|
692
|
+
log(' ⚠️ No OS keychain available. Password saved to ~/.nansen/wallets/.credentials (insecure — plaintext on disk).');
|
|
693
|
+
log(' Future wallet operations will retrieve the password automatically.');
|
|
694
|
+
log(' To improve security: migrate to OS keychain with `nansen wallet secure`,');
|
|
695
|
+
log(' or set NANSEN_WALLET_PASSWORD via a secrets manager.');
|
|
554
696
|
} else {
|
|
555
|
-
log(' ⚠️
|
|
556
|
-
log('
|
|
697
|
+
log(' ⚠️ CRITICAL: Password could not be saved anywhere (no keychain, no writable filesystem).');
|
|
698
|
+
log(' You MUST set NANSEN_WALLET_PASSWORD in your environment for ALL future wallet operations.');
|
|
699
|
+
log(' If you lose this password, your funds are UNRECOVERABLE.');
|
|
700
|
+
}
|
|
701
|
+
if (password !== null) {
|
|
702
|
+
log('');
|
|
703
|
+
log(' IMPORTANT: Back up your password separately (e.g. password manager).');
|
|
704
|
+
log(' If you lose access to this machine AND forget the password, funds are unrecoverable.');
|
|
705
|
+
log(' This is a hot wallet — do not deposit more than you can afford to lose.');
|
|
557
706
|
}
|
|
558
707
|
log('');
|
|
559
708
|
return;
|
|
@@ -572,7 +721,8 @@ export function buildWalletCommands(deps = {}) {
|
|
|
572
721
|
log('');
|
|
573
722
|
for (const w of result.wallets) {
|
|
574
723
|
const star = w.isDefault ? ' ★' : '';
|
|
575
|
-
|
|
724
|
+
const providerTag = w.provider === 'privy' ? ' (privy)' : '';
|
|
725
|
+
log(` ${w.name}${star}${providerTag}`);
|
|
576
726
|
log(` EVM: ${w.evm}`);
|
|
577
727
|
log(` Solana: ${w.solana}`);
|
|
578
728
|
log('');
|
|
@@ -589,7 +739,8 @@ export function buildWalletCommands(deps = {}) {
|
|
|
589
739
|
try {
|
|
590
740
|
const result = showWallet(name);
|
|
591
741
|
const star = result.isDefault ? ' ★' : '';
|
|
592
|
-
|
|
742
|
+
const providerTag = result.provider === 'privy' ? ' (privy)' : '';
|
|
743
|
+
log(`\n ${result.name}${star}${providerTag}`);
|
|
593
744
|
log(` EVM: ${result.evm}`);
|
|
594
745
|
log(` Solana: ${result.solana}`);
|
|
595
746
|
log(` Created: ${result.createdAt}\n`);
|
|
@@ -607,10 +758,14 @@ export function buildWalletCommands(deps = {}) {
|
|
|
607
758
|
exit(1);
|
|
608
759
|
return;
|
|
609
760
|
}
|
|
761
|
+
|
|
610
762
|
const config = getWalletConfig();
|
|
611
|
-
const password = config
|
|
612
|
-
|
|
613
|
-
|
|
763
|
+
const { password, error } = await resolvePasswordForCommand(config, flags, deps);
|
|
764
|
+
if (error) {
|
|
765
|
+
log(error);
|
|
766
|
+
exit(1);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
614
769
|
try {
|
|
615
770
|
const result = exportWallet(name, password);
|
|
616
771
|
log(`\n⚠️ Private keys for "${result.name}" — do not share!\n`);
|
|
@@ -652,13 +807,33 @@ export function buildWalletCommands(deps = {}) {
|
|
|
652
807
|
exit(1);
|
|
653
808
|
return;
|
|
654
809
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
: null;
|
|
810
|
+
|
|
811
|
+
// Check if this is a Privy wallet (no password needed)
|
|
812
|
+
let isPrivy = false;
|
|
659
813
|
try {
|
|
660
|
-
const
|
|
814
|
+
const walletFile = path.join(getWalletsDir(), `${name}.json`);
|
|
815
|
+
const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
|
|
816
|
+
if (data.provider === 'privy') isPrivy = true;
|
|
817
|
+
} catch { /* file might not exist, deleteWallet will throw */ }
|
|
818
|
+
|
|
819
|
+
let password = null;
|
|
820
|
+
if (!isPrivy) {
|
|
821
|
+
const config = getWalletConfig();
|
|
822
|
+
const resolved = await resolvePasswordForCommand(config, flags, deps);
|
|
823
|
+
if (resolved.error) {
|
|
824
|
+
log(resolved.error);
|
|
825
|
+
exit(1);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
password = resolved.password;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
try {
|
|
832
|
+
const result = await deleteWallet(name, password);
|
|
661
833
|
log(`✓ Wallet "${result.deleted}" deleted`);
|
|
834
|
+
if (isPrivy) {
|
|
835
|
+
log(` Note: server-side wallet still exists on Privy`);
|
|
836
|
+
}
|
|
662
837
|
if (result.newDefault) {
|
|
663
838
|
log(` New default: ${result.newDefault}`);
|
|
664
839
|
}
|
|
@@ -698,14 +873,32 @@ export function buildWalletCommands(deps = {}) {
|
|
|
698
873
|
}
|
|
699
874
|
|
|
700
875
|
const isWalletConnect = options.wallet === 'walletconnect' || options.wallet === 'wc';
|
|
876
|
+
|
|
877
|
+
// Check if the wallet is Privy (no password needed)
|
|
878
|
+
let isPrivyWallet = false;
|
|
879
|
+
if (!isWalletConnect) {
|
|
880
|
+
try {
|
|
881
|
+
const walletName = options.wallet || getWalletConfig().defaultWallet;
|
|
882
|
+
if (walletName) {
|
|
883
|
+
const walletFile = path.join(getWalletsDir(), `${walletName}.json`);
|
|
884
|
+
const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
|
|
885
|
+
if (data.provider === 'privy') isPrivyWallet = true;
|
|
886
|
+
}
|
|
887
|
+
} catch { /* ignore */ }
|
|
888
|
+
}
|
|
889
|
+
|
|
701
890
|
let password;
|
|
702
|
-
if (isWalletConnect) {
|
|
891
|
+
if (isWalletConnect || isPrivyWallet) {
|
|
703
892
|
password = null;
|
|
704
893
|
} else {
|
|
705
894
|
const sendConfig = getWalletConfig();
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
895
|
+
const resolved = await resolvePasswordForCommand(sendConfig, flags, deps);
|
|
896
|
+
if (resolved.error) {
|
|
897
|
+
log(resolved.error);
|
|
898
|
+
exit(1);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
password = resolved.password;
|
|
709
902
|
}
|
|
710
903
|
const dryRun = flags['dry-run'] || flags.dryRun;
|
|
711
904
|
|
|
@@ -723,7 +916,6 @@ export function buildWalletCommands(deps = {}) {
|
|
|
723
916
|
};
|
|
724
917
|
|
|
725
918
|
if (dryRun) {
|
|
726
|
-
// Build the transaction but don't broadcast
|
|
727
919
|
const result = await sendTokens(sendOpts);
|
|
728
920
|
log(`\nDry run — transaction not broadcast\n`);
|
|
729
921
|
log(` From: ${result.from}`);
|
|
@@ -755,46 +947,138 @@ export function buildWalletCommands(deps = {}) {
|
|
|
755
947
|
}
|
|
756
948
|
},
|
|
757
949
|
|
|
950
|
+
'forget-password': async () => {
|
|
951
|
+
const result = deletePassword();
|
|
952
|
+
if (result.keychain || result.file) {
|
|
953
|
+
log('✓ Password removed from:');
|
|
954
|
+
if (result.keychain) log(' - System keychain');
|
|
955
|
+
if (result.file) log(' - Credentials file (~/.nansen/wallets/.credentials)');
|
|
956
|
+
} else {
|
|
957
|
+
log('No saved password found (keychain or credentials file).');
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
|
|
961
|
+
'secure': async () => {
|
|
962
|
+
const { password, source } = retrievePassword();
|
|
963
|
+
if (!password) {
|
|
964
|
+
log(JSON.stringify({
|
|
965
|
+
error: 'NO_PASSWORD_FOUND',
|
|
966
|
+
message: 'No wallet password found in any store.',
|
|
967
|
+
resolution: [
|
|
968
|
+
'Set NANSEN_WALLET_PASSWORD and run: nansen wallet secure',
|
|
969
|
+
'This will store it in the OS keychain.',
|
|
970
|
+
],
|
|
971
|
+
}));
|
|
972
|
+
exit(1);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (source === 'keychain') {
|
|
977
|
+
log('✓ Password is already stored in the OS keychain (secure).');
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Verify password actually decrypts wallets before overwriting keychain
|
|
982
|
+
const walletConfig = getWalletConfig();
|
|
983
|
+
if (walletConfig.passwordHash && !verifyPassword(password, walletConfig)) {
|
|
984
|
+
log(JSON.stringify({
|
|
985
|
+
error: 'INCORRECT_PASSWORD',
|
|
986
|
+
message: `Password from '${source}' does not match the wallet's stored hash.`,
|
|
987
|
+
resolution: source === 'file'
|
|
988
|
+
? [
|
|
989
|
+
'The password in ~/.nansen/wallets/.credentials is incorrect.',
|
|
990
|
+
'Run: nansen wallet forget-password then re-run with the correct password: NANSEN_WALLET_PASSWORD=<pw> nansen wallet secure',
|
|
991
|
+
]
|
|
992
|
+
: [
|
|
993
|
+
'Unset NANSEN_WALLET_PASSWORD if it is stale, then re-run: nansen wallet secure',
|
|
994
|
+
],
|
|
995
|
+
}));
|
|
996
|
+
exit(1);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Try to migrate to keychain
|
|
1001
|
+
const { stored, method } = storePassword(password);
|
|
1002
|
+
if (stored && method === 'keychain') {
|
|
1003
|
+
const fileRemoved = deleteCredentialsFile();
|
|
1004
|
+
const fromLabel = source === 'file'
|
|
1005
|
+
? '~/.nansen/wallets/.credentials file'
|
|
1006
|
+
: 'NANSEN_WALLET_PASSWORD env var';
|
|
1007
|
+
log(`✓ Password migrated from ${fromLabel} → OS keychain (secure).`);
|
|
1008
|
+
if (fileRemoved) {
|
|
1009
|
+
log(' Removed ~/.nansen/wallets/.credentials.');
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
log(JSON.stringify({
|
|
1013
|
+
error: 'KEYCHAIN_UNAVAILABLE',
|
|
1014
|
+
message: source === 'file'
|
|
1015
|
+
? 'OS keychain is not available. Password remains in ~/.nansen/wallets/.credentials (insecure).'
|
|
1016
|
+
: 'OS keychain is not available. Password is only in the NANSEN_WALLET_PASSWORD env var (not persisted).',
|
|
1017
|
+
resolution: [
|
|
1018
|
+
'Set NANSEN_WALLET_PASSWORD in a secrets manager or system keyring',
|
|
1019
|
+
'Use a containerized secrets agent (e.g. Vault, 1Password CLI)',
|
|
1020
|
+
],
|
|
1021
|
+
}));
|
|
1022
|
+
exit(1);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
|
|
758
1027
|
'help': async () => {
|
|
759
1028
|
log(`
|
|
760
|
-
Wallet Management -
|
|
1029
|
+
Wallet Management - EVM and Solana wallets (local or Privy server-side)
|
|
761
1030
|
|
|
762
1031
|
USAGE:
|
|
763
1032
|
nansen wallet <command> [options]
|
|
764
1033
|
|
|
765
1034
|
COMMANDS:
|
|
766
|
-
create [--name <label>] [--unsafe-no-password]
|
|
1035
|
+
create [--name <label>] [--provider <local|privy>] [--unsafe-no-password]
|
|
767
1036
|
Create a new wallet pair (EVM + Solana)
|
|
768
1037
|
list List all wallets
|
|
769
1038
|
show <name> Show wallet addresses
|
|
770
|
-
export <name> Export private keys (requires password)
|
|
1039
|
+
export <name> Export private keys (local wallets only, requires password)
|
|
771
1040
|
default <name> Set the default wallet
|
|
772
|
-
delete <name> Delete a wallet
|
|
1041
|
+
delete <name> Delete a wallet
|
|
773
1042
|
send --to <address> --amount <number> --chain <evm|solana> [--token <address>] [--wallet <name>] [--max] [--dry-run]
|
|
774
1043
|
Send tokens or native currency (--max sends entire balance, --dry-run previews without sending)
|
|
1044
|
+
forget-password Remove saved password from all stores
|
|
1045
|
+
secure Migrate password from insecure storage to OS keychain
|
|
775
1046
|
|
|
776
1047
|
OPTIONS:
|
|
777
1048
|
--name <label> Wallet name (default: "default")
|
|
1049
|
+
--provider <local|privy> Wallet provider: "local" (default) stores encrypted keys on disk,
|
|
1050
|
+
"privy" creates server-side wallets via Privy API
|
|
778
1051
|
--to <address> Recipient address (required for send)
|
|
779
1052
|
--amount <number> Amount to send in human-readable format (required unless --max)
|
|
780
1053
|
--chain <evm|solana> Blockchain to use (required for send)
|
|
781
1054
|
--token <address> Token contract/mint address (optional, sends native if omitted)
|
|
782
1055
|
--wallet <name> Wallet to use (optional, uses default if omitted; use "walletconnect" or "wc" for WalletConnect, EVM only)
|
|
783
1056
|
--max Send entire balance (deducts gas for native transfers)
|
|
784
|
-
--unsafe-no-password Skip encryption — private keys stored UNENCRYPTED on disk (
|
|
1057
|
+
--unsafe-no-password Skip encryption — private keys stored UNENCRYPTED on disk (local only)
|
|
1058
|
+
--human Enable interactive prompts (for human terminal use only)
|
|
1059
|
+
|
|
1060
|
+
PASSWORD RESOLUTION (automatic, in order):
|
|
1061
|
+
1. NANSEN_WALLET_PASSWORD env var
|
|
1062
|
+
2. OS keychain (saved automatically on wallet create)
|
|
1063
|
+
3. Interactive prompt (only with --human flag)
|
|
785
1064
|
|
|
786
1065
|
ENVIRONMENT:
|
|
787
|
-
NANSEN_WALLET_PASSWORD
|
|
1066
|
+
NANSEN_WALLET_PASSWORD Wallet encryption password
|
|
1067
|
+
PRIVY_APP_ID Privy application ID (required for --provider privy)
|
|
1068
|
+
PRIVY_APP_SECRET Privy application secret (required for --provider privy)
|
|
1069
|
+
NANSEN_WALLET_PROVIDER Default provider for wallet create ("local" or "privy")
|
|
788
1070
|
NANSEN_EVM_RPC Custom EVM RPC endpoint
|
|
789
1071
|
NANSEN_SOLANA_RPC Custom Solana RPC endpoint
|
|
790
1072
|
|
|
791
1073
|
EXAMPLES:
|
|
792
|
-
nansen wallet create --name trading
|
|
1074
|
+
NANSEN_WALLET_PASSWORD=mypass nansen wallet create --name trading
|
|
1075
|
+
nansen wallet create --name agent-wallet --provider privy
|
|
793
1076
|
nansen wallet list
|
|
794
1077
|
nansen wallet export trading
|
|
795
1078
|
nansen wallet default trading
|
|
796
1079
|
nansen wallet send --to 0x742d35Cc... --amount 1.5 --chain evm
|
|
797
1080
|
nansen wallet send --to 9WzDXw... --amount 0.1 --chain solana --token So11...
|
|
1081
|
+
nansen wallet forget-password
|
|
798
1082
|
`);
|
|
799
1083
|
return;
|
|
800
1084
|
},
|