nansen-cli 1.5.1 → 1.7.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/src/cli.js CHANGED
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  import { NansenAPI, NansenError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, sleep } from './api.js';
7
+ import { buildWalletCommands } from './wallet.js';
8
+ import { buildTradingCommands } from './trading.js';
7
9
  import fs from 'fs';
8
10
  import { getUpdateNotification, scheduleUpdateCheck } from './update-check.js';
9
11
  import { createRequire } from 'module';
@@ -165,12 +167,20 @@ export const SCHEMA = {
165
167
  'token': {
166
168
  description: 'Token God Mode - deep analytics for any token',
167
169
  subcommands: {
170
+ 'indicators': {
171
+ description: 'Risk and reward indicators for a token (Nansen Score)',
172
+ options: {
173
+ token: { type: 'string', required: true, description: 'Token address' },
174
+ chain: { type: 'string', default: 'ethereum' }
175
+ },
176
+ returns: ['token_info[market_cap_usd, market_cap_group, is_stablecoin]', 'risk_indicators[indicator_type, score, signal, signal_percentile, last_trigger_on]', 'reward_indicators[indicator_type, score, signal, signal_percentile, last_trigger_on]']
177
+ },
168
178
  'info': {
169
179
  description: 'Get detailed information for a specific token',
170
180
  options: {
171
181
  token: { type: 'string', required: true, description: 'Token address' },
172
182
  chain: { type: 'string', default: 'solana' },
173
- timeframe: { type: 'string', default: '24h', enum: ['5m', '10m', '1h', '6h', '24h', '7d', '30d'] }
183
+ timeframe: { type: 'string', default: '1d', enum: ['5m', '1h', '6h', '12h', '1d', '7d'] }
174
184
  },
175
185
  returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'liquidity_usd']
176
186
  },
@@ -279,6 +289,16 @@ export const SCHEMA = {
279
289
  }
280
290
  }
281
291
  },
292
+ 'search': {
293
+ description: 'Search for tokens and entities across Nansen',
294
+ options: {
295
+ query: { type: 'string', required: true, description: 'Search query (token name, symbol, address, or entity)' },
296
+ type: { type: 'string', default: 'any', enum: ['token', 'entity', 'any'], description: 'Result type filter' },
297
+ chain: { type: 'string', description: 'Filter by chain (e.g., ethereum, solana)' },
298
+ limit: { type: 'number', default: 25, description: 'Max results (1-50)' }
299
+ },
300
+ returns: ['tokens[name, symbol, chain, address, price, volume_24h, market_cap, rank]', 'entities[name, tags, rank]', 'total_results']
301
+ },
282
302
  'points': {
283
303
  description: 'Nansen Points analytics',
284
304
  subcommands: {
@@ -299,7 +319,8 @@ export const SCHEMA = {
299
319
  fields: { type: 'string', description: 'Comma-separated list of fields to include in output' },
300
320
  'no-retry': { type: 'boolean', description: 'Disable automatic retry on rate limits/errors' },
301
321
  retries: { type: 'number', default: 3, description: 'Max retry attempts' },
302
- format: { type: 'string', enum: ['json', 'csv'], description: 'Output format (default: json)' }
322
+ format: { type: 'string', enum: ['json', 'csv'], description: 'Output format (default: json)' },
323
+ 'x402-payment-signature': { type: 'string', description: 'Pre-signed x402 payment signature header' }
303
324
  },
304
325
  chains: ['ethereum', 'solana', 'base', 'bnb', 'arbitrum', 'polygon', 'optimism', 'avalanche', 'linea', 'scroll', 'mantle', 'ronin', 'sei', 'plasma', 'sonic', 'monad', 'hyperevm', 'iotaevm'],
305
326
  smartMoneyLabels: ['Fund', 'Smart Trader', '30D Smart Trader', '90D Smart Trader', '180D Smart Trader', 'Smart HL Perps Trader']
@@ -820,14 +841,17 @@ COMMANDS:
820
841
  login Save your API key (interactive)
821
842
  logout Remove saved API key
822
843
  schema Output JSON schema for all commands (for agent introspection)
844
+ wallet Local wallet management (create, list, show, export, default, delete)
845
+ quote Get a DEX swap quote (chain, tokens, amount)
846
+ execute Sign and broadcast a quoted trade
823
847
  cache Cache management (clear)
824
848
  smart-money Smart Money analytics (netflow, dex-trades, perp-trades, holdings, dcas, historical-holdings)
825
849
  profiler Wallet profiling (balance, labels, transactions, pnl, pnl-summary, search,
826
850
  historical-balances, related-wallets, counterparties, perp-positions, perp-trades,
827
851
  batch, trace, compare)
828
- token Token God Mode (info, screener, holders, flows, dex-trades, pnl, who-bought-sold,
829
- flow-intelligence, transfers, jup-dca, perp-trades, perp-positions,
830
- perp-pnl-leaderboard)
852
+ token Token God Mode (info, indicators, screener, holders, flows, dex-trades, pnl,
853
+ who-bought-sold, flow-intelligence, transfers, jup-dca, perp-trades,
854
+ perp-positions, perp-pnl-leaderboard)
831
855
  portfolio Portfolio analytics (defi)
832
856
  perp Perpetual futures analytics (screener, leaderboard)
833
857
  points Nansen Points analytics (leaderboard)
@@ -847,6 +871,7 @@ GLOBAL OPTIONS:
847
871
  --symbol Token symbol (for perp endpoints)
848
872
  --no-retry Disable automatic retry on rate limits/errors
849
873
  --retries <n> Max retry attempts (default: 3)
874
+ --x402-payment-signature <sig> Pre-signed x402 payment signature header
850
875
  --cache Enable response caching (default: off)
851
876
  --no-cache Disable cache for this request
852
877
  --cache-ttl <s> Cache TTL in seconds (default: 300)
@@ -948,10 +973,16 @@ export function buildCommands(deps = {}) {
948
973
 
949
974
  return {
950
975
  'login': async (args, apiInstance, flags, options) => {
951
- log('Nansen CLI Login\n');
952
- log('Get your API key at: https://app.nansen.ai/api\n');
953
-
954
- const apiKey = await promptFn('Enter your API key: ', true);
976
+ // Support non-interactive: nansen login --api-key <key>
977
+ let apiKey = options['api-key'] || options.apiKey;
978
+
979
+ if (!apiKey) {
980
+ log('Nansen CLI Login\n');
981
+ log('Get your API key at: https://app.nansen.ai/api\n');
982
+ log('Tip: For non-interactive use: nansen login --api-key <key>\n');
983
+
984
+ apiKey = await promptFn('Enter your API key: ', true);
985
+ }
955
986
 
956
987
  if (!apiKey || apiKey.trim().length === 0) {
957
988
  log('\n❌ No API key provided');
@@ -1179,7 +1210,8 @@ export function buildCommands(deps = {}) {
1179
1210
  }
1180
1211
 
1181
1212
  const handlers = {
1182
- 'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe }),
1213
+ 'indicators': () => apiInstance.tokenIndicators({ tokenAddress, chain }),
1214
+ 'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe: options.timeframe }),
1183
1215
  'screener': async () => {
1184
1216
  const search = options.search;
1185
1217
  // When searching, fetch more results to filter from (API has no server-side search)
@@ -1292,6 +1324,15 @@ export function buildCommands(deps = {}) {
1292
1324
  return handlers[subcommand]();
1293
1325
  },
1294
1326
 
1327
+ 'search': async (args, apiInstance, flags, options) => {
1328
+ return apiInstance.generalSearch({
1329
+ query: args[0] || options.query,
1330
+ resultType: options.type,
1331
+ chain: options.chain,
1332
+ limit: options.limit
1333
+ });
1334
+ },
1335
+
1295
1336
  'points': async (args, apiInstance, flags, options) => {
1296
1337
  const subcommand = args[0] || 'help';
1297
1338
  const tier = options.tier;
@@ -1316,7 +1357,7 @@ export function buildCommands(deps = {}) {
1316
1357
  }
1317
1358
 
1318
1359
  // Commands that don't require API authentication
1319
- export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache'];
1360
+ export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache', 'wallet', 'quote', 'execute'];
1320
1361
 
1321
1362
  // Command aliases for convenience
1322
1363
  export const COMMAND_ALIASES = {
@@ -1459,7 +1500,7 @@ export async function runCLI(rawArgs, deps = {}) {
1459
1500
  scheduleUpdateCheck();
1460
1501
  const notify = () => { if (updateNotification) errorOutput(updateNotification); };
1461
1502
 
1462
- const commands = { ...buildCommands(deps), ...commandOverrides };
1503
+ const commands = { ...buildCommands(deps), ...buildWalletCommands(deps), ...buildTradingCommands(deps), ...commandOverrides };
1463
1504
 
1464
1505
  if (flags.version || flags.v) {
1465
1506
  output(VERSION);
@@ -1492,10 +1533,12 @@ export async function runCLI(rawArgs, deps = {}) {
1492
1533
  return { type: 'command-help', command };
1493
1534
  }
1494
1535
  }
1495
- // Fallback to main help
1496
- output(BANNER + HELP);
1497
- notify();
1498
- return { type: 'help' };
1536
+ // Commands with handlers (e.g. quote, execute) show their own usage
1537
+ if (command === 'help' || !commands[command]) {
1538
+ output(BANNER + HELP);
1539
+ notify();
1540
+ return { type: 'help' };
1541
+ }
1499
1542
  }
1500
1543
 
1501
1544
  if (!commands[command]) {
@@ -1538,7 +1581,11 @@ export async function runCLI(rawArgs, deps = {}) {
1538
1581
  ttl: options['cache-ttl'] !== undefined ? options['cache-ttl'] : 300
1539
1582
  };
1540
1583
 
1541
- const api = new NansenAPIClass(undefined, undefined, { retry: retryOptions, cache: cacheOptions });
1584
+ const defaultHeaders = {};
1585
+ if (options['x402-payment-signature']) {
1586
+ defaultHeaders['Payment-Signature'] = options['x402-payment-signature'];
1587
+ }
1588
+ const api = new NansenAPIClass(undefined, undefined, { retry: retryOptions, cache: cacheOptions, defaultHeaders });
1542
1589
  let result = await commands[command](subArgs, api, flags, options);
1543
1590
 
1544
1591
  // Apply field filtering if --fields is specified
@@ -1566,7 +1613,7 @@ export async function runCLI(rawArgs, deps = {}) {
1566
1613
  } catch (error) {
1567
1614
  const errorData = formatError(error);
1568
1615
  const formatted = formatOutput(errorData, { pretty, table, csv });
1569
- errorOutput(formatted.text);
1616
+ output(formatted.text);
1570
1617
  notify();
1571
1618
  exit(1);
1572
1619
  return { type: 'error', data: errorData };
package/src/crypto.js ADDED
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Shared cryptographic primitives for EVM transaction signing.
3
+ * Exports keccak256, secp256k1 ECDSA signing, RLP encoding.
4
+ * Zero external dependencies — uses Node.js built-in crypto only.
5
+ */
6
+
7
+ import crypto from "crypto";
8
+
9
+ // ============= Keccak-256 =============
10
+
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
+ 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;
96
+ }
97
+
98
+ // ============= secp256k1 ECDSA =============
99
+
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
+ /**
158
+ * Sign a 32-byte hash with secp256k1 ECDSA.
159
+ * Uses RFC 6979 deterministic k and low-S normalization (EIP-2).
160
+ * Returns { r, s, v } where v is the recovery ID (0 or 1).
161
+ */
162
+ 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; }
173
+ return {
174
+ r: Buffer.from(r.toString(16).padStart(64, '0'), 'hex'),
175
+ s: Buffer.from(s.toString(16).padStart(64, '0'), 'hex'),
176
+ v,
177
+ };
178
+ }
179
+
180
+ // ============= RLP Encoding =============
181
+
182
+ export function bigIntToMinBuf(n) {
183
+ if (n === 0n) return Buffer.alloc(0);
184
+ const hex = n.toString(16);
185
+ return Buffer.from(hex.length % 2 ? '0' + hex : hex, 'hex');
186
+ }
187
+
188
+ export function rlpEncode(input) {
189
+ 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]);
197
+ }
198
+
199
+ let data;
200
+ 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');
208
+ }
209
+
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]);
215
+ }