nansen-cli 1.30.1 → 1.30.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.30.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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
8
+
3
9
  ## 1.30.1
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.30.1",
3
+ "version": "1.30.2",
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",
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
- const validation = validateAddress(address, 'polygon');
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
- const validation = validateAddress(address, 'polygon');
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,
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
  /**
@@ -337,10 +345,12 @@ const TX_RECORD_TTL_MS = 30 * 24 * 3600 * 1000; // 30 days
337
345
  */
338
346
  export function saveTxRecord(txHash, { aggregator, requestId, fromChain, toChain }) {
339
347
  if (!txHash) return;
340
- const dir = getQuotesDir();
348
+ const filePath = safeQuotesPath(`tx-${txHash}.json`);
349
+ if (!filePath) return;
350
+ const dir = path.dirname(filePath);
341
351
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
342
352
  const data = { txHash, aggregator, requestId, fromChain, toChain, timestamp: Date.now() };
343
- fs.writeFileSync(path.join(dir, `tx-${txHash}.json`), JSON.stringify(data, null, 2), { mode: 0o600 });
353
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
344
354
  }
345
355
 
346
356
  /**
@@ -348,7 +358,8 @@ export function saveTxRecord(txHash, { aggregator, requestId, fromChain, toChain
348
358
  */
349
359
  export function loadTxRecord(txHash) {
350
360
  if (!txHash) return null;
351
- const filePath = path.join(getQuotesDir(), `tx-${txHash}.json`);
361
+ const filePath = safeQuotesPath(`tx-${txHash}.json`);
362
+ if (!filePath) return null;
352
363
  if (!fs.existsSync(filePath)) return null;
353
364
  try {
354
365
  const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -391,8 +402,8 @@ export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalle
391
402
  * Load a saved quote by ID.
392
403
  */
393
404
  export function loadQuote(quoteId) {
394
- const filePath = path.join(getQuotesDir(), `${quoteId}.json`);
395
- if (!fs.existsSync(filePath)) {
405
+ const filePath = safeQuotesPath(`${quoteId}.json`);
406
+ if (!filePath || !fs.existsSync(filePath)) {
396
407
  throw new Error(`Quote "${quoteId}" not found. Quotes expire after 1 hour.`);
397
408
  }
398
409
  const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -2006,6 +2017,11 @@ EXAMPLES:
2006
2017
  chain,
2007
2018
  simulate: !noSimulate && !gasless,
2008
2019
  };
2020
+
2021
+ // Include backend quoteId if available for better BI tracking
2022
+ if (currentQuote.metadata?.quoteId) {
2023
+ execParams.quoteId = currentQuote.metadata.quoteId;
2024
+ }
2009
2025
  // The backend's /execute schema is strict; sending fields it doesn't expect
2010
2026
  // for the (chain × aggregator × gasless) combination causes 502s or
2011
2027
  // "Unrecognized keys" rejections. The matrix we've validated against the