nansen-cli 1.36.2 → 1.38.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 +42 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/skills/nansen-trading/SKILL.md +2 -0
- package/skills/nansen-wallet-keychain-migration/SKILL.md +14 -12
- package/src/api.js +11 -0
- package/src/cli.js +122 -58
- package/src/cost-cache.js +19 -1
- package/src/doctor.js +480 -0
- package/src/keychain.js +46 -0
- package/src/perp.js +134 -5
- package/src/schema.json +93 -0
- package/src/telemetry.js +59 -2
- package/src/trade-validation.js +441 -0
- package/src/trading.js +387 -18
- package/src/update-check.js +45 -24
- package/src/walletconnect-trading.js +11 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.38.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#486](https://github.com/nansen-ai/nansen-cli/pull/486) [`b752d81`](https://github.com/nansen-ai/nansen-cli/commit/b752d81336a5d9bda3ad85e62a4d42d98c069e58) Thanks [@gulshngill](https://github.com/gulshngill)! - Add `nansen auth status` and `nansen doctor`. `auth status` is fully offline: it reports whether an API key is configured and where it comes from (env var vs config file, masked), the active base URL, x402 wallet readiness, and OS keychain availability. `doctor` runs health checks over the whole setup — Node version, config file validity and permissions, wallet storage and password hygiene (flags the insecure `.credentials` file), keychain availability, Privy env credentials, caches, and telemetry — with an actionable fix per finding, plus a safe unauthenticated connectivity probe (no credits consumed; skip it with `--offline`). `--json` returns machine-readable checks.
|
|
8
|
+
|
|
9
|
+
- [#494](https://github.com/nansen-ai/nansen-cli/pull/494) [`67027e6`](https://github.com/nansen-ai/nansen-cli/commit/67027e6a0faefcc797ec9407f199bc985dbbfc56) Thanks [@kome12](https://github.com/kome12)! - Harden EVM swap signing: scope ERC-20 approvals to the trade amount instead of granting an unlimited allowance, and validate the swap target before signing (reject an empty/zero address, a non-contract target, or a target equal to the token being sold). As a result, ERC-20 sells on Base now include a per-swap approval transaction. Native ETH swaps and all Solana swaps are unaffected. Note: this scopes approvals granted from now on; a pre-existing unlimited approval from an earlier version is not automatically reduced.
|
|
10
|
+
|
|
11
|
+
Also tightens the input validation on the quote a swap is signed from. Every approval-signing path (local, Privy, WalletConnect) now shares one encoder that requires a well-formed 20-byte spender, keeps the approved amount bounded (never unlimited) and within the request cap, and produces fixed-width approval calldata. EVM execution now requires complete request intent persisted by the quote command and revalidates each quote against it (chain, wallet, token pair, mode, and amount), so the signed transaction remains bound to what was requested. A same-chain swap whose transaction is a bare ERC-20 transfer/approve rather than a routed swap is refused (bridge routes excluded).
|
|
12
|
+
|
|
13
|
+
The swap-target contract check now fails closed: it retries and, if it still can't confirm the target carries contract code, refuses to sign rather than proceeding on an unverified target.
|
|
14
|
+
|
|
15
|
+
EVM (Base) exactOut swaps now require an explicit maximum input (spend ceiling) via `--max-input` in base units of the sell token. The quote persists that `maxInputAmount`, and the execute path refuses to sign, approve, or broadcast any quote whose input exceeds it, for native and ERC-20 swaps across all three EVM signing paths. Solana exactOut is unaffected and does not require the flag (there is no ERC-20 approval to scope on that path). Quotes already above the cap are dropped at quote time (and, when none fit, a clear `MAX_INPUT_EXCEEDED` error is returned) rather than saved and rejected only at execute. Relatedly, a quote missing a field the request-intent check needs (sell/buy token address or the bound amount) is now rejected rather than skipped, and the exactOut output binding accepts more-than-requested output (only a shortfall is rejected, since the input is independently capped).
|
|
16
|
+
|
|
17
|
+
The execute path also binds the signer to the wallet the quote was built for: it now refuses to sign a quote whose persisted wallet doesn't match the current signer (e.g. the default wallet changed between quote and execute), since the quoted transaction is constructed for a specific sender.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [#494](https://github.com/nansen-ai/nansen-cli/pull/494) [`54b9d41`](https://github.com/nansen-ai/nansen-cli/commit/54b9d41fc9f99cd68bce416b95a129fe9e858981) Thanks [@kome12](https://github.com/kome12)! - Fix exactOut `--max-input` so it bounds the slippage-buffered approval, not the bare quote input. Previously an exactOut ERC-20 quote whose raw input equalled the cap (e.g. 1,000,000 at 3% slippage) passed the max-input filter and was saved, but execution scoped a larger approval (1,030,000) that the approval encoder then rejected for exceeding the cap — bricking the trade across local, Privy, and WalletConnect flows. Both the quote-time filter and the execute-time spend check now measure the same buffered amount the approval encoder does, so a quote that clears the cap can always be signed.
|
|
22
|
+
|
|
23
|
+
## 1.37.0
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- [#481](https://github.com/nansen-ai/nansen-cli/pull/481) [`ccaa40b`](https://github.com/nansen-ai/nansen-cli/commit/ccaa40beb570c2a6df5917ded308c3bb1722eb70) Thanks [@kome12](https://github.com/kome12)! - Add `research profiler first-funder` command to look up the first wallet that funded an EVM address. The funder is the earliest address to send native gas, resolved across chains, returned with its Nansen label and the funding transaction.
|
|
28
|
+
|
|
29
|
+
- [#485](https://github.com/nansen-ai/nansen-cli/pull/485) [`b49c758`](https://github.com/nansen-ai/nansen-cli/commit/b49c75866379421c0738032e1f98a4a74b3fb5b4) Thanks [@MarcLlopart](https://github.com/MarcLlopart)! - `nansen perp order` and `perp close` now print the Hyperliquid order id (`oid`) and fill (size @ avg price) returned by the exchange, plus a ready-to-run `nansen perp cancel` command for any resting order — mirroring how spot trading surfaces its quote id. TP/SL bracket legs are labelled (parent / take-profit / stop-loss). Order ids are uint64; an id beyond JavaScript's safe integer range (2^53) is detected and its exact value and cancel hint are withheld rather than shown rounded, so a wrong id is never presented as actionable.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- [#483](https://github.com/nansen-ai/nansen-cli/pull/483) [`d0d10a2`](https://github.com/nansen-ai/nansen-cli/commit/d0d10a266e32aa086a8934acaa8e1d0b9ddff2e2) Thanks [@kome12](https://github.com/kome12)! - Unknown-command errors now detect when a whole multi-word command was passed as a single argument (a common shell-quoting mistake, e.g. `nansen "trade --help"` or an unquoted variable under zsh) and point at the likely cause instead of a bare "Unknown command".
|
|
34
|
+
|
|
35
|
+
- [#484](https://github.com/nansen-ai/nansen-cli/pull/484) [`bc5f774`](https://github.com/nansen-ai/nansen-cli/commit/bc5f774165df7103eff9ba5cfcdc660bcec5d752) Thanks [@kome12](https://github.com/kome12)! - Write the cost-map and update-check cache files atomically (temp file + rename) so concurrent `nansen` processes can no longer observe an empty or truncated cache.
|
|
36
|
+
|
|
37
|
+
- [#465](https://github.com/nansen-ai/nansen-cli/pull/465) [`4105193`](https://github.com/nansen-ai/nansen-cli/commit/41051932236a37121819e0d1bf47c8fb34422ec8) Thanks [@dobbydobap](https://github.com/dobbydobap)! - Fix `nansen quote --help`, `nansen trade quote --help`, and `nansen execute --help` to print the trade usage and exit with code 0 instead of erroring with exit code 1.
|
|
38
|
+
|
|
39
|
+
- [#485](https://github.com/nansen-ai/nansen-cli/pull/485) [`c9aaf58`](https://github.com/nansen-ai/nansen-cli/commit/c9aaf58923819c014588cb2c068600ad9872276e) Thanks [@MarcLlopart](https://github.com/MarcLlopart)! - `nansen perp order` / `perp close` now emit an anonymous `perp_order_completed` telemetry event after the Hyperliquid `/exchange` response is parsed. Perp orders bypass the Nansen API on submit (the CLI signs and posts straight to Hyperliquid), so this client-side event is the only signal that an order was placed. The payload is deliberately minimal — only the trade side and the Hyperliquid order id (omitted when it exceeded JS safe-integer precision); no asset, price, size, or fill detail is sent. The telemetry disclosure (CLI help footer and module docs) names exactly these fields. Honours the existing `DO_NOT_TRACK` / `NANSEN_NO_TELEMETRY` opt-out; order rejections remain covered by `cli_command_failed`.
|
|
40
|
+
|
|
41
|
+
- [#478](https://github.com/nansen-ai/nansen-cli/pull/478) [`758ce13`](https://github.com/nansen-ai/nansen-cli/commit/758ce13b7c65a5a88d20378ae1ad5cc7bba7d7ba) Thanks [@boleklebovski](https://github.com/boleklebovski)! - Document the missing `trade quote` and `trade execute` options in `src/schema.json`: `--swap-mode`, `--slippage`, `--auto-slippage`, `--max-auto-slippage`, `--quote`, `--quote-index` and `--no-simulate`. These options are already implemented and documented for humans, but were absent from the machine-readable schema.
|
|
42
|
+
|
|
43
|
+
- [#488](https://github.com/nansen-ai/nansen-cli/pull/488) [`f653b37`](https://github.com/nansen-ai/nansen-cli/commit/f653b3761a4abc8e8a45d3ff42cedf0241a8ff20) Thanks [@gulshngill](https://github.com/gulshngill)! - Warn on logout when `NANSEN_API_KEY` remains active in the environment.
|
|
44
|
+
|
|
3
45
|
## 1.36.2
|
|
4
46
|
|
|
5
47
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -238,7 +238,8 @@ Any field may be absent or `null`, meaning unknown — never assume zero. A low-
|
|
|
238
238
|
|---------|-----|
|
|
239
239
|
| `command not found` | `npm install -g nansen-cli` |
|
|
240
240
|
| Global install reports an older version | `npm i -g nansen-cli@latest --registry=https://registry.npmjs.org/ --prefer-online`, then check `which -a nansen` for stale binaries |
|
|
241
|
-
| `UNAUTHORIZED` after login | `
|
|
241
|
+
| `UNAUTHORIZED` after login | `nansen auth status` shows which key is active and where it comes from; re-run `nansen login` or set `NANSEN_API_KEY` |
|
|
242
|
+
| Anything else misbehaving | `nansen doctor` checks your whole setup (auth, wallets, caches, connectivity) with a fix per finding |
|
|
242
243
|
| Empty perp _research_ results | Use `--symbol BTC`, not `--token`. Perps are Hyperliquid-only. |
|
|
243
244
|
| `perp` _trading_ prints the usage banner | Trading needs `--coin BTC` (`--symbol` also works); see the Perpetuals section. |
|
|
244
245
|
| `UNSUPPORTED_FILTER` on token holders | Remove `--smart-money` — not all tokens have that data. |
|
package/package.json
CHANGED
|
@@ -216,6 +216,8 @@ nansen perp order --coin BTC --side sell --size 0.001 --price 95000 --type marke
|
|
|
216
216
|
- `--type`: `limit` (default) or `market`. `--tif`: `Gtc` (default), `Ioc`, `Alo`.
|
|
217
217
|
- `--slippage`: decimal in `[0,1]` for market orders (default `0.03` = 3%).
|
|
218
218
|
|
|
219
|
+
On success the command prints the Hyperliquid order id (`oid`) and the fill (size @ avg price). A resting (unfilled) order also prints a ready-to-run `nansen perp cancel --coin <coin> --oid <oid>`. Attached take-profit/stop-loss legs are labelled and each print their own `oid`.
|
|
220
|
+
|
|
219
221
|
## Close / cancel
|
|
220
222
|
|
|
221
223
|
```bash
|
|
@@ -31,24 +31,26 @@ Use this skill when a user already has a nansen-cli wallet set up with the
|
|
|
31
31
|
## Detect current state
|
|
32
32
|
|
|
33
33
|
`wallet show` only displays addresses and does NOT load or check the password.
|
|
34
|
-
To detect the actual password situation, check
|
|
34
|
+
To detect the actual password situation, check the stored password source directly:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
# 1.
|
|
38
|
-
nansen
|
|
37
|
+
# 1. Where is the password stored? Offline, decrypts nothing, prints no secrets
|
|
38
|
+
nansen auth status --pretty # → x402.password.source: "env" | "keychain" | "file" | null
|
|
39
39
|
|
|
40
|
-
# 2.
|
|
41
|
-
|
|
42
|
-
ls -la ~/.nansen/wallets/.credentials 2>/dev/null && echo "FOUND: .credentials file (insecure)"
|
|
40
|
+
# 2. Full setup check — flags the insecure .credentials file with a fix
|
|
41
|
+
nansen doctor --offline
|
|
43
42
|
|
|
44
|
-
# 3.
|
|
45
|
-
nansen
|
|
43
|
+
# 3. Legacy pattern doctor does not cover: password written to ~/.nansen/.env
|
|
44
|
+
ls -la ~/.nansen/.env 2>/dev/null && echo "FOUND: ~/.nansen/.env (insecure)"
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
Interpret
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
- `
|
|
47
|
+
Interpret `x402.password.source`:
|
|
48
|
+
- `"file"` → password in `.credentials` file, needs migration (Path B)
|
|
49
|
+
- `"keychain"` → already secure, no migration needed
|
|
50
|
+
- `null` → password not persisted anywhere (Path C or D)
|
|
51
|
+
- `"env"` → `NANSEN_WALLET_PASSWORD` is set; check where it is being exported from (a `.env` file → Path A)
|
|
52
|
+
|
|
53
|
+
Do NOT use `nansen wallet export` to probe the password state — it prints private keys.
|
|
52
54
|
|
|
53
55
|
## Migration paths
|
|
54
56
|
|
package/src/api.js
CHANGED
|
@@ -1012,6 +1012,17 @@ export class NansenAPI {
|
|
|
1012
1012
|
});
|
|
1013
1013
|
}
|
|
1014
1014
|
|
|
1015
|
+
async addressFirstFunder(params = {}) {
|
|
1016
|
+
const { address } = params;
|
|
1017
|
+
// EVM addresses only; the funder is resolved across chains server-side, so
|
|
1018
|
+
// chain is fixed to 'all' and the endpoint forbids any extra fields.
|
|
1019
|
+
if (address) requireValidAddress(address, 'ethereum');
|
|
1020
|
+
return this.request('/api/v1/profiler/address/first-funder', {
|
|
1021
|
+
address,
|
|
1022
|
+
chain: 'all'
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1015
1026
|
async addressCounterparties(params = {}) {
|
|
1016
1027
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
1017
1028
|
if (address) requireValidAddress(address, chain);
|
package/src/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { buildResearchCommands, RESEARCH_HISTORICAL_SUBCOMMANDS } from './comman
|
|
|
15
15
|
import { resolveAddress, isEnsName } from './ens.js';
|
|
16
16
|
import fs from 'fs';
|
|
17
17
|
import { getUpdateNotification, getUpgradeNotice, scheduleUpdateCheck } from './update-check.js';
|
|
18
|
+
import { getAuthStatus, runDoctorChecks, runConnectivityChecks, formatDoctorReport } from './doctor.js';
|
|
18
19
|
import { refreshCostMapIfStale, getCostForEndpoint, creditsCharged } from './cost-cache.js';
|
|
19
20
|
import { creditWarning, noticeWarnings } from './response-meta.js';
|
|
20
21
|
import { trackCommandSucceeded, trackCommandFailed } from './telemetry.js';
|
|
@@ -22,7 +23,7 @@ import { createRequire } from 'module';
|
|
|
22
23
|
import * as readline from 'readline';
|
|
23
24
|
|
|
24
25
|
const require = createRequire(import.meta.url);
|
|
25
|
-
const { version: VERSION } = require('../package.json');
|
|
26
|
+
const { version: VERSION, engines: ENGINES } = require('../package.json');
|
|
26
27
|
|
|
27
28
|
// ============= Schema Definition =============
|
|
28
29
|
|
|
@@ -189,7 +190,7 @@ export function parseArgs(args) {
|
|
|
189
190
|
const key = arg.slice(2);
|
|
190
191
|
const next = args[i + 1];
|
|
191
192
|
|
|
192
|
-
if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich' || key === 'full' || key === 'human' || key === 'enabled' || key === 'disabled' || key === 'expert' || key === 'json') {
|
|
193
|
+
if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich' || key === 'full' || key === 'human' || key === 'enabled' || key === 'disabled' || key === 'expert' || key === 'json' || key === 'offline') {
|
|
193
194
|
result.flags[key] = true;
|
|
194
195
|
} else if (next && (!next.startsWith('-') || /^-\d/.test(next))) {
|
|
195
196
|
// Try to parse as JSON first (for objects/arrays/booleans),
|
|
@@ -729,8 +730,10 @@ COMMANDS:
|
|
|
729
730
|
alerts list, create, update, toggle, delete
|
|
730
731
|
web search, fetch
|
|
731
732
|
account Show API key status, plan, and remaining credits
|
|
733
|
+
auth status — offline auth status: key source, wallets (no network)
|
|
732
734
|
login Save API key (--api-key <key>, --human, or NANSEN_API_KEY env var)
|
|
733
735
|
logout Remove saved API key
|
|
736
|
+
doctor Diagnostics: auth, wallets, caches, connectivity (--offline --json)
|
|
734
737
|
schema JSON schema for all commands (use "nansen schema <cmd>" for one)
|
|
735
738
|
cache clear
|
|
736
739
|
changelog --since <version> to filter
|
|
@@ -771,9 +774,54 @@ Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
|
|
|
771
774
|
Docs: https://docs.nansen.ai
|
|
772
775
|
Skills: npx skills add nansen-ai/nansen-cli (agent-optimised docs per command group)
|
|
773
776
|
|
|
774
|
-
Telemetry: anonymous usage stats
|
|
777
|
+
Telemetry: anonymous usage stats (commands, timing, errors). Perp order/close additionally send the order side and Hyperliquid order id. Disable: DO_NOT_TRACK=1
|
|
775
778
|
`;
|
|
776
779
|
|
|
780
|
+
// Usage text for the `trade` command group. Shared by the trade handler and the
|
|
781
|
+
// --help path in runCLI, so `nansen trade`, `nansen trade <sub> --help`, and the
|
|
782
|
+
// deprecated top-level `quote`/`execute --help` all show the same usage.
|
|
783
|
+
export const TRADE_USAGE = `nansen trade — DEX trading commands
|
|
784
|
+
|
|
785
|
+
SUBCOMMANDS:
|
|
786
|
+
quote Get a swap quote (price, route, fees)
|
|
787
|
+
execute Sign and broadcast a quoted swap
|
|
788
|
+
bridge-status Check cross-chain bridge transaction status
|
|
789
|
+
limit-order Limit order management (Solana only)
|
|
790
|
+
|
|
791
|
+
USAGE:
|
|
792
|
+
nansen trade quote --chain <chain> --from <token> --to <token> --amount <units> [--wallet <name>]
|
|
793
|
+
nansen trade quote --chain <chain> --to-chain <chain> --from <token> --to <token> --amount <units>
|
|
794
|
+
nansen trade execute --quote <quoteId> [--wallet <name>]
|
|
795
|
+
nansen trade bridge-status --tx-hash <hash> --from-chain <chain> --to-chain <chain>
|
|
796
|
+
nansen trade limit-order <create|list|cancel|update> [options]
|
|
797
|
+
|
|
798
|
+
EXAMPLES:
|
|
799
|
+
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
800
|
+
nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
|
|
801
|
+
nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
|
|
802
|
+
nansen trade execute --quote 1708900000000-abc123
|
|
803
|
+
nansen trade bridge-status --tx-hash 0xabc... --from-chain base --to-chain solana
|
|
804
|
+
nansen trade limit-order create --from SOL --to USDC --amount 1.5 --trigger-mint SOL --trigger-condition below --trigger-price 80
|
|
805
|
+
nansen trade limit-order list
|
|
806
|
+
|
|
807
|
+
WALLET:
|
|
808
|
+
--wallet <name> Use a named wallet, or "walletconnect" / "wc" for WalletConnect.
|
|
809
|
+
Defaults to the default local wallet if omitted.
|
|
810
|
+
|
|
811
|
+
SYMBOLS:
|
|
812
|
+
Common tokens resolve automatically: SOL, ETH, USDC, USDT, WETH
|
|
813
|
+
Raw addresses are also accepted.
|
|
814
|
+
|
|
815
|
+
CROSS-CHAIN NOTES (when using --to-chain):
|
|
816
|
+
Supported combos:
|
|
817
|
+
native → native (ETH <-> SOL)
|
|
818
|
+
USDC → USDC (both directions)
|
|
819
|
+
USDC → native (USDC → ETH or SOL)
|
|
820
|
+
native → USDC (ETH/SOL → USDC)
|
|
821
|
+
non-native → non-native — not supported (use USDC as intermediate)
|
|
822
|
+
Bridge providers: Li.Fi or Relay (selected automatically based on best price)
|
|
823
|
+
Typical bridge time: 1-5 minutes`;
|
|
824
|
+
|
|
777
825
|
// Helper to prompt for input (exported for mocking)
|
|
778
826
|
export async function prompt(question, hidden = false) {
|
|
779
827
|
return new Promise((resolve) => {
|
|
@@ -832,7 +880,8 @@ export function buildCommands(deps = {}) {
|
|
|
832
880
|
saveConfigFn = saveConfig,
|
|
833
881
|
deleteConfigFn = deleteConfig,
|
|
834
882
|
getConfigFileFn = getConfigFile,
|
|
835
|
-
isTTY = process.stdin.isTTY
|
|
883
|
+
isTTY = process.stdin.isTTY,
|
|
884
|
+
env = process.env
|
|
836
885
|
} = deps;
|
|
837
886
|
|
|
838
887
|
const cmds = {
|
|
@@ -840,6 +889,31 @@ export function buildCommands(deps = {}) {
|
|
|
840
889
|
return apiInstance.getAccount();
|
|
841
890
|
},
|
|
842
891
|
|
|
892
|
+
'auth': async (args, _apiInstance, _flags, _options) => {
|
|
893
|
+
const subcommand = args[0] || 'status';
|
|
894
|
+
if (subcommand !== 'status') {
|
|
895
|
+
throw new NansenError(`Unknown auth subcommand: ${subcommand}. Available: status`, ErrorCode.UNKNOWN);
|
|
896
|
+
}
|
|
897
|
+
return getAuthStatus();
|
|
898
|
+
},
|
|
899
|
+
|
|
900
|
+
'doctor': async (_args, _apiInstance, flags, _options) => {
|
|
901
|
+
const checks = runDoctorChecks({ cliVersion: VERSION, engines: ENGINES });
|
|
902
|
+
if (!flags.offline) {
|
|
903
|
+
checks.push(...await runConnectivityChecks());
|
|
904
|
+
}
|
|
905
|
+
if (flags.json) {
|
|
906
|
+
return {
|
|
907
|
+
version: VERSION,
|
|
908
|
+
offline: Boolean(flags.offline),
|
|
909
|
+
checks,
|
|
910
|
+
errors: checks.filter(c => c.status === 'error').length,
|
|
911
|
+
warnings: checks.filter(c => c.status === 'warn').length,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
log(formatDoctorReport(checks, { cliVersion: VERSION, offline: Boolean(flags.offline) }));
|
|
915
|
+
},
|
|
916
|
+
|
|
843
917
|
'web': async (args, apiInstance, flags, options) => {
|
|
844
918
|
const subcommand = args[0] || 'help';
|
|
845
919
|
const subArgs = args.slice(1);
|
|
@@ -1004,6 +1078,9 @@ export function buildCommands(deps = {}) {
|
|
|
1004
1078
|
} else {
|
|
1005
1079
|
log('No saved credentials found');
|
|
1006
1080
|
}
|
|
1081
|
+
if (env.NANSEN_API_KEY) {
|
|
1082
|
+
log('Warning: NANSEN_API_KEY remains active. Run: unset NANSEN_API_KEY');
|
|
1083
|
+
}
|
|
1007
1084
|
},
|
|
1008
1085
|
|
|
1009
1086
|
'help': async (_args, _apiInstance, _flags, _options) => {
|
|
@@ -1147,7 +1224,8 @@ export function buildCommands(deps = {}) {
|
|
|
1147
1224
|
let ensName;
|
|
1148
1225
|
if (address && isEnsName(address)) {
|
|
1149
1226
|
try {
|
|
1150
|
-
const
|
|
1227
|
+
const ensChain = subcommand === 'first-funder' ? 'ethereum' : chain;
|
|
1228
|
+
const resolved = await resolveAddress(address, ensChain);
|
|
1151
1229
|
address = resolved.address;
|
|
1152
1230
|
ensName = resolved.ensName;
|
|
1153
1231
|
} catch (err) {
|
|
@@ -1173,6 +1251,7 @@ export function buildCommands(deps = {}) {
|
|
|
1173
1251
|
'search': () => apiInstance.entitySearch({ query: options.query }),
|
|
1174
1252
|
'historical-balances': () => apiInstance.addressHistoricalBalances({ address, chain, filters, orderBy, pagination, days }),
|
|
1175
1253
|
'related-wallets': () => apiInstance.addressRelatedWallets({ address, chain, orderBy, pagination }),
|
|
1254
|
+
'first-funder': () => apiInstance.addressFirstFunder({ address }),
|
|
1176
1255
|
'counterparties': () => apiInstance.addressCounterparties({ address, chain, filters, orderBy, pagination, days }),
|
|
1177
1256
|
'pnl-summary': () => apiInstance.addressPnlSummary({ address, chain, orderBy, pagination, days }),
|
|
1178
1257
|
'perp-positions': () => apiInstance.addressPerpPositions({ address, filters, orderBy, pagination }),
|
|
@@ -1219,7 +1298,7 @@ export function buildCommands(deps = {}) {
|
|
|
1219
1298
|
return compareWallets(apiInstance, { addresses: addrs, chain, days });
|
|
1220
1299
|
},
|
|
1221
1300
|
'help': () => ({
|
|
1222
|
-
commands: ['balance', 'labels', 'transactions', 'pnl', 'search', 'historical-balances', 'related-wallets', 'counterparties', 'pnl-summary', 'perp-positions', 'perp-trades', 'dex-trades', 'batch', 'trace', 'compare'],
|
|
1301
|
+
commands: ['balance', 'labels', 'transactions', 'pnl', 'search', 'historical-balances', 'related-wallets', 'first-funder', 'counterparties', 'pnl-summary', 'perp-positions', 'perp-trades', 'dex-trades', 'batch', 'trace', 'compare'],
|
|
1223
1302
|
description: 'Wallet profiling endpoints',
|
|
1224
1303
|
example: 'nansen research profiler compare --addresses "0xABC...,0xDEF..." --chain ethereum'
|
|
1225
1304
|
})
|
|
@@ -1558,47 +1637,7 @@ export function buildCommands(deps = {}) {
|
|
|
1558
1637
|
cmds['trade'] = async (args, apiInstance, flags, options) => {
|
|
1559
1638
|
const sub = args[0];
|
|
1560
1639
|
if (!sub || sub === 'help') {
|
|
1561
|
-
log(
|
|
1562
|
-
|
|
1563
|
-
SUBCOMMANDS:
|
|
1564
|
-
quote Get a swap quote (price, route, fees)
|
|
1565
|
-
execute Sign and broadcast a quoted swap
|
|
1566
|
-
bridge-status Check cross-chain bridge transaction status
|
|
1567
|
-
limit-order Limit order management (Solana only)
|
|
1568
|
-
|
|
1569
|
-
USAGE:
|
|
1570
|
-
nansen trade quote --chain <chain> --from <token> --to <token> --amount <units> [--wallet <name>]
|
|
1571
|
-
nansen trade quote --chain <chain> --to-chain <chain> --from <token> --to <token> --amount <units>
|
|
1572
|
-
nansen trade execute --quote <quoteId> [--wallet <name>]
|
|
1573
|
-
nansen trade bridge-status --tx-hash <hash> --from-chain <chain> --to-chain <chain>
|
|
1574
|
-
nansen trade limit-order <create|list|cancel|update> [options]
|
|
1575
|
-
|
|
1576
|
-
EXAMPLES:
|
|
1577
|
-
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
1578
|
-
nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
|
|
1579
|
-
nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
|
|
1580
|
-
nansen trade execute --quote 1708900000000-abc123
|
|
1581
|
-
nansen trade bridge-status --tx-hash 0xabc... --from-chain base --to-chain solana
|
|
1582
|
-
nansen trade limit-order create --from SOL --to USDC --amount 1.5 --trigger-mint SOL --trigger-condition below --trigger-price 80
|
|
1583
|
-
nansen trade limit-order list
|
|
1584
|
-
|
|
1585
|
-
WALLET:
|
|
1586
|
-
--wallet <name> Use a named wallet, or "walletconnect" / "wc" for WalletConnect.
|
|
1587
|
-
Defaults to the default local wallet if omitted.
|
|
1588
|
-
|
|
1589
|
-
SYMBOLS:
|
|
1590
|
-
Common tokens resolve automatically: SOL, ETH, USDC, USDT, WETH
|
|
1591
|
-
Raw addresses are also accepted.
|
|
1592
|
-
|
|
1593
|
-
CROSS-CHAIN NOTES (when using --to-chain):
|
|
1594
|
-
Supported combos:
|
|
1595
|
-
native → native (ETH <-> SOL)
|
|
1596
|
-
USDC → USDC (both directions)
|
|
1597
|
-
USDC → native (USDC → ETH or SOL)
|
|
1598
|
-
native → USDC (ETH/SOL → USDC)
|
|
1599
|
-
non-native → non-native — not supported (use USDC as intermediate)
|
|
1600
|
-
Bridge providers: Li.Fi or Relay (selected automatically based on best price)
|
|
1601
|
-
Typical bridge time: 1-5 minutes`);
|
|
1640
|
+
log(TRADE_USAGE);
|
|
1602
1641
|
return;
|
|
1603
1642
|
}
|
|
1604
1643
|
if (sub === 'limit-order') {
|
|
@@ -1804,10 +1843,17 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1804
1843
|
const stream = flags.stream || flags.s;
|
|
1805
1844
|
const csv = options.format === 'csv';
|
|
1806
1845
|
|
|
1846
|
+
// `auth` and `doctor --offline` promise zero network activity — that
|
|
1847
|
+
// contract covers the background update-check fetch and telemetry too,
|
|
1848
|
+
// not just the command's own requests.
|
|
1849
|
+
const isOfflineCommand = command === 'auth' || (command === 'doctor' && flags.offline);
|
|
1850
|
+
const trackSucceeded = isOfflineCommand ? async () => {} : trackCommandSucceeded;
|
|
1851
|
+
const trackFailed = isOfflineCommand ? async () => {} : trackCommandFailed;
|
|
1852
|
+
|
|
1807
1853
|
// Update check (read cached result + schedule background refresh)
|
|
1808
1854
|
const updateNotification = getUpdateNotification(VERSION);
|
|
1809
1855
|
const upgradeNotice = getUpgradeNotice(VERSION);
|
|
1810
|
-
scheduleUpdateCheck();
|
|
1856
|
+
if (!isOfflineCommand) scheduleUpdateCheck();
|
|
1811
1857
|
const notify = () => {
|
|
1812
1858
|
if (upgradeNotice) errorOutput(upgradeNotice);
|
|
1813
1859
|
if (updateNotification) errorOutput(updateNotification);
|
|
@@ -1828,7 +1874,9 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1828
1874
|
}
|
|
1829
1875
|
|
|
1830
1876
|
if (command === 'help' || flags.help || flags.h) {
|
|
1831
|
-
|
|
1877
|
+
// Help for an offline command still owes the zero-network contract: the
|
|
1878
|
+
// cost-map refresh fetches the OpenAPI spec and writes ~/.nansen/cost-map.json.
|
|
1879
|
+
if (!isOfflineCommand) await refreshCostMapIfStale();
|
|
1832
1880
|
// Check for subcommand-specific help: nansen <command> <subcommand> --help
|
|
1833
1881
|
if (flags.help || flags.h) {
|
|
1834
1882
|
// Handle 'research <category> <sub> --help' (3-level)
|
|
@@ -1922,7 +1970,16 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1922
1970
|
return { type: 'command-help', command };
|
|
1923
1971
|
}
|
|
1924
1972
|
}
|
|
1925
|
-
//
|
|
1973
|
+
// The trade group (and the deprecated top-level quote/execute aliases) use
|
|
1974
|
+
// handler-based usage rather than schema help. Show it and exit 0, instead of
|
|
1975
|
+
// falling through to command execution, which would error on missing required
|
|
1976
|
+
// args and exit 1.
|
|
1977
|
+
if (command === 'trade' || DEPRECATED_TO_TRADE.has(command)) {
|
|
1978
|
+
output(deprecationNote(command) + TRADE_USAGE);
|
|
1979
|
+
notify();
|
|
1980
|
+
return { type: 'command-help', command };
|
|
1981
|
+
}
|
|
1982
|
+
// 'help' and unknown commands: full banner + command list
|
|
1926
1983
|
if (command === 'help' || !commands[command]) {
|
|
1927
1984
|
output(BANNER + HELP);
|
|
1928
1985
|
notify();
|
|
@@ -1939,13 +1996,20 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1939
1996
|
const chain = options.chain || null;
|
|
1940
1997
|
|
|
1941
1998
|
if (!commands[command]) {
|
|
1999
|
+
// A command token containing whitespace almost always means a multi-word
|
|
2000
|
+
// invocation was passed as a single argument — e.g. `nansen "trade --help"`,
|
|
2001
|
+
// or an unquoted shell variable under zsh (which, unlike bash, does not
|
|
2002
|
+
// word-split `$var`). Point the user straight at the cause instead of a bare
|
|
2003
|
+
// "Unknown command" that reads like a spurious failure.
|
|
1942
2004
|
const errorData = {
|
|
1943
|
-
error:
|
|
2005
|
+
error: /\s/.test(command)
|
|
2006
|
+
? `Unknown command: "${command}". This looks like multiple words passed as one argument — check your shell quoting (use \`nansen trade --help\`, not \`nansen "trade --help"\`).`
|
|
2007
|
+
: `Unknown command: ${command}`,
|
|
1944
2008
|
available: Object.keys(commands)
|
|
1945
2009
|
};
|
|
1946
2010
|
const formatted = formatOutput(errorData, { pretty, table });
|
|
1947
2011
|
output(formatted.text);
|
|
1948
|
-
await
|
|
2012
|
+
await trackFailed({ command: fullCommand, duration_ms: Date.now() - startTime, error_code: 'UNKNOWN_COMMAND', flags: usedFlags, chain });
|
|
1949
2013
|
exit(1);
|
|
1950
2014
|
return { type: 'error', data: errorData };
|
|
1951
2015
|
}
|
|
@@ -1998,7 +2062,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1998
2062
|
|
|
1999
2063
|
// Commands that handle their own output return undefined
|
|
2000
2064
|
if (result === undefined) {
|
|
2001
|
-
await
|
|
2065
|
+
await trackSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
2002
2066
|
return { type: 'no-output', command };
|
|
2003
2067
|
}
|
|
2004
2068
|
|
|
@@ -2006,7 +2070,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
2006
2070
|
if (command === 'schema') {
|
|
2007
2071
|
const formatted = formatOutput(result, { pretty, table: false });
|
|
2008
2072
|
output(formatted.text);
|
|
2009
|
-
await
|
|
2073
|
+
await trackSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
2010
2074
|
return { type: 'schema', data: result };
|
|
2011
2075
|
}
|
|
2012
2076
|
|
|
@@ -2019,7 +2083,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
2019
2083
|
// Alerts list with --table uses custom table format
|
|
2020
2084
|
if (command === 'alerts' && subcommand === 'list' && table) {
|
|
2021
2085
|
output(formatAlertsTable(result));
|
|
2022
|
-
await
|
|
2086
|
+
await trackSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
2023
2087
|
return { type: 'success', data: result };
|
|
2024
2088
|
}
|
|
2025
2089
|
|
|
@@ -2030,14 +2094,14 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
2030
2094
|
if (streamOutput) {
|
|
2031
2095
|
output(streamOutput);
|
|
2032
2096
|
}
|
|
2033
|
-
await
|
|
2097
|
+
await trackSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
2034
2098
|
return { type: 'stream', data: result };
|
|
2035
2099
|
}
|
|
2036
2100
|
|
|
2037
2101
|
const successData = { success: true, data: result };
|
|
2038
2102
|
const formatted = formatOutput(successData, { pretty, table, csv });
|
|
2039
2103
|
output(formatted.text);
|
|
2040
|
-
await
|
|
2104
|
+
await trackSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
2041
2105
|
return { type: csv ? 'csv' : 'success', data: result };
|
|
2042
2106
|
} catch (error) {
|
|
2043
2107
|
// Unified error envelope across all command families (perp/bridge/trade):
|
|
@@ -2052,7 +2116,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
2052
2116
|
const formatted = formatOutput(errorData, { pretty, table, csv });
|
|
2053
2117
|
output(formatted.text);
|
|
2054
2118
|
}
|
|
2055
|
-
await
|
|
2119
|
+
await trackFailed({
|
|
2056
2120
|
command: fullCommand,
|
|
2057
2121
|
duration_ms: Date.now() - startTime,
|
|
2058
2122
|
error_code: error.code || 'UNKNOWN',
|
package/src/cost-cache.js
CHANGED
|
@@ -14,6 +14,24 @@ const CACHE_FILE = path.join(CONFIG_DIR, 'cost-map.json');
|
|
|
14
14
|
const STALE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
15
15
|
const OPENAPI_URL = 'https://api.nansen.ai/openapi.json';
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Write `data` to `file` atomically: write to a unique temp file in the same
|
|
19
|
+
* directory, then rename over the target. rename(2) is atomic on POSIX, so a
|
|
20
|
+
* concurrent reader always sees either the old file or the fully-written new
|
|
21
|
+
* one — never a truncated/empty file. The temp name includes the pid so
|
|
22
|
+
* concurrent writers don't clobber each other's temp files.
|
|
23
|
+
*/
|
|
24
|
+
function writeAtomic(file, data) {
|
|
25
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
26
|
+
try {
|
|
27
|
+
fs.writeFileSync(tmp, data);
|
|
28
|
+
fs.renameSync(tmp, file);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
try { fs.unlinkSync(tmp); } catch { /* temp file may not exist */ }
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
/**
|
|
18
36
|
* Returns { free, pro } credit cost for the given API path, or null if unavailable.
|
|
19
37
|
*/
|
|
@@ -75,7 +93,7 @@ export async function refreshCostMapIfStale() {
|
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { mode: 0o700, recursive: true });
|
|
78
|
-
|
|
96
|
+
writeAtomic(CACHE_FILE, JSON.stringify({ costs, fetchedAt: Date.now() }));
|
|
79
97
|
} catch {
|
|
80
98
|
// silent — network failure, parse error, write error
|
|
81
99
|
}
|