nansen-cli 1.30.2 → 1.31.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 +12 -0
- package/package.json +2 -1
- package/src/api.js +145 -0
- package/src/cli.js +21 -1
- package/src/commands/research.js +314 -0
- package/src/schema.json +104 -0
- package/src/trading.js +9 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.31.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#437](https://github.com/nansen-ai/nansen-cli/pull/437) [`5ec7bd3`](https://github.com/nansen-ai/nansen-cli/commit/5ec7bd33f173cd712f9f592599e32b2a0d28fe0f) Thanks [@gulshngill](https://github.com/gulshngill)! - Add `nansen research` command with 11 subcommands for historical/point-in-time analytics: dex-trades, pnl-leaderboard, token-flow-summary, token-quant-scores, top-holders, who-bought-sold, smart-money-balances, token-screener, wallet-balances, tx-lookup, wallet-transactions. Labels and metrics resolve at the requested date rather than current state — useful for backtesting and historical research.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#440](https://github.com/nansen-ai/nansen-cli/pull/440) [`701dad4`](https://github.com/nansen-ai/nansen-cli/commit/701dad49f54323cf2b455376b1af3b012e2e0b71) Thanks [@kome12](https://github.com/kome12)! - Fix `research historical-token-screener` schema to mark `--to-date` as required (matching CLI and API behavior)
|
|
12
|
+
|
|
13
|
+
- [#442](https://github.com/nansen-ai/nansen-cli/pull/442) [`6edbb68`](https://github.com/nansen-ai/nansen-cli/commit/6edbb686b52e00b8725470a3f4409ff15f2ccb95) Thanks [@kome12](https://github.com/kome12)! - `research historical-token-flow-summary` now errors immediately when `--page` or `--limit` are passed (the endpoint returns a single aggregated row and does not support pagination). `research historical-smart-money-balances` now errors when `--sort` or `--order-by` are passed (the endpoint does not support ordering). Previously both flags were silently dropped.
|
|
14
|
+
|
|
3
15
|
## 1.30.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nansen-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.31.0",
|
|
4
4
|
"description": "AI-agent CLI for Nansen API analytics, DEX swaps, and cross-chain trading",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"@changesets/cli": "^2.29.4",
|
|
67
67
|
"@eslint/js": "^10.0.1",
|
|
68
68
|
"@vitest/coverage-v8": "^4.0.18",
|
|
69
|
+
"clawhub": "^0.9.0",
|
|
69
70
|
"eslint": "^10.0.2",
|
|
70
71
|
"globals": "^17.4.0",
|
|
71
72
|
"vitest": "^4.0.18"
|
package/src/api.js
CHANGED
|
@@ -1363,6 +1363,151 @@ export class NansenAPI {
|
|
|
1363
1363
|
});
|
|
1364
1364
|
}
|
|
1365
1365
|
|
|
1366
|
+
// ============= Research (Historical) Endpoints =============
|
|
1367
|
+
// Historical/point-in-time analytics: labels and metrics are resolved at the
|
|
1368
|
+
// requested date rather than current state. Useful for backtesting and historical
|
|
1369
|
+
// research. Some endpoints use a { from, to } date range; others use a single
|
|
1370
|
+
// as_of_date snapshot; the token-screener uses timeframe_days + optional to_date.
|
|
1371
|
+
|
|
1372
|
+
async researchDexTrades(params = {}) {
|
|
1373
|
+
const { tokenAddress, chain = 'solana', fromDate, toDate, filters = {}, orderBy, pagination } = params;
|
|
1374
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1375
|
+
return this.request('/api/v1beta1/tgm/historical-dex-trades', {
|
|
1376
|
+
token_address: tokenAddress,
|
|
1377
|
+
chain,
|
|
1378
|
+
date_range: { from: fromDate, to: toDate },
|
|
1379
|
+
filters,
|
|
1380
|
+
order_by: orderBy,
|
|
1381
|
+
pagination,
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
async researchPnlLeaderboard(params = {}) {
|
|
1386
|
+
const { tokenAddress, chain = 'solana', fromDate, toDate, filters = {}, orderBy, pagination } = params;
|
|
1387
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1388
|
+
return this.request('/api/v1beta1/tgm/historical-pnl-leaderboard', {
|
|
1389
|
+
token_address: tokenAddress,
|
|
1390
|
+
chain,
|
|
1391
|
+
date_range: { from: fromDate, to: toDate },
|
|
1392
|
+
filters,
|
|
1393
|
+
order_by: orderBy,
|
|
1394
|
+
pagination,
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
async researchTokenFlowSummary(params = {}) {
|
|
1399
|
+
// API does not support pagination on this endpoint.
|
|
1400
|
+
const { tokenAddress, chain = 'solana', fromDate, toDate, filters = {}, orderBy } = params;
|
|
1401
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1402
|
+
return this.request('/api/v1beta1/tgm/historical-token-flow-summary', {
|
|
1403
|
+
token_address: tokenAddress,
|
|
1404
|
+
chain,
|
|
1405
|
+
date_range: { from: fromDate, to: toDate },
|
|
1406
|
+
filters,
|
|
1407
|
+
order_by: orderBy,
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async researchTokenQuantScores(params = {}) {
|
|
1412
|
+
const { tokenAddress, chain = 'solana', asOfDate, filters = {}, orderBy, pagination } = params;
|
|
1413
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1414
|
+
return this.request('/api/v1beta1/tgm/historical-token-quant-scores', {
|
|
1415
|
+
token_address: tokenAddress,
|
|
1416
|
+
chain,
|
|
1417
|
+
as_of_date: asOfDate,
|
|
1418
|
+
filters,
|
|
1419
|
+
order_by: orderBy,
|
|
1420
|
+
pagination,
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
async researchTopHolders(params = {}) {
|
|
1425
|
+
const { tokenAddress, chain = 'solana', asOfDate, filters = {}, orderBy, pagination } = params;
|
|
1426
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1427
|
+
return this.request('/api/v1beta1/tgm/historical-top-holders', {
|
|
1428
|
+
token_address: tokenAddress,
|
|
1429
|
+
chain,
|
|
1430
|
+
as_of_date: asOfDate,
|
|
1431
|
+
filters,
|
|
1432
|
+
order_by: orderBy,
|
|
1433
|
+
pagination,
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
async researchWhoBoughtSold(params = {}) {
|
|
1438
|
+
const { tokenAddress, chain = 'solana', fromDate, toDate, buyOrSell = 'BUY', filters = {}, orderBy, pagination } = params;
|
|
1439
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1440
|
+
return this.request('/api/v1beta1/tgm/historical-who-bought-sold', {
|
|
1441
|
+
token_address: tokenAddress,
|
|
1442
|
+
chain,
|
|
1443
|
+
buy_or_sell: buyOrSell,
|
|
1444
|
+
date_range: { from: fromDate, to: toDate },
|
|
1445
|
+
filters,
|
|
1446
|
+
order_by: orderBy,
|
|
1447
|
+
pagination,
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async researchSmartMoneyBalances(params = {}) {
|
|
1452
|
+
// API does not support order_by on this endpoint.
|
|
1453
|
+
const { chains = ['solana'], asOfDate, filters = {}, pagination } = params;
|
|
1454
|
+
return this.request('/api/v1beta1/smart-money/historical-token-balances', {
|
|
1455
|
+
chains,
|
|
1456
|
+
as_of_date: asOfDate,
|
|
1457
|
+
filters,
|
|
1458
|
+
pagination,
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
async researchTokenScreener(params = {}) {
|
|
1463
|
+
const { chains = ['solana'], timeframeDays, toDate, filters = {}, orderBy, pagination } = params;
|
|
1464
|
+
return this.request('/api/v1beta1/token-screener/historical', {
|
|
1465
|
+
chains,
|
|
1466
|
+
timeframe_days: timeframeDays,
|
|
1467
|
+
to_date: toDate,
|
|
1468
|
+
filters,
|
|
1469
|
+
order_by: orderBy,
|
|
1470
|
+
pagination,
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
async researchWalletBalances(params = {}) {
|
|
1475
|
+
const { address, chain = 'ethereum', asOfDate, filters = {}, orderBy, pagination } = params;
|
|
1476
|
+
if (address) requireValidAddress(address, chain);
|
|
1477
|
+
return this.request('/api/v1beta1/profiler/address/historical-token-balances', {
|
|
1478
|
+
address,
|
|
1479
|
+
chain,
|
|
1480
|
+
as_of_date: asOfDate,
|
|
1481
|
+
filters,
|
|
1482
|
+
order_by: orderBy,
|
|
1483
|
+
pagination,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
async researchTxLookup(params = {}) {
|
|
1488
|
+
const { txHash, chain = 'ethereum', asOfDate, blockTimestamp } = params;
|
|
1489
|
+
const body = {
|
|
1490
|
+
transaction_hash: txHash,
|
|
1491
|
+
chain,
|
|
1492
|
+
as_of_date: asOfDate,
|
|
1493
|
+
};
|
|
1494
|
+
if (blockTimestamp) body.block_timestamp = blockTimestamp;
|
|
1495
|
+
return this.request('/api/v1beta1/profiler/historical-transaction-lookup', body);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
async researchWalletTransactions(params = {}) {
|
|
1499
|
+
const { address, chain = 'ethereum', asOfDate, filters = {}, orderBy, pagination } = params;
|
|
1500
|
+
if (address) requireValidAddress(address, chain);
|
|
1501
|
+
return this.request('/api/v1beta1/profiler/address/historical-transactions', {
|
|
1502
|
+
address,
|
|
1503
|
+
chain,
|
|
1504
|
+
as_of_date: asOfDate,
|
|
1505
|
+
filters,
|
|
1506
|
+
order_by: orderBy,
|
|
1507
|
+
pagination,
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1366
1511
|
// ============= Smart Alert Endpoints =============
|
|
1367
1512
|
|
|
1368
1513
|
async alertsList(params = {}) {
|
package/src/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { buildTradingCommands } from './trading.js';
|
|
|
9
9
|
import { buildLimitOrderCommands } from './limit-order.js';
|
|
10
10
|
import { formatAlertsTable, buildAlertsCommands } from './commands/alerts.js';
|
|
11
11
|
import { buildAgentCommands } from './commands/agent.js';
|
|
12
|
+
import { buildResearchCommands, RESEARCH_HISTORICAL_SUBCOMMANDS } from './commands/research.js';
|
|
12
13
|
import { resolveAddress, isEnsName } from './ens.js';
|
|
13
14
|
import fs from 'fs';
|
|
14
15
|
import { getUpdateNotification, getUpgradeNotice, scheduleUpdateCheck } from './update-check.js';
|
|
@@ -1471,19 +1472,25 @@ export function buildCommands(deps = {}) {
|
|
|
1471
1472
|
// 'research' delegates to the category handlers defined above
|
|
1472
1473
|
const RESEARCH_CATEGORIES = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points', 'prediction-market']);
|
|
1473
1474
|
|
|
1475
|
+
const researchHistorical = buildResearchCommands(deps).research;
|
|
1476
|
+
|
|
1474
1477
|
cmds['research'] = async (args, apiInstance, flags, options) => {
|
|
1475
1478
|
const rawCategory = args[0];
|
|
1476
1479
|
if (!rawCategory || rawCategory === 'help') {
|
|
1477
1480
|
return {
|
|
1478
1481
|
categories: [...RESEARCH_CATEGORIES],
|
|
1482
|
+
historical: [...RESEARCH_HISTORICAL_SUBCOMMANDS],
|
|
1479
1483
|
aliases: RESEARCH_CATEGORY_ALIASES,
|
|
1480
1484
|
description: 'Research and analytics commands',
|
|
1481
1485
|
example: 'nansen research smart-money netflow --chain solana'
|
|
1482
1486
|
};
|
|
1483
1487
|
}
|
|
1488
|
+
if (RESEARCH_HISTORICAL_SUBCOMMANDS.has(rawCategory)) {
|
|
1489
|
+
return researchHistorical(args, apiInstance, flags, options);
|
|
1490
|
+
}
|
|
1484
1491
|
const category = RESEARCH_CATEGORY_ALIASES[rawCategory] || rawCategory;
|
|
1485
1492
|
if (!RESEARCH_CATEGORIES.has(category)) {
|
|
1486
|
-
throw new NansenError(`Unknown research category: ${rawCategory}. Available: ${[...RESEARCH_CATEGORIES].join(', ')}`, ErrorCode.UNKNOWN);
|
|
1493
|
+
throw new NansenError(`Unknown research category: ${rawCategory}. Available: ${[...RESEARCH_CATEGORIES, ...RESEARCH_HISTORICAL_SUBCOMMANDS].join(', ')}`, ErrorCode.UNKNOWN);
|
|
1487
1494
|
}
|
|
1488
1495
|
return cmds[category](args.slice(1), apiInstance, flags, options);
|
|
1489
1496
|
};
|
|
@@ -1712,6 +1719,19 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1712
1719
|
if (catSchema.subcommands) {
|
|
1713
1720
|
lines.push('Subcommands: ' + Object.keys(catSchema.subcommands).join(', '));
|
|
1714
1721
|
lines.push(`Use: nansen research ${category} <subcommand> --help`);
|
|
1722
|
+
} else if (catSchema.options) {
|
|
1723
|
+
// Leaf historical subcommand: render options + example
|
|
1724
|
+
const params = Object.entries(catSchema.options).map(([name, opt]) => {
|
|
1725
|
+
const parts = [`--${name}`];
|
|
1726
|
+
if (opt.required) parts[0] += '*';
|
|
1727
|
+
if (opt.default !== undefined) parts.push(`(${opt.default})`);
|
|
1728
|
+
return parts.join(' ');
|
|
1729
|
+
});
|
|
1730
|
+
lines.push(`Params (* required): ${params.join(', ')}`);
|
|
1731
|
+
if (catSchema.endpoint) {
|
|
1732
|
+
const cost = getCostForEndpoint(catSchema.endpoint);
|
|
1733
|
+
if (cost) lines.push(`Cost: ${cost.free} credit${cost.free === 1 ? '' : 's'} (Free tier) / ${cost.pro} credit${cost.pro === 1 ? '' : 's'} (Pro tier)`);
|
|
1734
|
+
}
|
|
1715
1735
|
}
|
|
1716
1736
|
output(lines.join('\n'));
|
|
1717
1737
|
notify();
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nansen CLI - Research command
|
|
3
|
+
*
|
|
4
|
+
* Historical/point-in-time analytics. Each subcommand resolves labels and
|
|
5
|
+
* metrics at the requested date rather than current state — useful for
|
|
6
|
+
* backtesting and historical research.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { NansenError, ErrorCode } from '../api.js';
|
|
10
|
+
|
|
11
|
+
// Local copies of CLI helpers to avoid a circular import with src/cli.js.
|
|
12
|
+
function buildPagination(options) {
|
|
13
|
+
if (!options.limit && !options.page) return undefined;
|
|
14
|
+
return {
|
|
15
|
+
page: Math.max(1, parseInt(options.page, 10) || 1),
|
|
16
|
+
per_page: options.limit,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseSort(sortOption, orderByOption) {
|
|
21
|
+
if (orderByOption) return orderByOption;
|
|
22
|
+
if (!sortOption) return undefined;
|
|
23
|
+
const parts = String(sortOption).split(':');
|
|
24
|
+
const field = parts[0];
|
|
25
|
+
const direction = (parts[1] || 'desc').toUpperCase();
|
|
26
|
+
return [{ field, direction }];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SUBCOMMANDS = [
|
|
30
|
+
'historical-dex-trades',
|
|
31
|
+
'historical-pnl-leaderboard',
|
|
32
|
+
'historical-token-flow-summary',
|
|
33
|
+
'historical-token-quant-scores',
|
|
34
|
+
'historical-top-holders',
|
|
35
|
+
'historical-who-bought-sold',
|
|
36
|
+
'historical-smart-money-balances',
|
|
37
|
+
'historical-token-screener',
|
|
38
|
+
'historical-wallet-balances',
|
|
39
|
+
'historical-tx-lookup',
|
|
40
|
+
'historical-wallet-transactions',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export const RESEARCH_HISTORICAL_SUBCOMMANDS = new Set(SUBCOMMANDS);
|
|
44
|
+
|
|
45
|
+
function requireOptions(options, required) {
|
|
46
|
+
const missing = required.filter(name => !options[name]);
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
throw new NansenError(
|
|
49
|
+
`Required: ${missing.map(n => '--' + n).join(', ')}`,
|
|
50
|
+
ErrorCode.MISSING_PARAM,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveDateRange(options) {
|
|
56
|
+
return { fromDate: options['from-date'], toDate: options['to-date'] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseTimeframeDays(value) {
|
|
60
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
61
|
+
const n = parseInt(value, 10);
|
|
62
|
+
if (Number.isNaN(n)) {
|
|
63
|
+
throw new NansenError('--timeframe-days must be an integer', ErrorCode.INVALID_PARAMS);
|
|
64
|
+
}
|
|
65
|
+
return n;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseChains(options) {
|
|
69
|
+
if (options.chains) {
|
|
70
|
+
return Array.isArray(options.chains)
|
|
71
|
+
? options.chains
|
|
72
|
+
: String(options.chains).split(',').map(s => s.trim()).filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
if (options.chain) return [options.chain];
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const HELP_TOP = `nansen research — Historical/point-in-time analytics
|
|
79
|
+
|
|
80
|
+
SUBCOMMANDS:
|
|
81
|
+
historical-dex-trades Historical DEX trades for a token
|
|
82
|
+
historical-pnl-leaderboard Historical PnL leaderboard for a token
|
|
83
|
+
historical-token-flow-summary Historical token flow summary
|
|
84
|
+
historical-token-quant-scores Historical token quantitative scores
|
|
85
|
+
historical-top-holders Historical top holders of a token
|
|
86
|
+
historical-who-bought-sold Historical buyers/sellers of a token
|
|
87
|
+
historical-smart-money-balances Historical smart money token balances
|
|
88
|
+
historical-token-screener Historical token screener
|
|
89
|
+
historical-wallet-balances Historical token balances for a wallet
|
|
90
|
+
historical-tx-lookup Lookup a historical transaction by hash
|
|
91
|
+
historical-wallet-transactions Historical transactions for a wallet
|
|
92
|
+
|
|
93
|
+
COMMON OPTIONS:
|
|
94
|
+
--from-date <YYYY-MM-DD> Start of date range (for range-based subcommands)
|
|
95
|
+
--to-date <YYYY-MM-DD> End of date range (for range-based subcommands)
|
|
96
|
+
--as-of-date <YYYY-MM-DD> Snapshot date (for as-of-date subcommands)
|
|
97
|
+
--chain <chain> Chain (default: solana for tokens, ethereum for wallets)
|
|
98
|
+
--page <n> --limit <n> Pagination (not supported by historical-token-flow-summary)
|
|
99
|
+
--sort <field[:asc|desc]> Sort order (not supported by historical-smart-money-balances)
|
|
100
|
+
--filters '<json>' Filters as JSON object
|
|
101
|
+
|
|
102
|
+
Run: nansen research <subcommand> --help`;
|
|
103
|
+
|
|
104
|
+
const SUB_HELP = {
|
|
105
|
+
'historical-dex-trades': `nansen research historical-dex-trades — Historical DEX trades for a token
|
|
106
|
+
|
|
107
|
+
USAGE:
|
|
108
|
+
nansen research historical-dex-trades --token-address <addr> --from-date <YYYY-MM-DD> --to-date <YYYY-MM-DD> [--chain <chain>]`,
|
|
109
|
+
'historical-pnl-leaderboard': `nansen research historical-pnl-leaderboard — Historical PnL leaderboard for a token
|
|
110
|
+
|
|
111
|
+
USAGE:
|
|
112
|
+
nansen research historical-pnl-leaderboard --token-address <addr> --from-date <YYYY-MM-DD> --to-date <YYYY-MM-DD> [--chain <chain>]`,
|
|
113
|
+
'historical-token-flow-summary': `nansen research historical-token-flow-summary — Historical token flow summary
|
|
114
|
+
|
|
115
|
+
USAGE:
|
|
116
|
+
nansen research historical-token-flow-summary --token-address <addr> --from-date <YYYY-MM-DD> --to-date <YYYY-MM-DD> [--chain <chain>]
|
|
117
|
+
|
|
118
|
+
NOTE: This endpoint does not support pagination.`,
|
|
119
|
+
'historical-token-quant-scores': `nansen research historical-token-quant-scores — Historical token quantitative scores
|
|
120
|
+
|
|
121
|
+
USAGE:
|
|
122
|
+
nansen research historical-token-quant-scores --token-address <addr> --as-of-date <YYYY-MM-DD> [--chain <chain>]`,
|
|
123
|
+
'historical-top-holders': `nansen research historical-top-holders — Historical top holders of a token
|
|
124
|
+
|
|
125
|
+
USAGE:
|
|
126
|
+
nansen research historical-top-holders --token-address <addr> --as-of-date <YYYY-MM-DD> [--chain <chain>]`,
|
|
127
|
+
'historical-who-bought-sold': `nansen research historical-who-bought-sold — Historical buyers/sellers of a token
|
|
128
|
+
|
|
129
|
+
USAGE:
|
|
130
|
+
nansen research historical-who-bought-sold --token-address <addr> --from-date <YYYY-MM-DD> --to-date <YYYY-MM-DD> [--buy-or-sell BUY|SELL] [--chain <chain>]`,
|
|
131
|
+
'historical-smart-money-balances': `nansen research historical-smart-money-balances — Historical smart money token balances
|
|
132
|
+
|
|
133
|
+
USAGE:
|
|
134
|
+
nansen research historical-smart-money-balances --as-of-date <YYYY-MM-DD> [--chains c1,c2]
|
|
135
|
+
|
|
136
|
+
NOTE: This endpoint does not support order_by.`,
|
|
137
|
+
'historical-token-screener': `nansen research historical-token-screener — Historical token screener
|
|
138
|
+
|
|
139
|
+
USAGE:
|
|
140
|
+
nansen research historical-token-screener --timeframe-days <n> --to-date <YYYY-MM-DD> [--chains c1,c2]`,
|
|
141
|
+
'historical-wallet-balances': `nansen research historical-wallet-balances — Historical token balances for a wallet
|
|
142
|
+
|
|
143
|
+
USAGE:
|
|
144
|
+
nansen research historical-wallet-balances --address <addr> --as-of-date <YYYY-MM-DD> [--chain <chain>]`,
|
|
145
|
+
'historical-tx-lookup': `nansen research historical-tx-lookup — Lookup a historical transaction by hash
|
|
146
|
+
|
|
147
|
+
USAGE:
|
|
148
|
+
nansen research historical-tx-lookup --transaction-hash <hash> --as-of-date <YYYY-MM-DD> [--chain <chain>] [--block-timestamp "YYYY-MM-DD HH:MM:SS"]
|
|
149
|
+
|
|
150
|
+
NOTE: Providing --block-timestamp skips a slow hash-resolution step and returns results much faster.`,
|
|
151
|
+
'historical-wallet-transactions': `nansen research historical-wallet-transactions — Historical transactions for a wallet
|
|
152
|
+
|
|
153
|
+
USAGE:
|
|
154
|
+
nansen research historical-wallet-transactions --address <addr> --as-of-date <YYYY-MM-DD> [--chain <chain>]`,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export function buildResearchCommands(deps = {}) {
|
|
158
|
+
const { log = console.log } = deps;
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
'research': async (args, apiInstance, flags, options) => {
|
|
162
|
+
const sub = args[0];
|
|
163
|
+
|
|
164
|
+
if (!sub || sub === 'help') {
|
|
165
|
+
log(HELP_TOP);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!RESEARCH_HISTORICAL_SUBCOMMANDS.has(sub)) {
|
|
170
|
+
throw new NansenError(
|
|
171
|
+
`Unknown research subcommand: ${sub}. Available: ${SUBCOMMANDS.join(', ')}`,
|
|
172
|
+
ErrorCode.UNKNOWN,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (flags.help || flags.h || args[1] === 'help') {
|
|
177
|
+
log(SUB_HELP[sub] || HELP_TOP);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const orderBy = parseSort(options.sort, options['order-by']);
|
|
182
|
+
const pagination = buildPagination(options);
|
|
183
|
+
const filters = options.filters || {};
|
|
184
|
+
const { fromDate, toDate } = resolveDateRange(options);
|
|
185
|
+
const asOfDate = options['as-of-date'];
|
|
186
|
+
|
|
187
|
+
// Range-based token endpoints (require --from-date + --to-date)
|
|
188
|
+
const rangeTokenHandlers = {
|
|
189
|
+
'historical-dex-trades': () => apiInstance.researchDexTrades({
|
|
190
|
+
tokenAddress: options['token-address'] || options.token,
|
|
191
|
+
chain: options.chain,
|
|
192
|
+
fromDate, toDate, filters, orderBy, pagination,
|
|
193
|
+
}),
|
|
194
|
+
'historical-pnl-leaderboard': () => apiInstance.researchPnlLeaderboard({
|
|
195
|
+
tokenAddress: options['token-address'] || options.token,
|
|
196
|
+
chain: options.chain,
|
|
197
|
+
fromDate, toDate, filters, orderBy, pagination,
|
|
198
|
+
}),
|
|
199
|
+
'historical-token-flow-summary': () => apiInstance.researchTokenFlowSummary({
|
|
200
|
+
tokenAddress: options['token-address'] || options.token,
|
|
201
|
+
chain: options.chain,
|
|
202
|
+
fromDate, toDate, filters, orderBy,
|
|
203
|
+
}),
|
|
204
|
+
'historical-who-bought-sold': () => apiInstance.researchWhoBoughtSold({
|
|
205
|
+
tokenAddress: options['token-address'] || options.token,
|
|
206
|
+
chain: options.chain,
|
|
207
|
+
buyOrSell: options['buy-or-sell'],
|
|
208
|
+
fromDate, toDate, filters, orderBy, pagination,
|
|
209
|
+
}),
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
if (rangeTokenHandlers[sub]) {
|
|
213
|
+
requireOptions(
|
|
214
|
+
{ 'token-address': options['token-address'] || options.token, 'from-date': fromDate, 'to-date': toDate },
|
|
215
|
+
['token-address', 'from-date', 'to-date'],
|
|
216
|
+
);
|
|
217
|
+
if (sub === 'historical-token-flow-summary' && (options.page || options.limit)) {
|
|
218
|
+
throw new NansenError(
|
|
219
|
+
'historical-token-flow-summary does not support --page or --limit (endpoint returns a single aggregated row)',
|
|
220
|
+
ErrorCode.INVALID_PARAMS,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return rangeTokenHandlers[sub]();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// As-of-date token endpoints (require --as-of-date)
|
|
227
|
+
const asOfTokenHandlers = {
|
|
228
|
+
'historical-token-quant-scores': () => apiInstance.researchTokenQuantScores({
|
|
229
|
+
tokenAddress: options['token-address'] || options.token,
|
|
230
|
+
chain: options.chain,
|
|
231
|
+
asOfDate, filters, orderBy, pagination,
|
|
232
|
+
}),
|
|
233
|
+
'historical-top-holders': () => apiInstance.researchTopHolders({
|
|
234
|
+
tokenAddress: options['token-address'] || options.token,
|
|
235
|
+
chain: options.chain,
|
|
236
|
+
asOfDate, filters, orderBy, pagination,
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
if (asOfTokenHandlers[sub]) {
|
|
241
|
+
requireOptions(
|
|
242
|
+
{ 'token-address': options['token-address'] || options.token, 'as-of-date': asOfDate },
|
|
243
|
+
['token-address', 'as-of-date'],
|
|
244
|
+
);
|
|
245
|
+
return asOfTokenHandlers[sub]();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (sub === 'historical-smart-money-balances') {
|
|
249
|
+
if (options.sort || options['order-by']) {
|
|
250
|
+
throw new NansenError(
|
|
251
|
+
'historical-smart-money-balances does not support --sort or --order-by (endpoint does not support ordering)',
|
|
252
|
+
ErrorCode.INVALID_PARAMS,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
requireOptions({ 'as-of-date': asOfDate }, ['as-of-date']);
|
|
256
|
+
return apiInstance.researchSmartMoneyBalances({
|
|
257
|
+
chains: parseChains(options),
|
|
258
|
+
asOfDate, filters, pagination,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (sub === 'historical-token-screener') {
|
|
263
|
+
const timeframeDays = parseTimeframeDays(options['timeframe-days']);
|
|
264
|
+
requireOptions({ 'timeframe-days': timeframeDays, 'to-date': options['to-date'] }, ['timeframe-days', 'to-date']);
|
|
265
|
+
return apiInstance.researchTokenScreener({
|
|
266
|
+
chains: parseChains(options),
|
|
267
|
+
timeframeDays,
|
|
268
|
+
toDate: options['to-date'],
|
|
269
|
+
filters, orderBy, pagination,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (sub === 'historical-wallet-balances') {
|
|
274
|
+
requireOptions(
|
|
275
|
+
{ address: options.address, 'as-of-date': asOfDate },
|
|
276
|
+
['address', 'as-of-date'],
|
|
277
|
+
);
|
|
278
|
+
return apiInstance.researchWalletBalances({
|
|
279
|
+
address: options.address,
|
|
280
|
+
chain: options.chain,
|
|
281
|
+
asOfDate, filters, orderBy, pagination,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (sub === 'historical-tx-lookup') {
|
|
286
|
+
requireOptions(
|
|
287
|
+
{ 'transaction-hash': options['transaction-hash'], 'as-of-date': asOfDate },
|
|
288
|
+
['transaction-hash', 'as-of-date'],
|
|
289
|
+
);
|
|
290
|
+
return apiInstance.researchTxLookup({
|
|
291
|
+
txHash: options['transaction-hash'],
|
|
292
|
+
chain: options.chain,
|
|
293
|
+
asOfDate,
|
|
294
|
+
blockTimestamp: options['block-timestamp'],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (sub === 'historical-wallet-transactions') {
|
|
299
|
+
requireOptions(
|
|
300
|
+
{ address: options.address, 'as-of-date': asOfDate },
|
|
301
|
+
['address', 'as-of-date'],
|
|
302
|
+
);
|
|
303
|
+
return apiInstance.researchWalletTransactions({
|
|
304
|
+
address: options.address,
|
|
305
|
+
chain: options.chain,
|
|
306
|
+
asOfDate, filters, orderBy, pagination,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Should be unreachable because SUBCOMMANDS guarded above.
|
|
311
|
+
throw new NansenError(`Unknown research subcommand: ${sub}`, ErrorCode.UNKNOWN);
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
package/src/schema.json
CHANGED
|
@@ -697,6 +697,110 @@
|
|
|
697
697
|
"description": "Points leaderboard"
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
|
+
},
|
|
701
|
+
"historical-dex-trades": {
|
|
702
|
+
"endpoint": "/api/v1beta1/tgm/historical-dex-trades",
|
|
703
|
+
"description": "Historical DEX trades for a token at a point in time",
|
|
704
|
+
"options": {
|
|
705
|
+
"token-address": { "required": true, "description": "Token address" },
|
|
706
|
+
"from-date": { "required": true, "description": "Start of date range (YYYY-MM-DD)" },
|
|
707
|
+
"to-date": { "required": true, "description": "End of date range (YYYY-MM-DD)" },
|
|
708
|
+
"chain": { "default": "solana", "description": "Chain" }
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
"historical-pnl-leaderboard": {
|
|
712
|
+
"endpoint": "/api/v1beta1/tgm/historical-pnl-leaderboard",
|
|
713
|
+
"description": "Historical PnL leaderboard for a token",
|
|
714
|
+
"options": {
|
|
715
|
+
"token-address": { "required": true, "description": "Token address" },
|
|
716
|
+
"from-date": { "required": true, "description": "Start of date range (YYYY-MM-DD)" },
|
|
717
|
+
"to-date": { "required": true, "description": "End of date range (YYYY-MM-DD)" },
|
|
718
|
+
"chain": { "default": "solana", "description": "Chain" }
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
"historical-token-flow-summary": {
|
|
722
|
+
"endpoint": "/api/v1beta1/tgm/historical-token-flow-summary",
|
|
723
|
+
"description": "Historical token flow summary (no pagination)",
|
|
724
|
+
"options": {
|
|
725
|
+
"token-address": { "required": true, "description": "Token address" },
|
|
726
|
+
"from-date": { "required": true, "description": "Start of date range (YYYY-MM-DD)" },
|
|
727
|
+
"to-date": { "required": true, "description": "End of date range (YYYY-MM-DD)" },
|
|
728
|
+
"chain": { "default": "solana", "description": "Chain" }
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
"historical-token-quant-scores": {
|
|
732
|
+
"endpoint": "/api/v1beta1/tgm/historical-token-quant-scores",
|
|
733
|
+
"description": "Historical token quantitative scores at a snapshot date",
|
|
734
|
+
"options": {
|
|
735
|
+
"token-address": { "required": true, "description": "Token address" },
|
|
736
|
+
"as-of-date": { "required": true, "description": "Snapshot date (YYYY-MM-DD)" },
|
|
737
|
+
"chain": { "default": "solana", "description": "Chain" }
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
"historical-top-holders": {
|
|
741
|
+
"endpoint": "/api/v1beta1/tgm/historical-top-holders",
|
|
742
|
+
"description": "Historical top holders of a token at a snapshot date",
|
|
743
|
+
"options": {
|
|
744
|
+
"token-address": { "required": true, "description": "Token address" },
|
|
745
|
+
"as-of-date": { "required": true, "description": "Snapshot date (YYYY-MM-DD)" },
|
|
746
|
+
"chain": { "default": "solana", "description": "Chain" }
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
"historical-who-bought-sold": {
|
|
750
|
+
"endpoint": "/api/v1beta1/tgm/historical-who-bought-sold",
|
|
751
|
+
"description": "Historical buyers/sellers of a token",
|
|
752
|
+
"options": {
|
|
753
|
+
"token-address": { "required": true, "description": "Token address" },
|
|
754
|
+
"from-date": { "required": true, "description": "Start of date range (YYYY-MM-DD)" },
|
|
755
|
+
"to-date": { "required": true, "description": "End of date range (YYYY-MM-DD)" },
|
|
756
|
+
"buy-or-sell": { "default": "BUY", "description": "BUY or SELL" },
|
|
757
|
+
"chain": { "default": "solana", "description": "Chain" }
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
"historical-smart-money-balances": {
|
|
761
|
+
"endpoint": "/api/v1beta1/smart-money/historical-token-balances",
|
|
762
|
+
"description": "Historical smart money token balances at a snapshot date (no order_by)",
|
|
763
|
+
"options": {
|
|
764
|
+
"as-of-date": { "required": true, "description": "Snapshot date (YYYY-MM-DD)" },
|
|
765
|
+
"chains": { "default": "solana", "description": "Comma-separated chains" }
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
"historical-token-screener": {
|
|
769
|
+
"endpoint": "/api/v1beta1/token-screener/historical",
|
|
770
|
+
"description": "Historical token screener over a trailing window",
|
|
771
|
+
"options": {
|
|
772
|
+
"timeframe-days": { "required": true, "type": "number", "description": "Trailing window size in days" },
|
|
773
|
+
"to-date": { "required": true, "description": "End date for the window (YYYY-MM-DD)" },
|
|
774
|
+
"chains": { "default": "solana", "description": "Comma-separated chains" }
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
"historical-wallet-balances": {
|
|
778
|
+
"endpoint": "/api/v1beta1/profiler/address/historical-token-balances",
|
|
779
|
+
"description": "Historical token balances for a wallet at a snapshot date",
|
|
780
|
+
"options": {
|
|
781
|
+
"address": { "required": true, "description": "Wallet address" },
|
|
782
|
+
"as-of-date": { "required": true, "description": "Snapshot date (YYYY-MM-DD)" },
|
|
783
|
+
"chain": { "default": "ethereum", "description": "Chain" }
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
"historical-tx-lookup": {
|
|
787
|
+
"endpoint": "/api/v1beta1/profiler/historical-transaction-lookup",
|
|
788
|
+
"description": "Lookup a historical transaction by hash",
|
|
789
|
+
"options": {
|
|
790
|
+
"transaction-hash": { "required": true, "description": "Transaction hash (0x-prefixed, 66 chars)" },
|
|
791
|
+
"as-of-date": { "required": true, "description": "Reference date for label and pricing resolution (YYYY-MM-DD)" },
|
|
792
|
+
"block-timestamp": { "description": "Block timestamp (YYYY-MM-DD HH:MM:SS) — skips slow hash-resolution step if provided" },
|
|
793
|
+
"chain": { "default": "ethereum", "description": "Chain (ethereum, bnb, base)" }
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
"historical-wallet-transactions": {
|
|
797
|
+
"endpoint": "/api/v1beta1/profiler/address/historical-transactions",
|
|
798
|
+
"description": "Historical transactions for a wallet at a snapshot date",
|
|
799
|
+
"options": {
|
|
800
|
+
"address": { "required": true, "description": "Wallet address" },
|
|
801
|
+
"as-of-date": { "required": true, "description": "Snapshot date (YYYY-MM-DD)" },
|
|
802
|
+
"chain": { "default": "ethereum", "description": "Chain" }
|
|
803
|
+
}
|
|
700
804
|
}
|
|
701
805
|
}
|
|
702
806
|
},
|
package/src/trading.js
CHANGED
|
@@ -159,6 +159,7 @@ export async function getQuote(params) {
|
|
|
159
159
|
* @param {object} params
|
|
160
160
|
* @param {string} params.signedTransaction - Base64 (Solana) or 0x hex (EVM)
|
|
161
161
|
* @param {string} [params.chain] - Target chain name
|
|
162
|
+
* @param {string} [params.quoteId] - Backend quote ID for BI correlation
|
|
162
163
|
* @param {string} [params.requestId] - Optional Jupiter request ID (Solana only)
|
|
163
164
|
* @param {boolean} [params.simulate] - Run pre-broadcast simulation
|
|
164
165
|
* @returns {Promise<object>} Execution result
|
|
@@ -2018,16 +2019,19 @@ EXAMPLES:
|
|
|
2018
2019
|
simulate: !noSimulate && !gasless,
|
|
2019
2020
|
};
|
|
2020
2021
|
|
|
2021
|
-
//
|
|
2022
|
-
|
|
2023
|
-
|
|
2022
|
+
// Prefer the backend quote id saved from /quote; per-aggregator ids can
|
|
2023
|
+
// appear on individual quote metadata and are only a fallback.
|
|
2024
|
+
const backendQuoteId =
|
|
2025
|
+
quoteData.response?.metadata?.quoteId ?? currentQuote.metadata?.quoteId;
|
|
2026
|
+
if (backendQuoteId) {
|
|
2027
|
+
execParams.quoteId = backendQuoteId;
|
|
2024
2028
|
}
|
|
2025
2029
|
// The backend's /execute schema is strict; sending fields it doesn't expect
|
|
2026
2030
|
// for the (chain × aggregator × gasless) combination causes 502s or
|
|
2027
2031
|
// "Unrecognized keys" rejections. The matrix we've validated against the
|
|
2028
2032
|
// live backend:
|
|
2029
|
-
// - EVM signed (any aggregator): no
|
|
2030
|
-
// trigger schema errors.
|
|
2033
|
+
// - EVM signed (any aggregator): no aggregator/requestId fields.
|
|
2034
|
+
// Those trigger schema errors.
|
|
2031
2035
|
// - Solana signed (Jupiter/OKX): include requestId for Jupiter Ultra
|
|
2032
2036
|
// intent resolution.
|
|
2033
2037
|
// - Solana signed (Relay): omit requestId — backend tries to look it up
|