nansen-cli 1.11.2 → 1.13.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/src/trading.js CHANGED
@@ -8,7 +8,7 @@
8
8
  import crypto from 'crypto';
9
9
  import fs from 'fs';
10
10
  import path from 'path';
11
- import { exportWallet, getDefaultAddress, showWallet, listWallets } from './wallet.js';
11
+ import { exportWallet, getDefaultAddress, showWallet, listWallets, getWalletConfig } from './wallet.js';
12
12
  import { base58Decode } from './transfer.js';
13
13
  import { keccak256, signSecp256k1, rlpEncode } from './crypto.js';
14
14
  import { getWalletConnectAddress, sendTransactionViaWalletConnect, sendApprovalViaWalletConnect } from './walletconnect-trading.js';
@@ -957,7 +957,10 @@ EXAMPLES:
957
957
  let exported = null;
958
958
  if (!isWalletConnect) {
959
959
  // Get wallet credentials once (before the loop)
960
- const password = process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps);
960
+ const walletConfig = getWalletConfig();
961
+ const password = walletConfig.passwordHash
962
+ ? (process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps))
963
+ : null;
961
964
 
962
965
  let effectiveWalletName = walletName;
963
966
  if (!effectiveWalletName) {
package/src/transfer.js CHANGED
@@ -590,7 +590,7 @@ export async function sendTokens({ to, amount, chain, token = null, wallet = nul
590
590
  }
591
591
 
592
592
  const config = getWalletConfig();
593
- if (!verifyPassword(password, config)) throw new Error('Incorrect password');
593
+ if (config.passwordHash && !verifyPassword(password, config)) throw new Error('Incorrect password');
594
594
 
595
595
  const walletName = wallet || config.defaultWallet;
596
596
  if (!walletName) throw new Error('No wallet specified and no default wallet set');
package/src/wallet.js CHANGED
@@ -58,6 +58,10 @@ function deriveKey(password, salt) {
58
58
  * Returns a JSON-serializable object with all params needed for decryption.
59
59
  */
60
60
  export function encryptKey(privateKeyHex, password) {
61
+ if (password === null) {
62
+ return { data: privateKeyHex, encrypted: false };
63
+ }
64
+
61
65
  const salt = crypto.randomBytes(SALT_LEN);
62
66
  const iv = crypto.randomBytes(IV_LEN);
63
67
  const key = deriveKey(password, salt);
@@ -79,10 +83,31 @@ export function encryptKey(privateKeyHex, password) {
79
83
 
80
84
  /**
81
85
  * Decrypt a private key with a password.
82
- * @returns {string} Private key hex string
83
- * @throws {Error} If password is wrong
86
+ * For unencrypted wallets (encrypted: false), password is ignored and
87
+ * plaintext data is returned directly.
84
88
  */
85
89
  export function decryptKey(encryptedData, password) {
90
+ const ENCRYPTED_FIELDS = ['salt', 'iv', 'authTag', 'ciphertext'];
91
+ const hasEncryptionFields = ENCRYPTED_FIELDS.some(f => f in encryptedData);
92
+
93
+ // Unencrypted blob
94
+ if (encryptedData.encrypted === false) {
95
+ if (hasEncryptionFields) {
96
+ throw new Error('Wallet data corrupted or tampered');
97
+ }
98
+ return encryptedData.data;
99
+ }
100
+
101
+ // Encrypted blob with missing fields
102
+ if (!ENCRYPTED_FIELDS.every(f => f in encryptedData)) {
103
+ throw new Error('Wallet data corrupted or tampered');
104
+ }
105
+
106
+ // Encrypted blob but no password provided
107
+ if (password === null || password === undefined) {
108
+ throw new Error('Wallet is encrypted. Set NANSEN_WALLET_PASSWORD.');
109
+ }
110
+
86
111
  const salt = Buffer.from(encryptedData.salt, 'hex');
87
112
  const iv = Buffer.from(encryptedData.iv, 'hex');
88
113
  const authTag = Buffer.from(encryptedData.authTag, 'hex');
@@ -214,6 +239,7 @@ function getWalletFile(name) {
214
239
  */
215
240
  export function verifyPassword(password, config) {
216
241
  if (!config.passwordHash) return true; // No password set yet
242
+ if (password === null || password === undefined) return false;
217
243
  const { salt, hash } = config.passwordHash;
218
244
  const derived = crypto.scryptSync(password, Buffer.from(salt, 'hex'), 32, {
219
245
  N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: 256 * 1024 * 1024,
@@ -308,11 +334,22 @@ export function createWallet(name, password) {
308
334
  throw new Error(`Wallet "${name}" already exists`);
309
335
  }
310
336
 
311
- // If this is the first wallet, set the password hash
312
- if (!config.passwordHash) {
313
- config.passwordHash = hashPassword(password);
337
+ if (password === null) {
338
+ // Passwordless mode: reject if existing wallets are encrypted
339
+ if (config.passwordHash) {
340
+ throw new Error('Existing wallets are password-protected. Set NANSEN_WALLET_PASSWORD.');
341
+ }
314
342
  } else {
315
- if (!verifyPassword(password, config)) {
343
+ // Encrypted mode
344
+ if (!config.passwordHash) {
345
+ // First encrypted wallet: reject if passwordless wallets exist
346
+ const existingWallets = fs.readdirSync(getWalletsDir())
347
+ .filter(f => f.endsWith('.json') && f !== 'config.json');
348
+ if (existingWallets.length > 0) {
349
+ throw new Error('Existing wallets are passwordless. Cannot mix encrypted and unencrypted wallets.');
350
+ }
351
+ config.passwordHash = hashPassword(password);
352
+ } else if (!verifyPassword(password, config)) {
316
353
  throw new Error('Incorrect password');
317
354
  }
318
355
  }
@@ -380,7 +417,7 @@ export function exportWallet(name, password) {
380
417
  }
381
418
 
382
419
  const config = getWalletConfig();
383
- if (!verifyPassword(password, config)) {
420
+ if (config.passwordHash && !verifyPassword(password, config)) {
384
421
  throw new Error('Incorrect password');
385
422
  }
386
423
 
@@ -425,7 +462,7 @@ export function deleteWallet(name, password) {
425
462
  }
426
463
 
427
464
  const config = getWalletConfig();
428
- if (!verifyPassword(password, config)) {
465
+ if (config.passwordHash && !verifyPassword(password, config)) {
429
466
  throw new Error('Incorrect password');
430
467
  }
431
468
 
@@ -470,22 +507,35 @@ export function buildWalletCommands(deps = {}) {
470
507
  const handlers = {
471
508
  'create': async () => {
472
509
  const name = options.name || args[1] || 'default';
473
- const password = process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps);
474
- if (!password || password.length < 12) {
475
- log('❌ Password must be at least 12 characters');
510
+
511
+ let password;
512
+ if (flags['unsafe-no-password']) {
513
+ 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
+ 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.)');
476
519
  exit(1);
477
520
  return;
478
- }
479
-
480
- // Confirm password for first wallet (skip if set via env var)
481
- const config = getWalletConfig();
482
- if (!config.passwordHash && !process.env.NANSEN_WALLET_PASSWORD) {
483
- const confirm = await promptPassword('Confirm password: ', deps);
484
- if (password !== confirm) {
485
- log('❌ Passwords do not match');
521
+ } else {
522
+ password = process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps);
523
+ if (!password || password.length < 12) {
524
+ log('❌ Password must be at least 12 characters');
486
525
  exit(1);
487
526
  return;
488
527
  }
528
+
529
+ // Confirm password for first wallet (skip if set via env var)
530
+ const config = getWalletConfig();
531
+ if (!config.passwordHash && !process.env.NANSEN_WALLET_PASSWORD) {
532
+ const confirm = await promptPassword('Confirm password: ', deps);
533
+ if (password !== confirm) {
534
+ log('❌ Passwords do not match');
535
+ exit(1);
536
+ return;
537
+ }
538
+ }
489
539
  }
490
540
 
491
541
  try {
@@ -499,8 +549,12 @@ export function buildWalletCommands(deps = {}) {
499
549
  log(` Base (recommended, lower fees): send USDC to ${result.evm}`);
500
550
  log(` Solana: send USDC to ${result.solana}`);
501
551
  log('');
502
- log(' ⚠️ This is a hot wallet and is fundamentally insecure — do not deposit more than you can afford to lose.');
503
- log(' Store and handle your password securely, e.g. using a secrets manager or system keychain.');
552
+ if (password === null) {
553
+ log(' ⚠️ This is an UNENCRYPTED hot wallet private keys are stored in plaintext on disk.');
554
+ } else {
555
+ log(' ⚠️ This is a hot wallet and is fundamentally insecure — do not deposit more than you can afford to lose.');
556
+ log(' Store and handle your password securely, e.g. using a secrets manager or system keychain.');
557
+ }
504
558
  log('');
505
559
  return;
506
560
  } catch (err) {
@@ -553,7 +607,10 @@ export function buildWalletCommands(deps = {}) {
553
607
  exit(1);
554
608
  return;
555
609
  }
556
- const password = process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps);
610
+ const config = getWalletConfig();
611
+ const password = config.passwordHash
612
+ ? (process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps))
613
+ : null;
557
614
  try {
558
615
  const result = exportWallet(name, password);
559
616
  log(`\n⚠️ Private keys for "${result.name}" — do not share!\n`);
@@ -595,7 +652,10 @@ export function buildWalletCommands(deps = {}) {
595
652
  exit(1);
596
653
  return;
597
654
  }
598
- const password = process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps);
655
+ const config = getWalletConfig();
656
+ const password = config.passwordHash
657
+ ? (process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps))
658
+ : null;
599
659
  try {
600
660
  const result = deleteWallet(name, password);
601
661
  log(`✓ Wallet "${result.deleted}" deleted`);
@@ -638,7 +698,15 @@ export function buildWalletCommands(deps = {}) {
638
698
  }
639
699
 
640
700
  const isWalletConnect = options.wallet === 'walletconnect' || options.wallet === 'wc';
641
- const password = isWalletConnect ? null : (process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps));
701
+ let password;
702
+ if (isWalletConnect) {
703
+ password = null;
704
+ } else {
705
+ const sendConfig = getWalletConfig();
706
+ password = sendConfig.passwordHash
707
+ ? (process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps))
708
+ : null;
709
+ }
642
710
  const dryRun = flags['dry-run'] || flags.dryRun;
643
711
 
644
712
  try {
@@ -695,7 +763,8 @@ USAGE:
695
763
  nansen wallet <command> [options]
696
764
 
697
765
  COMMANDS:
698
- create [--name <label>] Create a new wallet pair (EVM + Solana)
766
+ create [--name <label>] [--unsafe-no-password]
767
+ Create a new wallet pair (EVM + Solana)
699
768
  list List all wallets
700
769
  show <name> Show wallet addresses
701
770
  export <name> Export private keys (requires password)
@@ -712,6 +781,7 @@ OPTIONS:
712
781
  --token <address> Token contract/mint address (optional, sends native if omitted)
713
782
  --wallet <name> Wallet to use (optional, uses default if omitted; use "walletconnect" or "wc" for WalletConnect, EVM only)
714
783
  --max Send entire balance (deducts gas for native transfers)
784
+ --unsafe-no-password Skip encryption — private keys stored UNENCRYPTED on disk (create only)
715
785
 
716
786
  ENVIRONMENT:
717
787
  NANSEN_WALLET_PASSWORD Password for non-interactive use (e.g. CI/scripts)
package/src/x402.js CHANGED
@@ -96,18 +96,23 @@ export async function* createPaymentSignatures(response, url, options = {}) {
96
96
  const ranked = rankRequirements(requirements);
97
97
  if (ranked.length === 0) return;
98
98
 
99
- const password = options.password || process.env.NANSEN_WALLET_PASSWORD;
100
- if (!password) return;
101
-
102
- let exportWallet, listWallets;
99
+ let exportWallet, listWallets, getWalletConfig;
103
100
  try {
104
101
  const walletMod = await import('./wallet.js');
105
102
  exportWallet = walletMod.exportWallet;
106
103
  listWallets = walletMod.listWallets;
104
+ getWalletConfig = walletMod.getWalletConfig;
107
105
  } catch {
108
106
  return;
109
107
  }
110
108
 
109
+ const walletConfig = getWalletConfig();
110
+ const password = walletConfig.passwordHash
111
+ ? (options.password || process.env.NANSEN_WALLET_PASSWORD || null)
112
+ : null;
113
+ // Encrypted wallets need a password -- silently skip if unavailable
114
+ if (walletConfig.passwordHash && password === null) return;
115
+
111
116
  const wallets = listWallets();
112
117
  if (wallets.wallets.length === 0) return;
113
118