nansen-cli 1.13.0 → 1.14.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 +19 -0
- package/package.json +1 -1
- package/src/api.js +1 -6
- package/src/chain-ids.js +11 -0
- package/src/cli.js +30 -22
- package/src/ens.js +1 -6
- package/src/keychain.js +229 -0
- package/src/schema.json +2 -0
- package/src/trading.js +26 -28
- package/src/wallet.js +248 -37
- package/src/x402.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#231](https://github.com/nansen-ai/nansen-cli/pull/231) [`c3968da`](https://github.com/nansen-ai/nansen-cli/commit/c3968dacb52521235ad6502321650148ac825d01) Thanks [@araa47](https://github.com/araa47)! - Agent-first secure wallet flow — OS keychain persistence, no interactive prompts
|
|
8
|
+
|
|
9
|
+
- **New `src/keychain.js`**: Password persistence via OS keychain (macOS Keychain / Linux secret-tool), with base64-encoded `.credentials` file fallback for containers/CI. Zero npm dependencies.
|
|
10
|
+
- **Non-interactive by default**: All readline prompts removed. Agents get structured JSON errors (`PASSWORD_REQUIRED`, `API_KEY_REQUIRED`) with actionable instructions. `--human` flag re-enables interactive mode.
|
|
11
|
+
- **Two-step wallet creation**: Agent asks user for password, runs `NANSEN_WALLET_PASSWORD=<pw> nansen wallet create`. Password auto-persists to keychain — all future operations are passwordless.
|
|
12
|
+
- **New commands**: `wallet secure` (migrate to keychain), `wallet forget-password` (clear from all stores).
|
|
13
|
+
- **Bug fixes**: Clear `passwordHash` on last wallet delete, verify password before keychain writes, exit non-zero when keychain migration fails, source-aware error messages.
|
|
14
|
+
- **New skill**: `nansen-wallet-migration` for migrating from old `~/.nansen/.env` storage to keychain.
|
|
15
|
+
|
|
16
|
+
## 1.13.1
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- [#225](https://github.com/nansen-ai/nansen-cli/pull/225) [`051e4a3`](https://github.com/nansen-ai/nansen-cli/commit/051e4a353641d000facd2133810c059981b9ac7f) Thanks [@TimNooren](https://github.com/TimNooren)! - Remove "recommended, lower fees" label from Base network in wallet create output
|
|
21
|
+
|
|
3
22
|
## 1.13.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
|
+
import { EVM_CHAINS } from './chain-ids.js';
|
|
9
10
|
|
|
10
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
|
|
@@ -255,12 +256,6 @@ const ADDRESS_PATTERNS = {
|
|
|
255
256
|
bitcoin: /^(1|3|bc1)[a-zA-HJ-NP-Z0-9]{25,62}$/,
|
|
256
257
|
};
|
|
257
258
|
|
|
258
|
-
const EVM_CHAINS = [
|
|
259
|
-
'ethereum', 'arbitrum', 'base', 'bnb', 'polygon', 'optimism',
|
|
260
|
-
'avalanche', 'linea', 'scroll', 'mantle', 'ronin',
|
|
261
|
-
'sei', 'plasma', 'sonic', 'monad', 'hyperevm', 'iotaevm'
|
|
262
|
-
];
|
|
263
|
-
|
|
264
259
|
/**
|
|
265
260
|
* Validate address format for a given chain
|
|
266
261
|
* @param {string} address - The address to validate
|
package/src/chain-ids.js
CHANGED
|
@@ -17,3 +17,14 @@ export const EVM_CHAIN_IDS = {
|
|
|
17
17
|
zksync: 324,
|
|
18
18
|
mantle: 5000,
|
|
19
19
|
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* All EVM chain names recognised by this CLI.
|
|
23
|
+
* Used for address-format validation (0x...) and ENS resolution gating.
|
|
24
|
+
* Import from here instead of defining an inline list per file.
|
|
25
|
+
*/
|
|
26
|
+
export const EVM_CHAINS = [
|
|
27
|
+
'ethereum', 'arbitrum', 'base', 'bnb', 'polygon', 'optimism',
|
|
28
|
+
'avalanche', 'linea', 'scroll', 'zksync', 'mantle', 'ronin',
|
|
29
|
+
'sei', 'plasma', 'sonic', 'unichain', 'monad', 'hyperevm', 'iotaevm',
|
|
30
|
+
];
|
package/src/cli.js
CHANGED
|
@@ -163,7 +163,7 @@ export function parseArgs(args) {
|
|
|
163
163
|
const key = arg.slice(2);
|
|
164
164
|
const next = args[i + 1];
|
|
165
165
|
|
|
166
|
-
if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich' || key === 'full') {
|
|
166
|
+
if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich' || key === 'full' || key === 'human') {
|
|
167
167
|
result.flags[key] = true;
|
|
168
168
|
} else if (next && !next.startsWith('-')) {
|
|
169
169
|
// Try to parse as JSON first (for objects/arrays/booleans),
|
|
@@ -669,8 +669,8 @@ USAGE: nansen <command> [subcommand] [options]
|
|
|
669
669
|
COMMANDS:
|
|
670
670
|
research smart-money, profiler, token, search, perp, portfolio, points
|
|
671
671
|
trade quote, execute
|
|
672
|
-
wallet create, list, show, export, default, delete
|
|
673
|
-
login Save API key (--api-key <key> or
|
|
672
|
+
wallet create, list, show, export, default, delete, forget-password
|
|
673
|
+
login Save API key (--api-key <key> or NANSEN_API_KEY env var)
|
|
674
674
|
logout Remove saved API key
|
|
675
675
|
schema JSON schema for all commands (use "nansen schema <cmd>" for one)
|
|
676
676
|
cache clear
|
|
@@ -684,7 +684,7 @@ EXAMPLES:
|
|
|
684
684
|
nansen research smart-money netflow --chain solana
|
|
685
685
|
nansen research token screener --chain solana --timeframe 24h
|
|
686
686
|
nansen research profiler balance --address 0x... --chain ethereum
|
|
687
|
-
nansen trade quote --chain base --from ETH --to USDC --amount
|
|
687
|
+
nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
|
|
688
688
|
|
|
689
689
|
Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, zksync, mantle, ronin, sei, plasma, sonic, unichain, monad, hyperevm, iotaevm
|
|
690
690
|
Trade chains: solana, base
|
|
@@ -761,43 +761,51 @@ export function buildCommands(deps = {}) {
|
|
|
761
761
|
if (flags.help || flags.h) {
|
|
762
762
|
log('nansen login - Save your Nansen API key\n');
|
|
763
763
|
log('USAGE:');
|
|
764
|
-
log(' nansen login
|
|
765
|
-
log(' nansen login
|
|
764
|
+
log(' nansen login --api-key <key>');
|
|
765
|
+
log(' NANSEN_API_KEY=<key> nansen login');
|
|
766
|
+
log(' nansen login --human (interactive prompt)\n');
|
|
766
767
|
log('OPTIONS:');
|
|
767
768
|
log(' --api-key <key> Your Nansen API key');
|
|
769
|
+
log(' --human Enable interactive prompt');
|
|
768
770
|
log(' --help Show this help\n');
|
|
769
771
|
log('Get your API key at: https://app.nansen.ai/api');
|
|
770
772
|
return;
|
|
771
773
|
}
|
|
772
774
|
|
|
773
|
-
// Support non-interactive: nansen login --api-key <key>
|
|
774
775
|
let apiKey = options['api-key'] || options.apiKey;
|
|
775
776
|
|
|
776
777
|
if (!apiKey) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
apiKey = process.env.NANSEN_API_KEY;
|
|
780
|
-
if (!apiKey) {
|
|
781
|
-
log('❌ No API key provided. Use: nansen login --api-key <key>\n Or set NANSEN_API_KEY environment variable.\n Get your API key at: https://app.nansen.ai/api');
|
|
782
|
-
exit(1);
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
} else {
|
|
786
|
-
log('Nansen CLI Login\n');
|
|
787
|
-
log('Get your API key at: https://app.nansen.ai/api\n');
|
|
788
|
-
log('Tip: For non-interactive use: nansen login --api-key <key>\n');
|
|
778
|
+
apiKey = process.env.NANSEN_API_KEY;
|
|
779
|
+
}
|
|
789
780
|
|
|
790
|
-
|
|
781
|
+
if (!apiKey && flags.human) {
|
|
782
|
+
if (!isTTY) {
|
|
783
|
+
log(JSON.stringify({
|
|
784
|
+
error: 'NOT_A_TTY',
|
|
785
|
+
message: '--human requires an interactive terminal. Use --api-key or NANSEN_API_KEY env var instead.',
|
|
786
|
+
}));
|
|
787
|
+
exit(1);
|
|
788
|
+
return;
|
|
791
789
|
}
|
|
790
|
+
log('Nansen CLI Login\n');
|
|
791
|
+
log('Get your API key at: https://app.nansen.ai/api\n');
|
|
792
|
+
apiKey = await promptFn('Enter your API key: ', true);
|
|
792
793
|
}
|
|
793
794
|
|
|
794
795
|
if (!apiKey || apiKey.trim().length === 0) {
|
|
795
|
-
log(
|
|
796
|
+
log(JSON.stringify({
|
|
797
|
+
error: 'API_KEY_REQUIRED',
|
|
798
|
+
message: 'No API key provided.',
|
|
799
|
+
resolution: [
|
|
800
|
+
'Run: nansen login --api-key <key>',
|
|
801
|
+
'Or set NANSEN_API_KEY environment variable',
|
|
802
|
+
'Get your API key at: https://app.nansen.ai/api',
|
|
803
|
+
],
|
|
804
|
+
}));
|
|
796
805
|
exit(1);
|
|
797
806
|
return;
|
|
798
807
|
}
|
|
799
808
|
|
|
800
|
-
// Save the config without validation
|
|
801
809
|
saveConfigFn({
|
|
802
810
|
apiKey: apiKey.trim(),
|
|
803
811
|
baseUrl: 'https://api.nansen.ai'
|
package/src/ens.js
CHANGED
|
@@ -6,15 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import https from 'https';
|
|
8
8
|
import { keccak256 } from './crypto.js';
|
|
9
|
+
import { EVM_CHAINS } from './chain-ids.js';
|
|
9
10
|
|
|
10
11
|
const ENS_PATTERN = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.eth$/;
|
|
11
12
|
|
|
12
|
-
const EVM_CHAINS = [
|
|
13
|
-
'ethereum', 'base', 'optimism', 'arbitrum', 'polygon', 'bnb',
|
|
14
|
-
'avalanche', 'fantom', 'gnosis', 'linea', 'scroll', 'zksync',
|
|
15
|
-
'blast', 'mantle', 'ronin', 'sei', 'plasma', 'sonic', 'unichain', 'monad', 'hyperevm', 'iotaevm'
|
|
16
|
-
];
|
|
17
|
-
|
|
18
13
|
/**
|
|
19
14
|
* Check if a string looks like an ENS name
|
|
20
15
|
*/
|
package/src/keychain.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nansen CLI - Password Persistence
|
|
3
|
+
* Stores/retrieves the wallet password, preferring the native OS credential
|
|
4
|
+
* store and falling back to a base64-encoded credentials file (not encrypted).
|
|
5
|
+
*
|
|
6
|
+
* Resolution order (read):
|
|
7
|
+
* 1. NANSEN_WALLET_PASSWORD env var
|
|
8
|
+
* 2. OS keychain (macOS Keychain / Linux secret-tool / Windows cmdkey)
|
|
9
|
+
* 3. ~/.nansen/wallets/.credentials file (insecure fallback)
|
|
10
|
+
*
|
|
11
|
+
* Storage order (write):
|
|
12
|
+
* 1. Try OS keychain first
|
|
13
|
+
* 2. Fall back to ~/.nansen/wallets/.credentials (chmod 600)
|
|
14
|
+
*
|
|
15
|
+
* Zero npm dependencies — uses native OS commands via child_process.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execFileSync } from 'child_process';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
|
|
22
|
+
const SERVICE = 'nansen-cli';
|
|
23
|
+
const ACCOUNT = 'wallet-password';
|
|
24
|
+
const TIMEOUT_MS = 5000;
|
|
25
|
+
|
|
26
|
+
function getCredentialsPath() {
|
|
27
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
28
|
+
return path.join(home, '.nansen', 'wallets', '.credentials');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============= OS Keychain =============
|
|
32
|
+
|
|
33
|
+
function keychainStore(password) {
|
|
34
|
+
try {
|
|
35
|
+
if (process.platform === 'darwin') {
|
|
36
|
+
// macOS `security` CLI requires -w <password> as argv — no stdin mode.
|
|
37
|
+
// Unlike secret-tool, omitting the value prompts from the TTY, not stdin.
|
|
38
|
+
// Exposure in process listings is brief (sub-second, execFileSync is synchronous).
|
|
39
|
+
execFileSync('/usr/bin/security', [
|
|
40
|
+
'add-generic-password',
|
|
41
|
+
'-s', SERVICE,
|
|
42
|
+
'-a', ACCOUNT,
|
|
43
|
+
'-w', password,
|
|
44
|
+
'-U',
|
|
45
|
+
], { timeout: TIMEOUT_MS, stdio: 'pipe' });
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (process.platform === 'linux') {
|
|
50
|
+
execFileSync('secret-tool', [
|
|
51
|
+
'store',
|
|
52
|
+
'--label', SERVICE,
|
|
53
|
+
'service', SERVICE,
|
|
54
|
+
'account', ACCOUNT,
|
|
55
|
+
], { input: password, timeout: TIMEOUT_MS, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Windows: no reliable built-in CLI for credential read-back.
|
|
60
|
+
// cmdkey stores but can't retrieve passwords. Falls through to .credentials file.
|
|
61
|
+
return false;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function keychainRetrieve() {
|
|
68
|
+
try {
|
|
69
|
+
if (process.platform === 'darwin') {
|
|
70
|
+
const result = execFileSync('/usr/bin/security', [
|
|
71
|
+
'find-generic-password',
|
|
72
|
+
'-s', SERVICE,
|
|
73
|
+
'-a', ACCOUNT,
|
|
74
|
+
'-w',
|
|
75
|
+
], { timeout: TIMEOUT_MS, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
76
|
+
const pw = result.toString().trim();
|
|
77
|
+
return pw || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (process.platform === 'linux') {
|
|
81
|
+
const result = execFileSync('secret-tool', [
|
|
82
|
+
'lookup',
|
|
83
|
+
'service', SERVICE,
|
|
84
|
+
'account', ACCOUNT,
|
|
85
|
+
], { timeout: TIMEOUT_MS, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
86
|
+
const pw = result.toString().trim();
|
|
87
|
+
return pw || null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Windows: no reliable built-in CLI for credential read-back.
|
|
91
|
+
return null;
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function keychainDeleteEntry() {
|
|
98
|
+
try {
|
|
99
|
+
if (process.platform === 'darwin') {
|
|
100
|
+
execFileSync('/usr/bin/security', [
|
|
101
|
+
'delete-generic-password',
|
|
102
|
+
'-s', SERVICE,
|
|
103
|
+
'-a', ACCOUNT,
|
|
104
|
+
], { timeout: TIMEOUT_MS, stdio: 'pipe' });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (process.platform === 'linux') {
|
|
109
|
+
execFileSync('secret-tool', [
|
|
110
|
+
'clear',
|
|
111
|
+
'service', SERVICE,
|
|
112
|
+
'account', ACCOUNT,
|
|
113
|
+
], { timeout: TIMEOUT_MS, stdio: 'pipe' });
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Windows: no keychain entries to delete (uses .credentials file).
|
|
118
|
+
return false;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============= Credentials File Fallback =============
|
|
125
|
+
|
|
126
|
+
function credentialsFileRead() {
|
|
127
|
+
try {
|
|
128
|
+
const filePath = getCredentialsPath();
|
|
129
|
+
if (!fs.existsSync(filePath)) return null;
|
|
130
|
+
const content = fs.readFileSync(filePath, 'utf8').trim();
|
|
131
|
+
// New format: base64-encoded (handles passwords with newlines/special chars)
|
|
132
|
+
const b64Match = content.match(/^NANSEN_WALLET_PASSWORD_B64=(.+)$/m);
|
|
133
|
+
if (b64Match) return Buffer.from(b64Match[1].trim(), 'base64').toString('utf8');
|
|
134
|
+
// Legacy format: plain text (backward compat)
|
|
135
|
+
const match = content.match(/^NANSEN_WALLET_PASSWORD=(.+)$/m);
|
|
136
|
+
return match ? match[1].trim() : null;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function credentialsFileWrite(password) {
|
|
143
|
+
try {
|
|
144
|
+
const filePath = getCredentialsPath();
|
|
145
|
+
const dir = path.dirname(filePath);
|
|
146
|
+
if (!fs.existsSync(dir)) {
|
|
147
|
+
fs.mkdirSync(dir, { mode: 0o700, recursive: true });
|
|
148
|
+
}
|
|
149
|
+
const encoded = Buffer.from(password, 'utf8').toString('base64');
|
|
150
|
+
fs.writeFileSync(filePath, `NANSEN_WALLET_PASSWORD_B64=${encoded}\n`, { mode: 0o600 });
|
|
151
|
+
return true;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function credentialsFileDelete() {
|
|
158
|
+
try {
|
|
159
|
+
const filePath = getCredentialsPath();
|
|
160
|
+
if (!fs.existsSync(filePath)) return false;
|
|
161
|
+
fs.unlinkSync(filePath);
|
|
162
|
+
return true;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============= Public API =============
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Store a password. Tries OS keychain first, falls back to .credentials file.
|
|
172
|
+
* @param {string} password
|
|
173
|
+
* @returns {{ stored: boolean, method: 'keychain'|'file'|'none' }}
|
|
174
|
+
*/
|
|
175
|
+
export function storePassword(password) {
|
|
176
|
+
if (keychainStore(password)) {
|
|
177
|
+
return { stored: true, method: 'keychain' };
|
|
178
|
+
}
|
|
179
|
+
if (credentialsFileWrite(password)) {
|
|
180
|
+
return { stored: true, method: 'file' };
|
|
181
|
+
}
|
|
182
|
+
return { stored: false, method: 'none' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Retrieve the wallet password from OS keychain or .credentials file.
|
|
187
|
+
* @returns {{ password: string|null, source: 'env'|'keychain'|'file'|null }}
|
|
188
|
+
*/
|
|
189
|
+
export function retrievePassword() {
|
|
190
|
+
const envPw = process.env.NANSEN_WALLET_PASSWORD;
|
|
191
|
+
if (envPw) return { password: envPw, source: 'env' };
|
|
192
|
+
|
|
193
|
+
const keychainPw = keychainRetrieve();
|
|
194
|
+
if (keychainPw) return { password: keychainPw, source: 'keychain' };
|
|
195
|
+
|
|
196
|
+
const filePw = credentialsFileRead();
|
|
197
|
+
if (filePw) return { password: filePw, source: 'file' };
|
|
198
|
+
|
|
199
|
+
return { password: null, source: null };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Delete the wallet password from all stores.
|
|
204
|
+
* @returns {{ keychain: boolean, file: boolean }}
|
|
205
|
+
*/
|
|
206
|
+
export function deletePassword() {
|
|
207
|
+
return {
|
|
208
|
+
keychain: keychainDeleteEntry(),
|
|
209
|
+
file: credentialsFileDelete(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Delete only the .credentials file (not the keychain entry).
|
|
215
|
+
* Used by `wallet secure` after migrating to keychain.
|
|
216
|
+
* @returns {boolean}
|
|
217
|
+
*/
|
|
218
|
+
export function deleteCredentialsFile() {
|
|
219
|
+
return credentialsFileDelete();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Resolve the wallet password from available sources.
|
|
224
|
+
* Order: env var → OS keychain → .credentials file → null
|
|
225
|
+
* @returns {string|null}
|
|
226
|
+
*/
|
|
227
|
+
export function resolvePassword() {
|
|
228
|
+
return retrievePassword().password;
|
|
229
|
+
}
|
package/src/schema.json
CHANGED
package/src/trading.js
CHANGED
|
@@ -12,6 +12,7 @@ import { exportWallet, getDefaultAddress, showWallet, listWallets, getWalletConf
|
|
|
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';
|
|
15
|
+
import { retrievePassword } from './keychain.js';
|
|
15
16
|
|
|
16
17
|
// ============= Constants =============
|
|
17
18
|
|
|
@@ -652,31 +653,15 @@ export function getWalletChainType(chainName) {
|
|
|
652
653
|
|
|
653
654
|
// ============= CLI Helpers =============
|
|
654
655
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (stdin.setRawMode) stdin.setRawMode(true);
|
|
665
|
-
stdin.resume();
|
|
666
|
-
const onData = (ch) => {
|
|
667
|
-
const c = ch.toString();
|
|
668
|
-
if (c === '\n' || c === '\r') {
|
|
669
|
-
if (stdin.setRawMode) stdin.setRawMode(wasRaw || false);
|
|
670
|
-
stdin.removeListener('data', onData);
|
|
671
|
-
process.stderr.write('\n');
|
|
672
|
-
rl.close();
|
|
673
|
-
resolve(input);
|
|
674
|
-
} else if (c === '\u0003') { rl.close(); process.exit(1); }
|
|
675
|
-
else if (c === '\u007f' || c === '\b') { input = input.slice(0, -1); }
|
|
676
|
-
else { input += c; }
|
|
677
|
-
};
|
|
678
|
-
stdin.on('data', onData);
|
|
679
|
-
});
|
|
656
|
+
function resolveTradePassword() {
|
|
657
|
+
const { password, source } = retrievePassword();
|
|
658
|
+
if (source === 'file') {
|
|
659
|
+
process.stderr.write(
|
|
660
|
+
'⚠️ Password loaded from ~/.nansen/wallets/.credentials (insecure — plaintext on disk).\n' +
|
|
661
|
+
' For better security, migrate to OS keychain: nansen wallet secure\n'
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return password;
|
|
680
665
|
}
|
|
681
666
|
|
|
682
667
|
function isNativeToken(mintAddress) {
|
|
@@ -958,9 +943,22 @@ EXAMPLES:
|
|
|
958
943
|
if (!isWalletConnect) {
|
|
959
944
|
// Get wallet credentials once (before the loop)
|
|
960
945
|
const walletConfig = getWalletConfig();
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
946
|
+
let password = null;
|
|
947
|
+
if (walletConfig.passwordHash) {
|
|
948
|
+
password = resolveTradePassword();
|
|
949
|
+
if (!password) {
|
|
950
|
+
log(JSON.stringify({
|
|
951
|
+
error: 'PASSWORD_REQUIRED',
|
|
952
|
+
message: 'Wallet is encrypted and no password was found.',
|
|
953
|
+
resolution: [
|
|
954
|
+
'Set NANSEN_WALLET_PASSWORD environment variable',
|
|
955
|
+
'Or run: nansen wallet create (password is saved to OS keychain automatically)',
|
|
956
|
+
],
|
|
957
|
+
}));
|
|
958
|
+
exit(1);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
964
962
|
|
|
965
963
|
let effectiveWalletName = walletName;
|
|
966
964
|
if (!effectiveWalletName) {
|
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
|
|
|
@@ -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
|
/**
|
|
@@ -468,12 +523,16 @@ export function deleteWallet(name, password) {
|
|
|
468
523
|
|
|
469
524
|
fs.unlinkSync(walletFile);
|
|
470
525
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
config.defaultWallet =
|
|
475
|
-
|
|
526
|
+
const remaining = fs.readdirSync(getWalletsDir()).filter(f => f.endsWith('.json') && f !== 'config.json');
|
|
527
|
+
|
|
528
|
+
if (remaining.length === 0) {
|
|
529
|
+
config.defaultWallet = null;
|
|
530
|
+
config.passwordHash = null;
|
|
531
|
+
deletePassword();
|
|
532
|
+
} else if (config.defaultWallet === name) {
|
|
533
|
+
config.defaultWallet = remaining[0].replace('.json', '');
|
|
476
534
|
}
|
|
535
|
+
saveWalletConfig(config);
|
|
477
536
|
|
|
478
537
|
return { deleted: name, newDefault: config.defaultWallet };
|
|
479
538
|
}
|
|
@@ -512,32 +571,74 @@ export function buildWalletCommands(deps = {}) {
|
|
|
512
571
|
if (flags['unsafe-no-password']) {
|
|
513
572
|
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
573
|
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
574
|
} else {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
575
|
+
// Step 1: Check env var and keychain
|
|
576
|
+
password = resolveWalletPassword();
|
|
577
|
+
|
|
578
|
+
// Step 2: If --human flag, allow interactive prompt (requires TTY)
|
|
579
|
+
if (!password && flags.human && !process.stdin.isTTY && !deps.promptFn) {
|
|
580
|
+
log(JSON.stringify({
|
|
581
|
+
error: 'NOT_A_TTY',
|
|
582
|
+
message: '--human requires an interactive terminal. Set NANSEN_WALLET_PASSWORD env var instead.',
|
|
583
|
+
}));
|
|
525
584
|
exit(1);
|
|
526
585
|
return;
|
|
527
586
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const confirm = await promptPassword('Confirm password: ', deps);
|
|
533
|
-
if (password !== confirm) {
|
|
534
|
-
log('❌ Passwords do not match');
|
|
587
|
+
if (!password && flags.human && (process.stdin.isTTY || deps.promptFn)) {
|
|
588
|
+
password = await promptPassword('Enter wallet password: ', deps);
|
|
589
|
+
if (password && password.length < 12) {
|
|
590
|
+
log('❌ Password must be at least 12 characters');
|
|
535
591
|
exit(1);
|
|
536
592
|
return;
|
|
537
593
|
}
|
|
594
|
+
if (password) {
|
|
595
|
+
const config = getWalletConfig();
|
|
596
|
+
if (!config.passwordHash) {
|
|
597
|
+
const confirm = await promptPassword('Confirm password: ', deps);
|
|
598
|
+
if (password !== confirm) {
|
|
599
|
+
log('❌ Passwords do not match');
|
|
600
|
+
exit(1);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Step 3: No password available — return structured error for agents
|
|
608
|
+
if (!password) {
|
|
609
|
+
log(JSON.stringify({
|
|
610
|
+
error: 'PASSWORD_REQUIRED',
|
|
611
|
+
message: 'A wallet password is required. Ask the user to provide one.',
|
|
612
|
+
instructions: 'Re-run with: NANSEN_WALLET_PASSWORD=<password> nansen wallet create',
|
|
613
|
+
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.',
|
|
614
|
+
}));
|
|
615
|
+
exit(1);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (password.length < 12) {
|
|
620
|
+
log('❌ Password must be at least 12 characters');
|
|
621
|
+
exit(1);
|
|
622
|
+
return;
|
|
538
623
|
}
|
|
539
624
|
}
|
|
540
625
|
|
|
626
|
+
// Verify password matches existing wallets BEFORE touching keychain
|
|
627
|
+
if (password !== null) {
|
|
628
|
+
const config = getWalletConfig();
|
|
629
|
+
if (config.passwordHash && !verifyPassword(password, config)) {
|
|
630
|
+
log('❌ Incorrect password — does not match existing wallets.');
|
|
631
|
+
exit(1);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Persist password BEFORE creating wallet so we know the storage situation
|
|
637
|
+
let storageResult = { stored: false, method: 'none' };
|
|
638
|
+
if (password !== null) {
|
|
639
|
+
storageResult = storePassword(password);
|
|
640
|
+
}
|
|
641
|
+
|
|
541
642
|
try {
|
|
542
643
|
const result = createWallet(name, password);
|
|
543
644
|
log(`\n✓ Wallet "${result.name}" created\n`);
|
|
@@ -546,14 +647,29 @@ export function buildWalletCommands(deps = {}) {
|
|
|
546
647
|
if (result.isDefault) log(` ★ Set as default wallet`);
|
|
547
648
|
log('');
|
|
548
649
|
log(' Fund this wallet to start making API calls or trading:');
|
|
549
|
-
log(` Base
|
|
650
|
+
log(` Base: send USDC to ${result.evm}`);
|
|
550
651
|
log(` Solana: send USDC to ${result.solana}`);
|
|
551
652
|
log('');
|
|
552
653
|
if (password === null) {
|
|
553
654
|
log(' ⚠️ This is an UNENCRYPTED hot wallet — private keys are stored in plaintext on disk.');
|
|
655
|
+
} else if (storageResult.stored && storageResult.method === 'keychain') {
|
|
656
|
+
log(' ✓ Password saved to system keychain (secure).');
|
|
657
|
+
log(' Future wallet operations will retrieve the password automatically.');
|
|
658
|
+
} else if (storageResult.stored && storageResult.method === 'file') {
|
|
659
|
+
log(' ⚠️ No OS keychain available. Password saved to ~/.nansen/wallets/.credentials (insecure — plaintext on disk).');
|
|
660
|
+
log(' Future wallet operations will retrieve the password automatically.');
|
|
661
|
+
log(' To improve security: migrate to OS keychain with `nansen wallet secure`,');
|
|
662
|
+
log(' or set NANSEN_WALLET_PASSWORD via a secrets manager.');
|
|
554
663
|
} else {
|
|
555
|
-
log(' ⚠️
|
|
556
|
-
log('
|
|
664
|
+
log(' ⚠️ CRITICAL: Password could not be saved anywhere (no keychain, no writable filesystem).');
|
|
665
|
+
log(' You MUST set NANSEN_WALLET_PASSWORD in your environment for ALL future wallet operations.');
|
|
666
|
+
log(' If you lose this password, your funds are UNRECOVERABLE.');
|
|
667
|
+
}
|
|
668
|
+
if (password !== null) {
|
|
669
|
+
log('');
|
|
670
|
+
log(' IMPORTANT: Back up your password separately (e.g. password manager).');
|
|
671
|
+
log(' If you lose access to this machine AND forget the password, funds are unrecoverable.');
|
|
672
|
+
log(' This is a hot wallet — do not deposit more than you can afford to lose.');
|
|
557
673
|
}
|
|
558
674
|
log('');
|
|
559
675
|
return;
|
|
@@ -608,9 +724,12 @@ export function buildWalletCommands(deps = {}) {
|
|
|
608
724
|
return;
|
|
609
725
|
}
|
|
610
726
|
const config = getWalletConfig();
|
|
611
|
-
const password = config
|
|
612
|
-
|
|
613
|
-
|
|
727
|
+
const { password, error } = await resolvePasswordForCommand(config, flags, deps);
|
|
728
|
+
if (error) {
|
|
729
|
+
log(error);
|
|
730
|
+
exit(1);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
614
733
|
try {
|
|
615
734
|
const result = exportWallet(name, password);
|
|
616
735
|
log(`\n⚠️ Private keys for "${result.name}" — do not share!\n`);
|
|
@@ -653,9 +772,12 @@ export function buildWalletCommands(deps = {}) {
|
|
|
653
772
|
return;
|
|
654
773
|
}
|
|
655
774
|
const config = getWalletConfig();
|
|
656
|
-
const password = config
|
|
657
|
-
|
|
658
|
-
|
|
775
|
+
const { password, error } = await resolvePasswordForCommand(config, flags, deps);
|
|
776
|
+
if (error) {
|
|
777
|
+
log(error);
|
|
778
|
+
exit(1);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
659
781
|
try {
|
|
660
782
|
const result = deleteWallet(name, password);
|
|
661
783
|
log(`✓ Wallet "${result.deleted}" deleted`);
|
|
@@ -703,9 +825,13 @@ export function buildWalletCommands(deps = {}) {
|
|
|
703
825
|
password = null;
|
|
704
826
|
} else {
|
|
705
827
|
const sendConfig = getWalletConfig();
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
828
|
+
const resolved = await resolvePasswordForCommand(sendConfig, flags, deps);
|
|
829
|
+
if (resolved.error) {
|
|
830
|
+
log(resolved.error);
|
|
831
|
+
exit(1);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
password = resolved.password;
|
|
709
835
|
}
|
|
710
836
|
const dryRun = flags['dry-run'] || flags.dryRun;
|
|
711
837
|
|
|
@@ -723,7 +849,6 @@ export function buildWalletCommands(deps = {}) {
|
|
|
723
849
|
};
|
|
724
850
|
|
|
725
851
|
if (dryRun) {
|
|
726
|
-
// Build the transaction but don't broadcast
|
|
727
852
|
const result = await sendTokens(sendOpts);
|
|
728
853
|
log(`\nDry run — transaction not broadcast\n`);
|
|
729
854
|
log(` From: ${result.from}`);
|
|
@@ -755,6 +880,83 @@ export function buildWalletCommands(deps = {}) {
|
|
|
755
880
|
}
|
|
756
881
|
},
|
|
757
882
|
|
|
883
|
+
'forget-password': async () => {
|
|
884
|
+
const result = deletePassword();
|
|
885
|
+
if (result.keychain || result.file) {
|
|
886
|
+
log('✓ Password removed from:');
|
|
887
|
+
if (result.keychain) log(' - System keychain');
|
|
888
|
+
if (result.file) log(' - Credentials file (~/.nansen/wallets/.credentials)');
|
|
889
|
+
} else {
|
|
890
|
+
log('No saved password found (keychain or credentials file).');
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
|
|
894
|
+
'secure': async () => {
|
|
895
|
+
const { password, source } = retrievePassword();
|
|
896
|
+
if (!password) {
|
|
897
|
+
log(JSON.stringify({
|
|
898
|
+
error: 'NO_PASSWORD_FOUND',
|
|
899
|
+
message: 'No wallet password found in any store.',
|
|
900
|
+
resolution: [
|
|
901
|
+
'Set NANSEN_WALLET_PASSWORD and run: nansen wallet secure',
|
|
902
|
+
'This will store it in the OS keychain.',
|
|
903
|
+
],
|
|
904
|
+
}));
|
|
905
|
+
exit(1);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (source === 'keychain') {
|
|
910
|
+
log('✓ Password is already stored in the OS keychain (secure).');
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Verify password actually decrypts wallets before overwriting keychain
|
|
915
|
+
const walletConfig = getWalletConfig();
|
|
916
|
+
if (walletConfig.passwordHash && !verifyPassword(password, walletConfig)) {
|
|
917
|
+
log(JSON.stringify({
|
|
918
|
+
error: 'INCORRECT_PASSWORD',
|
|
919
|
+
message: `Password from '${source}' does not match the wallet's stored hash.`,
|
|
920
|
+
resolution: source === 'file'
|
|
921
|
+
? [
|
|
922
|
+
'The password in ~/.nansen/wallets/.credentials is incorrect.',
|
|
923
|
+
'Run: nansen wallet forget-password then re-run with the correct password: NANSEN_WALLET_PASSWORD=<pw> nansen wallet secure',
|
|
924
|
+
]
|
|
925
|
+
: [
|
|
926
|
+
'Unset NANSEN_WALLET_PASSWORD if it is stale, then re-run: nansen wallet secure',
|
|
927
|
+
],
|
|
928
|
+
}));
|
|
929
|
+
exit(1);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Try to migrate to keychain
|
|
934
|
+
const { stored, method } = storePassword(password);
|
|
935
|
+
if (stored && method === 'keychain') {
|
|
936
|
+
const fileRemoved = deleteCredentialsFile();
|
|
937
|
+
const fromLabel = source === 'file'
|
|
938
|
+
? '~/.nansen/wallets/.credentials file'
|
|
939
|
+
: 'NANSEN_WALLET_PASSWORD env var';
|
|
940
|
+
log(`✓ Password migrated from ${fromLabel} → OS keychain (secure).`);
|
|
941
|
+
if (fileRemoved) {
|
|
942
|
+
log(' Removed ~/.nansen/wallets/.credentials.');
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
log(JSON.stringify({
|
|
946
|
+
error: 'KEYCHAIN_UNAVAILABLE',
|
|
947
|
+
message: source === 'file'
|
|
948
|
+
? 'OS keychain is not available. Password remains in ~/.nansen/wallets/.credentials (insecure).'
|
|
949
|
+
: 'OS keychain is not available. Password is only in the NANSEN_WALLET_PASSWORD env var (not persisted).',
|
|
950
|
+
resolution: [
|
|
951
|
+
'Set NANSEN_WALLET_PASSWORD in a secrets manager or system keyring',
|
|
952
|
+
'Use a containerized secrets agent (e.g. Vault, 1Password CLI)',
|
|
953
|
+
],
|
|
954
|
+
}));
|
|
955
|
+
exit(1);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
},
|
|
959
|
+
|
|
758
960
|
'help': async () => {
|
|
759
961
|
log(`
|
|
760
962
|
Wallet Management - Local key storage for EVM and Solana
|
|
@@ -772,6 +974,8 @@ COMMANDS:
|
|
|
772
974
|
delete <name> Delete a wallet (requires password)
|
|
773
975
|
send --to <address> --amount <number> --chain <evm|solana> [--token <address>] [--wallet <name>] [--max] [--dry-run]
|
|
774
976
|
Send tokens or native currency (--max sends entire balance, --dry-run previews without sending)
|
|
977
|
+
forget-password Remove saved password from all stores
|
|
978
|
+
secure Migrate password from insecure storage to OS keychain
|
|
775
979
|
|
|
776
980
|
OPTIONS:
|
|
777
981
|
--name <label> Wallet name (default: "default")
|
|
@@ -782,19 +986,26 @@ OPTIONS:
|
|
|
782
986
|
--wallet <name> Wallet to use (optional, uses default if omitted; use "walletconnect" or "wc" for WalletConnect, EVM only)
|
|
783
987
|
--max Send entire balance (deducts gas for native transfers)
|
|
784
988
|
--unsafe-no-password Skip encryption — private keys stored UNENCRYPTED on disk (create only)
|
|
989
|
+
--human Enable interactive prompts (for human terminal use only)
|
|
990
|
+
|
|
991
|
+
PASSWORD RESOLUTION (automatic, in order):
|
|
992
|
+
1. NANSEN_WALLET_PASSWORD env var
|
|
993
|
+
2. OS keychain (saved automatically on wallet create)
|
|
994
|
+
3. Interactive prompt (only with --human flag)
|
|
785
995
|
|
|
786
996
|
ENVIRONMENT:
|
|
787
|
-
NANSEN_WALLET_PASSWORD
|
|
997
|
+
NANSEN_WALLET_PASSWORD Wallet encryption password
|
|
788
998
|
NANSEN_EVM_RPC Custom EVM RPC endpoint
|
|
789
999
|
NANSEN_SOLANA_RPC Custom Solana RPC endpoint
|
|
790
1000
|
|
|
791
1001
|
EXAMPLES:
|
|
792
|
-
nansen wallet create --name trading
|
|
1002
|
+
NANSEN_WALLET_PASSWORD=mypass nansen wallet create --name trading
|
|
793
1003
|
nansen wallet list
|
|
794
1004
|
nansen wallet export trading
|
|
795
1005
|
nansen wallet default trading
|
|
796
1006
|
nansen wallet send --to 0x742d35Cc... --amount 1.5 --chain evm
|
|
797
1007
|
nansen wallet send --to 9WzDXw... --amount 0.1 --chain solana --token So11...
|
|
1008
|
+
nansen wallet forget-password
|
|
798
1009
|
`);
|
|
799
1010
|
return;
|
|
800
1011
|
},
|
package/src/x402.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
fetchRecentBlockhash,
|
|
12
12
|
getSolanaRpcUrl,
|
|
13
13
|
} from './x402-svm.js';
|
|
14
|
+
import { resolvePassword } from './keychain.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Parse PaymentRequirements from a 402 response.
|
|
@@ -108,9 +109,8 @@ export async function* createPaymentSignatures(response, url, options = {}) {
|
|
|
108
109
|
|
|
109
110
|
const walletConfig = getWalletConfig();
|
|
110
111
|
const password = walletConfig.passwordHash
|
|
111
|
-
? (options.password ||
|
|
112
|
+
? (options.password || resolvePassword() || null)
|
|
112
113
|
: null;
|
|
113
|
-
// Encrypted wallets need a password -- silently skip if unavailable
|
|
114
114
|
if (walletConfig.passwordHash && password === null) return;
|
|
115
115
|
|
|
116
116
|
const wallets = listWallets();
|