nansen-cli 1.21.0 → 1.22.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#336](https://github.com/nansen-ai/nansen-cli/pull/336) [`c3b1fbd`](https://github.com/nansen-ai/nansen-cli/commit/c3b1fbdee46c15326a1656bf27a314f6c55dddf8) Thanks [@kome12](https://github.com/kome12)! - Add --label option to `token flows` command to filter by holder segment (top_100_holders, smart_money, public_figure, whale, exchange).
8
+
9
+ - [#334](https://github.com/nansen-ai/nansen-cli/pull/334) [`83244c6`](https://github.com/nansen-ai/nansen-cli/commit/83244c658d4ece2072dea0c6ed405a088c98aa4f) Thanks [@kome12](https://github.com/kome12)! - Add `--include-stablecoins` flag to `token screener` command. Pass `--include-stablecoins false` to exclude stablecoins from screener results (API default is `true`). Supports combined usage with `--smart-money`.
10
+
11
+ - [#339](https://github.com/nansen-ai/nansen-cli/pull/339) [`27ebcfc`](https://github.com/nansen-ai/nansen-cli/commit/27ebcfc2a3afd836db595df6d5a2a5f9242b624c) Thanks [@TimNooren](https://github.com/TimNooren)! - Add --amount-unit token flag to trade quote for human-readable amounts
12
+
3
13
  ## 1.21.0
4
14
 
5
15
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -58,9 +58,16 @@ nansen trade execute --quote "$quote_id"
58
58
  | ETH | Base | `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` |
59
59
  | USDC | Base | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
60
60
 
61
- ## Amounts are in base units — NEVER USD
61
+ ## Amounts
62
62
 
63
- `--amount` accepts **integer base units only** (lamports, wei, etc). It is never a USD value.
63
+ By default, `--amount` accepts **integer base units** (lamports, wei, etc). Use `--amount-unit token` to specify human-readable token amounts instead — the CLI resolves decimals locally and sends base units to the API.
64
+
65
+ ```bash
66
+ # Base units (default)
67
+ nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
68
+ # Token units (0.5 SOL = 500000000 lamports, resolved automatically)
69
+ nansen trade quote --chain solana --from SOL --to USDC --amount 0.5 --amount-unit token
70
+ ```
64
71
 
65
72
  | Token | Decimals | 1 token = |
66
73
  |-------|----------|-----------|
@@ -68,7 +75,7 @@ nansen trade execute --quote "$quote_id"
68
75
  | ETH | 18 | `1000000000000000000` |
69
76
  | USDC | 6 | `1000000` |
70
77
 
71
- If the user says "$20 worth of X", you must convert USD → token amount base units. For example, to buy $20 of SOL at $150/SOL: $20 ÷ $150 = 0.1333 SOL = 133,300,000 lamports → `--amount 133300000`. Use a price lookup (e.g. `nansen research token info`) to get the current price first.
78
+ If the user says "$20 worth of X", you must convert USD → token amount, then either pass base units or use `--amount-unit token`. For example, to buy $20 of SOL at $150/SOL: $20 ÷ $150 = 0.1333 SOL → `--amount 0.1333 --amount-unit token`. Use a price lookup (e.g. `nansen research token info`) to get the current price first.
72
79
 
73
80
  ## Flags
74
81
 
@@ -77,7 +84,8 @@ If the user says "$20 worth of X", you must convert USD → token amount → bas
77
84
  | `--chain` | `solana` or `base` |
78
85
  | `--from` | Source token (symbol or address) |
79
86
  | `--to` | Destination token (symbol or address) |
80
- | `--amount` | Amount in base units (integer) |
87
+ | `--amount` | Amount in base units (integer), or token units with `--amount-unit token` |
88
+ | `--amount-unit` | Set to `token` to specify amount in token units (e.g. 0.5 SOL) |
81
89
  | `--wallet` | Wallet name (default: default wallet) |
82
90
  | `--slippage` | Slippage tolerance as decimal (e.g. 0.03) |
83
91
  | `--quote` | Quote ID for execute |
package/src/api.js CHANGED
@@ -974,7 +974,7 @@ export class NansenAPI {
974
974
  }
975
975
 
976
976
  async tokenFlows(params = {}) {
977
- const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 30, date } = params;
977
+ const { tokenAddress, chain = 'solana', label, filters = {}, orderBy, pagination, days = 30, date } = params;
978
978
  if (tokenAddress) {
979
979
  const validation = validateTokenAddress(tokenAddress, chain);
980
980
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
@@ -984,6 +984,7 @@ export class NansenAPI {
984
984
  token_address: tokenAddress,
985
985
  chain,
986
986
  date: dateRange,
987
+ label,
987
988
  filters,
988
989
  order_by: orderBy,
989
990
  pagination
package/src/cli.js CHANGED
@@ -1200,10 +1200,15 @@ export function buildCommands(deps = {}) {
1200
1200
  // Convenience filter for smart money only
1201
1201
  const onlySmartMoney = options['smart-money'] || flags['smart-money'] || false;
1202
1202
  if (onlySmartMoney) {
1203
- filters.include_smart_money_labels = filters.include_smart_money_labels ||
1203
+ filters.include_smart_money_labels = filters.include_smart_money_labels ||
1204
1204
  ['Fund', 'Smart Trader', '30D Smart Trader', '90D Smart Trader', '180D Smart Trader'];
1205
1205
  }
1206
1206
 
1207
+ const includeStablecoins = options['include-stablecoins'] ?? flags['include-stablecoins'];
1208
+ if (includeStablecoins !== undefined) {
1209
+ filters.include_stablecoins = includeStablecoins;
1210
+ }
1211
+
1207
1212
  const handlers = {
1208
1213
  'indicators': () => apiInstance.tokenIndicators({ tokenAddress, chain }),
1209
1214
  'ohlcv': () => apiInstance.tokenOhlcv({ tokenAddress, chain, timeframe: options.timeframe || '1d' }),
@@ -1235,7 +1240,8 @@ export function buildCommands(deps = {}) {
1235
1240
  'holders': () => apiInstance.tokenHolders({ tokenAddress, chain, labelType: onlySmartMoney ? 'smart_money' : 'all_holders', filters, orderBy, pagination }),
1236
1241
  'flows': () => {
1237
1242
  const date = parseDateOption(options.date, days);
1238
- return apiInstance.tokenFlows({ tokenAddress, chain, filters, orderBy, pagination, days, date });
1243
+ const label = options.label;
1244
+ return apiInstance.tokenFlows({ tokenAddress, chain, label, filters, orderBy, pagination, days, date });
1239
1245
  },
1240
1246
  'dex-trades': () => apiInstance.tokenDexTrades({ tokenAddress, chain, onlySmartMoney, filters, orderBy, pagination, days }),
1241
1247
  'pnl': () => apiInstance.tokenPnlLeaderboard({ tokenAddress, chain, filters, orderBy, pagination, days }),
@@ -1257,7 +1263,7 @@ export function buildCommands(deps = {}) {
1257
1263
  'help': () => ({
1258
1264
  commands: ['info', 'ohlcv', 'screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
1259
1265
  description: 'Token God Mode endpoints',
1260
- example: 'nansen token screener --chain solana --timeframe 24h --smart-money'
1266
+ example: 'nansen token screener --chain solana --timeframe 24h --smart-money --include-stablecoins false'
1261
1267
  })
1262
1268
  };
1263
1269
 
package/src/schema.json CHANGED
@@ -280,6 +280,11 @@
280
280
  },
281
281
  "days": {
282
282
  "default": 30
283
+ },
284
+ "label": {
285
+ "description": "Holder segment to filter flows by",
286
+ "enum": ["top_100_holders", "smart_money", "public_figure", "whale", "exchange"],
287
+ "default": "top_100_holders"
283
288
  }
284
289
  }
285
290
  },
@@ -463,6 +468,10 @@
463
468
  },
464
469
  "chain": {
465
470
  "default": "solana"
471
+ },
472
+ "include-stablecoins": {
473
+ "description": "Whether to include stablecoins in screener results (default true on API side)",
474
+ "default": true
466
475
  }
467
476
  }
468
477
  }
@@ -762,7 +771,11 @@
762
771
  "amount": {
763
772
  "type": "string",
764
773
  "required": true,
765
- "description": "Amount to swap"
774
+ "description": "Amount to swap (base units by default, or token units with --amount-unit token)"
775
+ },
776
+ "amount-unit": {
777
+ "type": "string",
778
+ "description": "\"token\" to specify amount in token units (e.g. 0.5 SOL), or \"base\" for base units (default). Decimals are resolved locally and the API always receives base units."
766
779
  },
767
780
  "wallet": {
768
781
  "type": "string",
package/src/trading.js CHANGED
@@ -683,6 +683,101 @@ export function getWrappedNativeFromWarning(tokenAddress, chain) {
683
683
  return null;
684
684
  }
685
685
 
686
+ // ============= Token Decimal Resolution =============
687
+
688
+ // Hardcoded decimals for well-known tokens — avoids RPC calls in the common case.
689
+ const KNOWN_DECIMALS = {
690
+ // Solana
691
+ 'So11111111111111111111111111111111111111112': 9, // SOL/WSOL
692
+ 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': 6, // USDC
693
+ 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': 6, // USDT
694
+ // Base (EVM) — lowercase for case-insensitive matching
695
+ '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee': 18, // ETH native
696
+ '0x4200000000000000000000000000000000000006': 18, // WETH
697
+ '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': 6, // USDC
698
+ '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2': 6, // USDT
699
+ };
700
+
701
+ /**
702
+ * Resolve the number of decimals for a token.
703
+ * Checks a hardcoded map first, then falls back to an RPC call.
704
+ * Solana: getAccountInfo with jsonParsed encoding.
705
+ * EVM: eth_call to decimals() selector 0x313ce567.
706
+ */
707
+ export async function resolveTokenDecimals(tokenAddress, chainName) {
708
+ // Normalise for map lookup (EVM addresses are case-insensitive)
709
+ const key = tokenAddress.startsWith('0x') ? tokenAddress.toLowerCase() : tokenAddress;
710
+ if (KNOWN_DECIMALS[key] !== undefined) return KNOWN_DECIMALS[key];
711
+
712
+ const chain = chainName.toLowerCase();
713
+ const chainConfig = CHAIN_MAP[chain];
714
+ if (!chainConfig) throw new Error(`Unknown chain: ${chain}`);
715
+
716
+ // Validate address format before making RPC calls.
717
+ // A bare symbol like "SOL" that didn't resolve means it's not recognized on this chain.
718
+ if (chainConfig.type === 'solana') {
719
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(tokenAddress)) {
720
+ throw new Error(`"${tokenAddress}" is not a recognized token on ${chainName}. Use a valid Solana address (base58, 32-44 chars).`);
721
+ }
722
+ } else {
723
+ if (!/^0x[0-9a-fA-F]{40}$/.test(tokenAddress)) {
724
+ throw new Error(`"${tokenAddress}" is not a recognized token on ${chainName}. Use a valid EVM address (0x + 40 hex chars).`);
725
+ }
726
+ }
727
+
728
+ if (chainConfig.type === 'solana') {
729
+ const rpcUrl = CHAIN_RPCS.solana;
730
+ const res = await fetch(rpcUrl, {
731
+ method: 'POST',
732
+ headers: { 'Content-Type': 'application/json' },
733
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'getAccountInfo', params: [tokenAddress, { encoding: 'jsonParsed' }] }),
734
+ });
735
+ const body = await res.json();
736
+ const decimals = body.result?.value?.data?.parsed?.info?.decimals;
737
+ if (decimals === undefined) throw new Error(`Could not resolve decimals for Solana token ${tokenAddress}`);
738
+ return decimals;
739
+ }
740
+
741
+ // EVM — eth_call to decimals()
742
+ const result = await evmRpcCall(chain, 'eth_call', [{ to: tokenAddress, data: '0x313ce567' }, 'latest']);
743
+ const decimals = parseInt(result, 16);
744
+ if (isNaN(decimals) || decimals > 255) throw new Error(`Could not resolve decimals for EVM token ${tokenAddress}`);
745
+ return decimals;
746
+ }
747
+
748
+ /**
749
+ * Convert a human-readable token amount to base units using string math.
750
+ * Avoids floating-point precision issues by operating on digit strings.
751
+ * Example: convertToBaseUnits('0.5', 9) => '500000000'
752
+ */
753
+ export function convertToBaseUnits(amount, decimals) {
754
+ const str = String(amount);
755
+ if (!/^\d+(\.\d+)?$/.test(str)) {
756
+ throw new Error(`Invalid amount: "${str}". Must be a non-negative number (e.g. "0.5", "100").`);
757
+ }
758
+ const dotIndex = str.indexOf('.');
759
+ if (dotIndex === -1) {
760
+ // Whole number — append zeros and strip leading zeros
761
+ const raw = str + '0'.repeat(decimals);
762
+ return raw.replace(/^0+/, '') || '0';
763
+ }
764
+ const whole = str.slice(0, dotIndex);
765
+ let frac = str.slice(dotIndex + 1);
766
+ if (frac.length > decimals) {
767
+ // Reject if meaningful (non-zero) digits would be lost
768
+ const excess = frac.slice(decimals);
769
+ if (/[1-9]/.test(excess)) {
770
+ throw new Error(`Amount "${str}" has more fractional digits than the token supports (${decimals} decimals). The smallest unit is ${decimals === 0 ? '1 token' : '0.' + '0'.repeat(decimals - 1) + '1'}.`);
771
+ }
772
+ frac = frac.slice(0, decimals);
773
+ } else {
774
+ frac = frac.padEnd(decimals, '0');
775
+ }
776
+ // Strip leading zeros from the combined result
777
+ const raw = (whole + frac).replace(/^0+/, '') || '0';
778
+ return raw;
779
+ }
780
+
686
781
  /**
687
782
  * Check if amount contains a decimal point (i.e. not in base units).
688
783
  * Returns an error string if invalid, or null if OK. Pure function.
@@ -690,8 +785,12 @@ export function getWrappedNativeFromWarning(tokenAddress, chain) {
690
785
  export function validateBaseUnitAmount(amount) {
691
786
  if (!amount) return null;
692
787
  const str = String(amount);
788
+ if (str.startsWith('-')) {
789
+ return 'Amount cannot be negative. Got: ' + str;
790
+ }
693
791
  if (str.includes('.')) {
694
- return 'Amount must be in base units (integer), not token units. ' +
792
+ return 'Amount must be in base units (integer). ' +
793
+ 'Use --amount-unit token to specify token amounts (e.g. --amount 0.5 --amount-unit token). ' +
695
794
  'Examples: 1000000000 lamports = 1 SOL, 1000000000000000000 wei = 1 ETH, ' +
696
795
  '1000000 = 1 USDC. Got: ' + str;
697
796
  }
@@ -745,6 +844,7 @@ export function buildTradingCommands(deps = {}) {
745
844
  const autoSlippage = flags['auto-slippage'] || flags.autoSlippage;
746
845
  const maxAutoSlippage = options['max-auto-slippage'];
747
846
  const swapMode = options['swap-mode'] || 'exactIn';
847
+ const amountUnit = options['amount-unit'];
748
848
 
749
849
  if (!chain || !from || !to || !amount) {
750
850
  log(`
@@ -760,6 +860,7 @@ OPTIONS:
760
860
  --from <symbol|address> Input token (symbol like SOL, USDC or address)
761
861
  --to <symbol|address> Output token (symbol like USDC, ETH or address)
762
862
  --amount <units> Amount in BASE UNITS (e.g. lamports, wei)
863
+ --amount-unit <unit> "token" to specify amount in token units (e.g. 0.5 SOL)
763
864
  --wallet <name> Wallet name (default: default wallet). Use "walletconnect" or "wc" for WalletConnect.
764
865
  --slippage <pct> Slippage as decimal (e.g. 0.03 for 3%). Default: 0.03
765
866
  --auto-slippage Enable auto slippage calculation
@@ -768,6 +869,7 @@ OPTIONS:
768
869
 
769
870
  EXAMPLES:
770
871
  nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
872
+ nansen trade quote --chain solana --from SOL --to USDC --amount 0.5 --amount-unit token
771
873
  nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
772
874
  nansen trade quote --chain solana --from So11111111111111111111111111111111111111112 --to EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v --amount 1000000000
773
875
  `);
@@ -775,13 +877,35 @@ EXAMPLES:
775
877
  return;
776
878
  }
777
879
 
778
- const amountError = validateBaseUnitAmount(amount);
779
- if (amountError) {
780
- log(`Error: ${amountError}`);
880
+ // Validate --amount-unit if provided
881
+ if (amountUnit && amountUnit !== 'token' && amountUnit !== 'base') {
882
+ log(`Error: Unknown --amount-unit "${amountUnit}". Supported values: token, base`);
781
883
  exit(1);
782
884
  return;
783
885
  }
784
886
 
887
+ // When --amount-unit token is used, resolve decimals and convert to base units.
888
+ // Otherwise, validate that the amount is already in base units (integer).
889
+ let resolvedAmount = amount;
890
+ if (amountUnit === 'token') {
891
+ try {
892
+ const tokenForDecimals = swapMode === 'exactOut' ? to : from;
893
+ const decimals = await resolveTokenDecimals(tokenForDecimals, chain);
894
+ resolvedAmount = convertToBaseUnits(amount, decimals);
895
+ } catch (err) {
896
+ log(`Error resolving token decimals: ${err.message}`);
897
+ exit(1);
898
+ return;
899
+ }
900
+ } else {
901
+ const amountError = validateBaseUnitAmount(amount);
902
+ if (amountError) {
903
+ log(`Error: ${amountError}`);
904
+ exit(1);
905
+ return;
906
+ }
907
+ }
908
+
785
909
  try {
786
910
  const chainConfig = resolveChain(chain);
787
911
  const chainType = chainConfig.type === 'evm' ? 'evm' : 'solana';
@@ -837,7 +961,7 @@ EXAMPLES:
837
961
  chainIndex: chainConfig.index,
838
962
  fromTokenAddress: from,
839
963
  toTokenAddress: to,
840
- amount,
964
+ amount: resolvedAmount,
841
965
  userWalletAddress: walletAddress,
842
966
  };
843
967
  if (slippage) params.slippagePercent = slippage;