nansen-cli 1.9.2 → 1.10.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 +26 -0
- package/README.md +41 -281
- package/package.json +7 -2
- package/src/api.js +5 -6
- package/src/cli.js +153 -220
- package/src/trading.js +22 -4
- package/src/transfer.js +10 -14
- package/src/wallet.js +2 -2
- package/src/walletconnect-exec.js +1 -1
- package/src/x402-svm.js +3 -3
- package/src/x402.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#125](https://github.com/nansen-ai/nansen-cli/pull/125) [`5a5a80a`](https://github.com/nansen-ai/nansen-cli/commit/5a5a80af9c2c5b93efcc707925b004f077e13c36) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Add modular skills/ directory with 7 agent-optimised SKILL.md files (nansen-token, nansen-smart-money, nansen-profiler, nansen-trade, nansen-wallet, nansen-perp, nansen-search) following the linear-cli pattern. Each skill has scoped frontmatter, agent routing descriptions, bash examples, and exit codes. Add skills nudge to `nansen --help` output.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#122](https://github.com/nansen-ai/nansen-cli/pull/122) [`9a1ada8`](https://github.com/nansen-ai/nansen-cli/commit/9a1ada8543cd6fdbcc10d2d5004fe2e2e1a88928) Thanks [@TimNooren](https://github.com/TimNooren)! - `nansen research <unknown>` and `nansen trade <unknown>` now exit with code 1 and return `{"success":false,...}` instead of silently exiting 0.
|
|
12
|
+
|
|
13
|
+
- [#138](https://github.com/nansen-ai/nansen-cli/pull/138) [`c61881f`](https://github.com/nansen-ai/nansen-cli/commit/c61881f5455b9fff7fb97841652a72af58ab8e0b) Thanks [@TimNooren](https://github.com/TimNooren)! - Fix `nansen login --help` to show usage instead of erroring. Previously, `--help` was silently ignored on TTY (showing the interactive prompt) and caused an error on non-TTY. Also fixes the post-login suggested command to use the non-deprecated `nansen research token screener` path.
|
|
14
|
+
|
|
15
|
+
- [#129](https://github.com/nansen-ai/nansen-cli/pull/129) [`eeabf89`](https://github.com/nansen-ai/nansen-cli/commit/eeabf8988dafc7fb1964fd2eef629f03c6a4420a) Thanks [@araa47](https://github.com/araa47)! - Fix `token ohlcv` sending unsupported pagination/limit params that caused 422 errors
|
|
16
|
+
|
|
17
|
+
- [#139](https://github.com/nansen-ai/nansen-cli/pull/139) [`e86dc68`](https://github.com/nansen-ai/nansen-cli/commit/e86dc6869f524d3dc59da4c7c04cb1ace1b7246b) Thanks [@TimNooren](https://github.com/TimNooren)! - Fix API key prompt masking: each keystroke was showing the real character followed by `*` (e.g. `f*o*o*`) because the readline interface was active alongside raw mode, causing double output. Moving readline creation into the non-hidden branch eliminates the double-echo and also fixes backspace incorrectly clearing the prompt label.
|
|
18
|
+
|
|
19
|
+
- [#129](https://github.com/nansen-ai/nansen-cli/pull/129) [`eeabf89`](https://github.com/nansen-ai/nansen-cli/commit/eeabf8988dafc7fb1964fd2eef629f03c6a4420a) Thanks [@araa47](https://github.com/araa47)! - Fix `trade quote` crash when no wallet exists — now shows actionable error instead of uncaught exception
|
|
20
|
+
|
|
21
|
+
- [#126](https://github.com/nansen-ai/nansen-cli/pull/126) [`f3b87e7`](https://github.com/nansen-ai/nansen-cli/commit/f3b87e7491d03d052d5d72fcc991de0c33caf51f) Thanks [@araa47](https://github.com/araa47)! - Remove root SKILL.md so `npx skills add nansen-ai/nansen-cli` correctly discovers all 7 skills in `skills/` instead of treating the repo as a single skill.
|
|
22
|
+
|
|
23
|
+
## 1.9.3
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- [#118](https://github.com/nansen-ai/nansen-cli/pull/118) [`0bd4c3c`](https://github.com/nansen-ai/nansen-cli/commit/0bd4c3c1946e575e2c2db5e02d17f266e79752a4) Thanks [@TimNooren](https://github.com/TimNooren)! - Show warning when trade quote price impact exceeds 5%, and show pin command to avoid fallback to worse quotes
|
|
28
|
+
|
|
3
29
|
## 1.9.2
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -2,226 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/nansen-cli)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/nansen-ai/nansen-cli/actions/workflows/ci.yml)
|
|
6
5
|
|
|
7
|
-
> **Built by agents, for agents.**
|
|
8
|
-
|
|
9
|
-
Command-line interface for the [Nansen API](https://docs.nansen.ai) with structured JSON output, designed for AI agents and automation.
|
|
6
|
+
> **Built by agents, for agents.** Command-line interface for the [Nansen API](https://docs.nansen.ai) with structured JSON output.
|
|
10
7
|
|
|
11
8
|
## Installation
|
|
12
9
|
|
|
13
10
|
```bash
|
|
14
|
-
# Install globally via npm
|
|
15
11
|
npm install -g nansen-cli
|
|
16
|
-
|
|
17
|
-
# Or run directly with npx
|
|
18
|
-
npx nansen-cli help
|
|
19
|
-
|
|
20
|
-
# Or clone and install locally
|
|
21
|
-
git clone https://github.com/nansen-ai/nansen-cli.git
|
|
22
|
-
cd nansen-cli
|
|
23
|
-
npm install
|
|
24
|
-
npm link
|
|
12
|
+
npx skills add nansen-ai/nansen-cli # load agent skill files
|
|
25
13
|
```
|
|
26
14
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
### For AI Agents (Recommended)
|
|
30
|
-
|
|
31
|
-
Use the [AI Agent Setup](https://app.nansen.ai/auth/agent-setup) flow:
|
|
32
|
-
|
|
33
|
-
1. Your agent will ask you to visit: **[app.nansen.ai/auth/agent-setup](https://app.nansen.ai/auth/agent-setup)**
|
|
34
|
-
2. Sign in with your Nansen account
|
|
35
|
-
3. Copy the message shown
|
|
36
|
-
4. Paste it back to your agent
|
|
37
|
-
|
|
38
|
-
Your agent saves the key and handles everything else automatically.
|
|
39
|
-
|
|
40
|
-
### Manual Setup
|
|
41
|
-
|
|
42
|
-
**Option 1: Interactive login**
|
|
43
|
-
```bash
|
|
44
|
-
nansen login
|
|
45
|
-
# Enter your API key when prompted
|
|
46
|
-
# ✓ Saved to ~/.nansen/config.json
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**Option 2: Environment variable (best for agents)**
|
|
50
|
-
```bash
|
|
51
|
-
export NANSEN_API_KEY=your-api-key
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
**Option 3: Direct config file**
|
|
55
|
-
```bash
|
|
56
|
-
mkdir -p ~/.nansen && echo '{"apiKey":"<key>","baseUrl":"https://api.nansen.ai"}' > ~/.nansen/config.json && chmod 600 ~/.nansen/config.json
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Get your API key at [app.nansen.ai/api](https://app.nansen.ai/api).
|
|
60
|
-
|
|
61
|
-
### Auth Priority
|
|
62
|
-
|
|
63
|
-
1. `NANSEN_API_KEY` env var (highest)
|
|
64
|
-
2. `~/.nansen/config.json` file
|
|
65
|
-
3. Interactive prompt
|
|
66
|
-
|
|
67
|
-
### Verify It Works
|
|
15
|
+
## Auth
|
|
68
16
|
|
|
69
17
|
```bash
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# Verify API access:
|
|
74
|
-
nansen research token screener --chain solana --limit 1
|
|
18
|
+
nansen login # interactive — saves to ~/.nansen/config.json
|
|
19
|
+
export NANSEN_API_KEY=... # or env var (highest priority)
|
|
75
20
|
```
|
|
76
21
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
# Get trending tokens on Solana
|
|
81
|
-
nansen research token screener --chain solana --timeframe 24h --pretty
|
|
82
|
-
|
|
83
|
-
# Check Smart Money activity
|
|
84
|
-
nansen research smart-money netflow --chain solana --pretty
|
|
85
|
-
|
|
86
|
-
# Profile a wallet
|
|
87
|
-
nansen research profiler balance --address 0x28c6c06298d514db089934071355e5743bf21d60 --chain ethereum --pretty
|
|
88
|
-
|
|
89
|
-
# Search for tokens/entities
|
|
90
|
-
nansen research search "Vitalik Buterin" --pretty
|
|
91
|
-
```
|
|
22
|
+
Get your API key at [app.nansen.ai/api](https://app.nansen.ai/api). AI agents can use the [Agent Setup](https://app.nansen.ai/auth/agent-setup) flow instead.
|
|
92
23
|
|
|
93
24
|
## Commands
|
|
94
25
|
|
|
95
|
-
All analytics live under `nansen research`, trading under `nansen trade`, and wallet management under `nansen wallet`.
|
|
96
|
-
|
|
97
|
-
### `research` - Research & Analytics
|
|
98
|
-
|
|
99
26
|
```
|
|
100
27
|
nansen research <category> <subcommand> [options]
|
|
28
|
+
nansen trade <subcommand> [options]
|
|
29
|
+
nansen wallet <subcommand> [options]
|
|
30
|
+
nansen schema [command] [--pretty] # full command reference (no API key needed)
|
|
101
31
|
```
|
|
102
32
|
|
|
103
|
-
**
|
|
104
|
-
|
|
105
|
-
#### `research smart-money` - Smart Money Analytics
|
|
106
|
-
|
|
107
|
-
Track trading and holding activity of sophisticated market participants.
|
|
33
|
+
**Research categories:** `smart-money` (`sm`), `token` (`tgm`), `profiler` (`prof`), `portfolio` (`port`), `search`, `perp`, `points`
|
|
108
34
|
|
|
109
|
-
|
|
110
|
-
|------------|-------------|
|
|
111
|
-
| `netflow` | Net capital flows (inflows vs outflows) |
|
|
112
|
-
| `dex-trades` | Real-time DEX trading activity |
|
|
113
|
-
| `perp-trades` | Perpetual trading on Hyperliquid |
|
|
114
|
-
| `holdings` | Aggregated token balances |
|
|
115
|
-
| `dcas` | DCA strategies on Jupiter |
|
|
116
|
-
| `historical-holdings` | Historical holdings over time |
|
|
35
|
+
**Trade:** `quote`, `execute` — DEX swaps via LiFi/Jupiter.
|
|
117
36
|
|
|
118
|
-
**
|
|
37
|
+
**Wallet:** `create`, `list`, `show`, `export`, `default`, `delete`, `send` — local keypairs (EVM + Solana).
|
|
119
38
|
|
|
120
|
-
|
|
39
|
+
Run `nansen schema --pretty` for the full subcommand and field reference.
|
|
121
40
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
nansen research profiler balance --address vitalik.eth
|
|
126
|
-
nansen research profiler labels --address nansen.eth --chain ethereum
|
|
127
|
-
nansen research profiler transactions --address vitalik.eth --table
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
ENS names are automatically resolved to `0x` addresses via public APIs (with onchain RPC fallback). Works on all EVM chains. The resolved name and address are included as `_ens` metadata in JSON output.
|
|
131
|
-
|
|
132
|
-
| Subcommand | Description |
|
|
133
|
-
|------------|-------------|
|
|
134
|
-
| `balance` | Current token holdings |
|
|
135
|
-
| `labels` | Behavioral and entity labels |
|
|
136
|
-
| `transactions` | Transaction history |
|
|
137
|
-
| `pnl` | PnL and trade performance |
|
|
138
|
-
| `search` | Search for entities by name |
|
|
139
|
-
| `historical-balances` | Historical balances over time |
|
|
140
|
-
| `related-wallets` | Find wallets related to an address |
|
|
141
|
-
| `counterparties` | Top counterparties by volume |
|
|
142
|
-
| `pnl-summary` | Summarized PnL metrics |
|
|
143
|
-
| `perp-positions` | Current perpetual positions |
|
|
144
|
-
| `perp-trades` | Perpetual trading history |
|
|
145
|
-
|
|
146
|
-
#### `research token` - Token God Mode
|
|
147
|
-
|
|
148
|
-
| Subcommand | Description |
|
|
149
|
-
|------------|-------------|
|
|
150
|
-
| `screener` | Discover and filter tokens |
|
|
151
|
-
| `holders` | Token holder analysis |
|
|
152
|
-
| `flows` | Token flow metrics |
|
|
153
|
-
| `dex-trades` | DEX trading activity |
|
|
154
|
-
| `pnl` | PnL leaderboard |
|
|
155
|
-
| `who-bought-sold` | Recent buyers and sellers |
|
|
156
|
-
| `flow-intelligence` | Detailed flow intelligence by label |
|
|
157
|
-
| `transfers` | Token transfer history |
|
|
158
|
-
| `jup-dca` | Jupiter DCA orders for token |
|
|
159
|
-
| `ohlcv` | OHLCV candle data for a token |
|
|
160
|
-
| `perp-trades` | Perp trades by token symbol |
|
|
161
|
-
| `perp-positions` | Open perp positions by token symbol |
|
|
162
|
-
| `perp-pnl-leaderboard` | Perp PnL leaderboard by token |
|
|
163
|
-
|
|
164
|
-
#### `research search` / `research perp` / `research portfolio` / `research points`
|
|
165
|
-
|
|
166
|
-
See `nansen research help` or `nansen schema --pretty` for full details.
|
|
167
|
-
|
|
168
|
-
### `trade` - DEX Trading
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
# Get a swap quote
|
|
172
|
-
nansen trade quote --from USDC --to SOL --amount 10 --chain solana
|
|
173
|
-
|
|
174
|
-
# Execute the swap
|
|
175
|
-
nansen trade execute --from USDC --to SOL --amount 10 --chain solana
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### `wallet` - Local Wallet Management
|
|
179
|
-
|
|
180
|
-
| Subcommand | Description |
|
|
181
|
-
|------------|-------------|
|
|
182
|
-
| `create` | Create a new wallet (EVM + Solana keypair) |
|
|
183
|
-
| `list` | List all wallets |
|
|
184
|
-
| `show` | Show wallet addresses |
|
|
185
|
-
| `export` | Export private keys |
|
|
186
|
-
| `default` | Set default wallet |
|
|
187
|
-
| `delete` | Delete a wallet |
|
|
188
|
-
| `send` | Send tokens (native or ERC-20/SPL) |
|
|
189
|
-
|
|
190
|
-
Wallets are passwordless by default (keys stored like SSH keys). Set `NANSEN_WALLET_PASSWORD` env var for encryption at rest.
|
|
191
|
-
|
|
192
|
-
### `schema` - Schema Discovery
|
|
193
|
-
|
|
194
|
-
No API key required. Machine-readable command reference for agent introspection.
|
|
195
|
-
|
|
196
|
-
```bash
|
|
197
|
-
nansen schema --pretty # All commands
|
|
198
|
-
nansen schema research --pretty # Research commands
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### Deprecated Flat Commands
|
|
202
|
-
|
|
203
|
-
The old flat commands (`nansen smart-money`, `nansen token`, `nansen profiler`, `nansen search`, `nansen perp`, `nansen portfolio`, `nansen points`, `nansen quote`, `nansen execute`) still work but print a deprecation warning to stderr. Use the new `research` and `trade` namespaces instead.
|
|
204
|
-
|
|
205
|
-
## Options
|
|
41
|
+
## Key Options
|
|
206
42
|
|
|
207
43
|
| Option | Description |
|
|
208
44
|
|--------|-------------|
|
|
209
|
-
| `--pretty` | Format JSON output for readability |
|
|
210
|
-
| `--table` | Format output as human-readable table |
|
|
211
|
-
| `--fields <list>` | Comma-separated fields to include (reduces response size) |
|
|
212
|
-
| `--stream` | Output as NDJSON for incremental processing |
|
|
213
|
-
| `--cache` / `--no-cache` | Enable/disable response caching |
|
|
214
|
-
| `--cache-ttl <s>` | Cache TTL in seconds (default: 300) |
|
|
215
45
|
| `--chain <chain>` | Blockchain to query |
|
|
216
|
-
| `--
|
|
217
|
-
| `--
|
|
218
|
-
| `--
|
|
219
|
-
| `--sort <field:dir>` | Sort results
|
|
220
|
-
| `--
|
|
221
|
-
| `--
|
|
46
|
+
| `--limit <n>` | Result count |
|
|
47
|
+
| `--timeframe <tf>` | Time window: `5m` `1h` `6h` `24h` `7d` `30d` |
|
|
48
|
+
| `--fields <list>` | Comma-separated fields (reduces response size) |
|
|
49
|
+
| `--sort <field:dir>` | Sort results, e.g. `--sort value_usd:desc` |
|
|
50
|
+
| `--pretty` | Human-readable JSON |
|
|
51
|
+
| `--table` | Table format |
|
|
52
|
+
| `--stream` | NDJSON output for large results |
|
|
222
53
|
| `--labels <label>` | Smart Money label filter |
|
|
223
|
-
| `--smart-money` | Filter for Smart Money only |
|
|
224
|
-
| `--timeframe <tf>` | Time window (5m, 10m, 1h, 6h, 24h, 7d, 30d) |
|
|
54
|
+
| `--smart-money` | Filter for Smart Money addresses only |
|
|
225
55
|
|
|
226
56
|
## Supported Chains
|
|
227
57
|
|
|
@@ -229,121 +59,51 @@ The old flat commands (`nansen smart-money`, `nansen token`, `nansen profiler`,
|
|
|
229
59
|
|
|
230
60
|
> Run `nansen schema` to get the current chain list (source of truth).
|
|
231
61
|
|
|
232
|
-
## Agent
|
|
233
|
-
|
|
234
|
-
### Reduce Token Burn with `--fields`
|
|
235
|
-
|
|
236
|
-
```bash
|
|
237
|
-
# ❌ Returns everything (huge JSON, wastes agent context)
|
|
238
|
-
nansen research smart-money netflow --chain solana
|
|
239
|
-
|
|
240
|
-
# ✅ Only what you need
|
|
241
|
-
nansen research smart-money netflow --chain solana --fields token_symbol,net_flow_usd,chain --limit 10
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
### Use `--stream` for Large Results
|
|
62
|
+
## Agent Tips
|
|
245
63
|
|
|
64
|
+
**Reduce token burn with `--fields`:**
|
|
246
65
|
```bash
|
|
247
|
-
|
|
248
|
-
nansen research token dex-trades --chain solana --limit 100 --stream
|
|
66
|
+
nansen research smart-money netflow --chain solana --fields token_symbol,net_flow_usd --limit 10
|
|
249
67
|
```
|
|
250
68
|
|
|
251
|
-
|
|
69
|
+
**Use `--stream` for large results** — outputs NDJSON instead of buffering a giant array.
|
|
252
70
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
1. CLI detects 402 response with payment requirements
|
|
256
|
-
2. Signs a USDC payment ($0.05/call) using your wallet
|
|
257
|
-
3. Retries the request with the payment signature
|
|
258
|
-
4. Falls back from EVM to Solana if first network has insufficient funds
|
|
259
|
-
|
|
260
|
-
```bash
|
|
261
|
-
# Fund your wallet, then API calls auto-pay
|
|
262
|
-
nansen wallet create
|
|
263
|
-
# Send USDC to the displayed address
|
|
264
|
-
nansen research smart-money netflow --chain solana # auto-pays if no API key
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
## Pagination
|
|
268
|
-
|
|
269
|
-
Use `--limit N` to control result count. The CLI always fetches page 1 (there is no `--page` flag).
|
|
270
|
-
|
|
271
|
-
**Detecting the last page:** If results returned < your `--limit`, you've reached the end.
|
|
71
|
+
**ENS names** work anywhere `--address` is accepted: `--address vitalik.eth`
|
|
272
72
|
|
|
273
73
|
## Output Format
|
|
274
74
|
|
|
275
|
-
### Response envelope
|
|
276
|
-
|
|
277
75
|
```json
|
|
278
|
-
|
|
279
|
-
{ "success":
|
|
280
|
-
|
|
281
|
-
// Error
|
|
282
|
-
{ "success": false, "error": "message", "code": "ERROR_CODE", "status": 401, "details": {...} }
|
|
76
|
+
{ "success": true, "data": <api_response> }
|
|
77
|
+
{ "success": false, "error": "message", "code": "ERROR_CODE", "status": 401 }
|
|
283
78
|
```
|
|
284
79
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
The `data` field structure differs across endpoints:
|
|
288
|
-
|
|
289
|
-
| Shape | Example endpoints |
|
|
290
|
-
|-------|------------------|
|
|
291
|
-
| `data.data` (array) | token screener |
|
|
292
|
-
| `data.results` (array) | entity search |
|
|
293
|
-
| `data.data.results` (array) | most profiler endpoints |
|
|
294
|
-
| `data.netflows` | smart-money netflow |
|
|
295
|
-
| `data.trades` | smart-money dex-trades |
|
|
296
|
-
| `data.holdings` | smart-money holdings |
|
|
297
|
-
| `data.holders` | token holders |
|
|
298
|
-
|
|
299
|
-
`--table` and `--stream` handle this automatically. For raw JSON parsing:
|
|
300
|
-
|
|
301
|
-
```bash
|
|
302
|
-
nansen research smart-money netflow --chain solana | jq 'keys, .data | keys'
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### Error codes
|
|
80
|
+
**Critical error codes:**
|
|
306
81
|
|
|
307
82
|
| Code | Action |
|
|
308
83
|
|------|--------|
|
|
309
|
-
| `CREDITS_EXHAUSTED` | Stop all API calls immediately
|
|
310
|
-
| `
|
|
84
|
+
| `CREDITS_EXHAUSTED` | Stop all API calls immediately. Check [app.nansen.ai](https://app.nansen.ai). |
|
|
85
|
+
| `UNAUTHORIZED` | Wrong or missing key. Re-auth. |
|
|
86
|
+
| `RATE_LIMITED` | Auto-retried by CLI. |
|
|
311
87
|
| `UNSUPPORTED_FILTER` | Remove the filter and retry. |
|
|
312
|
-
| `UNAUTHORIZED` | Key is wrong or missing. Re-auth. |
|
|
313
|
-
| `INVALID_ADDRESS` | Check address format for the chain. |
|
|
314
88
|
|
|
315
89
|
## Troubleshooting
|
|
316
90
|
|
|
317
91
|
| Symptom | Fix |
|
|
318
92
|
|---------|-----|
|
|
319
|
-
| `command not found
|
|
320
|
-
| `UNAUTHORIZED` after login |
|
|
321
|
-
|
|
|
322
|
-
|
|
|
323
|
-
|
|
|
324
|
-
| `UNSUPPORTED_FILTER` on token holders | Not all tokens have smart money data. Remove `--smart-money`. |
|
|
325
|
-
| `CREDITS_EXHAUSTED` | Check your plan at [app.nansen.ai](https://app.nansen.ai). |
|
|
93
|
+
| `command not found` | `npm install -g nansen-cli` |
|
|
94
|
+
| `UNAUTHORIZED` after login | `cat ~/.nansen/config.json` or set `NANSEN_API_KEY` |
|
|
95
|
+
| Empty perp results | Use `--symbol BTC`, not `--token`. Perps are Hyperliquid-only. |
|
|
96
|
+
| `UNSUPPORTED_FILTER` on token holders | Remove `--smart-money` — not all tokens have that data. |
|
|
97
|
+
| Huge JSON response | Use `--fields` to select columns. |
|
|
326
98
|
|
|
327
99
|
## Development
|
|
328
100
|
|
|
329
101
|
```bash
|
|
330
|
-
npm test #
|
|
331
|
-
npm run test:
|
|
332
|
-
npm run test:live # Against live API (needs NANSEN_API_KEY)
|
|
102
|
+
npm test # mocked tests, no API key needed
|
|
103
|
+
npm run test:live # live API (needs NANSEN_API_KEY)
|
|
333
104
|
```
|
|
334
105
|
|
|
335
|
-
See [AGENTS.md](AGENTS.md) for
|
|
336
|
-
|
|
337
|
-
## API Coverage
|
|
338
|
-
|
|
339
|
-
| Category | Endpoints | Coverage |
|
|
340
|
-
|----------|-----------|----------|
|
|
341
|
-
| Smart Money | 6 | 100% |
|
|
342
|
-
| Profiler | 11 | 100% |
|
|
343
|
-
| Token God Mode | 12 | 100% |
|
|
344
|
-
| Portfolio | 1 | 100% |
|
|
345
|
-
| Search | 1 | 100% |
|
|
346
|
-
| **Total** | **31** | **100%** |
|
|
106
|
+
See [AGENTS.md](AGENTS.md) for architecture and contributor guidance.
|
|
347
107
|
|
|
348
108
|
## License
|
|
349
109
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nansen-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Command-line interface for Nansen API - designed for AI agents",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"test:watch": "vitest",
|
|
19
19
|
"test:coverage": "vitest run --coverage",
|
|
20
20
|
"test:live": "NANSEN_LIVE_TEST=1 vitest run",
|
|
21
|
-
"test:
|
|
21
|
+
"test:trade": "vitest run --config vitest.e2e.config.js",
|
|
22
|
+
"lint": "eslint .",
|
|
23
|
+
"lint:fix": "eslint . --fix",
|
|
22
24
|
"changeset": "changeset",
|
|
23
25
|
"changeset:version": "changeset version",
|
|
24
26
|
"changeset:publish": "changeset publish"
|
|
@@ -52,7 +54,10 @@
|
|
|
52
54
|
"devDependencies": {
|
|
53
55
|
"@changesets/changelog-github": "^0.5.1",
|
|
54
56
|
"@changesets/cli": "^2.29.4",
|
|
57
|
+
"@eslint/js": "^10.0.1",
|
|
55
58
|
"@vitest/coverage-v8": "^4.0.18",
|
|
59
|
+
"eslint": "^10.0.2",
|
|
60
|
+
"globals": "^17.4.0",
|
|
56
61
|
"vitest": "^4.0.18"
|
|
57
62
|
}
|
|
58
63
|
}
|
package/src/api.js
CHANGED
|
@@ -194,9 +194,9 @@ export function getCachedResponse(endpoint, body, ttlSeconds = DEFAULT_CACHE_TTL
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
return { ...cached.data, _meta: { ...cached.data._meta, fromCache: true, cacheAge: Math.round(age) } };
|
|
197
|
-
} catch (
|
|
197
|
+
} catch (_e) {
|
|
198
198
|
// Invalid cache file, delete it
|
|
199
|
-
try { fs.unlinkSync(cacheFile); } catch {}
|
|
199
|
+
try { fs.unlinkSync(cacheFile); } catch { /* ignore */ }
|
|
200
200
|
return null;
|
|
201
201
|
}
|
|
202
202
|
}
|
|
@@ -305,7 +305,7 @@ function loadConfig() {
|
|
|
305
305
|
|
|
306
306
|
// ~/.nansen/config.json (from `nansen login`)
|
|
307
307
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
308
|
-
try { config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch (
|
|
308
|
+
try { config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch (_e) { /* ignore */ }
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
// Local config.json (for development)
|
|
@@ -476,7 +476,7 @@ export class NansenAPI {
|
|
|
476
476
|
let data;
|
|
477
477
|
try {
|
|
478
478
|
data = await response.json();
|
|
479
|
-
} catch (
|
|
479
|
+
} catch (_err) {
|
|
480
480
|
// Non-JSON response (rare, usually server errors)
|
|
481
481
|
const error = new NansenError(
|
|
482
482
|
`Invalid response from API (status ${response.status})`,
|
|
@@ -1039,7 +1039,7 @@ export class NansenAPI {
|
|
|
1039
1039
|
}
|
|
1040
1040
|
|
|
1041
1041
|
async tokenOhlcv(params = {}) {
|
|
1042
|
-
const { tokenAddress, chain = 'solana', timeframe
|
|
1042
|
+
const { tokenAddress, chain = 'solana', timeframe } = params;
|
|
1043
1043
|
if (tokenAddress) {
|
|
1044
1044
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1045
1045
|
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
@@ -1048,7 +1048,6 @@ export class NansenAPI {
|
|
|
1048
1048
|
token_address: tokenAddress,
|
|
1049
1049
|
chain,
|
|
1050
1050
|
timeframe,
|
|
1051
|
-
pagination
|
|
1052
1051
|
});
|
|
1053
1052
|
}
|
|
1054
1053
|
|
package/src/cli.js
CHANGED
|
@@ -88,7 +88,7 @@ export const SCHEMA = {
|
|
|
88
88
|
description: 'Token God Mode - deep analytics for any token',
|
|
89
89
|
subcommands: {
|
|
90
90
|
'indicators': { description: 'Risk and reward indicators for a token (Nansen Score)', options: { token: { type: 'string', required: true, description: 'Token address' }, chain: { type: 'string', default: 'ethereum' } }, returns: ['token_info[market_cap_usd, market_cap_group, is_stablecoin]', 'risk_indicators[indicator_type, score, signal, signal_percentile, last_trigger_on]', 'reward_indicators[indicator_type, score, signal, signal_percentile, last_trigger_on]'] },
|
|
91
|
-
'ohlcv': { description: 'OHLCV candle data for a token', options: { token: { type: 'string', required: true, description: 'Token address' }, chain: { type: 'string', default: 'solana' }, timeframe: { type: 'string', default: '1h', enum: ['1m', '5m', '15m', '30m', '1h', '2h', '4h', '1d', '1w', '1M'], description: 'Candle timeframe (e.g., 1h, 4h, 1d)' }
|
|
91
|
+
'ohlcv': { description: 'OHLCV candle data for a token', options: { token: { type: 'string', required: true, description: 'Token address' }, chain: { type: 'string', default: 'solana' }, timeframe: { type: 'string', default: '1h', enum: ['1m', '5m', '15m', '30m', '1h', '2h', '4h', '1d', '1w', '1M'], description: 'Candle timeframe (e.g., 1h, 4h, 1d)' } }, returns: ['timestamp', 'open', 'high', 'low', 'close', 'volume'] },
|
|
92
92
|
'info': { description: 'Get detailed information for a specific token', options: { token: { type: 'string', required: true, description: 'Token address' }, chain: { type: 'string', default: 'solana' }, timeframe: { type: 'string', default: '1d', enum: ['5m', '1h', '6h', '12h', '1d', '7d'] } }, returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'liquidity_usd'] },
|
|
93
93
|
'screener': { description: 'Discover and filter tokens', options: { chain: { type: 'string', default: 'solana' }, chains: { type: 'array' }, timeframe: { type: 'string', default: '24h', enum: ['5m', '10m', '1h', '6h', '24h', '7d', '30d'] }, 'smart-money': { type: 'boolean', description: 'Filter for Smart Money only' }, search: { type: 'string', description: 'Filter results by token symbol or name (client-side)' }, limit: { type: 'number' }, sort: { type: 'string' } }, returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'smart_money_holders'] },
|
|
94
94
|
'holders': { description: 'Token holder analysis', options: { token: { type: 'string', required: true }, chain: { type: 'string', default: 'solana' }, 'smart-money': { type: 'boolean' }, limit: { type: 'number' } }, returns: ['address', 'address_label', 'token_amount', 'total_outflow', 'total_inflow', 'balance_change_24h', 'balance_change_7d', 'balance_change_30d', 'ownership_percentage', 'value_usd'] },
|
|
@@ -220,7 +220,52 @@ export function parseFields(fieldsOption) {
|
|
|
220
220
|
return fieldsOption.split(',').map(f => f.trim()).filter(f => f.length > 0);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Produce a compact schema listing commands with params* notation.
|
|
225
|
+
* Use `nansen schema --full` for the verbose version.
|
|
226
|
+
*/
|
|
227
|
+
export function compactSchema(schema) {
|
|
228
|
+
function compactOptions(opts) {
|
|
229
|
+
if (!opts) return '';
|
|
230
|
+
return Object.entries(opts)
|
|
231
|
+
.map(([name, o]) => `${name}${o.required ? '*' : ''}`)
|
|
232
|
+
.join(', ');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function compactCmd(prefix, cmd) {
|
|
236
|
+
const entries = [];
|
|
237
|
+
if (cmd.subcommands) {
|
|
238
|
+
for (const [name, sub] of Object.entries(cmd.subcommands)) {
|
|
239
|
+
const path = prefix ? `${prefix} ${name}` : name;
|
|
240
|
+
if (sub.subcommands) {
|
|
241
|
+
entries.push(...compactCmd(path, sub));
|
|
242
|
+
} else {
|
|
243
|
+
const params = compactOptions(sub.options);
|
|
244
|
+
entries.push({ command: path, description: sub.description, params, returns: sub.returns });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
const params = compactOptions(cmd.options);
|
|
249
|
+
entries.push({ command: prefix, description: cmd.description, params, returns: cmd.returns });
|
|
250
|
+
}
|
|
251
|
+
return entries;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const commands = [];
|
|
255
|
+
for (const [name, cmd] of Object.entries(schema.commands)) {
|
|
256
|
+
commands.push(...compactCmd(name, cmd));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
version: schema.version,
|
|
261
|
+
params_legend: '* = required',
|
|
262
|
+
commands,
|
|
263
|
+
globalOptions: Object.keys(schema.globalOptions).join(', '),
|
|
264
|
+
chains: schema.chains,
|
|
265
|
+
smartMoneyLabels: schema.smartMoneyLabels
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
224
269
|
/**
|
|
225
270
|
* Compare two semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
226
271
|
*/
|
|
@@ -244,7 +289,7 @@ export function parseArgs(args) {
|
|
|
244
289
|
const key = arg.slice(2);
|
|
245
290
|
const next = args[i + 1];
|
|
246
291
|
|
|
247
|
-
if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich') {
|
|
292
|
+
if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich' || key === 'full') {
|
|
248
293
|
result.flags[key] = true;
|
|
249
294
|
} else if (next && !next.startsWith('-')) {
|
|
250
295
|
// Try to parse as JSON first (for objects/arrays/booleans),
|
|
@@ -635,7 +680,7 @@ export async function traceCounterparties(api, params = {}) {
|
|
|
635
680
|
queue.push({ addr: cpAddr, hop: hop + 1 });
|
|
636
681
|
}
|
|
637
682
|
}
|
|
638
|
-
} catch
|
|
683
|
+
} catch {
|
|
639
684
|
// Skip addresses that fail (404, etc) but continue the traversal
|
|
640
685
|
}
|
|
641
686
|
|
|
@@ -706,91 +751,41 @@ export async function compareWallets(api, params = {}) {
|
|
|
706
751
|
};
|
|
707
752
|
}
|
|
708
753
|
|
|
709
|
-
|
|
710
|
-
export const BANNER = `
|
|
711
|
-
███╗ ██╗ █████╗ ███╗ ██╗███████╗███████╗███╗ ██╗
|
|
712
|
-
████╗ ██║██╔══██╗████╗ ██║██╔════╝██╔════╝████╗ ██║
|
|
713
|
-
██╔██╗ ██║███████║██╔██╗ ██║███████╗█████╗ ██╔██╗ ██║
|
|
714
|
-
██║╚██╗██║██╔══██║██║╚██╗██║╚════██║██╔══╝ ██║╚██╗██║
|
|
715
|
-
██║ ╚████║██║ ██║██║ ╚████║███████║███████╗██║ ╚████║
|
|
716
|
-
╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═══╝
|
|
717
|
-
Surface The Signal
|
|
718
|
-
`;
|
|
754
|
+
export const BANNER = '';
|
|
719
755
|
|
|
720
|
-
|
|
721
|
-
export const HELP = `
|
|
722
|
-
Nansen CLI - Command-line interface for Nansen API
|
|
723
|
-
Designed for AI agents with structured JSON output.
|
|
756
|
+
export const HELP = `Nansen CLI v${VERSION} — structured JSON output for AI agents.
|
|
724
757
|
|
|
725
|
-
USAGE:
|
|
726
|
-
nansen <command> [subcommand] [options]
|
|
758
|
+
USAGE: nansen <command> [subcommand] [options]
|
|
727
759
|
|
|
728
760
|
COMMANDS:
|
|
729
|
-
research
|
|
730
|
-
trade
|
|
731
|
-
wallet
|
|
732
|
-
login
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
--pretty
|
|
740
|
-
|
|
741
|
-
--fields Comma-separated list of fields to include (e.g., --fields address,value_usd)
|
|
742
|
-
--chain Blockchain to query (ethereum, solana, base, etc.)
|
|
743
|
-
--chains Multiple chains as JSON array
|
|
744
|
-
--limit Number of results (shorthand for pagination)
|
|
745
|
-
--filters JSON object with filters
|
|
746
|
-
--sort Sort by field (e.g., --sort value_usd:desc)
|
|
747
|
-
--order-by JSON array with sort order (advanced)
|
|
748
|
-
--days Date range in days (default: 30 for most endpoints)
|
|
749
|
-
--symbol Token symbol (for perp endpoints)
|
|
750
|
-
--no-retry Disable automatic retry on rate limits/errors
|
|
751
|
-
--retries <n> Max retry attempts (default: 3)
|
|
752
|
-
--x402-payment-signature <sig> Pre-signed x402 payment signature header
|
|
753
|
-
--cache Enable response caching (default: off)
|
|
754
|
-
--no-cache Disable cache for this request
|
|
755
|
-
--cache-ttl <s> Cache TTL in seconds (default: 300)
|
|
756
|
-
--stream Output as JSON lines (NDJSON) for incremental processing
|
|
757
|
-
--format csv Output as CSV with header row
|
|
761
|
+
research smart-money, profiler, token, search, perp, portfolio, points
|
|
762
|
+
trade quote, execute
|
|
763
|
+
wallet create, list, show, export, default, delete
|
|
764
|
+
login Save API key (--api-key <key> or interactive)
|
|
765
|
+
logout Remove saved API key
|
|
766
|
+
schema JSON schema for all commands (use "nansen schema <cmd>" for one)
|
|
767
|
+
cache clear
|
|
768
|
+
changelog --since <version> to filter
|
|
769
|
+
|
|
770
|
+
OPTIONS: --chain --limit --sort field:dir --fields a,b --days N --filters '{}'
|
|
771
|
+
FORMAT: --pretty --table --format csv --stream (NDJSON)
|
|
772
|
+
RETRY: --no-retry --retries N --cache --cache-ttl N
|
|
758
773
|
|
|
759
774
|
EXAMPLES:
|
|
760
|
-
# Get Smart Money netflow on Solana
|
|
761
775
|
nansen research smart-money netflow --chain solana
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
nansen research token screener --chain solana --timeframe 24h --pretty
|
|
765
|
-
|
|
766
|
-
# Get wallet balance
|
|
767
|
-
nansen research profiler balance --address 0x123... --chain ethereum
|
|
768
|
-
|
|
769
|
-
# Search for tokens/entities
|
|
770
|
-
nansen research search "Vitalik"
|
|
771
|
-
|
|
772
|
-
# Get a DEX swap quote
|
|
776
|
+
nansen research token screener --chain solana --timeframe 24h
|
|
777
|
+
nansen research profiler balance --address 0x... --chain ethereum
|
|
773
778
|
nansen trade quote --chain ethereum --from ETH --to USDC --amount 1
|
|
774
779
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
180D Smart Trader, Smart HL Perps Trader
|
|
780
|
+
Chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
|
|
781
|
+
Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
|
|
778
782
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
avalanche, linea, scroll, mantle, ronin, sei,
|
|
782
|
-
plasma, sonic, monad, hyperevm, iotaevm
|
|
783
|
-
|
|
784
|
-
For more info: https://docs.nansen.ai
|
|
783
|
+
Docs: https://docs.nansen.ai
|
|
784
|
+
Skills: npx skills add nansen-ai/nansen-cli (agent-optimised docs per command group)
|
|
785
785
|
`;
|
|
786
786
|
|
|
787
787
|
// Helper to prompt for input (exported for mocking)
|
|
788
788
|
export async function prompt(question, hidden = false) {
|
|
789
|
-
const rl = readline.createInterface({
|
|
790
|
-
input: process.stdin,
|
|
791
|
-
output: process.stdout
|
|
792
|
-
});
|
|
793
|
-
|
|
794
789
|
return new Promise((resolve) => {
|
|
795
790
|
if (hidden && process.stdout.isTTY) {
|
|
796
791
|
process.stdout.write(question);
|
|
@@ -805,7 +800,6 @@ export async function prompt(question, hidden = false) {
|
|
|
805
800
|
process.stdin.pause();
|
|
806
801
|
process.stdin.removeListener('data', onData);
|
|
807
802
|
process.stdout.write('\n');
|
|
808
|
-
rl.close();
|
|
809
803
|
resolve(input);
|
|
810
804
|
} else if (char === '\u0003') {
|
|
811
805
|
// Ctrl+C
|
|
@@ -824,6 +818,10 @@ export async function prompt(question, hidden = false) {
|
|
|
824
818
|
|
|
825
819
|
process.stdin.on('data', onData);
|
|
826
820
|
} else {
|
|
821
|
+
const rl = readline.createInterface({
|
|
822
|
+
input: process.stdin,
|
|
823
|
+
output: process.stdout
|
|
824
|
+
});
|
|
827
825
|
rl.question(question, (answer) => {
|
|
828
826
|
rl.close();
|
|
829
827
|
resolve(answer);
|
|
@@ -836,11 +834,11 @@ export async function prompt(question, hidden = false) {
|
|
|
836
834
|
export function buildCommands(deps = {}) {
|
|
837
835
|
// Allow dependency injection for testing
|
|
838
836
|
const {
|
|
839
|
-
api = null,
|
|
837
|
+
api: _api = null,
|
|
840
838
|
promptFn = prompt,
|
|
841
839
|
log = console.log,
|
|
842
|
-
errorOutput = console.error,
|
|
843
|
-
NansenAPIClass = NansenAPI,
|
|
840
|
+
errorOutput: _errorOutput = console.error,
|
|
841
|
+
NansenAPIClass: _NansenAPIClass = NansenAPI,
|
|
844
842
|
saveConfigFn = saveConfig,
|
|
845
843
|
deleteConfigFn = deleteConfig,
|
|
846
844
|
getConfigFileFn = getConfigFile,
|
|
@@ -850,6 +848,18 @@ export function buildCommands(deps = {}) {
|
|
|
850
848
|
|
|
851
849
|
const cmds = {
|
|
852
850
|
'login': async (args, apiInstance, flags, options) => {
|
|
851
|
+
if (flags.help || flags.h) {
|
|
852
|
+
log('nansen login - Save your Nansen API key\n');
|
|
853
|
+
log('USAGE:');
|
|
854
|
+
log(' nansen login (interactive)');
|
|
855
|
+
log(' nansen login --api-key <key> (non-interactive)\n');
|
|
856
|
+
log('OPTIONS:');
|
|
857
|
+
log(' --api-key <key> Your Nansen API key');
|
|
858
|
+
log(' --help Show this help\n');
|
|
859
|
+
log('Get your API key at: https://app.nansen.ai/api');
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
853
863
|
// Support non-interactive: nansen login --api-key <key>
|
|
854
864
|
let apiKey = options['api-key'] || options.apiKey;
|
|
855
865
|
|
|
@@ -885,10 +895,10 @@ export function buildCommands(deps = {}) {
|
|
|
885
895
|
|
|
886
896
|
log(`✓ Saved to ${getConfigFileFn()}\n`);
|
|
887
897
|
log('You can now use the Nansen CLI. Try:');
|
|
888
|
-
log(' nansen token screener --chain solana --pretty');
|
|
898
|
+
log(' nansen research token screener --chain solana --pretty');
|
|
889
899
|
},
|
|
890
900
|
|
|
891
|
-
'logout': async (
|
|
901
|
+
'logout': async (_args, _apiInstance, _flags, _options) => {
|
|
892
902
|
const deleted = deleteConfigFn();
|
|
893
903
|
if (deleted) {
|
|
894
904
|
log(`✓ Removed ${getConfigFileFn()}`);
|
|
@@ -897,11 +907,11 @@ export function buildCommands(deps = {}) {
|
|
|
897
907
|
}
|
|
898
908
|
},
|
|
899
909
|
|
|
900
|
-
'help': async (
|
|
910
|
+
'help': async (_args, _apiInstance, _flags, _options) => {
|
|
901
911
|
log(HELP);
|
|
902
912
|
},
|
|
903
913
|
|
|
904
|
-
'changelog': async (
|
|
914
|
+
'changelog': async (_args, _apiInstance, _flags, _options) => {
|
|
905
915
|
const changelogPath = new URL('../CHANGELOG.md', import.meta.url).pathname;
|
|
906
916
|
let content;
|
|
907
917
|
try {
|
|
@@ -910,7 +920,7 @@ export function buildCommands(deps = {}) {
|
|
|
910
920
|
log('CHANGELOG.md not found. Visit https://github.com/nansen-ai/nansen-cli/blob/main/CHANGELOG.md');
|
|
911
921
|
return;
|
|
912
922
|
}
|
|
913
|
-
const since =
|
|
923
|
+
const since = _options.since;
|
|
914
924
|
if (since) {
|
|
915
925
|
// Show only entries from the given version onwards
|
|
916
926
|
const lines = content.split('\n');
|
|
@@ -936,8 +946,7 @@ export function buildCommands(deps = {}) {
|
|
|
936
946
|
}
|
|
937
947
|
},
|
|
938
948
|
|
|
939
|
-
'schema': async (args,
|
|
940
|
-
// Return schema for agent introspection
|
|
949
|
+
'schema': async (args, _apiInstance, flags, _options) => {
|
|
941
950
|
const subcommand = args[0];
|
|
942
951
|
const schemaEntry = subcommand && (SCHEMA.commands[subcommand] || SCHEMA.commands.research.subcommands[subcommand]);
|
|
943
952
|
|
|
@@ -951,11 +960,14 @@ export function buildCommands(deps = {}) {
|
|
|
951
960
|
};
|
|
952
961
|
}
|
|
953
962
|
|
|
954
|
-
|
|
955
|
-
|
|
963
|
+
if (flags.full) {
|
|
964
|
+
return SCHEMA;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
return compactSchema(SCHEMA);
|
|
956
968
|
},
|
|
957
969
|
|
|
958
|
-
'cache': async (args,
|
|
970
|
+
'cache': async (args, _apiInstance, _flags, _options) => {
|
|
959
971
|
const subcommand = args[0] || 'help';
|
|
960
972
|
|
|
961
973
|
const handlers = {
|
|
@@ -1139,7 +1151,7 @@ export function buildCommands(deps = {}) {
|
|
|
1139
1151
|
|
|
1140
1152
|
const handlers = {
|
|
1141
1153
|
'indicators': () => apiInstance.tokenIndicators({ tokenAddress, chain }),
|
|
1142
|
-
'ohlcv': () => apiInstance.tokenOhlcv({ tokenAddress, chain, timeframe: options.timeframe || '1d'
|
|
1154
|
+
'ohlcv': () => apiInstance.tokenOhlcv({ tokenAddress, chain, timeframe: options.timeframe || '1d' }),
|
|
1143
1155
|
'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe: options.timeframe }),
|
|
1144
1156
|
'screener': async () => {
|
|
1145
1157
|
const search = options.search;
|
|
@@ -1299,7 +1311,7 @@ export function buildCommands(deps = {}) {
|
|
|
1299
1311
|
}
|
|
1300
1312
|
const category = RESEARCH_CATEGORY_ALIASES[rawCategory] || rawCategory;
|
|
1301
1313
|
if (!RESEARCH_CATEGORIES.has(category)) {
|
|
1302
|
-
|
|
1314
|
+
throw new NansenError(`Unknown research category: ${rawCategory}. Available: ${[...RESEARCH_CATEGORIES].join(', ')}`, ErrorCode.UNKNOWN);
|
|
1303
1315
|
}
|
|
1304
1316
|
return cmds[category](args.slice(1), apiInstance, flags, options);
|
|
1305
1317
|
};
|
|
@@ -1335,10 +1347,7 @@ SYMBOLS:
|
|
|
1335
1347
|
return;
|
|
1336
1348
|
}
|
|
1337
1349
|
if (!tradingCmds[sub]) {
|
|
1338
|
-
|
|
1339
|
-
log(`Available: quote, execute`);
|
|
1340
|
-
log(`Run 'nansen trade help' for usage.`);
|
|
1341
|
-
return;
|
|
1350
|
+
throw new NansenError(`Unknown trade subcommand: ${sub}. Available: quote, execute`, ErrorCode.UNKNOWN);
|
|
1342
1351
|
}
|
|
1343
1352
|
return tradingCmds[sub](args.slice(1), apiInstance, flags, options);
|
|
1344
1353
|
};
|
|
@@ -1351,9 +1360,6 @@ export const DEPRECATED_TO_RESEARCH = new Set(['smart-money', 'profiler', 'token
|
|
|
1351
1360
|
// Subcommands that moved under 'trade'
|
|
1352
1361
|
export const DEPRECATED_TO_TRADE = new Set(['quote', 'execute']);
|
|
1353
1362
|
|
|
1354
|
-
// Commands that don't require API authentication
|
|
1355
|
-
export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache', 'wallet', 'trade', 'quote', 'execute', 'changelog'];
|
|
1356
|
-
|
|
1357
1363
|
// Command aliases: top-level shortcuts that resolve before routing
|
|
1358
1364
|
export const COMMAND_ALIASES = {
|
|
1359
1365
|
'tgm': 'token', // Token God Mode
|
|
@@ -1372,7 +1378,6 @@ export const RESEARCH_CATEGORY_ALIASES = {
|
|
|
1372
1378
|
|
|
1373
1379
|
// Generate help text for a specific subcommand using SCHEMA
|
|
1374
1380
|
export function generateSubcommandHelp(command, subcommand) {
|
|
1375
|
-
// Look up in top-level commands, then fall back to research subcommands
|
|
1376
1381
|
const cmdSchema = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
|
|
1377
1382
|
if (!cmdSchema) return null;
|
|
1378
1383
|
|
|
@@ -1380,100 +1385,36 @@ export function generateSubcommandHelp(command, subcommand) {
|
|
|
1380
1385
|
if (!subSchema) return null;
|
|
1381
1386
|
|
|
1382
1387
|
const lines = [];
|
|
1383
|
-
lines.push(
|
|
1384
|
-
|
|
1385
|
-
// Usage
|
|
1386
|
-
const requiredOpts = [];
|
|
1387
|
-
const optionalOpts = [];
|
|
1388
|
-
|
|
1388
|
+
lines.push(`${command} ${subcommand} — ${subSchema.description || 'No description'}`);
|
|
1389
|
+
|
|
1389
1390
|
if (subSchema.options) {
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
let usage = `USAGE:\n nansen ${command} ${subcommand}`;
|
|
1400
|
-
if (requiredOpts.length) {
|
|
1401
|
-
usage += ' ' + requiredOpts.map(o => `--${o} <value>`).join(' ');
|
|
1402
|
-
}
|
|
1403
|
-
if (optionalOpts.length) {
|
|
1404
|
-
usage += ' [options]';
|
|
1405
|
-
}
|
|
1406
|
-
lines.push(usage);
|
|
1407
|
-
|
|
1408
|
-
// Required options
|
|
1409
|
-
if (requiredOpts.length) {
|
|
1410
|
-
lines.push('\nREQUIRED:');
|
|
1411
|
-
for (const name of requiredOpts) {
|
|
1412
|
-
const opt = subSchema.options[name];
|
|
1413
|
-
const desc = opt.description || `${opt.type}`;
|
|
1414
|
-
lines.push(` --${name.padEnd(16)} ${desc}`);
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
// Optional options
|
|
1419
|
-
if (optionalOpts.length) {
|
|
1420
|
-
lines.push('\nOPTIONS:');
|
|
1421
|
-
for (const name of optionalOpts) {
|
|
1422
|
-
const opt = subSchema.options[name];
|
|
1423
|
-
const defaultStr = opt.default !== undefined ? ` (default: ${opt.default})` : '';
|
|
1424
|
-
const desc = (opt.description || opt.type) + defaultStr;
|
|
1425
|
-
lines.push(` --${name.padEnd(16)} ${desc}`);
|
|
1426
|
-
}
|
|
1391
|
+
const params = Object.entries(subSchema.options).map(([name, opt]) => {
|
|
1392
|
+
const parts = [`--${name}`];
|
|
1393
|
+
if (opt.required) parts[0] += '*';
|
|
1394
|
+
if (opt.default !== undefined) parts.push(`(${opt.default})`);
|
|
1395
|
+
if (opt.enum) parts.push(`[${opt.enum.join('|')}]`);
|
|
1396
|
+
return parts.join(' ');
|
|
1397
|
+
});
|
|
1398
|
+
lines.push(`Params (* required): ${params.join(', ')}`);
|
|
1427
1399
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
lines.push('\nRETURNS:');
|
|
1432
|
-
lines.push(` ${subSchema.returns.join(', ')}`);
|
|
1400
|
+
|
|
1401
|
+
if (subSchema.returns?.length) {
|
|
1402
|
+
lines.push(`Returns: ${subSchema.returns.join(', ')}`);
|
|
1433
1403
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
lines.push('\nEXAMPLES:');
|
|
1404
|
+
|
|
1405
|
+
const exampleValues = { address: '0x...', token: '0x...', query: '"term"', symbol: 'BTC', date: '2024-01-01' };
|
|
1437
1406
|
const chain = subSchema.options?.chain?.default || 'solana';
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
query: '"search term"',
|
|
1444
|
-
symbol: 'BTC',
|
|
1445
|
-
date: '2024-01-01'
|
|
1446
|
-
};
|
|
1447
|
-
|
|
1448
|
-
// Build example based on required options
|
|
1449
|
-
let example = ` nansen ${command} ${subcommand}`;
|
|
1450
|
-
for (const name of requiredOpts) {
|
|
1451
|
-
const value = exampleValues[name] || '<value>';
|
|
1452
|
-
example += ` --${name} ${value}`;
|
|
1407
|
+
let example = `nansen ${command} ${subcommand}`;
|
|
1408
|
+
if (subSchema.options) {
|
|
1409
|
+
for (const [name, opt] of Object.entries(subSchema.options)) {
|
|
1410
|
+
if (opt.required) example += ` --${name} ${exampleValues[name] || '<val>'}`;
|
|
1411
|
+
}
|
|
1453
1412
|
}
|
|
1454
|
-
if (subSchema.options?.chain && !
|
|
1413
|
+
if (subSchema.options?.chain && !subSchema.options.chain.required) {
|
|
1455
1414
|
example += ` --chain ${chain}`;
|
|
1456
1415
|
}
|
|
1457
|
-
example
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
// Add a filtered example if filters are supported
|
|
1461
|
-
if (subSchema.options?.filters || subSchema.options?.labels) {
|
|
1462
|
-
let filterExample = ` nansen ${command} ${subcommand}`;
|
|
1463
|
-
for (const name of requiredOpts) {
|
|
1464
|
-
const value = exampleValues[name] || '<value>';
|
|
1465
|
-
filterExample += ` --${name} ${value}`;
|
|
1466
|
-
}
|
|
1467
|
-
if (subSchema.options?.chain && !requiredOpts.includes('chain')) {
|
|
1468
|
-
filterExample += ` --chain ${chain}`;
|
|
1469
|
-
}
|
|
1470
|
-
if (subSchema.options?.labels) {
|
|
1471
|
-
filterExample += ' --labels "Smart Trader"';
|
|
1472
|
-
}
|
|
1473
|
-
filterExample += ' --limit 10 --table';
|
|
1474
|
-
lines.push(filterExample);
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1416
|
+
lines.push(`Example: ${example}`);
|
|
1417
|
+
|
|
1477
1418
|
return lines.join('\n');
|
|
1478
1419
|
}
|
|
1479
1420
|
|
|
@@ -1542,13 +1483,10 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1542
1483
|
const researchCat = SCHEMA.commands.research.subcommands[category];
|
|
1543
1484
|
if (researchCat) {
|
|
1544
1485
|
const catSchema = researchCat;
|
|
1545
|
-
const lines = [
|
|
1486
|
+
const lines = [`research ${category} — ${catSchema.description}`];
|
|
1546
1487
|
if (catSchema.subcommands) {
|
|
1547
|
-
lines.push('
|
|
1548
|
-
|
|
1549
|
-
lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
|
|
1550
|
-
}
|
|
1551
|
-
lines.push(`\nFor detailed help: nansen research ${category} <subcommand> --help`);
|
|
1488
|
+
lines.push('Subcommands: ' + Object.keys(catSchema.subcommands).join(', '));
|
|
1489
|
+
lines.push(`Use: nansen research ${category} <subcommand> --help`);
|
|
1552
1490
|
}
|
|
1553
1491
|
output(lines.join('\n'));
|
|
1554
1492
|
notify();
|
|
@@ -1577,13 +1515,10 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1577
1515
|
const cmdSchemaLookup = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
|
|
1578
1516
|
if (command && cmdSchemaLookup) {
|
|
1579
1517
|
const cmdSchema = cmdSchemaLookup;
|
|
1580
|
-
const lines = [
|
|
1518
|
+
const lines = [`${command} — ${cmdSchema.description}`];
|
|
1581
1519
|
if (cmdSchema.subcommands) {
|
|
1582
|
-
lines.push('
|
|
1583
|
-
|
|
1584
|
-
lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
|
|
1585
|
-
}
|
|
1586
|
-
lines.push(`\nFor detailed help: nansen ${command} <subcommand> --help`);
|
|
1520
|
+
lines.push('Subcommands: ' + Object.keys(cmdSchema.subcommands).join(', '));
|
|
1521
|
+
lines.push(`Use: nansen ${command} <subcommand> --help`);
|
|
1587
1522
|
}
|
|
1588
1523
|
output(lines.join('\n'));
|
|
1589
1524
|
notify();
|
|
@@ -1610,48 +1545,46 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1610
1545
|
return { type: 'error', data: errorData };
|
|
1611
1546
|
}
|
|
1612
1547
|
|
|
1613
|
-
// Commands that don't require API authentication
|
|
1614
|
-
if (NO_AUTH_COMMANDS.includes(command)) {
|
|
1615
|
-
const result = await commands[command](subArgs, null, flags, options);
|
|
1616
|
-
|
|
1617
|
-
// Schema command returns data that should be output
|
|
1618
|
-
if (command === 'schema' && result) {
|
|
1619
|
-
const formatted = formatOutput(result, { pretty, table: false });
|
|
1620
|
-
output(formatted.text);
|
|
1621
|
-
notify();
|
|
1622
|
-
return { type: 'schema', data: result };
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
notify();
|
|
1626
|
-
return { type: 'no-auth', command };
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
1548
|
try {
|
|
1630
1549
|
// Configure retry options
|
|
1631
|
-
const retryOptions = flags['no-retry']
|
|
1632
|
-
? { maxRetries: 0 }
|
|
1550
|
+
const retryOptions = flags['no-retry']
|
|
1551
|
+
? { maxRetries: 0 }
|
|
1633
1552
|
: { maxRetries: options.retries !== undefined ? (Number.isNaN(parseInt(options.retries, 10)) ? 3 : parseInt(options.retries, 10)) : 3 };
|
|
1634
|
-
|
|
1553
|
+
|
|
1635
1554
|
// Configure cache options
|
|
1636
1555
|
const cacheTtl = options['cache-ttl'] !== undefined ? parseInt(options['cache-ttl'], 10) : 300;
|
|
1637
1556
|
const cacheOptions = {
|
|
1638
1557
|
enabled: flags['cache'] && !flags['no-cache'],
|
|
1639
1558
|
ttl: Number.isNaN(cacheTtl) ? 300 : cacheTtl
|
|
1640
1559
|
};
|
|
1641
|
-
|
|
1560
|
+
|
|
1642
1561
|
const defaultHeaders = {};
|
|
1643
1562
|
if (options['x402-payment-signature']) {
|
|
1644
1563
|
defaultHeaders['Payment-Signature'] = options['x402-payment-signature'];
|
|
1645
1564
|
}
|
|
1646
1565
|
const api = new NansenAPIClass(undefined, undefined, { retry: retryOptions, cache: cacheOptions, defaultHeaders });
|
|
1647
1566
|
let result = await commands[command](subArgs, api, flags, options);
|
|
1648
|
-
|
|
1567
|
+
|
|
1568
|
+
// Commands that handle their own output return undefined
|
|
1569
|
+
if (result === undefined) {
|
|
1570
|
+
notify();
|
|
1571
|
+
return { type: 'no-output', command };
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// Schema returns data directly (not wrapped in { success, data })
|
|
1575
|
+
if (command === 'schema') {
|
|
1576
|
+
const formatted = formatOutput(result, { pretty, table: false });
|
|
1577
|
+
output(formatted.text);
|
|
1578
|
+
notify();
|
|
1579
|
+
return { type: 'schema', data: result };
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1649
1582
|
// Apply field filtering if --fields is specified
|
|
1650
1583
|
const fields = parseFields(options.fields);
|
|
1651
1584
|
if (fields) {
|
|
1652
1585
|
result = filterFields(result, fields);
|
|
1653
1586
|
}
|
|
1654
|
-
|
|
1587
|
+
|
|
1655
1588
|
// Output in requested format
|
|
1656
1589
|
if (stream) {
|
|
1657
1590
|
// Stream mode: output each record as a JSON line (NDJSON)
|
package/src/trading.js
CHANGED
|
@@ -742,7 +742,7 @@ export function validateBaseUnitAmount(amount) {
|
|
|
742
742
|
return null;
|
|
743
743
|
}
|
|
744
744
|
|
|
745
|
-
function formatQuote(quote, index) {
|
|
745
|
+
export function formatQuote(quote, index) {
|
|
746
746
|
const lines = [];
|
|
747
747
|
const label = index !== undefined ? ` Quote #${index + 1}` : ' Best Quote';
|
|
748
748
|
lines.push(`${label} (${quote.aggregator || 'unknown'})`);
|
|
@@ -750,10 +750,21 @@ function formatQuote(quote, index) {
|
|
|
750
750
|
lines.push(` Output: ${quote.outAmount} → ${quote.outputMint?.slice(0, 12)}...`);
|
|
751
751
|
if (quote.inUsdValue) lines.push(` In USD: $${quote.inUsdValue}`);
|
|
752
752
|
if (quote.outUsdValue) lines.push(` Out USD: $${quote.outUsdValue}`);
|
|
753
|
-
if (quote.priceImpactPct)
|
|
753
|
+
if (quote.priceImpactPct) {
|
|
754
|
+
const impactAbs = Math.abs(parseFloat(quote.priceImpactPct));
|
|
755
|
+
if (impactAbs <= 5) {
|
|
756
|
+
lines.push(` Price Impact: ${impactAbs}%`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
754
759
|
if (quote.tradingFeeInUsd) lines.push(` Trading Fee: $${quote.tradingFeeInUsd}`);
|
|
755
760
|
if (quote.networkFeeInUsd) lines.push(` Network Fee: $${quote.networkFeeInUsd}`);
|
|
756
761
|
if (quote.approvalAddress && !isNativeToken(quote.inputMint)) lines.push(` ⚠ Requires token approval to: ${quote.approvalAddress}`);
|
|
762
|
+
if (quote.priceImpactPct) {
|
|
763
|
+
const impactAbs = Math.abs(parseFloat(quote.priceImpactPct));
|
|
764
|
+
if (impactAbs > 5) {
|
|
765
|
+
lines.push(` ⚠ Price impact is ${impactAbs}%! You may lose significant value.`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
757
768
|
return lines.join('\n');
|
|
758
769
|
}
|
|
759
770
|
|
|
@@ -833,11 +844,15 @@ EXAMPLES:
|
|
|
833
844
|
const wallet = showWallet(walletName);
|
|
834
845
|
walletAddress = chainType === 'solana' ? wallet.solana : wallet.evm;
|
|
835
846
|
} else {
|
|
836
|
-
|
|
847
|
+
try {
|
|
848
|
+
walletAddress = getDefaultAddress(chainType);
|
|
849
|
+
} catch {
|
|
850
|
+
// No wallet configured — fall through to the check below
|
|
851
|
+
}
|
|
837
852
|
}
|
|
838
853
|
|
|
839
854
|
if (!walletAddress) {
|
|
840
|
-
errorOutput('No wallet found.
|
|
855
|
+
errorOutput('No wallet found. A wallet address is required for quotes because the trading API builds a transaction specific to the sender.\nCreate one with: nansen wallet create');
|
|
841
856
|
exit(1);
|
|
842
857
|
return;
|
|
843
858
|
}
|
|
@@ -877,6 +892,9 @@ EXAMPLES:
|
|
|
877
892
|
const quoteId = saveQuote(response, chain, isWalletConnect ? 'walletconnect' : 'local');
|
|
878
893
|
errorOutput(`\n Quote ID: ${quoteId}`);
|
|
879
894
|
errorOutput(` Execute: nansen trade execute --quote ${quoteId}`);
|
|
895
|
+
if (response.quotes.length > 1) {
|
|
896
|
+
errorOutput(` Pin #1: nansen trade execute --quote ${quoteId} --quote-index 0`);
|
|
897
|
+
}
|
|
880
898
|
|
|
881
899
|
if (response.quotes[0]?.approvalAddress && !isNativeToken(response.quotes[0]?.inputMint)) {
|
|
882
900
|
errorOutput(`\n Warning: This token swap requires an ERC-20 approval step.`);
|
package/src/transfer.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import crypto from 'crypto';
|
|
8
8
|
import { base58Encode, exportWallet, getWalletConfig, verifyPassword } from './wallet.js';
|
|
9
|
-
import { keccak256, signSecp256k1, rlpEncode
|
|
9
|
+
import { keccak256, signSecp256k1, rlpEncode } from './crypto.js';
|
|
10
10
|
import { getWalletConnectAddress, sendTransactionViaWalletConnect } from './walletconnect-trading.js';
|
|
11
11
|
import { EVM_CHAIN_IDS } from './chain-ids.js';
|
|
12
12
|
|
|
@@ -217,7 +217,6 @@ async function buildEvmTransaction({ to, amount, token, privateKey, chain, max =
|
|
|
217
217
|
const safeReserve = l2GasCost * 3n;
|
|
218
218
|
if (ethBalance <= safeReserve) throw new Error(`Insufficient balance: ${ethBalance} wei (need > ${safeReserve} for gas + L1 fees)`);
|
|
219
219
|
txValue = ethBalance - safeReserve;
|
|
220
|
-
amount = txValue;
|
|
221
220
|
stderr(` Max send: ${formatAmount(txValue, 18)} ETH (reserved ${formatAmount(safeReserve, 18)} for gas)`);
|
|
222
221
|
} else {
|
|
223
222
|
txValue = amount;
|
|
@@ -375,7 +374,7 @@ async function buildSolanaTransaction({ to, amount, amountStr, token, privateKey
|
|
|
375
374
|
}
|
|
376
375
|
} catch (e) {
|
|
377
376
|
if (e.message.includes('Insufficient')) throw e;
|
|
378
|
-
throw new Error(`Source token account not found. Do you hold this token
|
|
377
|
+
throw new Error(`Source token account not found. Do you hold this token?`, { cause: e });
|
|
379
378
|
}
|
|
380
379
|
|
|
381
380
|
// TransferChecked instruction data: [12, amount u64 LE, decimals u8]
|
|
@@ -568,7 +567,7 @@ async function validateErc20Token(rpcUrl, tokenAddress) {
|
|
|
568
567
|
return decimals;
|
|
569
568
|
} catch (e) {
|
|
570
569
|
if (e.message.includes('not a valid')) throw e;
|
|
571
|
-
throw new Error(`Contract ${tokenAddress} does not implement ERC-20 decimals() — may not be a valid token
|
|
570
|
+
throw new Error(`Contract ${tokenAddress} does not implement ERC-20 decimals() — may not be a valid token`, { cause: e });
|
|
572
571
|
}
|
|
573
572
|
}
|
|
574
573
|
|
|
@@ -626,7 +625,7 @@ export async function sendTokens({ to, amount, chain, token = null, wallet = nul
|
|
|
626
625
|
} else if (max && token) {
|
|
627
626
|
// Max SPL: full token balance
|
|
628
627
|
const rpcUrl = CHAIN_RPCS.solana;
|
|
629
|
-
const { tokenProgram, decimals } = await getTokenInfo(rpcUrl, token);
|
|
628
|
+
const { tokenProgram, decimals: _decimals } = await getTokenInfo(rpcUrl, token);
|
|
630
629
|
const sourceATA = deriveATA(walletData.solana.address, token, tokenProgram);
|
|
631
630
|
const sourceAtaAddr = base58Encode(sourceATA);
|
|
632
631
|
const ataInfo = await rpcCall(rpcUrl, 'getTokenAccountBalance', [sourceAtaAddr]);
|
|
@@ -721,7 +720,7 @@ async function sendTokensViaWalletConnect({ to, amount, chain, token, max, dryRu
|
|
|
721
720
|
const wcAddress = await getWalletConnectAddress();
|
|
722
721
|
if (!wcAddress) throw new Error('No WalletConnect session active. Run: walletconnect connect');
|
|
723
722
|
|
|
724
|
-
let txTo, txValue, txData
|
|
723
|
+
let txTo, txValue, txData;
|
|
725
724
|
|
|
726
725
|
if (token) {
|
|
727
726
|
// Validate ERC-20 contract
|
|
@@ -730,7 +729,7 @@ async function sendTokensViaWalletConnect({ to, amount, chain, token, max, dryRu
|
|
|
730
729
|
throw new Error(`Address ${token} is not a contract — not a valid ERC-20 token`);
|
|
731
730
|
}
|
|
732
731
|
const decResult = await rpcCall(rpcUrl, 'eth_call', [{ to: token, data: '0x313ce567' }, 'latest']);
|
|
733
|
-
decimals = parseInt(decResult, 16);
|
|
732
|
+
const decimals = parseInt(decResult, 16);
|
|
734
733
|
|
|
735
734
|
if (max) {
|
|
736
735
|
// Max ERC-20: full token balance
|
|
@@ -809,14 +808,11 @@ async function sendTokensViaWalletConnect({ to, amount, chain, token, max, dryRu
|
|
|
809
808
|
chainId,
|
|
810
809
|
});
|
|
811
810
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
} else if (wcResult.signedTransaction) {
|
|
816
|
-
txHash = await broadcastTransaction(wcResult.signedTransaction, chain);
|
|
817
|
-
} else {
|
|
811
|
+
const txHash = await (async () => {
|
|
812
|
+
if (wcResult.txHash) return wcResult.txHash;
|
|
813
|
+
if (wcResult.signedTransaction) return broadcastTransaction(wcResult.signedTransaction, chain);
|
|
818
814
|
throw new Error('No transaction hash or signed transaction returned from WalletConnect');
|
|
819
|
-
}
|
|
815
|
+
})();
|
|
820
816
|
|
|
821
817
|
// Wait for confirmation
|
|
822
818
|
const confirmation = await waitForEvmConfirmation(rpcUrl, txHash);
|
package/src/wallet.js
CHANGED
|
@@ -26,7 +26,7 @@ const SCRYPT_P = 1;
|
|
|
26
26
|
const SCRYPT_KEYLEN = 32;
|
|
27
27
|
const SALT_LEN = 16;
|
|
28
28
|
const IV_LEN = 12;
|
|
29
|
-
const
|
|
29
|
+
const _AUTH_TAG_LEN = 16;
|
|
30
30
|
|
|
31
31
|
import { keccak256 } from './crypto.js';
|
|
32
32
|
|
|
@@ -481,7 +481,7 @@ export function getDefaultAddress(chainType = 'evm') {
|
|
|
481
481
|
* Build wallet command handlers for integration into CLI.
|
|
482
482
|
*/
|
|
483
483
|
export function buildWalletCommands(deps = {}) {
|
|
484
|
-
const { log = console.log, promptFn, exit = process.exit } = deps;
|
|
484
|
+
const { log = console.log, promptFn: _promptFn, exit = process.exit } = deps;
|
|
485
485
|
|
|
486
486
|
return {
|
|
487
487
|
'wallet': async (args, apiInstance, flags, options) => {
|
|
@@ -11,7 +11,7 @@ import { execFile } from 'child_process';
|
|
|
11
11
|
*/
|
|
12
12
|
export function wcExec(cmd, args, timeoutMs = 10000) {
|
|
13
13
|
return new Promise((resolve, reject) => {
|
|
14
|
-
execFile(cmd, args, { timeout: timeoutMs }, (err, stdout,
|
|
14
|
+
execFile(cmd, args, { timeout: timeoutMs }, (err, stdout, _stderr) => {
|
|
15
15
|
if (err) {
|
|
16
16
|
reject(new Error(err.message));
|
|
17
17
|
return;
|
package/src/x402-svm.js
CHANGED
|
@@ -30,11 +30,11 @@ export function base58Encode(buf) {
|
|
|
30
30
|
// ============= Constants =============
|
|
31
31
|
|
|
32
32
|
const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
|
|
33
|
-
const
|
|
33
|
+
const _TOKEN_2022_PROGRAM = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
|
|
34
34
|
const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
|
|
35
35
|
const MEMO_PROGRAM = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
|
|
36
36
|
const ATA_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
|
|
37
|
-
const
|
|
37
|
+
const _SYSTEM_PROGRAM = '11111111111111111111111111111111';
|
|
38
38
|
|
|
39
39
|
const DEFAULT_COMPUTE_UNIT_LIMIT = 20000;
|
|
40
40
|
const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
|
|
@@ -186,7 +186,7 @@ function modInverse(a, mod) {
|
|
|
186
186
|
* Build a Solana MessageV0 from accounts and instructions.
|
|
187
187
|
* Simplified builder for x402 payment transactions.
|
|
188
188
|
*/
|
|
189
|
-
function buildMessageV0({ feePayer, instructions, recentBlockhash, accounts }) {
|
|
189
|
+
function buildMessageV0({ feePayer, instructions, recentBlockhash, accounts: _accounts }) {
|
|
190
190
|
// All unique accounts in order: feePayer first, then signers, then rest
|
|
191
191
|
const accountMap = new Map();
|
|
192
192
|
const feePayerKey = feePayer;
|
package/src/x402.js
CHANGED
|
@@ -154,7 +154,7 @@ export async function createPaymentSignature(response, url, options = {}) {
|
|
|
154
154
|
*/
|
|
155
155
|
export async function checkX402Balance(network) {
|
|
156
156
|
try {
|
|
157
|
-
const { listWallets, exportWallet } = await import('./wallet.js');
|
|
157
|
+
const { listWallets, exportWallet: _exportWallet } = await import('./wallet.js');
|
|
158
158
|
const wallets = listWallets();
|
|
159
159
|
if (!wallets.defaultWallet) return null;
|
|
160
160
|
|