nansen-cli 1.0.2 → 1.1.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/CLAUDE.md +185 -0
- package/README.md +52 -10
- package/TODO.md +47 -0
- package/package.json +1 -1
- package/src/api.js +259 -42
- package/src/cli.js +843 -0
- package/src/index.js +4 -499
package/src/index.js
CHANGED
|
@@ -7,506 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* All output is JSON for easy parsing by AI agents.
|
|
9
9
|
* Use --pretty for human-readable formatting.
|
|
10
|
+
*
|
|
11
|
+
* Core logic lives in cli.js for testability.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
|
-
import {
|
|
13
|
-
import * as readline from 'readline';
|
|
14
|
-
|
|
15
|
-
// Parse command line arguments
|
|
16
|
-
function parseArgs(args) {
|
|
17
|
-
const result = { _: [], flags: {}, options: {} };
|
|
18
|
-
|
|
19
|
-
for (let i = 0; i < args.length; i++) {
|
|
20
|
-
const arg = args[i];
|
|
21
|
-
|
|
22
|
-
if (arg.startsWith('--')) {
|
|
23
|
-
const key = arg.slice(2);
|
|
24
|
-
const next = args[i + 1];
|
|
25
|
-
|
|
26
|
-
if (key === 'pretty' || key === 'help' || key === 'table') {
|
|
27
|
-
result.flags[key] = true;
|
|
28
|
-
} else if (next && !next.startsWith('-')) {
|
|
29
|
-
// Try to parse as JSON first
|
|
30
|
-
try {
|
|
31
|
-
result.options[key] = JSON.parse(next);
|
|
32
|
-
} catch {
|
|
33
|
-
result.options[key] = next;
|
|
34
|
-
}
|
|
35
|
-
i++;
|
|
36
|
-
} else {
|
|
37
|
-
result.flags[key] = true;
|
|
38
|
-
}
|
|
39
|
-
} else if (arg.startsWith('-')) {
|
|
40
|
-
result.flags[arg.slice(1)] = true;
|
|
41
|
-
} else {
|
|
42
|
-
result._.push(arg);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return result;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Table formatter for human-readable output
|
|
50
|
-
function formatTable(data) {
|
|
51
|
-
// Extract array of records from various response shapes
|
|
52
|
-
let records = [];
|
|
53
|
-
if (Array.isArray(data)) {
|
|
54
|
-
records = data;
|
|
55
|
-
} else if (data?.data && Array.isArray(data.data)) {
|
|
56
|
-
records = data.data;
|
|
57
|
-
} else if (data?.results && Array.isArray(data.results)) {
|
|
58
|
-
records = data.results;
|
|
59
|
-
} else if (data?.data?.results && Array.isArray(data.data.results)) {
|
|
60
|
-
records = data.data.results;
|
|
61
|
-
} else if (typeof data === 'object' && data !== null) {
|
|
62
|
-
// Single object - convert to array
|
|
63
|
-
records = [data];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (records.length === 0) {
|
|
67
|
-
return 'No data';
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Get columns from first record, prioritize common useful fields
|
|
71
|
-
const priorityFields = ['token_symbol', 'token_name', 'symbol', 'name', 'address', 'label', 'chain', 'value_usd', 'amount', 'pnl_usd', 'price_usd', 'volume_usd', 'net_flow_usd', 'timestamp', 'block_timestamp'];
|
|
72
|
-
const allKeys = [...new Set(records.flatMap(r => Object.keys(r)))];
|
|
73
|
-
|
|
74
|
-
// Sort: priority fields first, then alphabetically
|
|
75
|
-
const columns = allKeys.sort((a, b) => {
|
|
76
|
-
const aIdx = priorityFields.indexOf(a);
|
|
77
|
-
const bIdx = priorityFields.indexOf(b);
|
|
78
|
-
if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
|
|
79
|
-
if (aIdx !== -1) return -1;
|
|
80
|
-
if (bIdx !== -1) return 1;
|
|
81
|
-
return a.localeCompare(b);
|
|
82
|
-
}).slice(0, 8); // Limit to 8 columns for readability
|
|
83
|
-
|
|
84
|
-
// Calculate column widths
|
|
85
|
-
const widths = columns.map(col => {
|
|
86
|
-
const headerLen = col.length;
|
|
87
|
-
const maxDataLen = Math.max(...records.map(r => {
|
|
88
|
-
const val = formatValue(r[col]);
|
|
89
|
-
return val.length;
|
|
90
|
-
}));
|
|
91
|
-
return Math.min(Math.max(headerLen, maxDataLen), 30); // Cap at 30 chars
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Build table
|
|
95
|
-
const separator = '─';
|
|
96
|
-
const lines = [];
|
|
97
|
-
|
|
98
|
-
// Header
|
|
99
|
-
const header = columns.map((col, i) => col.padEnd(widths[i])).join(' │ ');
|
|
100
|
-
lines.push(header);
|
|
101
|
-
lines.push(widths.map(w => separator.repeat(w)).join('─┼─'));
|
|
102
|
-
|
|
103
|
-
// Rows
|
|
104
|
-
for (const record of records.slice(0, 50)) { // Limit to 50 rows
|
|
105
|
-
const row = columns.map((col, i) => {
|
|
106
|
-
const val = formatValue(record[col]);
|
|
107
|
-
return val.slice(0, widths[i]).padEnd(widths[i]);
|
|
108
|
-
}).join(' │ ');
|
|
109
|
-
lines.push(row);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (records.length > 50) {
|
|
113
|
-
lines.push(`... and ${records.length - 50} more rows`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return lines.join('\n');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function formatValue(val) {
|
|
120
|
-
if (val === null || val === undefined) return '';
|
|
121
|
-
if (typeof val === 'number') {
|
|
122
|
-
if (Math.abs(val) >= 1000000) return (val / 1000000).toFixed(2) + 'M';
|
|
123
|
-
if (Math.abs(val) >= 1000) return (val / 1000).toFixed(2) + 'K';
|
|
124
|
-
if (Number.isInteger(val)) return val.toString();
|
|
125
|
-
return val.toFixed(2);
|
|
126
|
-
}
|
|
127
|
-
if (typeof val === 'object') return JSON.stringify(val);
|
|
128
|
-
return String(val);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Output helper
|
|
132
|
-
function output(data, pretty = false, table = false) {
|
|
133
|
-
if (table) {
|
|
134
|
-
if (data.success === false) {
|
|
135
|
-
console.error(`Error: ${data.error}`);
|
|
136
|
-
} else {
|
|
137
|
-
const tableData = data.data || data;
|
|
138
|
-
console.log(formatTable(tableData));
|
|
139
|
-
}
|
|
140
|
-
} else if (pretty) {
|
|
141
|
-
console.log(JSON.stringify(data, null, 2));
|
|
142
|
-
} else {
|
|
143
|
-
console.log(JSON.stringify(data));
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Error output
|
|
148
|
-
function errorOutput(error, pretty = false, table = false) {
|
|
149
|
-
const errorData = {
|
|
150
|
-
success: false,
|
|
151
|
-
error: error.message,
|
|
152
|
-
status: error.status,
|
|
153
|
-
details: error.data
|
|
154
|
-
};
|
|
155
|
-
output(errorData, pretty, table);
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Parse simple sort syntax: "field:direction" or "field" (defaults to DESC)
|
|
160
|
-
function parseSort(sortOption, orderByOption) {
|
|
161
|
-
// If --order-by is provided, use it (full JSON control)
|
|
162
|
-
if (orderByOption) return orderByOption;
|
|
163
|
-
|
|
164
|
-
// If no --sort, return undefined
|
|
165
|
-
if (!sortOption) return undefined;
|
|
166
|
-
|
|
167
|
-
// Parse --sort field:direction or --sort field
|
|
168
|
-
const parts = sortOption.split(':');
|
|
169
|
-
const field = parts[0];
|
|
170
|
-
const direction = (parts[1] || 'desc').toUpperCase();
|
|
171
|
-
|
|
172
|
-
return [{ field, direction }];
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Help text
|
|
176
|
-
const HELP = `
|
|
177
|
-
Nansen CLI - Command-line interface for Nansen API
|
|
178
|
-
Designed for AI agents with structured JSON output.
|
|
179
|
-
|
|
180
|
-
USAGE:
|
|
181
|
-
nansen <command> [subcommand] [options]
|
|
182
|
-
|
|
183
|
-
COMMANDS:
|
|
184
|
-
login Save your API key (interactive)
|
|
185
|
-
logout Remove saved API key
|
|
186
|
-
smart-money Smart Money analytics (netflow, dex-trades, holdings, dcas, historical-holdings)
|
|
187
|
-
profiler Wallet profiling (balance, labels, transactions, pnl, perp-positions, perp-trades)
|
|
188
|
-
token Token God Mode (screener, holders, flows, trades, pnl, perp-trades, perp-positions)
|
|
189
|
-
portfolio Portfolio analytics (defi-holdings)
|
|
190
|
-
help Show this help message
|
|
191
|
-
|
|
192
|
-
GLOBAL OPTIONS:
|
|
193
|
-
--pretty Format JSON output for readability
|
|
194
|
-
--table Format output as human-readable table
|
|
195
|
-
--chain Blockchain to query (ethereum, solana, base, etc.)
|
|
196
|
-
--chains Multiple chains as JSON array
|
|
197
|
-
--limit Number of results (shorthand for pagination)
|
|
198
|
-
--filters JSON object with filters
|
|
199
|
-
--sort Sort by field (e.g., --sort value_usd:desc)
|
|
200
|
-
--order-by JSON array with sort order (advanced)
|
|
201
|
-
--days Date range in days (default: 30 for most endpoints)
|
|
202
|
-
--symbol Token symbol (for perp endpoints)
|
|
203
|
-
|
|
204
|
-
EXAMPLES:
|
|
205
|
-
# Get Smart Money netflow on Solana
|
|
206
|
-
nansen smart-money netflow --chain solana
|
|
207
|
-
|
|
208
|
-
# Get top tokens by Smart Money activity
|
|
209
|
-
nansen token screener --chain solana --timeframe 24h --pretty
|
|
210
|
-
|
|
211
|
-
# Get wallet balance
|
|
212
|
-
nansen profiler balance --address 0x123... --chain ethereum
|
|
213
|
-
|
|
214
|
-
# Get wallet labels
|
|
215
|
-
nansen profiler labels --address 0x123... --chain ethereum
|
|
216
|
-
|
|
217
|
-
# Search for entity
|
|
218
|
-
nansen profiler search --query "Vitalik"
|
|
219
|
-
|
|
220
|
-
# Get token holders with filters
|
|
221
|
-
nansen token holders --token 0x123... --filters '{"only_smart_money":true}'
|
|
222
|
-
|
|
223
|
-
SMART MONEY LABELS:
|
|
224
|
-
Fund, Smart Trader, 30D Smart Trader, 90D Smart Trader,
|
|
225
|
-
180D Smart Trader, Smart HL Perps Trader
|
|
226
|
-
|
|
227
|
-
SUPPORTED CHAINS:
|
|
228
|
-
ethereum, solana, base, bnb, arbitrum, polygon, optimism,
|
|
229
|
-
avalanche, linea, scroll, zksync, mantle, ronin, sei,
|
|
230
|
-
plasma, sonic, unichain, monad, hyperevm, iotaevm
|
|
231
|
-
|
|
232
|
-
For more info: https://docs.nansen.ai
|
|
233
|
-
`;
|
|
234
|
-
|
|
235
|
-
// Command handlers
|
|
236
|
-
// Helper to prompt for input
|
|
237
|
-
async function prompt(question, hidden = false) {
|
|
238
|
-
const rl = readline.createInterface({
|
|
239
|
-
input: process.stdin,
|
|
240
|
-
output: process.stdout
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
return new Promise((resolve) => {
|
|
244
|
-
if (hidden && process.stdout.isTTY) {
|
|
245
|
-
process.stdout.write(question);
|
|
246
|
-
let input = '';
|
|
247
|
-
process.stdin.setRawMode(true);
|
|
248
|
-
process.stdin.resume();
|
|
249
|
-
process.stdin.setEncoding('utf8');
|
|
250
|
-
|
|
251
|
-
const onData = (char) => {
|
|
252
|
-
if (char === '\n' || char === '\r') {
|
|
253
|
-
process.stdin.setRawMode(false);
|
|
254
|
-
process.stdin.pause();
|
|
255
|
-
process.stdin.removeListener('data', onData);
|
|
256
|
-
process.stdout.write('\n');
|
|
257
|
-
rl.close();
|
|
258
|
-
resolve(input);
|
|
259
|
-
} else if (char === '\u0003') {
|
|
260
|
-
// Ctrl+C
|
|
261
|
-
process.exit();
|
|
262
|
-
} else if (char === '\u007F' || char === '\b') {
|
|
263
|
-
// Backspace
|
|
264
|
-
if (input.length > 0) {
|
|
265
|
-
input = input.slice(0, -1);
|
|
266
|
-
process.stdout.write('\b \b');
|
|
267
|
-
}
|
|
268
|
-
} else {
|
|
269
|
-
input += char;
|
|
270
|
-
process.stdout.write('*');
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
process.stdin.on('data', onData);
|
|
275
|
-
} else {
|
|
276
|
-
rl.question(question, (answer) => {
|
|
277
|
-
rl.close();
|
|
278
|
-
resolve(answer);
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const commands = {
|
|
285
|
-
'login': async (args, api, flags) => {
|
|
286
|
-
console.log('Nansen CLI Login\n');
|
|
287
|
-
console.log('Get your API key at: https://app.nansen.ai/api\n');
|
|
288
|
-
|
|
289
|
-
const apiKey = await prompt('Enter your API key: ', true);
|
|
290
|
-
|
|
291
|
-
if (!apiKey || apiKey.trim().length === 0) {
|
|
292
|
-
console.log('\n❌ No API key provided');
|
|
293
|
-
process.exit(1);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Validate the key with a test request
|
|
297
|
-
console.log('\nValidating API key...');
|
|
298
|
-
try {
|
|
299
|
-
const testApi = new NansenAPI(apiKey.trim());
|
|
300
|
-
await testApi.tokenScreener({ chains: ['solana'], pagination: { page: 1, per_page: 1 } });
|
|
301
|
-
|
|
302
|
-
// Save the config
|
|
303
|
-
saveConfig({
|
|
304
|
-
apiKey: apiKey.trim(),
|
|
305
|
-
baseUrl: 'https://api.nansen.ai'
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
console.log('✓ API key validated');
|
|
309
|
-
console.log(`✓ Saved to ${getConfigFile()}\n`);
|
|
310
|
-
console.log('You can now use the Nansen CLI. Try:');
|
|
311
|
-
console.log(' nansen token screener --chain solana --pretty');
|
|
312
|
-
} catch (error) {
|
|
313
|
-
console.log(`\n❌ Invalid API key: ${error.message}`);
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
},
|
|
317
|
-
|
|
318
|
-
'logout': async (args, api, flags) => {
|
|
319
|
-
const deleted = deleteConfig();
|
|
320
|
-
if (deleted) {
|
|
321
|
-
console.log(`✓ Removed ${getConfigFile()}`);
|
|
322
|
-
} else {
|
|
323
|
-
console.log('No saved credentials found');
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
|
|
327
|
-
'help': async (args, api, flags) => {
|
|
328
|
-
console.log(HELP);
|
|
329
|
-
},
|
|
330
|
-
|
|
331
|
-
'smart-money': async (args, api, flags, options) => {
|
|
332
|
-
const subcommand = args[0] || 'help';
|
|
333
|
-
const chain = options.chain || 'solana';
|
|
334
|
-
const chains = options.chains || [chain];
|
|
335
|
-
const filters = options.filters || {};
|
|
336
|
-
const orderBy = parseSort(options.sort, options['order-by']);
|
|
337
|
-
const pagination = options.limit ? { page: 1, per_page: options.limit } : undefined;
|
|
338
|
-
|
|
339
|
-
// Add smart money label filter if specified
|
|
340
|
-
if (options.labels) {
|
|
341
|
-
filters.include_smart_money_labels = Array.isArray(options.labels)
|
|
342
|
-
? options.labels
|
|
343
|
-
: [options.labels];
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const days = options.days ? parseInt(options.days) : 30;
|
|
347
|
-
|
|
348
|
-
const handlers = {
|
|
349
|
-
'netflow': () => api.smartMoneyNetflow({ chains, filters, orderBy, pagination }),
|
|
350
|
-
'dex-trades': () => api.smartMoneyDexTrades({ chains, filters, orderBy, pagination }),
|
|
351
|
-
'perp-trades': () => api.smartMoneyPerpTrades({ filters, orderBy, pagination }),
|
|
352
|
-
'holdings': () => api.smartMoneyHoldings({ chains, filters, orderBy, pagination }),
|
|
353
|
-
'dcas': () => api.smartMoneyDcas({ filters, orderBy, pagination }),
|
|
354
|
-
'historical-holdings': () => api.smartMoneyHistoricalHoldings({ chains, filters, orderBy, pagination, days }),
|
|
355
|
-
'help': () => ({
|
|
356
|
-
commands: ['netflow', 'dex-trades', 'perp-trades', 'holdings', 'dcas', 'historical-holdings'],
|
|
357
|
-
description: 'Smart Money analytics endpoints',
|
|
358
|
-
example: 'nansen smart-money netflow --chain solana --labels Fund'
|
|
359
|
-
})
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
if (!handlers[subcommand]) {
|
|
363
|
-
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return handlers[subcommand]();
|
|
367
|
-
},
|
|
368
|
-
|
|
369
|
-
'profiler': async (args, api, flags, options) => {
|
|
370
|
-
const subcommand = args[0] || 'help';
|
|
371
|
-
const address = options.address;
|
|
372
|
-
const entityName = options.entity || options['entity-name'];
|
|
373
|
-
const chain = options.chain || 'ethereum';
|
|
374
|
-
const filters = options.filters || {};
|
|
375
|
-
const orderBy = parseSort(options.sort, options['order-by']);
|
|
376
|
-
const pagination = options.limit ? { page: 1, recordsPerPage: options.limit } : undefined;
|
|
377
|
-
const days = options.days ? parseInt(options.days) : 30;
|
|
378
|
-
|
|
379
|
-
const handlers = {
|
|
380
|
-
'balance': () => api.addressBalance({ address, entityName, chain, filters, orderBy }),
|
|
381
|
-
'labels': () => api.addressLabels({ address, chain, pagination }),
|
|
382
|
-
'transactions': () => api.addressTransactions({ address, chain, filters, orderBy, pagination }),
|
|
383
|
-
'pnl': () => api.addressPnl({ address, chain }),
|
|
384
|
-
'search': () => api.entitySearch({ query: options.query, pagination }),
|
|
385
|
-
'historical-balances': () => api.addressHistoricalBalances({ address, chain, filters, orderBy, pagination, days }),
|
|
386
|
-
'related-wallets': () => api.addressRelatedWallets({ address, chain, filters, orderBy, pagination }),
|
|
387
|
-
'counterparties': () => api.addressCounterparties({ address, chain, filters, orderBy, pagination, days }),
|
|
388
|
-
'pnl-summary': () => api.addressPnlSummary({ address, chain, filters, orderBy, pagination, days }),
|
|
389
|
-
'perp-positions': () => api.addressPerpPositions({ address, filters, orderBy, pagination }),
|
|
390
|
-
'perp-trades': () => api.addressPerpTrades({ address, filters, orderBy, pagination, days }),
|
|
391
|
-
'help': () => ({
|
|
392
|
-
commands: ['balance', 'labels', 'transactions', 'pnl', 'search', 'historical-balances', 'related-wallets', 'counterparties', 'pnl-summary', 'perp-positions', 'perp-trades'],
|
|
393
|
-
description: 'Wallet profiling endpoints',
|
|
394
|
-
example: 'nansen profiler balance --address 0x123... --chain ethereum'
|
|
395
|
-
})
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
if (!handlers[subcommand]) {
|
|
399
|
-
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return handlers[subcommand]();
|
|
403
|
-
},
|
|
404
|
-
|
|
405
|
-
'token': async (args, api, flags, options) => {
|
|
406
|
-
const subcommand = args[0] || 'help';
|
|
407
|
-
const tokenAddress = options.token || options['token-address'];
|
|
408
|
-
const tokenSymbol = options.symbol || options['token-symbol'];
|
|
409
|
-
const chain = options.chain || 'solana';
|
|
410
|
-
const chains = options.chains || [chain];
|
|
411
|
-
const timeframe = options.timeframe || '24h';
|
|
412
|
-
const filters = options.filters || {};
|
|
413
|
-
const orderBy = parseSort(options.sort, options['order-by']);
|
|
414
|
-
const pagination = options.limit ? { page: 1, per_page: options.limit } : undefined;
|
|
415
|
-
const days = options.days ? parseInt(options.days) : 30;
|
|
416
|
-
|
|
417
|
-
// Convenience filter for smart money only
|
|
418
|
-
const onlySmartMoney = options['smart-money'] || flags['smart-money'] || false;
|
|
419
|
-
if (onlySmartMoney) {
|
|
420
|
-
filters.only_smart_money = true;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const handlers = {
|
|
424
|
-
'screener': () => api.tokenScreener({ chains, timeframe, filters, orderBy, pagination }),
|
|
425
|
-
'holders': () => api.tokenHolders({ tokenAddress, chain, filters, orderBy, pagination }),
|
|
426
|
-
'flows': () => api.tokenFlows({ tokenAddress, chain, filters, orderBy, pagination }),
|
|
427
|
-
'dex-trades': () => api.tokenDexTrades({ tokenAddress, chain, onlySmartMoney, filters, orderBy, pagination, days }),
|
|
428
|
-
'pnl': () => api.tokenPnlLeaderboard({ tokenAddress, chain, filters, orderBy, pagination, days }),
|
|
429
|
-
'who-bought-sold': () => api.tokenWhoBoughtSold({ tokenAddress, chain, filters, orderBy, pagination }),
|
|
430
|
-
'flow-intelligence': () => api.tokenFlowIntelligence({ tokenAddress, chain, filters, orderBy, pagination }),
|
|
431
|
-
'transfers': () => api.tokenTransfers({ tokenAddress, chain, filters, orderBy, pagination, days }),
|
|
432
|
-
'jup-dca': () => api.tokenJupDca({ tokenAddress, filters, orderBy, pagination }),
|
|
433
|
-
'perp-trades': () => api.tokenPerpTrades({ tokenSymbol, filters, orderBy, pagination, days }),
|
|
434
|
-
'perp-positions': () => api.tokenPerpPositions({ tokenSymbol, filters, orderBy, pagination }),
|
|
435
|
-
'perp-pnl-leaderboard': () => api.tokenPerpPnlLeaderboard({ tokenSymbol, filters, orderBy, pagination, days }),
|
|
436
|
-
'help': () => ({
|
|
437
|
-
commands: ['screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
|
|
438
|
-
description: 'Token God Mode endpoints',
|
|
439
|
-
example: 'nansen token screener --chain solana --timeframe 24h --smart-money'
|
|
440
|
-
})
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
if (!handlers[subcommand]) {
|
|
444
|
-
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return handlers[subcommand]();
|
|
448
|
-
},
|
|
449
|
-
|
|
450
|
-
'portfolio': async (args, api, flags, options) => {
|
|
451
|
-
const subcommand = args[0] || 'help';
|
|
452
|
-
const walletAddress = options.wallet || options.address;
|
|
453
|
-
|
|
454
|
-
const handlers = {
|
|
455
|
-
'defi': () => api.portfolioDefiHoldings({ walletAddress }),
|
|
456
|
-
'defi-holdings': () => api.portfolioDefiHoldings({ walletAddress }),
|
|
457
|
-
'help': () => ({
|
|
458
|
-
commands: ['defi', 'defi-holdings'],
|
|
459
|
-
description: 'Portfolio analytics endpoints',
|
|
460
|
-
example: 'nansen portfolio defi --wallet 0x123...'
|
|
461
|
-
})
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
if (!handlers[subcommand]) {
|
|
465
|
-
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return handlers[subcommand]();
|
|
469
|
-
}
|
|
470
|
-
};
|
|
14
|
+
import { runCLI } from './cli.js';
|
|
471
15
|
|
|
472
16
|
// Main entry point
|
|
473
|
-
|
|
474
|
-
const rawArgs = process.argv.slice(2);
|
|
475
|
-
const { _: positional, flags, options } = parseArgs(rawArgs);
|
|
476
|
-
|
|
477
|
-
const command = positional[0] || 'help';
|
|
478
|
-
const subArgs = positional.slice(1);
|
|
479
|
-
const pretty = flags.pretty || flags.p;
|
|
480
|
-
const table = flags.table || flags.t;
|
|
481
|
-
|
|
482
|
-
if (command === 'help' || flags.help || flags.h) {
|
|
483
|
-
console.log(HELP);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (!commands[command]) {
|
|
488
|
-
output({
|
|
489
|
-
error: `Unknown command: ${command}`,
|
|
490
|
-
available: Object.keys(commands)
|
|
491
|
-
}, pretty, table);
|
|
492
|
-
process.exit(1);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Commands that don't require API authentication
|
|
496
|
-
const noAuthCommands = ['login', 'logout', 'help'];
|
|
497
|
-
|
|
498
|
-
if (noAuthCommands.includes(command)) {
|
|
499
|
-
await commands[command](subArgs, null, flags, options);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
try {
|
|
504
|
-
const api = new NansenAPI();
|
|
505
|
-
const result = await commands[command](subArgs, api, flags, options);
|
|
506
|
-
output({ success: true, data: result }, pretty, table);
|
|
507
|
-
} catch (error) {
|
|
508
|
-
errorOutput(error, pretty, table);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
main();
|
|
17
|
+
runCLI(process.argv.slice(2));
|