nansen-cli 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/api.js CHANGED
@@ -391,6 +391,17 @@ function parseRetryAfter(headerValue) {
391
391
  return null;
392
392
  }
393
393
 
394
+ /**
395
+ * Build a date range from today back N days
396
+ * @param {number} days - Number of days back from today
397
+ * @returns {{from: string, to: string}} Date range with YYYY-MM-DD strings
398
+ */
399
+ export function buildDateRange(days) {
400
+ const to = new Date().toISOString().split('T')[0];
401
+ const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
402
+ return { from, to };
403
+ }
404
+
394
405
  export class NansenAPI {
395
406
  constructor(apiKey = config.apiKey, baseUrl = config.baseUrl, options = {}) {
396
407
  this.apiKey = apiKey || null;
@@ -494,8 +505,11 @@ export class NansenAPI {
494
505
  } else if (code === ErrorCode.CREDITS_EXHAUSTED) {
495
506
  message = message.replace(/\.+$/, '') + '. No retry will help. Check your Nansen dashboard for credit balance.';
496
507
  } else if (code === ErrorCode.PAYMENT_REQUIRED) {
497
- // Try x402 auto-payment with fallback across payment networks
498
- if (!this.defaultHeaders['Payment-Signature']) {
508
+ // Try x402 auto-payment: local wallet (with network fallback), then WalletConnect
509
+ const hasManualSignature = !!(this.defaultHeaders['Payment-Signature'] || options.headers?.['Payment-Signature']);
510
+
511
+ if (!hasManualSignature) {
512
+ // 1. Try local wallet with fallback across payment networks
499
513
  try {
500
514
  const { createPaymentSignatures } = await import('./x402.js');
501
515
  for await (const { signature, network } of createPaymentSignatures(response, url)) {
@@ -526,17 +540,54 @@ export class NansenAPI {
526
540
  }
527
541
  // This payment option was rejected, try next
528
542
  }
529
- } catch { /* x402 auto-pay unavailable, fall through */ }
530
- }
531
- message = 'Payment required. To access this endpoint:\n • Set an API key: nansen login --api-key <key> (get one at https://app.nansen.ai/api)\n • Or pay per call: nansen wallet create, fund with USDC on Base or Solana (from $0.01/call, min $0.05 balance)\n • Docs: https://docs.x402.org';
532
- const paymentHeader = response.headers.get('payment-required');
533
- if (paymentHeader) {
534
- try {
535
- data.paymentRequirements = JSON.parse(atob(paymentHeader));
536
- } catch {
537
- data.paymentRequiredRaw = paymentHeader;
543
+ } catch { /* local wallet unavailable, try WalletConnect */ }
544
+
545
+ // 2. Fall back to WalletConnect (walletconnect-x402.js)
546
+ // (local wallet returns early on success above, so we always reach here if it failed)
547
+ {
548
+ let paymentRequirements;
549
+ const paymentHeader = response.headers.get('payment-required');
550
+ if (paymentHeader) {
551
+ try {
552
+ paymentRequirements = JSON.parse(atob(paymentHeader));
553
+ } catch {
554
+ data.paymentRequiredRaw = paymentHeader;
555
+ }
556
+ }
557
+ if (!paymentRequirements && data.paymentRequirements) {
558
+ paymentRequirements = data.paymentRequirements;
559
+ }
560
+
561
+ if (paymentRequirements) {
562
+ try {
563
+ const { handleX402Payment } = await import('./walletconnect-x402.js');
564
+ const paymentSignature = await handleX402Payment(paymentRequirements);
565
+ const paidResponse = await fetch(url, {
566
+ method: 'POST',
567
+ headers: {
568
+ 'Content-Type': 'application/json',
569
+ 'X-Client-Type': 'nansen-cli',
570
+ 'X-Client-Version': packageVersion,
571
+ 'Payment-Signature': paymentSignature,
572
+ ...this.defaultHeaders,
573
+ ...options.headers,
574
+ },
575
+ body: JSON.stringify(NansenAPI.cleanBody(body)),
576
+ });
577
+ if (paidResponse.ok) {
578
+ return await paidResponse.json();
579
+ }
580
+ } catch (x402Err) {
581
+ message = `x402 auto-payment failed: ${x402Err.message}`;
582
+ }
583
+ data.paymentRequirements = paymentRequirements;
584
+ }
538
585
  }
539
586
  }
587
+
588
+ if (!message || message === data.message) {
589
+ message = 'Payment required (x402). Sign the paymentRequirements below per https://docs.x402.org and pass the result with --x402-payment-signature <value>.';
590
+ }
540
591
  }
541
592
 
542
593
  lastError = new NansenError(message, code, response.status, {
@@ -624,11 +675,9 @@ export class NansenAPI {
624
675
 
625
676
  async smartMoneyHistoricalHoldings(params = {}) {
626
677
  const { chains = ['solana'], filters = {}, orderBy, pagination, days = 30 } = params;
627
- const to = new Date().toISOString().split('T')[0];
628
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
629
678
  return this.request('/api/v1/smart-money/historical-holdings', {
630
679
  chains,
631
- date_range: { from, to },
680
+ date_range: buildDateRange(days),
632
681
  filters,
633
682
  order_by: orderBy,
634
683
  pagination
@@ -671,11 +720,7 @@ export class NansenAPI {
671
720
  const validation = validateAddress(address, chain);
672
721
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
673
722
  }
674
- const dateRange = date || (() => {
675
- const to = new Date().toISOString().split('T')[0];
676
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
677
- return { from, to };
678
- })();
723
+ const dateRange = date || buildDateRange(days);
679
724
  return this.request('/api/v1/profiler/address/transactions', {
680
725
  address,
681
726
  chain,
@@ -692,13 +737,7 @@ export class NansenAPI {
692
737
  const validation = validateAddress(address, chain);
693
738
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
694
739
  }
695
- // Build date range
696
- let dateRange = date;
697
- if (!dateRange) {
698
- const to = new Date().toISOString().split('T')[0];
699
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
700
- dateRange = { from, to };
701
- }
740
+ const dateRange = date || buildDateRange(days);
702
741
  return this.request('/api/v1/profiler/address/pnl', {
703
742
  address,
704
743
  chain,
@@ -734,12 +773,10 @@ export class NansenAPI {
734
773
  const validation = validateAddress(address, chain);
735
774
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
736
775
  }
737
- const to = new Date().toISOString().split('T')[0];
738
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
739
776
  return this.request('/api/v1/profiler/address/historical-balances', {
740
777
  address,
741
778
  chain,
742
- date: { from, to },
779
+ date: buildDateRange(days),
743
780
  filters,
744
781
  order_by: orderBy,
745
782
  pagination
@@ -766,12 +803,10 @@ export class NansenAPI {
766
803
  const validation = validateAddress(address, chain);
767
804
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
768
805
  }
769
- const to = new Date().toISOString().split('T')[0];
770
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
771
806
  return this.request('/api/v1/profiler/address/counterparties', {
772
807
  address,
773
808
  chain,
774
- date: { from, to },
809
+ date: buildDateRange(days),
775
810
  filters,
776
811
  order_by: orderBy,
777
812
  pagination
@@ -784,12 +819,10 @@ export class NansenAPI {
784
819
  const validation = validateAddress(address, chain);
785
820
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
786
821
  }
787
- const to = new Date().toISOString().split('T')[0];
788
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
789
822
  return this.request('/api/v1/profiler/address/pnl-summary', {
790
823
  address,
791
824
  chain,
792
- date: { from, to },
825
+ date: buildDateRange(days),
793
826
  order_by: orderBy,
794
827
  pagination
795
828
  });
@@ -808,11 +841,9 @@ export class NansenAPI {
808
841
 
809
842
  async addressPerpTrades(params = {}) {
810
843
  const { address, filters = {}, orderBy, pagination, days = 30 } = params;
811
- const to = new Date().toISOString().split('T')[0];
812
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
813
844
  return this.request('/api/v1/profiler/perp-trades', {
814
845
  address,
815
- date: { from, to },
846
+ date: buildDateRange(days),
816
847
  filters,
817
848
  order_by: orderBy,
818
849
  pagination
@@ -854,11 +885,7 @@ export class NansenAPI {
854
885
  const validation = validateTokenAddress(tokenAddress, chain);
855
886
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
856
887
  }
857
- const dateRange = date || (() => {
858
- const to = new Date().toISOString().split('T')[0];
859
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
860
- return { from, to };
861
- })();
888
+ const dateRange = date || buildDateRange(days);
862
889
  return this.request('/api/v1/tgm/flows', {
863
890
  token_address: tokenAddress,
864
891
  chain,
@@ -875,9 +902,6 @@ export class NansenAPI {
875
902
  const validation = validateTokenAddress(tokenAddress, chain);
876
903
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
877
904
  }
878
- const to = new Date().toISOString().split('T')[0];
879
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
880
-
881
905
  // Apply smart money filter via filters object
882
906
  if (onlySmartMoney) {
883
907
  filters.include_smart_money_labels = filters.include_smart_money_labels ||
@@ -887,7 +911,7 @@ export class NansenAPI {
887
911
  return this.request('/api/v1/tgm/dex-trades', {
888
912
  token_address: tokenAddress,
889
913
  chain,
890
- date: { from, to },
914
+ date: buildDateRange(days),
891
915
  filters,
892
916
  order_by: orderBy,
893
917
  pagination
@@ -900,12 +924,10 @@ export class NansenAPI {
900
924
  const validation = validateTokenAddress(tokenAddress, chain);
901
925
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
902
926
  }
903
- const to = new Date().toISOString().split('T')[0];
904
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
905
927
  return this.request('/api/v1/tgm/pnl-leaderboard', {
906
928
  token_address: tokenAddress,
907
929
  chain,
908
- date: { from, to },
930
+ date: buildDateRange(days),
909
931
  filters,
910
932
  order_by: orderBy,
911
933
  pagination
@@ -918,11 +940,7 @@ export class NansenAPI {
918
940
  const validation = validateTokenAddress(tokenAddress, chain);
919
941
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
920
942
  }
921
- const dateRange = date || (() => {
922
- const to = new Date().toISOString().split('T')[0];
923
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
924
- return { from, to };
925
- })();
943
+ const dateRange = date || buildDateRange(days);
926
944
  return this.request('/api/v1/tgm/who-bought-sold', {
927
945
  token_address: tokenAddress,
928
946
  chain,
@@ -951,12 +969,10 @@ export class NansenAPI {
951
969
  const validation = validateTokenAddress(tokenAddress, chain);
952
970
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
953
971
  }
954
- const to = new Date().toISOString().split('T')[0];
955
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
956
972
  return this.request('/api/v1/tgm/transfers', {
957
973
  token_address: tokenAddress,
958
974
  chain,
959
- date: { from, to },
975
+ date: buildDateRange(days),
960
976
  filters,
961
977
  order_by: orderBy,
962
978
  pagination
@@ -980,11 +996,9 @@ export class NansenAPI {
980
996
 
981
997
  async tokenPerpTrades(params = {}) {
982
998
  const { tokenSymbol, filters = {}, orderBy, pagination, days = 30 } = params;
983
- const to = new Date().toISOString().split('T')[0];
984
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
985
999
  return this.request('/api/v1/tgm/perp-trades', {
986
1000
  token_symbol: tokenSymbol,
987
- date: { from, to },
1001
+ date: buildDateRange(days),
988
1002
  filters,
989
1003
  order_by: orderBy,
990
1004
  pagination
@@ -1003,11 +1017,9 @@ export class NansenAPI {
1003
1017
 
1004
1018
  async tokenPerpPnlLeaderboard(params = {}) {
1005
1019
  const { tokenSymbol, filters = {}, orderBy, pagination, days = 30 } = params;
1006
- const to = new Date().toISOString().split('T')[0];
1007
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
1008
1020
  return this.request('/api/v1/tgm/perp-pnl-leaderboard', {
1009
1021
  token_symbol: tokenSymbol,
1010
- date: { from, to },
1022
+ date: buildDateRange(days),
1011
1023
  filters,
1012
1024
  order_by: orderBy,
1013
1025
  pagination
@@ -1026,6 +1038,20 @@ export class NansenAPI {
1026
1038
  });
1027
1039
  }
1028
1040
 
1041
+ async tokenOhlcv(params = {}) {
1042
+ const { tokenAddress, chain = 'solana', timeframe, pagination } = params;
1043
+ if (tokenAddress) {
1044
+ const validation = validateTokenAddress(tokenAddress, chain);
1045
+ if (!validation.valid) throw new NansenError(validation.error, validation.code);
1046
+ }
1047
+ return this.request('/api/v1/tgm/token-ohlcv', {
1048
+ token_address: tokenAddress,
1049
+ chain,
1050
+ timeframe,
1051
+ pagination
1052
+ });
1053
+ }
1054
+
1029
1055
  async tokenInformation(params = {}) {
1030
1056
  const { tokenAddress, chain = 'solana', timeframe = '1d' } = params;
1031
1057
  if (tokenAddress) {
@@ -1043,10 +1069,8 @@ export class NansenAPI {
1043
1069
 
1044
1070
  async perpScreener(params = {}) {
1045
1071
  const { filters = {}, orderBy, pagination, days = 30 } = params;
1046
- const to = new Date().toISOString().split('T')[0];
1047
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
1048
1072
  return this.request('/api/v1/perp-screener', {
1049
- date: { from, to },
1073
+ date: buildDateRange(days),
1050
1074
  filters,
1051
1075
  order_by: orderBy,
1052
1076
  pagination
@@ -1055,10 +1079,8 @@ export class NansenAPI {
1055
1079
 
1056
1080
  async perpLeaderboard(params = {}) {
1057
1081
  const { filters = {}, orderBy, pagination, days = 30 } = params;
1058
- const to = new Date().toISOString().split('T')[0];
1059
- const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
1060
1082
  return this.request('/api/v1/perp-leaderboard', {
1061
- date: { from, to },
1083
+ date: buildDateRange(days),
1062
1084
  filters,
1063
1085
  order_by: orderBy,
1064
1086
  pagination
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Canonical EVM chain name → numeric chain ID mapping.
3
+ *
4
+ * Single source of truth — import from here instead of defining inline.
5
+ */
6
+
7
+ export const EVM_CHAIN_IDS = {
8
+ ethereum: 1,
9
+ base: 8453,
10
+ optimism: 10,
11
+ arbitrum: 42161,
12
+ polygon: 137,
13
+ avalanche: 43114,
14
+ bnb: 56,
15
+ linea: 59144,
16
+ scroll: 534352,
17
+ zksync: 324,
18
+ mantle: 5000,
19
+ };