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/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/api.js +110 -1
- package/src/cli.js +117 -19
- package/src/schema.json +383 -196
- package/src/trading.js +5 -2
- package/src/transfer.js +1 -1
- package/src/wallet.js +95 -25
- package/src/x402.js +9 -4
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
|
|
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
|
-
*
|
|
83
|
-
*
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
config.passwordHash
|
|
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
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
503
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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>]
|
|
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
|
-
|
|
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
|
|