nansen-cli 1.34.0 → 1.36.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/cli.js CHANGED
@@ -5,6 +5,8 @@
5
5
 
6
6
  import { NansenAPI, NansenError, CommandError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, normalizeAddress, sleep } from './api.js';
7
7
  import { buildWalletCommands } from './wallet.js';
8
+ import { buildBridgeCommands, formatBridgeRoutes } from './bridge.js';
9
+ import { buildPerpCommands } from './perp.js';
8
10
  import { buildTradingCommands } from './trading.js';
9
11
  import { buildLimitOrderCommands } from './limit-order.js';
10
12
  import { formatAlertsTable, buildAlertsCommands } from './commands/alerts.js';
@@ -14,6 +16,7 @@ import { resolveAddress, isEnsName } from './ens.js';
14
16
  import fs from 'fs';
15
17
  import { getUpdateNotification, getUpgradeNotice, scheduleUpdateCheck } from './update-check.js';
16
18
  import { refreshCostMapIfStale, getCostForEndpoint } from './cost-cache.js';
19
+ import { creditWarning, noticeWarnings } from './response-meta.js';
17
20
  import { trackCommandSucceeded, trackCommandFailed } from './telemetry.js';
18
21
  import { createRequire } from 'module';
19
22
  import * as readline from 'readline';
@@ -365,6 +368,19 @@ export function formatOutput(data, { pretty = false, table = false, csv = false
365
368
  }
366
369
  }
367
370
 
371
+ // Codes whose message is a usage banner written for a human to read: multi-line,
372
+ // indented, with a blank line between sections. Serialising one into the error
373
+ // envelope turns every newline into a literal \n and makes it unreadable, so an
374
+ // interactive terminal gets the message as written instead. Piped or explicitly
375
+ // formatted output still gets the envelope, so agents keep one shape to branch on.
376
+ export const USAGE_ERROR_CODES = new Set(['MISSING_PARAM', 'MISSING_ARGS']);
377
+
378
+ export function isUsageError(errorData, { pretty, table, csv, stream, isTTY }) {
379
+ if (!USAGE_ERROR_CODES.has(errorData.code)) return false;
380
+ if (pretty || table || csv || stream) return false;
381
+ return !!isTTY;
382
+ }
383
+
368
384
  // Format error data (returns object, does not exit)
369
385
  export function formatError(error) {
370
386
  const details = error.details ?? error.data ?? null;
@@ -701,6 +717,8 @@ USAGE: nansen <command> [subcommand] [options]
701
717
 
702
718
  COMMANDS:
703
719
  trade DEX swaps/bridges: quote, execute, bridge-status, limit-order
720
+ bridge Hyperliquid bridge: quote, execute, status (EVM <-> HL)
721
+ perp Hyperliquid perps: order, cancel, close, leverage, positions
704
722
  research analytics: smart-money, profiler, token, search, perp, portfolio, points
705
723
  wallet create, list, show, export, default, delete, forget-password
706
724
  agent Ask the Nansen AI research agent (fast/expert modes)
@@ -724,6 +742,12 @@ TRADING:
724
742
  nansen trade limit-order create --from SOL --to USDC --amount 1.5 --trigger-mint SOL --trigger-condition below --trigger-price 80
725
743
  Supports Solana/Base DEX swaps, cross-chain bridges, and Solana limit orders.
726
744
 
745
+ BRIDGE (Hyperliquid):
746
+ nansen bridge quote --from-chain base --to-chain hyperliquid --from-token USDC --amount 1000000
747
+ nansen bridge execute --quote <quoteId>
748
+ nansen bridge status --request-id <id>
749
+ Supports EVM chains (ethereum, base, arbitrum, polygon, bnb) <-> Hyperliquid.
750
+
727
751
  EXAMPLES:
728
752
  nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
729
753
  nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
@@ -737,6 +761,7 @@ DEPRECATED ALIASES (still work, will be removed in a future version):
737
761
 
738
762
  Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
739
763
  Trade chains: solana, base
764
+ Bridge chains: ethereum, base, arbitrum, polygon, bnb, hyperliquid
740
765
  Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
741
766
 
742
767
  Docs: https://docs.nansen.ai
@@ -1488,6 +1513,11 @@ export function buildCommands(deps = {}) {
1488
1513
  // 'research' delegates to the category handlers defined above
1489
1514
  const RESEARCH_CATEGORIES = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points', 'prediction-market']);
1490
1515
 
1516
+ // The analytics-only perp handler, captured before the trading wrapper below
1517
+ // replaces cmds['perp']. Both the wrapper and the research dispatch route to
1518
+ // it, so it has to be taken exactly once, here.
1519
+ const perpAnalytics = cmds['perp'];
1520
+
1491
1521
  const researchHistorical = buildResearchCommands(deps).research;
1492
1522
 
1493
1523
  cmds['research'] = async (args, apiInstance, flags, options) => {
@@ -1508,6 +1538,13 @@ export function buildCommands(deps = {}) {
1508
1538
  if (!RESEARCH_CATEGORIES.has(category)) {
1509
1539
  throw new NansenError(`Unknown research category: ${rawCategory}. Available: ${[...RESEARCH_CATEGORIES, ...RESEARCH_HISTORICAL_SUBCOMMANDS].join(', ')}`, ErrorCode.UNKNOWN);
1510
1540
  }
1541
+ // `research perp` reaches only the analytics half (screener/leaderboard) —
1542
+ // the trading subcommands live at the top level. Routing its help through
1543
+ // cmds['perp'] printed the trading help, advertising order/close/leverage
1544
+ // from a command that can't run them.
1545
+ if (category === 'perp' && (!args[1] || args[1] === 'help')) {
1546
+ return perpAnalytics(['help'], apiInstance, flags, options);
1547
+ }
1511
1548
  return cmds[category](args.slice(1), apiInstance, flags, options);
1512
1549
  };
1513
1550
 
@@ -1589,11 +1626,83 @@ USAGE:
1589
1626
  return tradingCmds[sub](args.slice(1), apiInstance, flags, options);
1590
1627
  };
1591
1628
 
1629
+ // 'bridge' delegates to quote/execute/status from buildBridgeCommands
1630
+ const bridgeCmds = buildBridgeCommands(deps);
1631
+ cmds['bridge'] = async (args, apiInstance, flags, options) => {
1632
+ const sub = args[0];
1633
+ if (!sub || sub === 'help') {
1634
+ log(`nansen bridge — Hyperliquid bridge commands (EVM <-> Hyperliquid via Relay)
1635
+
1636
+ SUBCOMMANDS:
1637
+ quote Get a bridge quote
1638
+ execute Execute a bridge quote (sign + broadcast)
1639
+ status Check bridge transaction status
1640
+
1641
+ USAGE:
1642
+ nansen bridge quote --from-chain base --to-chain hyperliquid --from-token USDC --amount 1000000
1643
+ nansen bridge execute --quote <quoteId>
1644
+ nansen bridge status --request-id <id>
1645
+
1646
+ SUPPORTED ROUTES:
1647
+ ${formatBridgeRoutes()}`);
1648
+ return;
1649
+ }
1650
+ if (!bridgeCmds[sub]) {
1651
+ throw new NansenError(`Unknown bridge subcommand: ${sub}. Available: quote, execute, status`, ErrorCode.UNKNOWN);
1652
+ }
1653
+ return bridgeCmds[sub](args.slice(1), apiInstance, flags, options);
1654
+ };
1655
+
1656
+ // 'perp' delegates to buildPerpCommands. The trading subcommands are added on
1657
+ // top of the pre-existing perp analytics command, so capture that handler and
1658
+ // keep screener/leaderboard reachable instead of shadowing them — both
1659
+ // `nansen perp screener` and `nansen research perp screener` route through here.
1660
+ const perpCmds = buildPerpCommands(deps);
1661
+ const PERP_ANALYTICS_SUBCOMMANDS = new Set(['screener', 'leaderboard']);
1662
+ cmds['perp'] = async (args, apiInstance, flags, options) => {
1663
+ const sub = args[0];
1664
+ if (!sub || sub === 'help') {
1665
+ log(`nansen perp — Hyperliquid perpetual trading
1666
+
1667
+ SUBCOMMANDS:
1668
+ order Place a perp order (market/limit with optional TP/SL)
1669
+ cancel Cancel an open order
1670
+ close Close a position (reduce-only market order)
1671
+ leverage Set leverage and margin mode
1672
+ transfer Move USDC between Spot and Perps balances
1673
+ approve-builder-fee Authorize the Nansen builder fee (one-time; auto-fired on first trade)
1674
+ positions View open positions
1675
+ orders View open orders
1676
+ account View account state (balance, equity, margin, spot)
1677
+ meta View available assets
1678
+ screener Perp market screener (analytics)
1679
+ leaderboard Perp trader leaderboard (analytics)
1680
+
1681
+ USAGE:
1682
+ nansen perp order --coin BTC --side buy --size 0.001 --price 50000 --type limit
1683
+ nansen perp cancel --coin BTC --oid 12345
1684
+ nansen perp close --coin BTC --size 0.001 --price 100000 --side sell
1685
+ nansen perp leverage --coin BTC --leverage 10 --margin-type cross
1686
+ nansen perp transfer --direction spot-to-perp --amount 25
1687
+ nansen perp approve-builder-fee
1688
+ nansen perp positions
1689
+ nansen perp account`);
1690
+ return;
1691
+ }
1692
+ if (!perpCmds[sub]) {
1693
+ if (PERP_ANALYTICS_SUBCOMMANDS.has(sub)) {
1694
+ return perpAnalytics(args, apiInstance, flags, options);
1695
+ }
1696
+ throw new NansenError(`Unknown perp subcommand: ${sub}. Available: order, cancel, close, leverage, transfer, approve-builder-fee, positions, orders, account, meta, screener, leaderboard`, ErrorCode.UNKNOWN);
1697
+ }
1698
+ return perpCmds[sub](args.slice(1), apiInstance, flags, options);
1699
+ };
1700
+
1592
1701
  return cmds;
1593
1702
  }
1594
1703
 
1595
1704
  // Categories that moved under 'research'
1596
- export const DEPRECATED_TO_RESEARCH = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points']);
1705
+ export const DEPRECATED_TO_RESEARCH = new Set(['smart-money', 'profiler', 'token', 'search', 'portfolio', 'points']);
1597
1706
  // Subcommands that moved under 'trade'
1598
1707
  export const DEPRECATED_TO_TRADE = new Set(['quote', 'execute']);
1599
1708
 
@@ -1670,7 +1779,10 @@ export async function runCLI(rawArgs, deps = {}) {
1670
1779
  errorOutput = console.error,
1671
1780
  exit = process.exit,
1672
1781
  NansenAPIClass = NansenAPI,
1673
- commandOverrides = {}
1782
+ commandOverrides = {},
1783
+ // Injectable so tests can exercise both renderings; defaults to the real
1784
+ // terminal, which is false under a pipe or in CI.
1785
+ isTTY = process.stdout.isTTY,
1674
1786
  } = deps;
1675
1787
 
1676
1788
  const { _: positional, flags, options } = parseArgs(rawArgs);
@@ -1852,8 +1964,25 @@ export async function runCLI(rawArgs, deps = {}) {
1852
1964
  defaultHeaders['Payment-Signature'] = options['x402-payment-signature'];
1853
1965
  }
1854
1966
  const api = new NansenAPIClass(undefined, undefined, { retry: retryOptions, cache: cacheOptions, defaultHeaders });
1967
+
1968
+ // Deprecated top-level aliases otherwise run silently (the notice was only
1969
+ // shown in --help). Warn on stderr so it doesn't pollute parsed stdout.
1970
+ if (DEPRECATED_TO_TRADE.has(command)) {
1971
+ process.stderr.write(`Note: "nansen ${command}" is deprecated. Use "nansen trade ${command}" instead.\n`);
1972
+ } else if (DEPRECATED_TO_RESEARCH.has(command)) {
1973
+ process.stderr.write(`Note: "nansen ${command}" is deprecated. Use "nansen research ${command}" instead.\n`);
1974
+ }
1975
+
1855
1976
  let result = await commands[command](subArgs, api, flags, options);
1856
1977
 
1978
+ // Credit balance warning, from the headers on the call just made. Goes to
1979
+ // stderr so it never contaminates the JSON on stdout that agents parse.
1980
+ // Placed before every return path below so it fires for operational
1981
+ // commands too, which print their own output and return undefined.
1982
+ const lowCredits = creditWarning(api.lastResponseMeta);
1983
+ if (lowCredits) errorOutput(lowCredits);
1984
+ for (const notice of noticeWarnings(api.lastResponseMeta)) errorOutput(notice);
1985
+
1857
1986
  // Commands that handle their own output return undefined
1858
1987
  if (result === undefined) {
1859
1988
  await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
@@ -1898,12 +2027,15 @@ export async function runCLI(rawArgs, deps = {}) {
1898
2027
  await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
1899
2028
  return { type: csv ? 'csv' : 'success', data: result };
1900
2029
  } catch (error) {
1901
- let errorData;
1902
- if (error instanceof CommandError) {
1903
- output(error.data ? JSON.stringify(error.data) : error.message);
1904
- errorData = { error: error.message, code: error.code };
2030
+ // Unified error envelope across all command families (perp/bridge/trade):
2031
+ // every failure serializes through formatError as
2032
+ // {success:false, error, code, status, details}. A CommandError's structured
2033
+ // data (e.g. PASSWORD_REQUIRED resolution steps) is preserved under `details`,
2034
+ // so agents get one consistent shape to branch on regardless of command.
2035
+ const errorData = formatError(error);
2036
+ if (isUsageError(errorData, { pretty, table, csv, stream, isTTY })) {
2037
+ output(errorData.error);
1905
2038
  } else {
1906
- errorData = formatError(error);
1907
2039
  const formatted = formatOutput(errorData, { pretty, table, csv });
1908
2040
  output(formatted.text);
1909
2041
  }