nansen-cli 1.11.1 → 1.12.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/package.json +10 -3
- package/src/api.js +3 -1
- package/src/cli.js +58 -8
- package/src/crypto.js +49 -176
- package/src/schema.json +3 -1
- package/src/trading.js +5 -2
- package/src/transfer.js +3 -16
- package/src/wallet.js +98 -45
- package/src/x402-svm.js +4 -44
- package/src/x402.js +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#207](https://github.com/nansen-ai/nansen-cli/pull/207) [`73ca500`](https://github.com/nansen-ai/nansen-cli/commit/73ca5009c03ad541673165ca6b50f33ff4cc1673) Thanks [@TimNooren](https://github.com/TimNooren)! - Add --unsafe-no-password flag to wallet create for agent-friendly passwordless wallets.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#212](https://github.com/nansen-ai/nansen-cli/pull/212) [`726c29d`](https://github.com/nansen-ai/nansen-cli/commit/726c29d2676c8a37772299c6237b44890493dfa5) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Clarify empty input handling in parseAddressList with explicit early return
|
|
12
|
+
|
|
13
|
+
- [#218](https://github.com/nansen-ai/nansen-cli/pull/218) [`8c4dd71`](https://github.com/nansen-ai/nansen-cli/commit/8c4dd71ce215026e149a6b097540c46622f13d3a) Thanks [@TimNooren](https://github.com/TimNooren)! - fix: `nansen changelog --since <version>` now correctly filters changeset-format entries (## x.y.z) in addition to Keep a Changelog entries (## [x.y.z])
|
|
14
|
+
|
|
15
|
+
- [#209](https://github.com/nansen-ai/nansen-cli/pull/209) [`a6dc1ed`](https://github.com/nansen-ai/nansen-cli/commit/a6dc1ed9dc40ad3506e7debe09746d463a70c14d) Thanks [@0xlaveen](https://github.com/0xlaveen)! - fix: prevent --help from executing destructive commands (logout, schema, cache)
|
|
16
|
+
|
|
17
|
+
## 1.11.2
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [#205](https://github.com/nansen-ai/nansen-cli/pull/205) [`dba24aa`](https://github.com/nansen-ai/nansen-cli/commit/dba24aaa64b2083fdbaa85a002758bfe21d9f4a0) Thanks [@TimNooren](https://github.com/TimNooren)! - Add hot wallet and password handling warnings to wallet create output
|
|
22
|
+
|
|
3
23
|
## 1.11.1
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nansen-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Command-line interface for Nansen API - designed for AI agents",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"test:watch": "vitest",
|
|
20
20
|
"test:coverage": "vitest run --coverage",
|
|
21
21
|
"test:live": "NANSEN_LIVE_TEST=1 vitest run",
|
|
22
|
-
"test:trade": "vitest run --config vitest.e2e.config.js",
|
|
22
|
+
"test:trade": "vitest run --config vitest.e2e.config.js src/__tests__/trade.e2e.test.js",
|
|
23
|
+
"test:send": "vitest run --config vitest.e2e.config.js src/__tests__/send.e2e.test.js",
|
|
23
24
|
"lint": "eslint .",
|
|
24
25
|
"lint:fix": "eslint . --fix",
|
|
25
26
|
"changeset": "changeset",
|
|
@@ -60,5 +61,11 @@
|
|
|
60
61
|
"eslint": "^10.0.2",
|
|
61
62
|
"globals": "^17.4.0",
|
|
62
63
|
"vitest": "^4.0.18"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@ethereumjs/rlp": "^10.1.1",
|
|
67
|
+
"@noble/curves": "^2.0.1",
|
|
68
|
+
"@noble/hashes": "^2.0.1",
|
|
69
|
+
"@scure/base": "^2.0.0"
|
|
63
70
|
}
|
|
64
|
-
}
|
|
71
|
+
}
|
package/src/api.js
CHANGED
|
@@ -582,7 +582,9 @@ export class NansenAPI {
|
|
|
582
582
|
// No API key and no payment wallet — guide the user to login rather than
|
|
583
583
|
// showing a confusing x402 payment dump they can't act on.
|
|
584
584
|
// TODO: full fix would skip x402 entirely when no apiKey is set — see PR #<this PR number>
|
|
585
|
-
message = 'No API key configured.
|
|
585
|
+
message = 'No API key configured. Two ways to authenticate:\n' +
|
|
586
|
+
' 1. API key: nansen login --api-key <key> (get key at https://app.nansen.ai/api)\n' +
|
|
587
|
+
' 2. x402 micropayment: nansen wallet create + fund with USDC (no API key needed)';
|
|
586
588
|
} else {
|
|
587
589
|
message = `x402 auto-payment failed: ${x402Err.message}`;
|
|
588
590
|
}
|
package/src/cli.js
CHANGED
|
@@ -439,6 +439,37 @@ async function enrichTransfers(result, apiInstance, chain) {
|
|
|
439
439
|
return result;
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
+
// ============= Address Parsing =============
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Parse an --addresses option that may arrive as:
|
|
446
|
+
* - a pre-parsed array (arg parser split it)
|
|
447
|
+
* - a JSON array string: '["0x…","0x…"]'
|
|
448
|
+
* - a comma-separated string: "0x…,0x…"
|
|
449
|
+
* Non-array JSON values (objects, numbers, booleans) are rejected.
|
|
450
|
+
*/
|
|
451
|
+
export function parseAddressList(raw) {
|
|
452
|
+
if (Array.isArray(raw)) {
|
|
453
|
+
return raw.map(a => String(a).trim()).filter(Boolean);
|
|
454
|
+
}
|
|
455
|
+
if (!raw) return [];
|
|
456
|
+
|
|
457
|
+
const s = String(raw);
|
|
458
|
+
try {
|
|
459
|
+
const parsed = JSON.parse(s);
|
|
460
|
+
if (Array.isArray(parsed)) {
|
|
461
|
+
return parsed.map(a => String(a).trim()).filter(Boolean);
|
|
462
|
+
}
|
|
463
|
+
throw new NansenError(
|
|
464
|
+
'--addresses must be a comma-separated list or JSON array, got: ' + typeof parsed,
|
|
465
|
+
ErrorCode.INVALID_PARAMS
|
|
466
|
+
);
|
|
467
|
+
} catch (e) {
|
|
468
|
+
if (e instanceof NansenError) throw e;
|
|
469
|
+
return s.split(',').map(a => a.trim()).filter(Boolean);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
442
473
|
// ============= Composite Functions =============
|
|
443
474
|
|
|
444
475
|
export async function batchProfile(api, params = {}) {
|
|
@@ -644,7 +675,7 @@ EXAMPLES:
|
|
|
644
675
|
nansen research profiler balance --address 0x... --chain ethereum
|
|
645
676
|
nansen trade quote --chain base --from ETH --to USDC --amount 1
|
|
646
677
|
|
|
647
|
-
Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
|
|
678
|
+
Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, zksync, mantle, ronin, sei, plasma, sonic, unichain, monad, hyperevm, iotaevm
|
|
648
679
|
Trade chains: solana, base
|
|
649
680
|
Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
|
|
650
681
|
|
|
@@ -780,6 +811,10 @@ export function buildCommands(deps = {}) {
|
|
|
780
811
|
},
|
|
781
812
|
|
|
782
813
|
'changelog': async (_args, _apiInstance, _flags, _options) => {
|
|
814
|
+
if (_flags.help || _flags.h) {
|
|
815
|
+
log('changelog — Show release history\n\nUsage:\n nansen changelog [--since <version>]\n\nOptions:\n --since <version> Show only entries for versions >= this version\n\nExamples:\n nansen changelog\n nansen changelog --since 1.10.0');
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
783
818
|
const changelogPath = new URL('../CHANGELOG.md', import.meta.url).pathname;
|
|
784
819
|
let content;
|
|
785
820
|
try {
|
|
@@ -795,10 +830,10 @@ export function buildCommands(deps = {}) {
|
|
|
795
830
|
const filtered = [];
|
|
796
831
|
let include = false;
|
|
797
832
|
for (const line of lines) {
|
|
798
|
-
// Match ## [x.y.z]
|
|
799
|
-
const match = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
|
|
833
|
+
// Match ## [x.y.z] (Keep a Changelog format) or ## x.y.z (changeset format)
|
|
834
|
+
const match = line.match(/^## \[(\d+\.\d+\.\d+)\]|^## (\d+\.\d+\.\d+)\b/);
|
|
800
835
|
if (match) {
|
|
801
|
-
const ver = match[1];
|
|
836
|
+
const ver = match[1] || match[2];
|
|
802
837
|
// Compare: include versions >= since, stop at versions < since
|
|
803
838
|
if (compareSemver(ver, since) >= 0) {
|
|
804
839
|
include = true;
|
|
@@ -945,7 +980,7 @@ export function buildCommands(deps = {}) {
|
|
|
945
980
|
'batch': () => {
|
|
946
981
|
let addresses = [];
|
|
947
982
|
if (options.addresses) {
|
|
948
|
-
addresses = options.addresses
|
|
983
|
+
addresses = parseAddressList(options.addresses);
|
|
949
984
|
} else if (options.file) {
|
|
950
985
|
const content = fs.readFileSync(options.file, 'utf8');
|
|
951
986
|
try {
|
|
@@ -976,13 +1011,13 @@ export function buildCommands(deps = {}) {
|
|
|
976
1011
|
return traceCounterparties(apiInstance, { address, chain, depth, width, days, delayMs });
|
|
977
1012
|
},
|
|
978
1013
|
'compare': () => {
|
|
979
|
-
const addrs = (options.addresses
|
|
1014
|
+
const addrs = parseAddressList(options.addresses);
|
|
980
1015
|
return compareWallets(apiInstance, { addresses: addrs, chain, days });
|
|
981
1016
|
},
|
|
982
1017
|
'help': () => ({
|
|
983
1018
|
commands: ['balance', 'labels', 'transactions', 'pnl', 'search', 'historical-balances', 'related-wallets', 'counterparties', 'pnl-summary', 'perp-positions', 'perp-trades', 'batch', 'trace', 'compare'],
|
|
984
1019
|
description: 'Wallet profiling endpoints',
|
|
985
|
-
example: 'nansen profiler
|
|
1020
|
+
example: 'nansen research profiler compare --addresses "0xABC...,0xDEF..." --chain ethereum'
|
|
986
1021
|
})
|
|
987
1022
|
};
|
|
988
1023
|
|
|
@@ -1294,7 +1329,8 @@ export function generateSubcommandHelp(command, subcommand) {
|
|
|
1294
1329
|
|
|
1295
1330
|
const exampleValues = { address: '0x...', token: '0x...', query: '"term"', symbol: 'BTC', date: '2024-01-01' };
|
|
1296
1331
|
const chain = subSchema.options?.chain?.default || 'solana';
|
|
1297
|
-
|
|
1332
|
+
const prefix = DEPRECATED_TO_RESEARCH.has(command) ? `research ${command}` : command;
|
|
1333
|
+
let example = `nansen ${prefix} ${subcommand}`;
|
|
1298
1334
|
if (subSchema.options) {
|
|
1299
1335
|
for (const [name, opt] of Object.entries(subSchema.options)) {
|
|
1300
1336
|
if (opt.required) example += ` --${name} ${exampleValues[name] || '<val>'}`;
|
|
@@ -1408,6 +1444,20 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1408
1444
|
return { type: 'command-help', command };
|
|
1409
1445
|
}
|
|
1410
1446
|
}
|
|
1447
|
+
// Simple commands (logout, schema, cache) — show help instead of executing
|
|
1448
|
+
// Prevents destructive commands like logout from running when user just wants help
|
|
1449
|
+
if (commands[command]) {
|
|
1450
|
+
const simpleHelp = {
|
|
1451
|
+
'logout': 'nansen logout — Remove saved API key from ~/.nansen/config.json',
|
|
1452
|
+
'schema': 'nansen schema [command] [--pretty] — Show JSON schema for all commands (or a specific command)',
|
|
1453
|
+
'cache': 'nansen cache clear — Clear the API response cache',
|
|
1454
|
+
};
|
|
1455
|
+
if (simpleHelp[command]) {
|
|
1456
|
+
output(simpleHelp[command]);
|
|
1457
|
+
notify();
|
|
1458
|
+
return { type: 'command-help', command };
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1411
1461
|
// Commands with handlers (e.g. quote, execute) show their own usage
|
|
1412
1462
|
if (command === 'help' || !commands[command]) {
|
|
1413
1463
|
output(BANNER + HELP);
|
package/src/crypto.js
CHANGED
|
@@ -1,178 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared cryptographic primitives for EVM transaction signing.
|
|
3
3
|
* Exports keccak256, secp256k1 ECDSA signing, RLP encoding.
|
|
4
|
-
*
|
|
4
|
+
* Uses audited libraries: @noble/hashes, @noble/curves, @ethereumjs/rlp.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
8
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
9
|
+
import { RLP } from "@ethereumjs/rlp";
|
|
8
10
|
|
|
9
11
|
// ============= Keccak-256 =============
|
|
10
12
|
|
|
11
|
-
// Keccak-256 (NOT SHA3-256; Ethereum uses original Keccak with 0x01 padding).
|
|
12
|
-
// Uses a flat 25-element state array (lanes indexed as state[x + 5*y])
|
|
13
|
-
// with BigInt64 arithmetic.
|
|
14
|
-
|
|
15
|
-
const RC = [
|
|
16
|
-
0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
|
|
17
|
-
0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
|
|
18
|
-
0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
|
|
19
|
-
0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
|
|
20
|
-
0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
|
|
21
|
-
0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
const ROT = [
|
|
25
|
-
0, 1, 62, 28, 27,
|
|
26
|
-
36, 44, 6, 55, 20,
|
|
27
|
-
3, 10, 43, 25, 39,
|
|
28
|
-
41, 45, 15, 21, 8,
|
|
29
|
-
18, 2, 61, 56, 14,
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const M = 0xffffffffffffffffn;
|
|
33
|
-
|
|
34
|
-
function rot64(v, r) {
|
|
35
|
-
return r === 0 ? v : ((v << BigInt(r)) | (v >> BigInt(64 - r))) & M;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function keccakF(s) {
|
|
39
|
-
for (let round = 0; round < 24; round++) {
|
|
40
|
-
const c0 = s[0] ^ s[5] ^ s[10] ^ s[15] ^ s[20];
|
|
41
|
-
const c1 = s[1] ^ s[6] ^ s[11] ^ s[16] ^ s[21];
|
|
42
|
-
const c2 = s[2] ^ s[7] ^ s[12] ^ s[17] ^ s[22];
|
|
43
|
-
const c3 = s[3] ^ s[8] ^ s[13] ^ s[18] ^ s[23];
|
|
44
|
-
const c4 = s[4] ^ s[9] ^ s[14] ^ s[19] ^ s[24];
|
|
45
|
-
const d0 = (c4 ^ rot64(c1, 1)) & M;
|
|
46
|
-
const d1 = (c0 ^ rot64(c2, 1)) & M;
|
|
47
|
-
const d2 = (c1 ^ rot64(c3, 1)) & M;
|
|
48
|
-
const d3 = (c2 ^ rot64(c4, 1)) & M;
|
|
49
|
-
const d4 = (c3 ^ rot64(c0, 1)) & M;
|
|
50
|
-
for (let y = 0; y < 25; y += 5) {
|
|
51
|
-
s[y] = (s[y] ^ d0) & M;
|
|
52
|
-
s[y + 1] = (s[y + 1] ^ d1) & M;
|
|
53
|
-
s[y + 2] = (s[y + 2] ^ d2) & M;
|
|
54
|
-
s[y + 3] = (s[y + 3] ^ d3) & M;
|
|
55
|
-
s[y + 4] = (s[y + 4] ^ d4) & M;
|
|
56
|
-
}
|
|
57
|
-
const t = new Array(25);
|
|
58
|
-
for (let x = 0; x < 5; x++) {
|
|
59
|
-
for (let y = 0; y < 5; y++) {
|
|
60
|
-
const src = x + 5 * y;
|
|
61
|
-
const dst = y + 5 * ((2 * x + 3 * y) % 5);
|
|
62
|
-
t[dst] = rot64(s[src], ROT[src]);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
for (let y = 0; y < 25; y += 5) {
|
|
66
|
-
const t0 = t[y], t1 = t[y+1], t2 = t[y+2], t3 = t[y+3], t4 = t[y+4];
|
|
67
|
-
s[y] = (t0 ^ ((~t1 & M) & t2)) & M;
|
|
68
|
-
s[y+1] = (t1 ^ ((~t2 & M) & t3)) & M;
|
|
69
|
-
s[y+2] = (t2 ^ ((~t3 & M) & t4)) & M;
|
|
70
|
-
s[y+3] = (t3 ^ ((~t4 & M) & t0)) & M;
|
|
71
|
-
s[y+4] = (t4 ^ ((~t0 & M) & t1)) & M;
|
|
72
|
-
}
|
|
73
|
-
s[0] = (s[0] ^ RC[round]) & M;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
13
|
export function keccak256(input) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const blocks = Math.max(1, Math.ceil((input.length + 1) / rate));
|
|
81
|
-
const padded = Buffer.alloc(blocks * rate);
|
|
82
|
-
input.copy(padded);
|
|
83
|
-
padded[input.length] ^= 0x01;
|
|
84
|
-
padded[padded.length - 1] ^= 0x80;
|
|
85
|
-
for (let off = 0; off < padded.length; off += rate) {
|
|
86
|
-
for (let i = 0; i < 17; i++) {
|
|
87
|
-
s[i] ^= padded.readBigUInt64LE(off + i * 8);
|
|
88
|
-
}
|
|
89
|
-
keccakF(s);
|
|
90
|
-
}
|
|
91
|
-
const out = Buffer.alloc(32);
|
|
92
|
-
for (let i = 0; i < 4; i++) {
|
|
93
|
-
out.writeBigUInt64LE(s[i] & M, i * 8);
|
|
94
|
-
}
|
|
95
|
-
return out;
|
|
14
|
+
const hash = keccak_256(input);
|
|
15
|
+
return Buffer.from(hash);
|
|
96
16
|
}
|
|
97
17
|
|
|
98
18
|
// ============= secp256k1 ECDSA =============
|
|
99
19
|
|
|
100
|
-
const P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2Fn;
|
|
101
|
-
const N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n;
|
|
102
|
-
const Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798n;
|
|
103
|
-
const Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8n;
|
|
104
|
-
|
|
105
|
-
function modInv(a, m) {
|
|
106
|
-
let [old_r, r] = [((a % m) + m) % m, m];
|
|
107
|
-
let [old_s, s] = [1n, 0n];
|
|
108
|
-
while (r !== 0n) {
|
|
109
|
-
const q = old_r / r;
|
|
110
|
-
[old_r, r] = [r, old_r - q * r];
|
|
111
|
-
[old_s, s] = [s, old_s - q * s];
|
|
112
|
-
}
|
|
113
|
-
return ((old_s % m) + m) % m;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function ptAdd(x1, y1, x2, y2) {
|
|
117
|
-
if (x1 === null) return [x2, y2];
|
|
118
|
-
if (x2 === null) return [x1, y1];
|
|
119
|
-
if (x1 === x2 && y1 === y2) {
|
|
120
|
-
const lam = (3n * x1 * x1 * modInv(2n * y1, P)) % P;
|
|
121
|
-
const x3 = ((lam * lam - 2n * x1) % P + P) % P;
|
|
122
|
-
return [x3, ((lam * (x1 - x3) - y1) % P + P) % P];
|
|
123
|
-
}
|
|
124
|
-
if (x1 === x2) return [null, null];
|
|
125
|
-
const lam = (((y2 - y1) % P + P) * modInv(((x2 - x1) % P + P) % P, P)) % P;
|
|
126
|
-
const x3 = ((lam * lam - x1 - x2) % P + P) % P;
|
|
127
|
-
return [x3, ((lam * (x1 - x3) - y1) % P + P) % P];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function ptMul(k, x, y) {
|
|
131
|
-
let [rx, ry] = [null, null];
|
|
132
|
-
let [qx, qy] = [x, y];
|
|
133
|
-
while (k > 0n) {
|
|
134
|
-
if (k & 1n) [rx, ry] = ptAdd(rx, ry, qx, qy);
|
|
135
|
-
[qx, qy] = ptAdd(qx, qy, qx, qy);
|
|
136
|
-
k >>= 1n;
|
|
137
|
-
}
|
|
138
|
-
return [rx, ry];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function rfc6979k(privBuf, hash) {
|
|
142
|
-
let v = Buffer.alloc(32, 0x01);
|
|
143
|
-
let k = Buffer.alloc(32, 0x00);
|
|
144
|
-
k = crypto.createHmac('sha256', k).update(Buffer.concat([v, Buffer.from([0x00]), privBuf, hash])).digest();
|
|
145
|
-
v = crypto.createHmac('sha256', k).update(v).digest();
|
|
146
|
-
k = crypto.createHmac('sha256', k).update(Buffer.concat([v, Buffer.from([0x01]), privBuf, hash])).digest();
|
|
147
|
-
v = crypto.createHmac('sha256', k).update(v).digest();
|
|
148
|
-
while (true) {
|
|
149
|
-
v = crypto.createHmac('sha256', k).update(v).digest();
|
|
150
|
-
const candidate = BigInt('0x' + v.toString('hex'));
|
|
151
|
-
if (candidate >= 1n && candidate < N) return candidate;
|
|
152
|
-
k = crypto.createHmac('sha256', k).update(Buffer.concat([v, Buffer.from([0x00])])).digest();
|
|
153
|
-
v = crypto.createHmac('sha256', k).update(v).digest();
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
20
|
/**
|
|
158
21
|
* Sign a 32-byte hash with secp256k1 ECDSA.
|
|
159
22
|
* Uses RFC 6979 deterministic k and low-S normalization (EIP-2).
|
|
160
23
|
* Returns { r, s, v } where v is the recovery ID (0 or 1).
|
|
161
24
|
*/
|
|
162
25
|
export function signSecp256k1(hash, privateKey) {
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
let
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
26
|
+
const sigBytes = secp256k1.sign(hash, privateKey, { prehash: false, lowS: true });
|
|
27
|
+
const sig = secp256k1.Signature.fromBytes(sigBytes);
|
|
28
|
+
const pubKey = secp256k1.getPublicKey(privateKey, false);
|
|
29
|
+
const pubKeyHex = Buffer.from(pubKey).toString("hex");
|
|
30
|
+
|
|
31
|
+
// Determine recovery bit by trying both values
|
|
32
|
+
let v = 0;
|
|
33
|
+
for (const bit of [0, 1]) {
|
|
34
|
+
const recovered = sig.addRecoveryBit(bit).recoverPublicKey(hash);
|
|
35
|
+
if (Buffer.from(recovered.toBytes(false)).toString("hex") === pubKeyHex) {
|
|
36
|
+
v = bit;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
173
41
|
return {
|
|
174
|
-
r: Buffer.from(r.toString(16).padStart(64,
|
|
175
|
-
s: Buffer.from(s.toString(16).padStart(64,
|
|
42
|
+
r: Buffer.from(sig.r.toString(16).padStart(64, "0"), "hex"),
|
|
43
|
+
s: Buffer.from(sig.s.toString(16).padStart(64, "0"), "hex"),
|
|
176
44
|
v,
|
|
177
45
|
};
|
|
178
46
|
}
|
|
@@ -182,34 +50,39 @@ export function signSecp256k1(hash, privateKey) {
|
|
|
182
50
|
export function bigIntToMinBuf(n) {
|
|
183
51
|
if (n === 0n) return Buffer.alloc(0);
|
|
184
52
|
const hex = n.toString(16);
|
|
185
|
-
return Buffer.from(hex.length % 2 ?
|
|
53
|
+
return Buffer.from(hex.length % 2 ? "0" + hex : hex, "hex");
|
|
186
54
|
}
|
|
187
55
|
|
|
188
56
|
export function rlpEncode(input) {
|
|
57
|
+
return Buffer.from(RLP.encode(toRlpInput(input)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Convert our legacy input format to what @ethereumjs/rlp expects.
|
|
62
|
+
* Handles: arrays (recursive), Buffers, hex strings, empty values.
|
|
63
|
+
*/
|
|
64
|
+
function toRlpInput(input) {
|
|
189
65
|
if (Array.isArray(input)) {
|
|
190
|
-
|
|
191
|
-
const payload = Buffer.concat(encoded);
|
|
192
|
-
if (payload.length < 56) {
|
|
193
|
-
return Buffer.concat([Buffer.from([0xc0 + payload.length]), payload]);
|
|
194
|
-
}
|
|
195
|
-
const lenBytes = bigIntToMinBuf(BigInt(payload.length));
|
|
196
|
-
return Buffer.concat([Buffer.from([0xf7 + lenBytes.length]), lenBytes, payload]);
|
|
66
|
+
return input.map(toRlpInput);
|
|
197
67
|
}
|
|
198
68
|
|
|
199
|
-
let data;
|
|
200
69
|
if (Buffer.isBuffer(input)) {
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
70
|
+
return Uint8Array.from(input);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (input instanceof Uint8Array) {
|
|
74
|
+
return input;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof input === "string") {
|
|
78
|
+
let hex = input.replace(/^0x/, "");
|
|
79
|
+
// Strip leading zeros to match legacy behavior for hex-string values
|
|
80
|
+
// (chain IDs, nonces, gas prices, etc. passed as hex strings)
|
|
81
|
+
hex = hex.replace(/^0+/, "");
|
|
82
|
+
if (hex === "" || hex.length === 0) return Uint8Array.from([]);
|
|
83
|
+
if (hex.length % 2) hex = "0" + hex;
|
|
84
|
+
return Uint8Array.from(Buffer.from(hex, "hex"));
|
|
208
85
|
}
|
|
209
86
|
|
|
210
|
-
|
|
211
|
-
if (data.length === 1 && data[0] < 0x80) return data;
|
|
212
|
-
if (data.length < 56) return Buffer.concat([Buffer.from([0x80 + data.length]), data]);
|
|
213
|
-
const lenBytes = bigIntToMinBuf(BigInt(data.length));
|
|
214
|
-
return Buffer.concat([Buffer.from([0xb7 + lenBytes.length]), lenBytes, data]);
|
|
87
|
+
return Uint8Array.from([]);
|
|
215
88
|
}
|
package/src/schema.json
CHANGED
|
@@ -1622,7 +1622,7 @@
|
|
|
1622
1622
|
},
|
|
1623
1623
|
"wallet": {
|
|
1624
1624
|
"type": "string",
|
|
1625
|
-
"description": "Wallet name (or \"walletconnect\"/\"wc\" for WalletConnect, EVM only). A configured wallet is required
|
|
1625
|
+
"description": "Wallet name (or \"walletconnect\"/\"wc\" for WalletConnect, EVM only). A configured wallet is required \u2014 run `nansen wallet create` if you haven't set one up yet."
|
|
1626
1626
|
}
|
|
1627
1627
|
},
|
|
1628
1628
|
"prerequisites": [
|
|
@@ -1692,11 +1692,13 @@
|
|
|
1692
1692
|
"avalanche",
|
|
1693
1693
|
"linea",
|
|
1694
1694
|
"scroll",
|
|
1695
|
+
"zksync",
|
|
1695
1696
|
"mantle",
|
|
1696
1697
|
"ronin",
|
|
1697
1698
|
"sei",
|
|
1698
1699
|
"plasma",
|
|
1699
1700
|
"sonic",
|
|
1701
|
+
"unichain",
|
|
1700
1702
|
"monad",
|
|
1701
1703
|
"hyperevm",
|
|
1702
1704
|
"iotaevm"
|
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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Nansen CLI - Token Transfer
|
|
3
3
|
* Send native and ERC-20/SPL tokens on EVM and Solana chains.
|
|
4
|
-
* Zero external dependencies — uses Node.js built-in crypto only.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import crypto from 'crypto';
|
|
7
|
+
import { base58 } from '@scure/base';
|
|
8
8
|
import { base58Encode, exportWallet, getWalletConfig, verifyPassword } from './wallet.js';
|
|
9
9
|
import { keccak256, signSecp256k1, rlpEncode } from './crypto.js';
|
|
10
10
|
import { getWalletConnectAddress, sendTransactionViaWalletConnect } from './walletconnect-trading.js';
|
|
@@ -34,21 +34,8 @@ const CHAIN_IDS = { ...EVM_CHAIN_IDS, evm: 1 };
|
|
|
34
34
|
|
|
35
35
|
// ============= Base58 =============
|
|
36
36
|
|
|
37
|
-
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
38
|
-
|
|
39
37
|
function base58Decode(str) {
|
|
40
|
-
|
|
41
|
-
for (const ch of str) {
|
|
42
|
-
const idx = BASE58_ALPHABET.indexOf(ch);
|
|
43
|
-
if (idx === -1) throw new Error(`Invalid base58 character: ${ch}`);
|
|
44
|
-
num = num * 58n + BigInt(idx);
|
|
45
|
-
}
|
|
46
|
-
const hex = num.toString(16);
|
|
47
|
-
const paddedHex = hex.length % 2 ? '0' + hex : hex;
|
|
48
|
-
const bytes = num === 0n ? [] : [...Buffer.from(paddedHex, 'hex')];
|
|
49
|
-
let leadingZeros = 0;
|
|
50
|
-
for (const ch of str) { if (ch === '1') leadingZeros++; else break; }
|
|
51
|
-
return Buffer.from([...Array(leadingZeros).fill(0), ...bytes]);
|
|
38
|
+
return Buffer.from(base58.decode(str));
|
|
52
39
|
}
|
|
53
40
|
|
|
54
41
|
function base58DecodePubkey(str) {
|
|
@@ -603,7 +590,7 @@ export async function sendTokens({ to, amount, chain, token = null, wallet = nul
|
|
|
603
590
|
}
|
|
604
591
|
|
|
605
592
|
const config = getWalletConfig();
|
|
606
|
-
if (!verifyPassword(password, config)) throw new Error('Incorrect password');
|
|
593
|
+
if (config.passwordHash && !verifyPassword(password, config)) throw new Error('Incorrect password');
|
|
607
594
|
|
|
608
595
|
const walletName = wallet || config.defaultWallet;
|
|
609
596
|
if (!walletName) throw new Error('No wallet specified and no default wallet set');
|
package/src/wallet.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Nansen CLI - Wallet Management
|
|
3
3
|
* Local key generation and storage for EVM and Solana chains.
|
|
4
|
-
* Zero external dependencies — uses Node.js built-in crypto only.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import crypto from 'crypto';
|
|
8
7
|
import fs from 'fs';
|
|
9
8
|
import path from 'path';
|
|
10
9
|
import * as readline from 'readline';
|
|
10
|
+
import { base58 } from '@scure/base';
|
|
11
11
|
|
|
12
12
|
// ============= Constants =============
|
|
13
13
|
|
|
@@ -32,31 +32,11 @@ import { keccak256 } from './crypto.js';
|
|
|
32
32
|
|
|
33
33
|
// ============= Base58 Encoding (for Solana) =============
|
|
34
34
|
|
|
35
|
-
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
36
|
-
|
|
37
35
|
/**
|
|
38
36
|
* Encode a Buffer to base58 string.
|
|
39
37
|
*/
|
|
40
38
|
export function base58Encode(buf) {
|
|
41
|
-
|
|
42
|
-
for (const byte of buf) {
|
|
43
|
-
num = num * 256n + BigInt(byte);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let str = '';
|
|
47
|
-
while (num > 0n) {
|
|
48
|
-
const rem = Number(num % 58n);
|
|
49
|
-
num = num / 58n;
|
|
50
|
-
str = BASE58_ALPHABET[rem] + str;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Leading zeros → leading '1's
|
|
54
|
-
for (const byte of buf) {
|
|
55
|
-
if (byte === 0) str = '1' + str;
|
|
56
|
-
else break;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return str || '1';
|
|
39
|
+
return base58.encode(buf instanceof Uint8Array ? buf : Uint8Array.from(buf));
|
|
60
40
|
}
|
|
61
41
|
|
|
62
42
|
// ============= Encryption =============
|
|
@@ -78,6 +58,10 @@ function deriveKey(password, salt) {
|
|
|
78
58
|
* Returns a JSON-serializable object with all params needed for decryption.
|
|
79
59
|
*/
|
|
80
60
|
export function encryptKey(privateKeyHex, password) {
|
|
61
|
+
if (password === null) {
|
|
62
|
+
return { data: privateKeyHex, encrypted: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
81
65
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
82
66
|
const iv = crypto.randomBytes(IV_LEN);
|
|
83
67
|
const key = deriveKey(password, salt);
|
|
@@ -99,10 +83,31 @@ export function encryptKey(privateKeyHex, password) {
|
|
|
99
83
|
|
|
100
84
|
/**
|
|
101
85
|
* Decrypt a private key with a password.
|
|
102
|
-
*
|
|
103
|
-
*
|
|
86
|
+
* For unencrypted wallets (encrypted: false), password is ignored and
|
|
87
|
+
* plaintext data is returned directly.
|
|
104
88
|
*/
|
|
105
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
|
+
|
|
106
111
|
const salt = Buffer.from(encryptedData.salt, 'hex');
|
|
107
112
|
const iv = Buffer.from(encryptedData.iv, 'hex');
|
|
108
113
|
const authTag = Buffer.from(encryptedData.authTag, 'hex');
|
|
@@ -234,6 +239,7 @@ function getWalletFile(name) {
|
|
|
234
239
|
*/
|
|
235
240
|
export function verifyPassword(password, config) {
|
|
236
241
|
if (!config.passwordHash) return true; // No password set yet
|
|
242
|
+
if (password === null || password === undefined) return false;
|
|
237
243
|
const { salt, hash } = config.passwordHash;
|
|
238
244
|
const derived = crypto.scryptSync(password, Buffer.from(salt, 'hex'), 32, {
|
|
239
245
|
N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: 256 * 1024 * 1024,
|
|
@@ -328,11 +334,22 @@ export function createWallet(name, password) {
|
|
|
328
334
|
throw new Error(`Wallet "${name}" already exists`);
|
|
329
335
|
}
|
|
330
336
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
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
|
+
}
|
|
334
342
|
} else {
|
|
335
|
-
|
|
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)) {
|
|
336
353
|
throw new Error('Incorrect password');
|
|
337
354
|
}
|
|
338
355
|
}
|
|
@@ -400,7 +417,7 @@ export function exportWallet(name, password) {
|
|
|
400
417
|
}
|
|
401
418
|
|
|
402
419
|
const config = getWalletConfig();
|
|
403
|
-
if (!verifyPassword(password, config)) {
|
|
420
|
+
if (config.passwordHash && !verifyPassword(password, config)) {
|
|
404
421
|
throw new Error('Incorrect password');
|
|
405
422
|
}
|
|
406
423
|
|
|
@@ -445,7 +462,7 @@ export function deleteWallet(name, password) {
|
|
|
445
462
|
}
|
|
446
463
|
|
|
447
464
|
const config = getWalletConfig();
|
|
448
|
-
if (!verifyPassword(password, config)) {
|
|
465
|
+
if (config.passwordHash && !verifyPassword(password, config)) {
|
|
449
466
|
throw new Error('Incorrect password');
|
|
450
467
|
}
|
|
451
468
|
|
|
@@ -490,22 +507,35 @@ export function buildWalletCommands(deps = {}) {
|
|
|
490
507
|
const handlers = {
|
|
491
508
|
'create': async () => {
|
|
492
509
|
const name = options.name || args[1] || 'default';
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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.)');
|
|
496
519
|
exit(1);
|
|
497
520
|
return;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (!config.passwordHash && !process.env.NANSEN_WALLET_PASSWORD) {
|
|
503
|
-
const confirm = await promptPassword('Confirm password: ', deps);
|
|
504
|
-
if (password !== confirm) {
|
|
505
|
-
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');
|
|
506
525
|
exit(1);
|
|
507
526
|
return;
|
|
508
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
|
+
}
|
|
509
539
|
}
|
|
510
540
|
|
|
511
541
|
try {
|
|
@@ -519,6 +549,13 @@ export function buildWalletCommands(deps = {}) {
|
|
|
519
549
|
log(` Base (recommended, lower fees): send USDC to ${result.evm}`);
|
|
520
550
|
log(` Solana: send USDC to ${result.solana}`);
|
|
521
551
|
log('');
|
|
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
|
+
}
|
|
558
|
+
log('');
|
|
522
559
|
return;
|
|
523
560
|
} catch (err) {
|
|
524
561
|
log(`❌ ${err.message}`);
|
|
@@ -570,7 +607,10 @@ export function buildWalletCommands(deps = {}) {
|
|
|
570
607
|
exit(1);
|
|
571
608
|
return;
|
|
572
609
|
}
|
|
573
|
-
const
|
|
610
|
+
const config = getWalletConfig();
|
|
611
|
+
const password = config.passwordHash
|
|
612
|
+
? (process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps))
|
|
613
|
+
: null;
|
|
574
614
|
try {
|
|
575
615
|
const result = exportWallet(name, password);
|
|
576
616
|
log(`\n⚠️ Private keys for "${result.name}" — do not share!\n`);
|
|
@@ -612,7 +652,10 @@ export function buildWalletCommands(deps = {}) {
|
|
|
612
652
|
exit(1);
|
|
613
653
|
return;
|
|
614
654
|
}
|
|
615
|
-
const
|
|
655
|
+
const config = getWalletConfig();
|
|
656
|
+
const password = config.passwordHash
|
|
657
|
+
? (process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps))
|
|
658
|
+
: null;
|
|
616
659
|
try {
|
|
617
660
|
const result = deleteWallet(name, password);
|
|
618
661
|
log(`✓ Wallet "${result.deleted}" deleted`);
|
|
@@ -655,7 +698,15 @@ export function buildWalletCommands(deps = {}) {
|
|
|
655
698
|
}
|
|
656
699
|
|
|
657
700
|
const isWalletConnect = options.wallet === 'walletconnect' || options.wallet === 'wc';
|
|
658
|
-
|
|
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
|
+
}
|
|
659
710
|
const dryRun = flags['dry-run'] || flags.dryRun;
|
|
660
711
|
|
|
661
712
|
try {
|
|
@@ -712,7 +763,8 @@ USAGE:
|
|
|
712
763
|
nansen wallet <command> [options]
|
|
713
764
|
|
|
714
765
|
COMMANDS:
|
|
715
|
-
create [--name <label>]
|
|
766
|
+
create [--name <label>] [--unsafe-no-password]
|
|
767
|
+
Create a new wallet pair (EVM + Solana)
|
|
716
768
|
list List all wallets
|
|
717
769
|
show <name> Show wallet addresses
|
|
718
770
|
export <name> Export private keys (requires password)
|
|
@@ -729,6 +781,7 @@ OPTIONS:
|
|
|
729
781
|
--token <address> Token contract/mint address (optional, sends native if omitted)
|
|
730
782
|
--wallet <name> Wallet to use (optional, uses default if omitted; use "walletconnect" or "wc" for WalletConnect, EVM only)
|
|
731
783
|
--max Send entire balance (deducts gas for native transfers)
|
|
784
|
+
--unsafe-no-password Skip encryption — private keys stored UNENCRYPTED on disk (create only)
|
|
732
785
|
|
|
733
786
|
ENVIRONMENT:
|
|
734
787
|
NANSEN_WALLET_PASSWORD Password for non-interactive use (e.g. CI/scripts)
|
package/src/x402-svm.js
CHANGED
|
@@ -1,30 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Nansen CLI - x402 Solana Auto-Payment
|
|
3
3
|
* Implements SPL TransferChecked transaction building for x402 payments.
|
|
4
|
-
* Zero external dependencies — uses Node.js built-in crypto + wallet.js base58.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import crypto from 'crypto';
|
|
7
|
+
import { base58 } from '@scure/base';
|
|
8
8
|
|
|
9
|
-
// ============= Base58 Encode
|
|
10
|
-
const BASE58_ALPHABET_STR = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
9
|
+
// ============= Base58 Encode =============
|
|
11
10
|
|
|
12
11
|
export function base58Encode(buf) {
|
|
13
|
-
|
|
14
|
-
for (const byte of buf) {
|
|
15
|
-
num = num * 256n + BigInt(byte);
|
|
16
|
-
}
|
|
17
|
-
let str = '';
|
|
18
|
-
while (num > 0n) {
|
|
19
|
-
const rem = Number(num % 58n);
|
|
20
|
-
num = num / 58n;
|
|
21
|
-
str = BASE58_ALPHABET_STR[rem] + str;
|
|
22
|
-
}
|
|
23
|
-
for (const byte of buf) {
|
|
24
|
-
if (byte === 0) str = '1' + str;
|
|
25
|
-
else break;
|
|
26
|
-
}
|
|
27
|
-
return str || '1';
|
|
12
|
+
return base58.encode(buf instanceof Uint8Array ? buf : Uint8Array.from(buf));
|
|
28
13
|
}
|
|
29
14
|
|
|
30
15
|
// ============= Constants =============
|
|
@@ -41,33 +26,8 @@ const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
|
|
|
41
26
|
|
|
42
27
|
// ============= Base58 Decode =============
|
|
43
28
|
|
|
44
|
-
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
45
|
-
const BASE58_MAP = new Uint8Array(128);
|
|
46
|
-
for (let i = 0; i < BASE58_ALPHABET.length; i++) {
|
|
47
|
-
BASE58_MAP[BASE58_ALPHABET.charCodeAt(i)] = i;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
29
|
export function base58Decode(str) {
|
|
51
|
-
|
|
52
|
-
for (const ch of str) {
|
|
53
|
-
num = num * 58n + BigInt(BASE58_MAP[ch.charCodeAt(0)]);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Count leading '1's → leading zero bytes
|
|
57
|
-
let leadingZeros = 0;
|
|
58
|
-
for (const ch of str) {
|
|
59
|
-
if (ch === '1') leadingZeros++;
|
|
60
|
-
else break;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (num === 0n) return Buffer.alloc(leadingZeros || 1);
|
|
64
|
-
|
|
65
|
-
// Convert to bytes
|
|
66
|
-
const hex = num.toString(16);
|
|
67
|
-
const paddedHex = hex.length % 2 ? '0' + hex : hex;
|
|
68
|
-
const bytes = Buffer.from(paddedHex, 'hex');
|
|
69
|
-
|
|
70
|
-
return Buffer.concat([Buffer.alloc(leadingZeros), bytes]);
|
|
30
|
+
return Buffer.from(base58.decode(str));
|
|
71
31
|
}
|
|
72
32
|
|
|
73
33
|
/**
|
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
|
|