nansen-cli 1.35.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';
@@ -366,6 +368,19 @@ export function formatOutput(data, { pretty = false, table = false, csv = false
366
368
  }
367
369
  }
368
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
+
369
384
  // Format error data (returns object, does not exit)
370
385
  export function formatError(error) {
371
386
  const details = error.details ?? error.data ?? null;
@@ -702,6 +717,8 @@ USAGE: nansen <command> [subcommand] [options]
702
717
 
703
718
  COMMANDS:
704
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
705
722
  research analytics: smart-money, profiler, token, search, perp, portfolio, points
706
723
  wallet create, list, show, export, default, delete, forget-password
707
724
  agent Ask the Nansen AI research agent (fast/expert modes)
@@ -725,6 +742,12 @@ TRADING:
725
742
  nansen trade limit-order create --from SOL --to USDC --amount 1.5 --trigger-mint SOL --trigger-condition below --trigger-price 80
726
743
  Supports Solana/Base DEX swaps, cross-chain bridges, and Solana limit orders.
727
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
+
728
751
  EXAMPLES:
729
752
  nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
730
753
  nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
@@ -738,6 +761,7 @@ DEPRECATED ALIASES (still work, will be removed in a future version):
738
761
 
739
762
  Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
740
763
  Trade chains: solana, base
764
+ Bridge chains: ethereum, base, arbitrum, polygon, bnb, hyperliquid
741
765
  Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
742
766
 
743
767
  Docs: https://docs.nansen.ai
@@ -1489,6 +1513,11 @@ export function buildCommands(deps = {}) {
1489
1513
  // 'research' delegates to the category handlers defined above
1490
1514
  const RESEARCH_CATEGORIES = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points', 'prediction-market']);
1491
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
+
1492
1521
  const researchHistorical = buildResearchCommands(deps).research;
1493
1522
 
1494
1523
  cmds['research'] = async (args, apiInstance, flags, options) => {
@@ -1509,6 +1538,13 @@ export function buildCommands(deps = {}) {
1509
1538
  if (!RESEARCH_CATEGORIES.has(category)) {
1510
1539
  throw new NansenError(`Unknown research category: ${rawCategory}. Available: ${[...RESEARCH_CATEGORIES, ...RESEARCH_HISTORICAL_SUBCOMMANDS].join(', ')}`, ErrorCode.UNKNOWN);
1511
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
+ }
1512
1548
  return cmds[category](args.slice(1), apiInstance, flags, options);
1513
1549
  };
1514
1550
 
@@ -1590,11 +1626,83 @@ USAGE:
1590
1626
  return tradingCmds[sub](args.slice(1), apiInstance, flags, options);
1591
1627
  };
1592
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
+
1593
1701
  return cmds;
1594
1702
  }
1595
1703
 
1596
1704
  // Categories that moved under 'research'
1597
- 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']);
1598
1706
  // Subcommands that moved under 'trade'
1599
1707
  export const DEPRECATED_TO_TRADE = new Set(['quote', 'execute']);
1600
1708
 
@@ -1671,7 +1779,10 @@ export async function runCLI(rawArgs, deps = {}) {
1671
1779
  errorOutput = console.error,
1672
1780
  exit = process.exit,
1673
1781
  NansenAPIClass = NansenAPI,
1674
- 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,
1675
1786
  } = deps;
1676
1787
 
1677
1788
  const { _: positional, flags, options } = parseArgs(rawArgs);
@@ -1853,6 +1964,15 @@ export async function runCLI(rawArgs, deps = {}) {
1853
1964
  defaultHeaders['Payment-Signature'] = options['x402-payment-signature'];
1854
1965
  }
1855
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
+
1856
1976
  let result = await commands[command](subArgs, api, flags, options);
1857
1977
 
1858
1978
  // Credit balance warning, from the headers on the call just made. Goes to
@@ -1907,12 +2027,15 @@ export async function runCLI(rawArgs, deps = {}) {
1907
2027
  await trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
1908
2028
  return { type: csv ? 'csv' : 'success', data: result };
1909
2029
  } catch (error) {
1910
- let errorData;
1911
- if (error instanceof CommandError) {
1912
- output(error.data ? JSON.stringify(error.data) : error.message);
1913
- 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);
1914
2038
  } else {
1915
- errorData = formatError(error);
1916
2039
  const formatted = formatOutput(errorData, { pretty, table, csv });
1917
2040
  output(formatted.text);
1918
2041
  }