nansen-cli 1.11.1 → 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 CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 1.11.1
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.11.1",
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/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
- * Zero external dependencies uses Node.js built-in crypto only.
4
+ * Uses audited libraries: @noble/hashes, @noble/curves, @ethereumjs/rlp.
5
5
  */
6
6
 
7
- import crypto from "crypto";
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 rate = 136;
79
- const s = new Array(25).fill(0n);
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 z = BigInt('0x' + hash.toString('hex'));
164
- const d = BigInt('0x' + privateKey.toString('hex'));
165
- const k = rfc6979k(privateKey, hash);
166
- const [rx, ry] = ptMul(k, Gx, Gy);
167
- const r = rx % N;
168
- if (r === 0n) throw new Error('Invalid signature: r=0');
169
- let s = (modInv(k, N) * ((z + r * d) % N)) % N;
170
- if (s === 0n) throw new Error('Invalid signature: s=0');
171
- let v = (ry % 2n === 0n) ? 0 : 1;
172
- if (s > N >> 1n) { s = N - s; v ^= 1; }
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, '0'), 'hex'),
175
- s: Buffer.from(s.toString(16).padStart(64, '0'), 'hex'),
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 ? '0' + hex : hex, 'hex');
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
- const encoded = input.map(rlpEncode);
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
- data = input;
202
- } else {
203
- let hex = (typeof input === 'string' ? input : '').replace(/^0x/, '');
204
- hex = hex.replace(/^0+/, '');
205
- if (hex === '' || hex.length === 0) return Buffer.from([0x80]);
206
- if (hex.length % 2) hex = '0' + hex;
207
- data = Buffer.from(hex, 'hex');
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
- if (data.length === 0) return Buffer.from([0x80]);
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/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
- let num = 0n;
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
- let num = 0n;
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}`);
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 (inline from wallet.js PR #26) =============
10
- const BASE58_ALPHABET_STR = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
9
+ // ============= Base58 Encode =============
11
10
 
12
11
  export function base58Encode(buf) {
13
- let num = 0n;
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
- let num = 0n;
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
  /**