nansen-cli 1.30.1 → 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 +18 -0
- package/package.json +2 -1
- package/src/api.js +182 -80
- package/src/cli.js +21 -1
- package/src/commands/research.js +314 -0
- package/src/schema.json +104 -0
- package/src/trading.js +27 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
15
|
+
## 1.30.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#431](https://github.com/nansen-ai/nansen-cli/pull/431) [`c2c033b`](https://github.com/nansen-ai/nansen-cli/commit/c2c033b86f9dab59df9497ce971ef6f267ee3669) Thanks [@MarcLlopart](https://github.com/MarcLlopart)! - Pass backend quoteId in execute requests for BI correlation
|
|
20
|
+
|
|
3
21
|
## 1.30.1
|
|
4
22
|
|
|
5
23
|
### 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
|
@@ -336,6 +336,22 @@ export function validateTokenAddress(tokenAddress, chain = 'solana') {
|
|
|
336
336
|
return validateAddress(tokenAddress, chain);
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Throw if address is present but invalid.
|
|
341
|
+
*/
|
|
342
|
+
function requireValidAddress(address, chain) {
|
|
343
|
+
const v = validateAddress(address, chain);
|
|
344
|
+
if (!v.valid) throw new NansenError(v.error, v.code);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Throw if token address is present but invalid.
|
|
349
|
+
*/
|
|
350
|
+
function requireValidToken(tokenAddress, chain) {
|
|
351
|
+
const v = validateTokenAddress(tokenAddress, chain);
|
|
352
|
+
if (!v.valid) throw new NansenError(v.error, v.code);
|
|
353
|
+
}
|
|
354
|
+
|
|
339
355
|
function loadConfig() {
|
|
340
356
|
// Base config from files, then env vars override individual fields
|
|
341
357
|
let config = null;
|
|
@@ -791,10 +807,7 @@ export class NansenAPI {
|
|
|
791
807
|
|
|
792
808
|
async addressBalance(params = {}) {
|
|
793
809
|
const { address, entityName, chain = 'all', hideSpamToken = true, filters = {}, orderBy } = params;
|
|
794
|
-
if (address)
|
|
795
|
-
const validation = validateAddress(address, chain);
|
|
796
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
797
|
-
}
|
|
810
|
+
if (address) requireValidAddress(address, chain);
|
|
798
811
|
return this.request('/api/v1/profiler/address/current-balance', {
|
|
799
812
|
address,
|
|
800
813
|
entity_name: entityName,
|
|
@@ -807,10 +820,7 @@ export class NansenAPI {
|
|
|
807
820
|
|
|
808
821
|
async addressLabels(params = {}) {
|
|
809
822
|
const { address, chain = 'ethereum', pagination = { page: 1, per_page: 100 } } = params;
|
|
810
|
-
if (address)
|
|
811
|
-
const validation = validateAddress(address, chain);
|
|
812
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
813
|
-
}
|
|
823
|
+
if (address) requireValidAddress(address, chain);
|
|
814
824
|
return this.request('/api/beta/profiler/address/labels', {
|
|
815
825
|
parameters: { address, chain },
|
|
816
826
|
pagination
|
|
@@ -819,10 +829,7 @@ export class NansenAPI {
|
|
|
819
829
|
|
|
820
830
|
async addressTransactions(params = {}) {
|
|
821
831
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30, date } = params;
|
|
822
|
-
if (address)
|
|
823
|
-
const validation = validateAddress(address, chain);
|
|
824
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
825
|
-
}
|
|
832
|
+
if (address) requireValidAddress(address, chain);
|
|
826
833
|
const dateRange = date || buildDateRange(days);
|
|
827
834
|
return this.request('/api/v1/profiler/address/transactions', {
|
|
828
835
|
address,
|
|
@@ -836,10 +843,7 @@ export class NansenAPI {
|
|
|
836
843
|
|
|
837
844
|
async addressPnl(params = {}) {
|
|
838
845
|
const { address, chain = 'ethereum', date, days = 30, filters = {}, orderBy, pagination } = params;
|
|
839
|
-
if (address)
|
|
840
|
-
const validation = validateAddress(address, chain);
|
|
841
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
842
|
-
}
|
|
846
|
+
if (address) requireValidAddress(address, chain);
|
|
843
847
|
const dateRange = date || buildDateRange(days);
|
|
844
848
|
return this.request('/api/v1/profiler/address/pnl', {
|
|
845
849
|
address,
|
|
@@ -899,10 +903,7 @@ export class NansenAPI {
|
|
|
899
903
|
|
|
900
904
|
async addressHistoricalBalances(params = {}) {
|
|
901
905
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
902
|
-
if (address)
|
|
903
|
-
const validation = validateAddress(address, chain);
|
|
904
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
905
|
-
}
|
|
906
|
+
if (address) requireValidAddress(address, chain);
|
|
906
907
|
return this.request('/api/v1/profiler/address/historical-balances', {
|
|
907
908
|
address,
|
|
908
909
|
chain,
|
|
@@ -915,10 +916,7 @@ export class NansenAPI {
|
|
|
915
916
|
|
|
916
917
|
async addressRelatedWallets(params = {}) {
|
|
917
918
|
const { address, chain = 'ethereum', orderBy, pagination } = params;
|
|
918
|
-
if (address)
|
|
919
|
-
const validation = validateAddress(address, chain);
|
|
920
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
921
|
-
}
|
|
919
|
+
if (address) requireValidAddress(address, chain);
|
|
922
920
|
return this.request('/api/v1/profiler/address/related-wallets', {
|
|
923
921
|
address,
|
|
924
922
|
chain,
|
|
@@ -929,10 +927,7 @@ export class NansenAPI {
|
|
|
929
927
|
|
|
930
928
|
async addressCounterparties(params = {}) {
|
|
931
929
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
932
|
-
if (address)
|
|
933
|
-
const validation = validateAddress(address, chain);
|
|
934
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
935
|
-
}
|
|
930
|
+
if (address) requireValidAddress(address, chain);
|
|
936
931
|
return this.request('/api/v1/profiler/address/counterparties', {
|
|
937
932
|
address,
|
|
938
933
|
chain,
|
|
@@ -947,10 +942,7 @@ export class NansenAPI {
|
|
|
947
942
|
// Note: pnl-summary endpoint is non-paginated (returns aggregate stats, not a list).
|
|
948
943
|
// Pagination param intentionally omitted from this request.
|
|
949
944
|
const { address, chain = 'ethereum', orderBy, days = 30 } = params;
|
|
950
|
-
if (address)
|
|
951
|
-
const validation = validateAddress(address, chain);
|
|
952
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
953
|
-
}
|
|
945
|
+
if (address) requireValidAddress(address, chain);
|
|
954
946
|
return this.request('/api/v1/profiler/address/pnl-summary', {
|
|
955
947
|
address,
|
|
956
948
|
chain,
|
|
@@ -996,10 +988,7 @@ export class NansenAPI {
|
|
|
996
988
|
|
|
997
989
|
async tokenHolders(params = {}) {
|
|
998
990
|
const { tokenAddress, chain = 'solana', labelType = 'all_holders', filters = {}, orderBy, pagination, withLabels } = params;
|
|
999
|
-
if (tokenAddress)
|
|
1000
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1001
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1002
|
-
}
|
|
991
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1003
992
|
const body = {
|
|
1004
993
|
token_address: tokenAddress,
|
|
1005
994
|
chain,
|
|
@@ -1014,10 +1003,7 @@ export class NansenAPI {
|
|
|
1014
1003
|
|
|
1015
1004
|
async tokenFlows(params = {}) {
|
|
1016
1005
|
const { tokenAddress, chain = 'solana', label, filters = {}, orderBy, pagination, days = 30, date } = params;
|
|
1017
|
-
if (tokenAddress)
|
|
1018
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1019
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1020
|
-
}
|
|
1006
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1021
1007
|
const dateRange = date || buildDateRange(days);
|
|
1022
1008
|
return this.request('/api/v1/tgm/flows', {
|
|
1023
1009
|
token_address: tokenAddress,
|
|
@@ -1032,10 +1018,7 @@ export class NansenAPI {
|
|
|
1032
1018
|
|
|
1033
1019
|
async tokenDexTrades(params = {}) {
|
|
1034
1020
|
const { tokenAddress, chain = 'solana', onlySmartMoney = false, filters = {}, orderBy, pagination, days = 7 } = params;
|
|
1035
|
-
if (tokenAddress)
|
|
1036
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1037
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1038
|
-
}
|
|
1021
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1039
1022
|
// Apply smart money filter via filters object
|
|
1040
1023
|
if (onlySmartMoney) {
|
|
1041
1024
|
filters.include_smart_money_labels = filters.include_smart_money_labels ||
|
|
@@ -1054,10 +1037,7 @@ export class NansenAPI {
|
|
|
1054
1037
|
|
|
1055
1038
|
async tokenPnlLeaderboard(params = {}) {
|
|
1056
1039
|
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 30, withLabels } = params;
|
|
1057
|
-
if (tokenAddress)
|
|
1058
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1059
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1060
|
-
}
|
|
1040
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1061
1041
|
const body = {
|
|
1062
1042
|
token_address: tokenAddress,
|
|
1063
1043
|
chain,
|
|
@@ -1072,10 +1052,7 @@ export class NansenAPI {
|
|
|
1072
1052
|
|
|
1073
1053
|
async tokenWhoBoughtSold(params = {}) {
|
|
1074
1054
|
const { tokenAddress, chain = 'solana', buyOrSell = 'BUY', filters = {}, orderBy, pagination, days = 30, date } = params;
|
|
1075
|
-
if (tokenAddress)
|
|
1076
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1077
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1078
|
-
}
|
|
1055
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1079
1056
|
const dateRange = date || buildDateRange(days);
|
|
1080
1057
|
return this.request('/api/v1/tgm/who-bought-sold', {
|
|
1081
1058
|
token_address: tokenAddress,
|
|
@@ -1090,10 +1067,7 @@ export class NansenAPI {
|
|
|
1090
1067
|
|
|
1091
1068
|
async tokenFlowIntelligence(params = {}) {
|
|
1092
1069
|
const { tokenAddress, chain = 'solana', timeframe = '1d' } = params;
|
|
1093
|
-
if (tokenAddress)
|
|
1094
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1095
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1096
|
-
}
|
|
1070
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1097
1071
|
return this.request('/api/v1/tgm/flow-intelligence', {
|
|
1098
1072
|
token_address: tokenAddress,
|
|
1099
1073
|
chain,
|
|
@@ -1103,10 +1077,7 @@ export class NansenAPI {
|
|
|
1103
1077
|
|
|
1104
1078
|
async tokenTransfers(params = {}) {
|
|
1105
1079
|
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 7 } = params;
|
|
1106
|
-
if (tokenAddress)
|
|
1107
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1108
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1109
|
-
}
|
|
1080
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1110
1081
|
return this.request('/api/v1/tgm/transfers', {
|
|
1111
1082
|
token_address: tokenAddress,
|
|
1112
1083
|
chain,
|
|
@@ -1120,10 +1091,7 @@ export class NansenAPI {
|
|
|
1120
1091
|
async tokenJupDca(params = {}) {
|
|
1121
1092
|
const { tokenAddress, filters = {}, orderBy, pagination } = params;
|
|
1122
1093
|
// JUP DCA is Solana-only
|
|
1123
|
-
if (tokenAddress)
|
|
1124
|
-
const validation = validateTokenAddress(tokenAddress, 'solana');
|
|
1125
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1126
|
-
}
|
|
1094
|
+
if (tokenAddress) requireValidToken(tokenAddress, 'solana');
|
|
1127
1095
|
return this.request('/api/v1/tgm/jup-dca', {
|
|
1128
1096
|
token_address: tokenAddress,
|
|
1129
1097
|
filters,
|
|
@@ -1168,10 +1136,7 @@ export class NansenAPI {
|
|
|
1168
1136
|
|
|
1169
1137
|
async tokenIndicators(params = {}) {
|
|
1170
1138
|
const { tokenAddress, chain = 'ethereum' } = params;
|
|
1171
|
-
if (tokenAddress)
|
|
1172
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1173
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1174
|
-
}
|
|
1139
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1175
1140
|
return this.request('/api/v1/tgm/indicators', {
|
|
1176
1141
|
token_address: tokenAddress,
|
|
1177
1142
|
chain
|
|
@@ -1180,10 +1145,7 @@ export class NansenAPI {
|
|
|
1180
1145
|
|
|
1181
1146
|
async tokenOhlcv(params = {}) {
|
|
1182
1147
|
const { tokenAddress, chain = 'solana', timeframe } = params;
|
|
1183
|
-
if (tokenAddress)
|
|
1184
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1185
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1186
|
-
}
|
|
1148
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1187
1149
|
return this.request('/api/v1/tgm/token-ohlcv', {
|
|
1188
1150
|
token_address: tokenAddress,
|
|
1189
1151
|
chain,
|
|
@@ -1193,10 +1155,7 @@ export class NansenAPI {
|
|
|
1193
1155
|
|
|
1194
1156
|
async tokenInformation(params = {}) {
|
|
1195
1157
|
const { tokenAddress, chain = 'solana', timeframe = '1d' } = params;
|
|
1196
|
-
if (tokenAddress)
|
|
1197
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1198
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1199
|
-
}
|
|
1158
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1200
1159
|
return this.request('/api/v1/tgm/token-information', {
|
|
1201
1160
|
token_address: tokenAddress,
|
|
1202
1161
|
chain,
|
|
@@ -1279,8 +1238,7 @@ export class NansenAPI {
|
|
|
1279
1238
|
async pmTradesByAddress(params = {}) {
|
|
1280
1239
|
const { address, orderBy, pagination } = params;
|
|
1281
1240
|
// Polymarket runs exclusively on Polygon
|
|
1282
|
-
|
|
1283
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1241
|
+
requireValidAddress(address, 'polygon');
|
|
1284
1242
|
return this.request('/api/v1/prediction-market/trades-by-address', {
|
|
1285
1243
|
address,
|
|
1286
1244
|
order_by: orderBy,
|
|
@@ -1351,8 +1309,7 @@ export class NansenAPI {
|
|
|
1351
1309
|
async pmPnlByAddress(params = {}) {
|
|
1352
1310
|
const { address, orderBy, pagination } = params;
|
|
1353
1311
|
// Polymarket runs exclusively on Polygon
|
|
1354
|
-
|
|
1355
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1312
|
+
requireValidAddress(address, 'polygon');
|
|
1356
1313
|
return this.request('/api/v1/prediction-market/pnl-by-address', {
|
|
1357
1314
|
address,
|
|
1358
1315
|
order_by: orderBy,
|
|
@@ -1406,6 +1363,151 @@ export class NansenAPI {
|
|
|
1406
1363
|
});
|
|
1407
1364
|
}
|
|
1408
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
|
+
|
|
1409
1511
|
// ============= Smart Alert Endpoints =============
|
|
1410
1512
|
|
|
1411
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
|
@@ -104,6 +104,14 @@ function getQuotesDir() {
|
|
|
104
104
|
return path.join(configDir, 'quotes');
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Resolve a filename inside the quotes dir, rejecting path traversal.
|
|
108
|
+
function safeQuotesPath(filename) {
|
|
109
|
+
const base = path.resolve(getQuotesDir());
|
|
110
|
+
const target = path.resolve(base, filename);
|
|
111
|
+
if (path.relative(base, target).startsWith('..')) return null;
|
|
112
|
+
return target;
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
// ============= Trading API Client =============
|
|
108
116
|
|
|
109
117
|
/**
|
|
@@ -151,6 +159,7 @@ export async function getQuote(params) {
|
|
|
151
159
|
* @param {object} params
|
|
152
160
|
* @param {string} params.signedTransaction - Base64 (Solana) or 0x hex (EVM)
|
|
153
161
|
* @param {string} [params.chain] - Target chain name
|
|
162
|
+
* @param {string} [params.quoteId] - Backend quote ID for BI correlation
|
|
154
163
|
* @param {string} [params.requestId] - Optional Jupiter request ID (Solana only)
|
|
155
164
|
* @param {boolean} [params.simulate] - Run pre-broadcast simulation
|
|
156
165
|
* @returns {Promise<object>} Execution result
|
|
@@ -337,10 +346,12 @@ const TX_RECORD_TTL_MS = 30 * 24 * 3600 * 1000; // 30 days
|
|
|
337
346
|
*/
|
|
338
347
|
export function saveTxRecord(txHash, { aggregator, requestId, fromChain, toChain }) {
|
|
339
348
|
if (!txHash) return;
|
|
340
|
-
const
|
|
349
|
+
const filePath = safeQuotesPath(`tx-${txHash}.json`);
|
|
350
|
+
if (!filePath) return;
|
|
351
|
+
const dir = path.dirname(filePath);
|
|
341
352
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
342
353
|
const data = { txHash, aggregator, requestId, fromChain, toChain, timestamp: Date.now() };
|
|
343
|
-
fs.writeFileSync(
|
|
354
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
344
355
|
}
|
|
345
356
|
|
|
346
357
|
/**
|
|
@@ -348,7 +359,8 @@ export function saveTxRecord(txHash, { aggregator, requestId, fromChain, toChain
|
|
|
348
359
|
*/
|
|
349
360
|
export function loadTxRecord(txHash) {
|
|
350
361
|
if (!txHash) return null;
|
|
351
|
-
const filePath =
|
|
362
|
+
const filePath = safeQuotesPath(`tx-${txHash}.json`);
|
|
363
|
+
if (!filePath) return null;
|
|
352
364
|
if (!fs.existsSync(filePath)) return null;
|
|
353
365
|
try {
|
|
354
366
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -391,8 +403,8 @@ export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalle
|
|
|
391
403
|
* Load a saved quote by ID.
|
|
392
404
|
*/
|
|
393
405
|
export function loadQuote(quoteId) {
|
|
394
|
-
const filePath =
|
|
395
|
-
if (!fs.existsSync(filePath)) {
|
|
406
|
+
const filePath = safeQuotesPath(`${quoteId}.json`);
|
|
407
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
396
408
|
throw new Error(`Quote "${quoteId}" not found. Quotes expire after 1 hour.`);
|
|
397
409
|
}
|
|
398
410
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -2006,12 +2018,20 @@ EXAMPLES:
|
|
|
2006
2018
|
chain,
|
|
2007
2019
|
simulate: !noSimulate && !gasless,
|
|
2008
2020
|
};
|
|
2021
|
+
|
|
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;
|
|
2028
|
+
}
|
|
2009
2029
|
// The backend's /execute schema is strict; sending fields it doesn't expect
|
|
2010
2030
|
// for the (chain × aggregator × gasless) combination causes 502s or
|
|
2011
2031
|
// "Unrecognized keys" rejections. The matrix we've validated against the
|
|
2012
2032
|
// live backend:
|
|
2013
|
-
// - EVM signed (any aggregator): no
|
|
2014
|
-
// trigger schema errors.
|
|
2033
|
+
// - EVM signed (any aggregator): no aggregator/requestId fields.
|
|
2034
|
+
// Those trigger schema errors.
|
|
2015
2035
|
// - Solana signed (Jupiter/OKX): include requestId for Jupiter Ultra
|
|
2016
2036
|
// intent resolution.
|
|
2017
2037
|
// - Solana signed (Relay): omit requestId — backend tries to look it up
|