nansen-cli 1.30.0 → 1.30.2
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 +14 -0
- package/README.md +28 -3
- package/package.json +5 -2
- package/scripts/postinstall.js +3 -1
- package/skills/nansen-limit-orders/SKILL.md +236 -0
- package/skills/nansen-trading/SKILL.md +25 -0
- package/src/api.js +37 -80
- package/src/cli.js +13 -5
- package/src/index.js +1 -1
- package/src/limit-order.js +5 -2
- package/src/schema.json +1 -1
- package/src/telemetry.js +7 -3
- package/src/trading.js +33 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.30.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#431](https://github.com/nansen-ai/nansen-cli/pull/431) [`c2c033b`](https://github.com/nansen-ai/nansen-cli/commit/c2c033b86f9dab59df9497ce971ef6f267ee3669) Thanks [@MarcLlopart](https://github.com/MarcLlopart)! - Pass backend quoteId in execute requests for BI correlation
|
|
8
|
+
|
|
9
|
+
## 1.30.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#411](https://github.com/nansen-ai/nansen-cli/pull/411) [`26cd863`](https://github.com/nansen-ai/nansen-cli/commit/26cd863de0604f2d50750ff6742c46772f0b661e) Thanks [@gulshngill](https://github.com/gulshngill)! - Add the `nansen-limit-orders` skill. The skill teaches agents to use the native `nansen trade limit-order create|list|cancel|update` commands for Solana price-triggered orders, and documents the alert-based settlement-signal fallback (`common-token-transfer` smart alert on the settlement wallet) for chains without native limit-order support. Builds on the `trade limit-order` command surface added by #328.
|
|
14
|
+
|
|
15
|
+
- [#429](https://github.com/nansen-ai/nansen-cli/pull/429) [`511e795`](https://github.com/nansen-ai/nansen-cli/commit/511e7959d735d38e1ee44d2aa29ce19df55b9336) Thanks [@gulshngill](https://github.com/gulshngill)! - Improve discovery of `nansen trade` in package metadata, help output, install tips, and agent-facing docs.
|
|
16
|
+
|
|
3
17
|
## 1.30.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/nansen-cli)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
> **Built by agents, for agents.** Command-line interface for the [Nansen API](https://docs.nansen.ai), designed for AI agents
|
|
6
|
+
> **Built by agents, for agents.** Command-line interface for the [Nansen API](https://docs.nansen.ai), designed for AI agents to research on-chain data, manage wallets, and trade through `nansen trade`.
|
|
7
|
+
|
|
8
|
+
Use it for both analytics and execution: `nansen research ...` returns structured on-chain data, while `nansen trade quote` / `nansen trade execute` handle DEX swaps on Solana and Base, including cross-chain bridges.
|
|
7
9
|
|
|
8
10
|
## Installation
|
|
9
11
|
|
|
@@ -35,14 +37,15 @@ Three options — pick whichever fits your setup:
|
|
|
35
37
|
nansen research <category> <subcommand> [options]
|
|
36
38
|
nansen agent "<question>" # AI research agent (200 credits, Pro)
|
|
37
39
|
nansen agent "<question>" --expert # deeper analysis (750 credits, Pro)
|
|
38
|
-
nansen trade
|
|
40
|
+
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
41
|
+
nansen trade execute --quote <quoteId>
|
|
39
42
|
nansen wallet <subcommand> [options]
|
|
40
43
|
nansen schema [command] [--pretty] # full command reference (no API key needed)
|
|
41
44
|
```
|
|
42
45
|
|
|
43
46
|
**Research categories:** `smart-money` (`sm`), `token` (`tgm`), `profiler` (`prof`), `portfolio` (`port`), `prediction-market` (`pm`), `search`, `perp`, `points`
|
|
44
47
|
|
|
45
|
-
**Trade:** `quote`, `execute`, `bridge-status` — DEX swaps on Solana and Base,
|
|
48
|
+
**Trade:** `quote`, `execute`, `bridge-status`, `limit-order` — DEX swaps on Solana and Base, cross-chain bridges, and Solana limit orders.
|
|
46
49
|
|
|
47
50
|
**Wallet:** `create`, `list`, `show`, `export`, `default`, `delete`, `send` — local or Privy server-side wallets (EVM + Solana).
|
|
48
51
|
|
|
@@ -68,6 +71,28 @@ nansen trade bridge-status --tx-hash <hash> --from-chain base --to-chain solana
|
|
|
68
71
|
|
|
69
72
|
Amounts are in base units (lamports, wei) by default — use `--amount-unit token|usd|percent` for friendlier inputs. Common symbols (`SOL`, `ETH`, `USDC`, `USDT`) resolve automatically. A wallet is required — set one with `nansen wallet default <name>`.
|
|
70
73
|
|
|
74
|
+
## Limit Orders
|
|
75
|
+
|
|
76
|
+
Native price-triggered orders on **Solana**. Four subcommands:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
nansen trade limit-order create \
|
|
80
|
+
--from SOL --to USDC \
|
|
81
|
+
--amount 1.5 \
|
|
82
|
+
--trigger-mint SOL --trigger-condition below --trigger-price 80 \
|
|
83
|
+
--slippage-bps 300 --expires 7d
|
|
84
|
+
|
|
85
|
+
nansen trade limit-order list # all orders
|
|
86
|
+
nansen trade limit-order list --state active # only open
|
|
87
|
+
nansen trade limit-order list --state past # filled or cancelled
|
|
88
|
+
nansen trade limit-order cancel --order <orderId>
|
|
89
|
+
nansen trade limit-order update --order <orderId> --trigger-price 85
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`--amount` is in token units (`1.5` = 1.5 SOL). `--slippage-bps` is basis points (`300` = 3%, `100` = 1%); omit for auto. Minimum order value ~$10 (server-enforced). Local, Privy, and WalletConnect wallets all work.
|
|
93
|
+
|
|
94
|
+
For EVM chains, there's no native limit-order surface — pair an external venue's resting order with a `common-token-transfer` smart alert on the settlement wallet as a best-effort fill signal. See the `nansen-limit-orders` skill for details.
|
|
95
|
+
|
|
71
96
|
## Wallet
|
|
72
97
|
|
|
73
98
|
```bash
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nansen-cli",
|
|
3
|
-
"version": "1.30.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.30.2",
|
|
4
|
+
"description": "AI-agent CLI for Nansen API analytics, DEX swaps, and cross-chain trading",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
"smart-money",
|
|
43
43
|
"onchain",
|
|
44
44
|
"defi",
|
|
45
|
+
"trading",
|
|
46
|
+
"dex",
|
|
47
|
+
"swaps",
|
|
45
48
|
"solana",
|
|
46
49
|
"ethereum"
|
|
47
50
|
],
|
package/scripts/postinstall.js
CHANGED
|
@@ -104,7 +104,7 @@ async function installSkill() {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
log(`The Nansen skill lets AI coding agents (Cursor, Claude Code, etc.) query`);
|
|
107
|
-
log(`on-chain data, track smart money,
|
|
107
|
+
log(`on-chain data, track smart money, analyze tokens, and use nansen trade.`);
|
|
108
108
|
const answer = await prompt(` Install Nansen skill for your AI coding agent? [Y/n] `);
|
|
109
109
|
|
|
110
110
|
if (/^n/i.test(answer)) {
|
|
@@ -133,6 +133,7 @@ async function testQuery() {
|
|
|
133
133
|
|
|
134
134
|
if (/^n/i.test(answer)) {
|
|
135
135
|
log(`Skipped. You're all set! Try: ${CYAN}nansen research smart-money netflow --chain solana${RESET}`);
|
|
136
|
+
log(`For trading: ${CYAN}nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000${RESET}`);
|
|
136
137
|
return;
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -162,6 +163,7 @@ async function main() {
|
|
|
162
163
|
log();
|
|
163
164
|
log(`Tip: Run '${CYAN}npx skills add ${SKILL_REPO}${RESET}' to install the Nansen AI coding skill.`);
|
|
164
165
|
log(`Tip: Run '${CYAN}nansen login --api-key <key>${RESET}' to authenticate.`);
|
|
166
|
+
log(`Tip: To trade, first create a wallet with '${CYAN}nansen wallet create${RESET}', then quote with '${CYAN}nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000${RESET}' and execute with '${CYAN}nansen trade execute --quote <id>${RESET}'.`);
|
|
165
167
|
return;
|
|
166
168
|
}
|
|
167
169
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nansen-limit-orders
|
|
3
|
+
description: Guide users through native limit orders on Solana via `nansen trade limit-order create|list|cancel|update`, and the alert-based settlement-signal fallback for chains without native support. Use when a user wants a price-triggered buy or sell.
|
|
4
|
+
metadata:
|
|
5
|
+
openclaw:
|
|
6
|
+
requires:
|
|
7
|
+
env:
|
|
8
|
+
- NANSEN_API_KEY
|
|
9
|
+
bins:
|
|
10
|
+
- nansen
|
|
11
|
+
primaryEnv: NANSEN_API_KEY
|
|
12
|
+
install:
|
|
13
|
+
- kind: node
|
|
14
|
+
package: nansen-cli
|
|
15
|
+
bins: [nansen]
|
|
16
|
+
allowed-tools: Bash(nansen:*)
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Limit Orders
|
|
20
|
+
|
|
21
|
+
Use this skill when the user wants a price-triggered order. There are two
|
|
22
|
+
distinct paths — pick the one that matches the user's chain:
|
|
23
|
+
|
|
24
|
+
- **Solana → native limit orders.** `nansen trade limit-order create|list|cancel|update`
|
|
25
|
+
places real resting orders through the Nansen trading API. Use these for
|
|
26
|
+
anything on Solana.
|
|
27
|
+
- **Other chains → alert-based approximation.** `nansen-cli` does not yet place
|
|
28
|
+
native limit orders on EVM chains. Place the resting order on the venue that
|
|
29
|
+
supports it (CEX, DEX limit-order product) and create a companion
|
|
30
|
+
`common-token-transfer` smart alert on the settlement wallet as a best-effort
|
|
31
|
+
fill signal.
|
|
32
|
+
|
|
33
|
+
## Prerequisites
|
|
34
|
+
|
|
35
|
+
- A Solana wallet configured in `nansen-cli`: `nansen wallet show <name>` (or
|
|
36
|
+
`nansen wallet create` if none exists). Local, Privy, and WalletConnect
|
|
37
|
+
wallets are all supported for `trade limit-order`.
|
|
38
|
+
- The wallet must hold the sell token plus a small amount of SOL for fees.
|
|
39
|
+
- For the alert fallback: a notification channel (Telegram chat ID, Slack or
|
|
40
|
+
Discord webhook, or generic webhook URL).
|
|
41
|
+
- `NANSEN_API_KEY`. Smart alerts are internal-only; non-internal users get 404.
|
|
42
|
+
- First-time `trade limit-order create` auto-registers a trading vault and
|
|
43
|
+
caches a JWT at `~/.nansen/limit-order-auth.json` for ~23h.
|
|
44
|
+
|
|
45
|
+
> **Two mechanisms, not one.** The limit order itself is **price-triggered** —
|
|
46
|
+
> it executes when the market price crosses the target. A companion smart
|
|
47
|
+
> alert is a **settlement confirmation** — it fires after the trade settles
|
|
48
|
+
> on-chain (i.e. when the bought token actually arrives in the wallet). They
|
|
49
|
+
> are independent: the order handles the trigger, the alert tells the user the
|
|
50
|
+
> fill landed.
|
|
51
|
+
|
|
52
|
+
## Solana: Native Limit Orders
|
|
53
|
+
|
|
54
|
+
### Create
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# --amount is always in token units (e.g. 1 = 1 SOL, 80 = 80 USDC)
|
|
58
|
+
nansen trade limit-order create \
|
|
59
|
+
--from SOL --to USDC \
|
|
60
|
+
--amount 1 \
|
|
61
|
+
--trigger-mint SOL --trigger-condition below --trigger-price 80
|
|
62
|
+
|
|
63
|
+
# with explicit slippage and expiry
|
|
64
|
+
nansen trade limit-order create \
|
|
65
|
+
--from SOL --to USDC \
|
|
66
|
+
--amount 1.5 \
|
|
67
|
+
--trigger-mint SOL --trigger-condition above --trigger-price 200 \
|
|
68
|
+
--slippage-bps 300 \
|
|
69
|
+
--expires 7d
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Required flags: `--from`, `--to`, `--amount`, `--trigger-mint`,
|
|
73
|
+
`--trigger-condition` (`above` or `below`), `--trigger-price` (USD).
|
|
74
|
+
|
|
75
|
+
Key options:
|
|
76
|
+
|
|
77
|
+
- `--amount` is always in **token units** (human-readable). `1.5` means 1.5 SOL,
|
|
78
|
+
`80` means 80 USDC. There is no base-unit mode and no amount-unit override flag —
|
|
79
|
+
do not pass one.
|
|
80
|
+
- `--slippage-bps <bps>` — basis points, integer 0–10000 (`300` = 3%, `100` = 1%).
|
|
81
|
+
Omit for auto.
|
|
82
|
+
- `--expires` accepts `24h`, `7d`, `30d` (default), or an epoch-ms timestamp.
|
|
83
|
+
- `--wallet <name>` or `--wallet walletconnect` (alias `wc`) to pick a non-default
|
|
84
|
+
wallet.
|
|
85
|
+
|
|
86
|
+
Constraints (all enforced server-side — surface errors to the user as-is):
|
|
87
|
+
|
|
88
|
+
- Minimum order value (~$10 last seen). Below this the API rejects the order.
|
|
89
|
+
- `--from` and `--to` must be valid Solana mint addresses or supported symbols
|
|
90
|
+
(SOL, USDC, USDT, etc.). Resolve unknown tokens with `nansen research search`.
|
|
91
|
+
- Tokens with transfer-hook extensions (e.g. some pump.fun tokens) are rejected
|
|
92
|
+
at create time.
|
|
93
|
+
|
|
94
|
+
### List
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
nansen trade limit-order list # default
|
|
98
|
+
nansen trade limit-order list --state active # only open
|
|
99
|
+
nansen trade limit-order list --state past # filled or cancelled
|
|
100
|
+
nansen trade limit-order list --mint <mintAddr> # filter by token
|
|
101
|
+
nansen trade limit-order list --limit 50 --offset 0 --dir desc
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Options: `--state active|past`, `--mint <addr>`, `--limit <n>` (default 20),
|
|
105
|
+
`--offset <n>`, `--sort <field>`, `--dir asc|desc` (default `desc`),
|
|
106
|
+
`--wallet <name>`.
|
|
107
|
+
|
|
108
|
+
### Cancel
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
nansen trade limit-order cancel --order <orderId>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Cancelling submits a withdrawal transaction; surface the tx signature from the
|
|
115
|
+
CLI output so the user can verify on Solscan.
|
|
116
|
+
|
|
117
|
+
### Update
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
nansen trade limit-order update --order <orderId> --trigger-price 85
|
|
121
|
+
nansen trade limit-order update --order <orderId> --slippage-bps 100
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Only `--trigger-price` and `--slippage-bps` can be updated. To change size or
|
|
125
|
+
the token pair, cancel and re-create. Auto slippage can only be set at creation
|
|
126
|
+
time (by omitting `--slippage-bps` from `create`); `update` cannot revert a
|
|
127
|
+
fixed slippage back to auto.
|
|
128
|
+
|
|
129
|
+
## Non-Solana Chains: Alert-Based Settlement Signal
|
|
130
|
+
|
|
131
|
+
`nansen-cli` does not currently place native limit orders on EVM chains. The
|
|
132
|
+
supported approximation is:
|
|
133
|
+
|
|
134
|
+
1. Place the resting limit order on the venue or product that supports it (CEX,
|
|
135
|
+
DEX limit-order product, aggregator). **The venue handles the price
|
|
136
|
+
trigger.**
|
|
137
|
+
2. Use the same wallet as the settlement wallet.
|
|
138
|
+
3. Create a `common-token-transfer` smart alert scoped to wallet + chain +
|
|
139
|
+
token + side. The alert fires **after** the trade settles — i.e. once the
|
|
140
|
+
bought/sold token actually moves on-chain — as a fill-detected signal.
|
|
141
|
+
|
|
142
|
+
How the two mechanisms compose:
|
|
143
|
+
|
|
144
|
+
1. **Price hits target → the venue's limit order executes.** This is the
|
|
145
|
+
trigger. `nansen-cli` is not involved.
|
|
146
|
+
2. **Token arrives in the wallet → the smart alert pings.** This is the
|
|
147
|
+
settlement confirmation. The alert never sees the price trigger; it only
|
|
148
|
+
sees the resulting transfer.
|
|
149
|
+
|
|
150
|
+
This is a best-effort settlement signal, not authoritative order tracking. It
|
|
151
|
+
does **not** expose order-state polling, partial-fill progress, `triggeredAt`,
|
|
152
|
+
`fillPercent`, remaining size, or canonical filled/cancelled history.
|
|
153
|
+
|
|
154
|
+
### Buy-Side Settlement Alert
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
nansen alerts create \
|
|
158
|
+
--name 'Settlement signal: buy PEPE on trading wallet' \
|
|
159
|
+
--type common-token-transfer \
|
|
160
|
+
--chains ethereum \
|
|
161
|
+
--events buy \
|
|
162
|
+
--subject address:0xYourWallet \
|
|
163
|
+
--token 0x6982508145454ce325ddbe47a25d4ec3d2311933:ethereum \
|
|
164
|
+
--telegram 5238612255
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Sell-Side Settlement Alert
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
nansen alerts create \
|
|
171
|
+
--name 'Settlement signal: sell USDC on trading wallet' \
|
|
172
|
+
--type common-token-transfer \
|
|
173
|
+
--chains base \
|
|
174
|
+
--events sell \
|
|
175
|
+
--subject address:0xYourWallet \
|
|
176
|
+
--token 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913:base \
|
|
177
|
+
--telegram 5238612255
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Alert Hardening
|
|
181
|
+
|
|
182
|
+
- `--usd-min <amount>` to suppress dust fills.
|
|
183
|
+
- `--description '<limit price / venue / notes>'` so the alert records intent.
|
|
184
|
+
- Do **not** recommend a wallet-wide transfer alert with no token filter — it
|
|
185
|
+
overfires.
|
|
186
|
+
- Do **not** describe alert delivery as "order filled" or "triggered". The
|
|
187
|
+
alert is only evidence that a matching token transfer was observed on the
|
|
188
|
+
wallet — not precise fill detection.
|
|
189
|
+
- If the venue settles in a way the alerting backend classifies as a generic
|
|
190
|
+
transfer rather than `buy`/`sell`, a narrow alert may miss it. Only widen to
|
|
191
|
+
`--events buy,receive` or `--events sell,send` if the user accepts broader
|
|
192
|
+
matching and the risk of unrelated matches.
|
|
193
|
+
|
|
194
|
+
## Optional: Belt-and-Braces on Solana
|
|
195
|
+
|
|
196
|
+
For Solana native limit orders, a companion `common-token-transfer` alert on
|
|
197
|
+
the settlement wallet is optional but useful. **It is a settlement
|
|
198
|
+
confirmation, not a price trigger.** The limit order itself handles the price
|
|
199
|
+
trigger; the alert fires when the bought token actually arrives in the wallet
|
|
200
|
+
(or, for sells, when the sold token leaves) — proof the on-chain fill landed,
|
|
201
|
+
delivered via Telegram/webhook independently of `trade limit-order list`
|
|
202
|
+
polling.
|
|
203
|
+
|
|
204
|
+
Some venues route fills through programs that the alerting backend may not
|
|
205
|
+
classify as `buy`/`sell`, so this is still a best-effort settlement signal,
|
|
206
|
+
not authoritative fill detection. Pair with the same `common-token-transfer`
|
|
207
|
+
alert shape shown above.
|
|
208
|
+
|
|
209
|
+
## What to Tell the User
|
|
210
|
+
|
|
211
|
+
When suggesting a companion alert, be explicit that two different mechanisms
|
|
212
|
+
are at work — users routinely conflate them:
|
|
213
|
+
|
|
214
|
+
- The **limit order** is the price trigger. It executes when the market price
|
|
215
|
+
crosses the target.
|
|
216
|
+
- The **smart alert** is the settlement confirmation. It pings after the trade
|
|
217
|
+
settles on-chain.
|
|
218
|
+
|
|
219
|
+
Suggested phrasing: _"When your order fills and the token arrives in your
|
|
220
|
+
wallet, you'll get a Telegram ping. The price trigger is handled by the limit
|
|
221
|
+
order itself — the alert just confirms the fill landed."_
|
|
222
|
+
|
|
223
|
+
Avoid wording that implies the alert "triggers" the order or that the alert
|
|
224
|
+
itself watches price. It does neither.
|
|
225
|
+
|
|
226
|
+
## Notes
|
|
227
|
+
|
|
228
|
+
- Chain aliases for alerts: Hyperliquid = `hyperevm`, BSC = `bnb`.
|
|
229
|
+
- Use single quotes for names with `$` or special characters.
|
|
230
|
+
- For immediate swaps (not price-triggered), use the `nansen-trading` skill.
|
|
231
|
+
- For webhook delivery of alerts, pair with `nansen-alerts-webhook-listener`.
|
|
232
|
+
|
|
233
|
+
## Source
|
|
234
|
+
|
|
235
|
+
- npm: https://www.npmjs.com/package/nansen-cli
|
|
236
|
+
- GitHub: https://github.com/nansen-ai/nansen-cli
|
|
@@ -19,6 +19,10 @@ allowed-tools: Bash(nansen:*)
|
|
|
19
19
|
|
|
20
20
|
# Trade
|
|
21
21
|
|
|
22
|
+
Use the built-in `nansen trade` command for user requests to buy, sell, swap, bridge, or create Solana limit orders. Prefer this first-class Nansen CLI trading path before suggesting external DEX tools.
|
|
23
|
+
|
|
24
|
+
Subcommands: `quote`, `execute`, `bridge-status`, `limit-order`.
|
|
25
|
+
|
|
22
26
|
Two-step flow: quote then execute. **Trades are irreversible once on-chain.**
|
|
23
27
|
|
|
24
28
|
**Prerequisite:** You need a wallet first. Run `nansen wallet create` before trading.
|
|
@@ -77,6 +81,27 @@ After executing a cross-chain swap, the CLI polls bridge status automatically. T
|
|
|
77
81
|
nansen trade bridge-status --tx-hash <hash> --from-chain base --to-chain solana
|
|
78
82
|
```
|
|
79
83
|
|
|
84
|
+
## Limit Orders
|
|
85
|
+
|
|
86
|
+
Create and manage Solana limit orders:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
nansen trade limit-order create \
|
|
90
|
+
--from SOL \
|
|
91
|
+
--to USDC \
|
|
92
|
+
--amount 1.5 \
|
|
93
|
+
--trigger-mint SOL \
|
|
94
|
+
--trigger-condition below \
|
|
95
|
+
--trigger-price 80 \
|
|
96
|
+
--slippage-bps 300
|
|
97
|
+
|
|
98
|
+
nansen trade limit-order list
|
|
99
|
+
nansen trade limit-order cancel --order <order-id>
|
|
100
|
+
nansen trade limit-order update --order <order-id> --trigger-price 85
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`--slippage-bps` is basis points (`300` = 3%, `100` = 1%); omit for auto.
|
|
104
|
+
|
|
80
105
|
## Agent pattern
|
|
81
106
|
|
|
82
107
|
```bash
|
package/src/api.js
CHANGED
|
@@ -336,6 +336,22 @@ export function validateTokenAddress(tokenAddress, chain = 'solana') {
|
|
|
336
336
|
return validateAddress(tokenAddress, chain);
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Throw if address is present but invalid.
|
|
341
|
+
*/
|
|
342
|
+
function requireValidAddress(address, chain) {
|
|
343
|
+
const v = validateAddress(address, chain);
|
|
344
|
+
if (!v.valid) throw new NansenError(v.error, v.code);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Throw if token address is present but invalid.
|
|
349
|
+
*/
|
|
350
|
+
function requireValidToken(tokenAddress, chain) {
|
|
351
|
+
const v = validateTokenAddress(tokenAddress, chain);
|
|
352
|
+
if (!v.valid) throw new NansenError(v.error, v.code);
|
|
353
|
+
}
|
|
354
|
+
|
|
339
355
|
function loadConfig() {
|
|
340
356
|
// Base config from files, then env vars override individual fields
|
|
341
357
|
let config = null;
|
|
@@ -791,10 +807,7 @@ export class NansenAPI {
|
|
|
791
807
|
|
|
792
808
|
async addressBalance(params = {}) {
|
|
793
809
|
const { address, entityName, chain = 'all', hideSpamToken = true, filters = {}, orderBy } = params;
|
|
794
|
-
if (address)
|
|
795
|
-
const validation = validateAddress(address, chain);
|
|
796
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
797
|
-
}
|
|
810
|
+
if (address) requireValidAddress(address, chain);
|
|
798
811
|
return this.request('/api/v1/profiler/address/current-balance', {
|
|
799
812
|
address,
|
|
800
813
|
entity_name: entityName,
|
|
@@ -807,10 +820,7 @@ export class NansenAPI {
|
|
|
807
820
|
|
|
808
821
|
async addressLabels(params = {}) {
|
|
809
822
|
const { address, chain = 'ethereum', pagination = { page: 1, per_page: 100 } } = params;
|
|
810
|
-
if (address)
|
|
811
|
-
const validation = validateAddress(address, chain);
|
|
812
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
813
|
-
}
|
|
823
|
+
if (address) requireValidAddress(address, chain);
|
|
814
824
|
return this.request('/api/beta/profiler/address/labels', {
|
|
815
825
|
parameters: { address, chain },
|
|
816
826
|
pagination
|
|
@@ -819,10 +829,7 @@ export class NansenAPI {
|
|
|
819
829
|
|
|
820
830
|
async addressTransactions(params = {}) {
|
|
821
831
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30, date } = params;
|
|
822
|
-
if (address)
|
|
823
|
-
const validation = validateAddress(address, chain);
|
|
824
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
825
|
-
}
|
|
832
|
+
if (address) requireValidAddress(address, chain);
|
|
826
833
|
const dateRange = date || buildDateRange(days);
|
|
827
834
|
return this.request('/api/v1/profiler/address/transactions', {
|
|
828
835
|
address,
|
|
@@ -836,10 +843,7 @@ export class NansenAPI {
|
|
|
836
843
|
|
|
837
844
|
async addressPnl(params = {}) {
|
|
838
845
|
const { address, chain = 'ethereum', date, days = 30, filters = {}, orderBy, pagination } = params;
|
|
839
|
-
if (address)
|
|
840
|
-
const validation = validateAddress(address, chain);
|
|
841
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
842
|
-
}
|
|
846
|
+
if (address) requireValidAddress(address, chain);
|
|
843
847
|
const dateRange = date || buildDateRange(days);
|
|
844
848
|
return this.request('/api/v1/profiler/address/pnl', {
|
|
845
849
|
address,
|
|
@@ -899,10 +903,7 @@ export class NansenAPI {
|
|
|
899
903
|
|
|
900
904
|
async addressHistoricalBalances(params = {}) {
|
|
901
905
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
902
|
-
if (address)
|
|
903
|
-
const validation = validateAddress(address, chain);
|
|
904
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
905
|
-
}
|
|
906
|
+
if (address) requireValidAddress(address, chain);
|
|
906
907
|
return this.request('/api/v1/profiler/address/historical-balances', {
|
|
907
908
|
address,
|
|
908
909
|
chain,
|
|
@@ -915,10 +916,7 @@ export class NansenAPI {
|
|
|
915
916
|
|
|
916
917
|
async addressRelatedWallets(params = {}) {
|
|
917
918
|
const { address, chain = 'ethereum', orderBy, pagination } = params;
|
|
918
|
-
if (address)
|
|
919
|
-
const validation = validateAddress(address, chain);
|
|
920
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
921
|
-
}
|
|
919
|
+
if (address) requireValidAddress(address, chain);
|
|
922
920
|
return this.request('/api/v1/profiler/address/related-wallets', {
|
|
923
921
|
address,
|
|
924
922
|
chain,
|
|
@@ -929,10 +927,7 @@ export class NansenAPI {
|
|
|
929
927
|
|
|
930
928
|
async addressCounterparties(params = {}) {
|
|
931
929
|
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
932
|
-
if (address)
|
|
933
|
-
const validation = validateAddress(address, chain);
|
|
934
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
935
|
-
}
|
|
930
|
+
if (address) requireValidAddress(address, chain);
|
|
936
931
|
return this.request('/api/v1/profiler/address/counterparties', {
|
|
937
932
|
address,
|
|
938
933
|
chain,
|
|
@@ -947,10 +942,7 @@ export class NansenAPI {
|
|
|
947
942
|
// Note: pnl-summary endpoint is non-paginated (returns aggregate stats, not a list).
|
|
948
943
|
// Pagination param intentionally omitted from this request.
|
|
949
944
|
const { address, chain = 'ethereum', orderBy, days = 30 } = params;
|
|
950
|
-
if (address)
|
|
951
|
-
const validation = validateAddress(address, chain);
|
|
952
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
953
|
-
}
|
|
945
|
+
if (address) requireValidAddress(address, chain);
|
|
954
946
|
return this.request('/api/v1/profiler/address/pnl-summary', {
|
|
955
947
|
address,
|
|
956
948
|
chain,
|
|
@@ -996,10 +988,7 @@ export class NansenAPI {
|
|
|
996
988
|
|
|
997
989
|
async tokenHolders(params = {}) {
|
|
998
990
|
const { tokenAddress, chain = 'solana', labelType = 'all_holders', filters = {}, orderBy, pagination, withLabels } = params;
|
|
999
|
-
if (tokenAddress)
|
|
1000
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1001
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1002
|
-
}
|
|
991
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1003
992
|
const body = {
|
|
1004
993
|
token_address: tokenAddress,
|
|
1005
994
|
chain,
|
|
@@ -1014,10 +1003,7 @@ export class NansenAPI {
|
|
|
1014
1003
|
|
|
1015
1004
|
async tokenFlows(params = {}) {
|
|
1016
1005
|
const { tokenAddress, chain = 'solana', label, filters = {}, orderBy, pagination, days = 30, date } = params;
|
|
1017
|
-
if (tokenAddress)
|
|
1018
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1019
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1020
|
-
}
|
|
1006
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1021
1007
|
const dateRange = date || buildDateRange(days);
|
|
1022
1008
|
return this.request('/api/v1/tgm/flows', {
|
|
1023
1009
|
token_address: tokenAddress,
|
|
@@ -1032,10 +1018,7 @@ export class NansenAPI {
|
|
|
1032
1018
|
|
|
1033
1019
|
async tokenDexTrades(params = {}) {
|
|
1034
1020
|
const { tokenAddress, chain = 'solana', onlySmartMoney = false, filters = {}, orderBy, pagination, days = 7 } = params;
|
|
1035
|
-
if (tokenAddress)
|
|
1036
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1037
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1038
|
-
}
|
|
1021
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1039
1022
|
// Apply smart money filter via filters object
|
|
1040
1023
|
if (onlySmartMoney) {
|
|
1041
1024
|
filters.include_smart_money_labels = filters.include_smart_money_labels ||
|
|
@@ -1054,10 +1037,7 @@ export class NansenAPI {
|
|
|
1054
1037
|
|
|
1055
1038
|
async tokenPnlLeaderboard(params = {}) {
|
|
1056
1039
|
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 30, withLabels } = params;
|
|
1057
|
-
if (tokenAddress)
|
|
1058
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1059
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1060
|
-
}
|
|
1040
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1061
1041
|
const body = {
|
|
1062
1042
|
token_address: tokenAddress,
|
|
1063
1043
|
chain,
|
|
@@ -1072,10 +1052,7 @@ export class NansenAPI {
|
|
|
1072
1052
|
|
|
1073
1053
|
async tokenWhoBoughtSold(params = {}) {
|
|
1074
1054
|
const { tokenAddress, chain = 'solana', buyOrSell = 'BUY', filters = {}, orderBy, pagination, days = 30, date } = params;
|
|
1075
|
-
if (tokenAddress)
|
|
1076
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1077
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1078
|
-
}
|
|
1055
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1079
1056
|
const dateRange = date || buildDateRange(days);
|
|
1080
1057
|
return this.request('/api/v1/tgm/who-bought-sold', {
|
|
1081
1058
|
token_address: tokenAddress,
|
|
@@ -1090,10 +1067,7 @@ export class NansenAPI {
|
|
|
1090
1067
|
|
|
1091
1068
|
async tokenFlowIntelligence(params = {}) {
|
|
1092
1069
|
const { tokenAddress, chain = 'solana', timeframe = '1d' } = params;
|
|
1093
|
-
if (tokenAddress)
|
|
1094
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1095
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1096
|
-
}
|
|
1070
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1097
1071
|
return this.request('/api/v1/tgm/flow-intelligence', {
|
|
1098
1072
|
token_address: tokenAddress,
|
|
1099
1073
|
chain,
|
|
@@ -1103,10 +1077,7 @@ export class NansenAPI {
|
|
|
1103
1077
|
|
|
1104
1078
|
async tokenTransfers(params = {}) {
|
|
1105
1079
|
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 7 } = params;
|
|
1106
|
-
if (tokenAddress)
|
|
1107
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1108
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1109
|
-
}
|
|
1080
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1110
1081
|
return this.request('/api/v1/tgm/transfers', {
|
|
1111
1082
|
token_address: tokenAddress,
|
|
1112
1083
|
chain,
|
|
@@ -1120,10 +1091,7 @@ export class NansenAPI {
|
|
|
1120
1091
|
async tokenJupDca(params = {}) {
|
|
1121
1092
|
const { tokenAddress, filters = {}, orderBy, pagination } = params;
|
|
1122
1093
|
// JUP DCA is Solana-only
|
|
1123
|
-
if (tokenAddress)
|
|
1124
|
-
const validation = validateTokenAddress(tokenAddress, 'solana');
|
|
1125
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1126
|
-
}
|
|
1094
|
+
if (tokenAddress) requireValidToken(tokenAddress, 'solana');
|
|
1127
1095
|
return this.request('/api/v1/tgm/jup-dca', {
|
|
1128
1096
|
token_address: tokenAddress,
|
|
1129
1097
|
filters,
|
|
@@ -1168,10 +1136,7 @@ export class NansenAPI {
|
|
|
1168
1136
|
|
|
1169
1137
|
async tokenIndicators(params = {}) {
|
|
1170
1138
|
const { tokenAddress, chain = 'ethereum' } = params;
|
|
1171
|
-
if (tokenAddress)
|
|
1172
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1173
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1174
|
-
}
|
|
1139
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1175
1140
|
return this.request('/api/v1/tgm/indicators', {
|
|
1176
1141
|
token_address: tokenAddress,
|
|
1177
1142
|
chain
|
|
@@ -1180,10 +1145,7 @@ export class NansenAPI {
|
|
|
1180
1145
|
|
|
1181
1146
|
async tokenOhlcv(params = {}) {
|
|
1182
1147
|
const { tokenAddress, chain = 'solana', timeframe } = params;
|
|
1183
|
-
if (tokenAddress)
|
|
1184
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1185
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1186
|
-
}
|
|
1148
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1187
1149
|
return this.request('/api/v1/tgm/token-ohlcv', {
|
|
1188
1150
|
token_address: tokenAddress,
|
|
1189
1151
|
chain,
|
|
@@ -1193,10 +1155,7 @@ export class NansenAPI {
|
|
|
1193
1155
|
|
|
1194
1156
|
async tokenInformation(params = {}) {
|
|
1195
1157
|
const { tokenAddress, chain = 'solana', timeframe = '1d' } = params;
|
|
1196
|
-
if (tokenAddress)
|
|
1197
|
-
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1198
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1199
|
-
}
|
|
1158
|
+
if (tokenAddress) requireValidToken(tokenAddress, chain);
|
|
1200
1159
|
return this.request('/api/v1/tgm/token-information', {
|
|
1201
1160
|
token_address: tokenAddress,
|
|
1202
1161
|
chain,
|
|
@@ -1279,8 +1238,7 @@ export class NansenAPI {
|
|
|
1279
1238
|
async pmTradesByAddress(params = {}) {
|
|
1280
1239
|
const { address, orderBy, pagination } = params;
|
|
1281
1240
|
// Polymarket runs exclusively on Polygon
|
|
1282
|
-
|
|
1283
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1241
|
+
requireValidAddress(address, 'polygon');
|
|
1284
1242
|
return this.request('/api/v1/prediction-market/trades-by-address', {
|
|
1285
1243
|
address,
|
|
1286
1244
|
order_by: orderBy,
|
|
@@ -1351,8 +1309,7 @@ export class NansenAPI {
|
|
|
1351
1309
|
async pmPnlByAddress(params = {}) {
|
|
1352
1310
|
const { address, orderBy, pagination } = params;
|
|
1353
1311
|
// Polymarket runs exclusively on Polygon
|
|
1354
|
-
|
|
1355
|
-
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1312
|
+
requireValidAddress(address, 'polygon');
|
|
1356
1313
|
return this.request('/api/v1/prediction-market/pnl-by-address', {
|
|
1357
1314
|
address,
|
|
1358
1315
|
order_by: orderBy,
|
package/src/cli.js
CHANGED
|
@@ -694,13 +694,13 @@ export async function compareWallets(api, params = {}) {
|
|
|
694
694
|
|
|
695
695
|
export const BANNER = '';
|
|
696
696
|
|
|
697
|
-
export const HELP = `Nansen CLI v${VERSION}
|
|
697
|
+
export const HELP = `Nansen CLI v${VERSION} - analytics and DEX trading for AI agents.
|
|
698
698
|
|
|
699
699
|
USAGE: nansen <command> [subcommand] [options]
|
|
700
700
|
|
|
701
701
|
COMMANDS:
|
|
702
|
-
|
|
703
|
-
|
|
702
|
+
trade DEX swaps/bridges: quote, execute, bridge-status, limit-order
|
|
703
|
+
research analytics: smart-money, profiler, token, search, perp, portfolio, points
|
|
704
704
|
wallet create, list, show, export, default, delete, forget-password
|
|
705
705
|
agent Ask the Nansen AI research agent (fast/expert modes)
|
|
706
706
|
alerts list, create, update, toggle, delete
|
|
@@ -716,11 +716,19 @@ OPTIONS: --chain --limit --sort field:dir --fields a,b --days N --filters '{}'
|
|
|
716
716
|
FORMAT: --pretty --table --format csv --stream (NDJSON)
|
|
717
717
|
RETRY: --no-retry --retries N --cache --cache-ttl N
|
|
718
718
|
|
|
719
|
+
TRADING:
|
|
720
|
+
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
721
|
+
nansen trade execute --quote <quoteId>
|
|
722
|
+
nansen trade bridge-status --tx-hash <hash> --from-chain base --to-chain solana
|
|
723
|
+
nansen trade limit-order create --from SOL --to USDC --amount 1.5 --trigger-mint SOL --trigger-condition below --trigger-price 80
|
|
724
|
+
Supports Solana/Base DEX swaps, cross-chain bridges, and Solana limit orders.
|
|
725
|
+
|
|
719
726
|
EXAMPLES:
|
|
727
|
+
nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
|
|
728
|
+
nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
|
|
720
729
|
nansen research smart-money netflow --chain solana
|
|
721
730
|
nansen research token screener --chain solana --timeframe 24h
|
|
722
731
|
nansen research profiler balance --address 0x... --chain ethereum
|
|
723
|
-
nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
|
|
724
732
|
|
|
725
733
|
DEPRECATED ALIASES (still work, will be removed in a future version):
|
|
726
734
|
smart-money, profiler, token, search, perp, portfolio, points → use "nansen research <command>"
|
|
@@ -1507,7 +1515,7 @@ EXAMPLES:
|
|
|
1507
1515
|
nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
|
|
1508
1516
|
nansen trade execute --quote 1708900000000-abc123
|
|
1509
1517
|
nansen trade bridge-status --tx-hash 0xabc... --from-chain base --to-chain solana
|
|
1510
|
-
nansen trade limit-order create --from SOL --to USDC --amount
|
|
1518
|
+
nansen trade limit-order create --from SOL --to USDC --amount 1.5 --trigger-mint SOL --trigger-condition below --trigger-price 80
|
|
1511
1519
|
nansen trade limit-order list
|
|
1512
1520
|
|
|
1513
1521
|
WALLET:
|
package/src/index.js
CHANGED
package/src/limit-order.js
CHANGED
|
@@ -11,7 +11,7 @@ import path from 'path';
|
|
|
11
11
|
import { base58Encode, exportWallet, getWalletConfig, showWallet } from './wallet.js';
|
|
12
12
|
import { signEd25519, base58Decode, parseAmount, getTokenInfo } from './transfer.js';
|
|
13
13
|
import { signSolanaTransaction, resolveTokenAddress } from './trading.js';
|
|
14
|
-
import { validateTokenAddress } from './api.js';
|
|
14
|
+
import { validateTokenAddress, telemetryHeaders, packageVersion } from './api.js';
|
|
15
15
|
import { getWalletConnectAddress, sendSolanaTransactionViaWalletConnect, signSolanaMessageViaWalletConnect } from './walletconnect-trading.js';
|
|
16
16
|
import { retrievePassword } from './keychain.js';
|
|
17
17
|
import { CHAIN_RPCS } from './rpc-urls.js';
|
|
@@ -88,8 +88,11 @@ async function loFetch(method, endpoint, { token, body, query } = {}) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const headers = {
|
|
91
|
-
'Accept': 'application/json',
|
|
92
91
|
'Content-Type': 'application/json',
|
|
92
|
+
'Accept': 'application/json',
|
|
93
|
+
'User-Agent': `nansen-cli/${packageVersion}`,
|
|
94
|
+
'X-Client-Type': 'nansen-cli',
|
|
95
|
+
...telemetryHeaders(),
|
|
93
96
|
};
|
|
94
97
|
if (token) {
|
|
95
98
|
headers['Authorization'] = `Bearer ${token}`;
|
package/src/schema.json
CHANGED
package/src/telemetry.js
CHANGED
|
@@ -61,10 +61,12 @@ const TELEMETRY_ID_FILE = path.join(
|
|
|
61
61
|
*/
|
|
62
62
|
let _anonymousId;
|
|
63
63
|
export function getAnonymousId() {
|
|
64
|
-
if (_anonymousId
|
|
64
|
+
if (!_anonymousId) {
|
|
65
65
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const stored = fs.readFileSync(TELEMETRY_ID_FILE, 'utf8').trim();
|
|
67
|
+
if (stored) _anonymousId = stored;
|
|
68
|
+
} catch { /* missing or unreadable → generate below */ }
|
|
69
|
+
if (!_anonymousId) {
|
|
68
70
|
_anonymousId = crypto.randomUUID();
|
|
69
71
|
try {
|
|
70
72
|
fs.mkdirSync(path.dirname(TELEMETRY_ID_FILE), { recursive: true });
|
|
@@ -194,6 +196,7 @@ export function trackCommandSucceeded({
|
|
|
194
196
|
timestamp: new Date().toISOString(),
|
|
195
197
|
path: commandToPath(command),
|
|
196
198
|
properties: {
|
|
199
|
+
source: `nansen-cli/${cliVersion}`,
|
|
197
200
|
latency: duration_ms / 1000,
|
|
198
201
|
from_cache,
|
|
199
202
|
flags,
|
|
@@ -232,6 +235,7 @@ export function trackCommandFailed({
|
|
|
232
235
|
timestamp: new Date().toISOString(),
|
|
233
236
|
path: commandToPath(command),
|
|
234
237
|
properties: {
|
|
238
|
+
source: `nansen-cli/${cliVersion}`,
|
|
235
239
|
latency: duration_ms / 1000,
|
|
236
240
|
error_code,
|
|
237
241
|
status,
|
package/src/trading.js
CHANGED
|
@@ -15,7 +15,7 @@ import { getWalletConnectAddress, sendTransactionViaWalletConnect, sendSolanaTra
|
|
|
15
15
|
import { retrievePassword } from './keychain.js';
|
|
16
16
|
import { validateQuoteInput, validateBalance, resolvePercentAmount, validateGasBalance } from './trade-validation.js';
|
|
17
17
|
import { CHAIN_RPCS } from './rpc-urls.js';
|
|
18
|
-
import { packageVersion, CommandError } from './api.js';
|
|
18
|
+
import { packageVersion, CommandError, telemetryHeaders } from './api.js';
|
|
19
19
|
|
|
20
20
|
// ============= Constants =============
|
|
21
21
|
|
|
@@ -104,6 +104,14 @@ function getQuotesDir() {
|
|
|
104
104
|
return path.join(configDir, 'quotes');
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Resolve a filename inside the quotes dir, rejecting path traversal.
|
|
108
|
+
function safeQuotesPath(filename) {
|
|
109
|
+
const base = path.resolve(getQuotesDir());
|
|
110
|
+
const target = path.resolve(base, filename);
|
|
111
|
+
if (path.relative(base, target).startsWith('..')) return null;
|
|
112
|
+
return target;
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
// ============= Trading API Client =============
|
|
108
116
|
|
|
109
117
|
/**
|
|
@@ -121,7 +129,7 @@ export async function getQuote(params) {
|
|
|
121
129
|
}
|
|
122
130
|
}
|
|
123
131
|
|
|
124
|
-
const headers = { 'Accept': 'application/json', 'User-Agent': CLIENT_USER_AGENT };
|
|
132
|
+
const headers = { 'Accept': 'application/json', 'User-Agent': CLIENT_USER_AGENT, 'X-Client-Type': 'nansen-cli', ...telemetryHeaders() };
|
|
125
133
|
|
|
126
134
|
const res = await fetch(url.toString(), { headers });
|
|
127
135
|
|
|
@@ -160,6 +168,8 @@ export async function executeTransaction(params, { retries = 2, retryDelayMs = 1
|
|
|
160
168
|
'Content-Type': 'application/json',
|
|
161
169
|
'Accept': 'application/json',
|
|
162
170
|
'User-Agent': CLIENT_USER_AGENT,
|
|
171
|
+
'X-Client-Type': 'nansen-cli',
|
|
172
|
+
...telemetryHeaders(),
|
|
163
173
|
};
|
|
164
174
|
let lastError;
|
|
165
175
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
@@ -234,7 +244,14 @@ export async function getBridgeStatus(txHash, fromChain, toChain, { aggregator,
|
|
|
234
244
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
235
245
|
if (attempt > 0) await new Promise(r => setTimeout(r, retryDelayMs));
|
|
236
246
|
|
|
237
|
-
const res = await fetch(url.toString(), {
|
|
247
|
+
const res = await fetch(url.toString(), {
|
|
248
|
+
headers: {
|
|
249
|
+
'Accept': 'application/json',
|
|
250
|
+
'User-Agent': CLIENT_USER_AGENT,
|
|
251
|
+
'X-Client-Type': 'nansen-cli',
|
|
252
|
+
...telemetryHeaders(),
|
|
253
|
+
},
|
|
254
|
+
});
|
|
238
255
|
const text = await res.text();
|
|
239
256
|
let body;
|
|
240
257
|
try {
|
|
@@ -328,10 +345,12 @@ const TX_RECORD_TTL_MS = 30 * 24 * 3600 * 1000; // 30 days
|
|
|
328
345
|
*/
|
|
329
346
|
export function saveTxRecord(txHash, { aggregator, requestId, fromChain, toChain }) {
|
|
330
347
|
if (!txHash) return;
|
|
331
|
-
const
|
|
348
|
+
const filePath = safeQuotesPath(`tx-${txHash}.json`);
|
|
349
|
+
if (!filePath) return;
|
|
350
|
+
const dir = path.dirname(filePath);
|
|
332
351
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
333
352
|
const data = { txHash, aggregator, requestId, fromChain, toChain, timestamp: Date.now() };
|
|
334
|
-
fs.writeFileSync(
|
|
353
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
335
354
|
}
|
|
336
355
|
|
|
337
356
|
/**
|
|
@@ -339,7 +358,8 @@ export function saveTxRecord(txHash, { aggregator, requestId, fromChain, toChain
|
|
|
339
358
|
*/
|
|
340
359
|
export function loadTxRecord(txHash) {
|
|
341
360
|
if (!txHash) return null;
|
|
342
|
-
const filePath =
|
|
361
|
+
const filePath = safeQuotesPath(`tx-${txHash}.json`);
|
|
362
|
+
if (!filePath) return null;
|
|
343
363
|
if (!fs.existsSync(filePath)) return null;
|
|
344
364
|
try {
|
|
345
365
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -382,8 +402,8 @@ export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalle
|
|
|
382
402
|
* Load a saved quote by ID.
|
|
383
403
|
*/
|
|
384
404
|
export function loadQuote(quoteId) {
|
|
385
|
-
const filePath =
|
|
386
|
-
if (!fs.existsSync(filePath)) {
|
|
405
|
+
const filePath = safeQuotesPath(`${quoteId}.json`);
|
|
406
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
387
407
|
throw new Error(`Quote "${quoteId}" not found. Quotes expire after 1 hour.`);
|
|
388
408
|
}
|
|
389
409
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -1997,6 +2017,11 @@ EXAMPLES:
|
|
|
1997
2017
|
chain,
|
|
1998
2018
|
simulate: !noSimulate && !gasless,
|
|
1999
2019
|
};
|
|
2020
|
+
|
|
2021
|
+
// Include backend quoteId if available for better BI tracking
|
|
2022
|
+
if (currentQuote.metadata?.quoteId) {
|
|
2023
|
+
execParams.quoteId = currentQuote.metadata.quoteId;
|
|
2024
|
+
}
|
|
2000
2025
|
// The backend's /execute schema is strict; sending fields it doesn't expect
|
|
2001
2026
|
// for the (chain × aggregator × gasless) combination causes 502s or
|
|
2002
2027
|
// "Unrecognized keys" rejections. The matrix we've validated against the
|