nansen-cli 1.11.0 → 1.11.2
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 +14 -0
- package/package.json +10 -3
- package/src/api.js +13 -2
- package/src/cli.js +29 -13
- package/src/crypto.js +49 -176
- package/src/schema.json +28 -0
- package/src/transfer.js +2 -15
- package/src/wallet.js +10 -22
- package/src/walletconnect-x402.js +3 -1
- package/src/x402-svm.js +4 -44
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.11.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#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
|
|
8
|
+
|
|
9
|
+
## 1.11.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#194](https://github.com/nansen-ai/nansen-cli/pull/194) [`89225f5`](https://github.com/nansen-ai/nansen-cli/commit/89225f5d5b566f7eda77b1876c77545c2feb6a1c) Thanks [@TimNooren](https://github.com/TimNooren)! - fix: --help on trade subcommands and wallet subcommands now shows full help identical to the no-args case
|
|
14
|
+
|
|
15
|
+
- [#199](https://github.com/nansen-ai/nansen-cli/pull/199) [`9ae981e`](https://github.com/nansen-ai/nansen-cli/commit/9ae981e6dcf7fa663e47a3608afab7f12e0a9463) Thanks [@TimNooren](https://github.com/TimNooren)! - fix: replace misleading `walletconnect connect` command reference in x402 payment error with actionable guidance mentioning both local wallet (`nansen wallet create`) and external WalletConnect CLI options
|
|
16
|
+
|
|
3
17
|
## 1.11.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nansen-cli",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.2",
|
|
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
|
@@ -578,9 +578,20 @@ export class NansenAPI {
|
|
|
578
578
|
return await paidResponse.json();
|
|
579
579
|
}
|
|
580
580
|
} catch (x402Err) {
|
|
581
|
-
|
|
581
|
+
if (!this.apiKey) {
|
|
582
|
+
// No API key and no payment wallet — guide the user to login rather than
|
|
583
|
+
// showing a confusing x402 payment dump they can't act on.
|
|
584
|
+
// TODO: full fix would skip x402 entirely when no apiKey is set — see PR #<this PR number>
|
|
585
|
+
message = 'No API key configured. Run: nansen login --api-key <key>. Get your key at https://app.nansen.ai/api';
|
|
586
|
+
} else {
|
|
587
|
+
message = `x402 auto-payment failed: ${x402Err.message}`;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// Only include raw payment requirements in the error details when the user
|
|
591
|
+
// has an API key — for unauthenticated users they add noise, not signal.
|
|
592
|
+
if (this.apiKey) {
|
|
593
|
+
data.paymentRequirements = paymentRequirements;
|
|
582
594
|
}
|
|
583
|
-
data.paymentRequirements = paymentRequirements;
|
|
584
595
|
}
|
|
585
596
|
}
|
|
586
597
|
}
|
package/src/cli.js
CHANGED
|
@@ -642,9 +642,10 @@ EXAMPLES:
|
|
|
642
642
|
nansen research smart-money netflow --chain solana
|
|
643
643
|
nansen research token screener --chain solana --timeframe 24h
|
|
644
644
|
nansen research profiler balance --address 0x... --chain ethereum
|
|
645
|
-
nansen trade quote --chain
|
|
645
|
+
nansen trade quote --chain base --from ETH --to USDC --amount 1
|
|
646
646
|
|
|
647
|
-
|
|
647
|
+
Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
|
|
648
|
+
Trade chains: solana, base
|
|
648
649
|
Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
|
|
649
650
|
|
|
650
651
|
Docs: https://docs.nansen.ai
|
|
@@ -1079,6 +1080,28 @@ export function buildCommands(deps = {}) {
|
|
|
1079
1080
|
|
|
1080
1081
|
let result = await handlers[subcommand]();
|
|
1081
1082
|
|
|
1083
|
+
// Warn when OHLCV price data is null (backend coverage gap)
|
|
1084
|
+
// Volume comes from on-chain DEX data and is always available, but price/market_cap
|
|
1085
|
+
// requires a price oracle — some tokens are not tracked and return all-null price fields.
|
|
1086
|
+
if (subcommand === 'ohlcv') {
|
|
1087
|
+
const candles = Array.isArray(result?.data) ? result.data : [];
|
|
1088
|
+
if (candles.length === 0) {
|
|
1089
|
+
process.stderr.write(`⚠️ No OHLCV data returned for token ${tokenAddress} on ${chain}.\n`);
|
|
1090
|
+
} else {
|
|
1091
|
+
const hasPrice = candles.some(c => c.open !== null || c.close !== null);
|
|
1092
|
+
const hasVolume = candles.some(c => c.volume !== null);
|
|
1093
|
+
if (!hasPrice && hasVolume) {
|
|
1094
|
+
process.stderr.write(
|
|
1095
|
+
`⚠️ Price data unavailable for token ${tokenAddress} on ${chain}.\n` +
|
|
1096
|
+
` open/high/low/close, volume_usd, and market_cap are null.\n` +
|
|
1097
|
+
` Volume (raw token units) is available. This token may not be tracked by Nansen's price oracle.\n`
|
|
1098
|
+
);
|
|
1099
|
+
} else if (!hasPrice && !hasVolume) {
|
|
1100
|
+
process.stderr.write(`⚠️ No OHLCV data available for token ${tokenAddress} on ${chain}.\n`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1082
1105
|
// Enrich transfers with Nansen labels for from/to addresses
|
|
1083
1106
|
if (subcommand === 'transfers' && (options.enrich || flags.enrich)) {
|
|
1084
1107
|
result = await enrichTransfers(result, apiInstance, chain);
|
|
@@ -1360,17 +1383,9 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1360
1383
|
return { type: 'command-help', command: `research ${category}` };
|
|
1361
1384
|
}
|
|
1362
1385
|
}
|
|
1363
|
-
// Handle 'trade <sub> --help'
|
|
1364
|
-
if (command === 'trade' && subcommand) {
|
|
1365
|
-
const tradeSchema = SCHEMA.commands.trade?.subcommands?.[subcommand];
|
|
1366
|
-
if (tradeSchema) {
|
|
1367
|
-
output(`\ntrade ${subcommand} - ${tradeSchema.description}\n`);
|
|
1368
|
-
notify();
|
|
1369
|
-
return { type: 'subcommand-help', command: 'trade', subcommand };
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
1386
|
// First try subcommand help
|
|
1373
|
-
|
|
1387
|
+
// Skip for 'trade' — its handlers show their own rich usage when required args are missing
|
|
1388
|
+
if (command && subcommand && command !== 'trade') {
|
|
1374
1389
|
const subHelp = generateSubcommandHelp(command, subcommand);
|
|
1375
1390
|
if (subHelp) {
|
|
1376
1391
|
output(subHelp);
|
|
@@ -1379,7 +1394,8 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1379
1394
|
}
|
|
1380
1395
|
}
|
|
1381
1396
|
// Then try command-level help (list subcommands)
|
|
1382
|
-
|
|
1397
|
+
// Skip for 'trade' — let the handler show its own usage
|
|
1398
|
+
const cmdSchemaLookup = command !== 'trade' && (SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command]);
|
|
1383
1399
|
if (command && cmdSchemaLookup) {
|
|
1384
1400
|
const cmdSchema = cmdSchemaLookup;
|
|
1385
1401
|
const lines = [`${command} — ${cmdSchema.description}`];
|
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
|
@@ -157,6 +157,14 @@
|
|
|
157
157
|
"page": {
|
|
158
158
|
"type": "number",
|
|
159
159
|
"description": "Page number for paginated results (default: 1)"
|
|
160
|
+
},
|
|
161
|
+
"sort": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"description": "Sort field:direction (e.g., value_usd:desc)"
|
|
164
|
+
},
|
|
165
|
+
"filters": {
|
|
166
|
+
"type": "object",
|
|
167
|
+
"description": "Additional filters as JSON"
|
|
160
168
|
}
|
|
161
169
|
},
|
|
162
170
|
"returns": [
|
|
@@ -178,6 +186,14 @@
|
|
|
178
186
|
"limit": {
|
|
179
187
|
"type": "number"
|
|
180
188
|
},
|
|
189
|
+
"labels": {
|
|
190
|
+
"type": "string|array",
|
|
191
|
+
"description": "Smart Money label filter"
|
|
192
|
+
},
|
|
193
|
+
"sort": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"description": "Sort field:direction (e.g., deposit_value_usd:desc)"
|
|
196
|
+
},
|
|
181
197
|
"filters": {
|
|
182
198
|
"type": "object"
|
|
183
199
|
},
|
|
@@ -224,6 +240,18 @@
|
|
|
224
240
|
"page": {
|
|
225
241
|
"type": "number",
|
|
226
242
|
"description": "Page number for paginated results (default: 1)"
|
|
243
|
+
},
|
|
244
|
+
"labels": {
|
|
245
|
+
"type": "string|array",
|
|
246
|
+
"description": "Smart Money label filter"
|
|
247
|
+
},
|
|
248
|
+
"sort": {
|
|
249
|
+
"type": "string",
|
|
250
|
+
"description": "Sort field:direction (e.g., value_usd:desc)"
|
|
251
|
+
},
|
|
252
|
+
"filters": {
|
|
253
|
+
"type": "object",
|
|
254
|
+
"description": "Additional filters as JSON"
|
|
227
255
|
}
|
|
228
256
|
},
|
|
229
257
|
"returns": [
|
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) {
|
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 =============
|
|
@@ -519,6 +499,9 @@ export function buildWalletCommands(deps = {}) {
|
|
|
519
499
|
log(` Base (recommended, lower fees): send USDC to ${result.evm}`);
|
|
520
500
|
log(` Solana: send USDC to ${result.solana}`);
|
|
521
501
|
log('');
|
|
502
|
+
log(' ⚠️ This is a hot wallet and is fundamentally insecure — do not deposit more than you can afford to lose.');
|
|
503
|
+
log(' Store and handle your password securely, e.g. using a secrets manager or system keychain.');
|
|
504
|
+
log('');
|
|
522
505
|
return;
|
|
523
506
|
} catch (err) {
|
|
524
507
|
log(`❌ ${err.message}`);
|
|
@@ -753,6 +736,11 @@ EXAMPLES:
|
|
|
753
736
|
return;
|
|
754
737
|
}
|
|
755
738
|
|
|
739
|
+
// --help on any wallet subcommand shows wallet help instead of executing
|
|
740
|
+
if (flags.help || flags.h) {
|
|
741
|
+
return handlers['help']();
|
|
742
|
+
}
|
|
743
|
+
|
|
756
744
|
return handlers[subcommand]();
|
|
757
745
|
},
|
|
758
746
|
};
|
|
@@ -142,7 +142,9 @@ export async function handleX402Payment(paymentRequirements) {
|
|
|
142
142
|
const wallet = await checkWalletConnection();
|
|
143
143
|
if (!wallet) {
|
|
144
144
|
throw new NansenError(
|
|
145
|
-
'x402 payment required but no wallet connected.
|
|
145
|
+
'x402 payment required but no wallet connected. ' +
|
|
146
|
+
'To pay automatically: create a local wallet with `nansen wallet create` (then set NANSEN_WALLET_PASSWORD), ' +
|
|
147
|
+
'or connect an external wallet via the `walletconnect` CLI (`walletconnect connect`).',
|
|
146
148
|
ErrorCode.PAYMENT_REQUIRED,
|
|
147
149
|
402
|
|
148
150
|
);
|
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
|
/**
|