nansen-cli 1.26.1 → 1.27.1

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.27.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#408](https://github.com/nansen-ai/nansen-cli/pull/408) [`d1e9787`](https://github.com/nansen-ai/nansen-cli/commit/d1e97871b897153a9e1cd587897ff2f4bbed6c88) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Add cross-chain notes to trade help text and document --to-chain constraints in schema.json.
8
+
9
+ ## 1.27.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#403](https://github.com/nansen-ai/nansen-cli/pull/403) [`fe53dbe`](https://github.com/nansen-ai/nansen-cli/commit/fe53dbe8cf743b970e145e7ad7a00470f16f28df) Thanks [@marius-reed](https://github.com/marius-reed)! - Add prediction market filtering (order_by, volume/liquidity/OI/trader/price/date filters, neg_risk, tags) and address-summary endpoint
14
+
15
+ ### Patch Changes
16
+
17
+ - [#402](https://github.com/nansen-ai/nansen-cli/pull/402) [`cfd94ce`](https://github.com/nansen-ai/nansen-cli/commit/cfd94ce1e1880e36fb0c97d0ecfe37e614898006) Thanks [@TimNooren](https://github.com/TimNooren)! - Enforce USDC or native token on one side of every swap
18
+
3
19
  ## 1.26.1
4
20
 
5
21
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.26.1",
3
+ "version": "1.27.1",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,386 @@
1
+ ---
2
+ name: nansen-alerts-webhook-listener
3
+ description: Set up a local webhook server to receive Nansen smart alerts in real-time with HMAC signature verification and public tunneling. Use when a user wants to listen for alerts on their local machine.
4
+ metadata:
5
+ openclaw:
6
+ requires:
7
+ env:
8
+ - NANSEN_API_KEY
9
+ bins:
10
+ - nansen
11
+ - node
12
+ primaryEnv: NANSEN_API_KEY
13
+ install:
14
+ - kind: node
15
+ package: nansen-cli
16
+ bins: [nansen]
17
+ allowed-tools: Bash(nansen:*), Bash(node:*), Bash(npx:*), Bash(ngrok:*), Write
18
+ ---
19
+
20
+ # Alert Webhook Listener
21
+
22
+ Set up a local HTTP server to receive Nansen smart alert webhook payloads in real-time.
23
+
24
+ ## How It Works
25
+
26
+ Nansen smart alerts support a **webhook** channel type. When an alert fires, Nansen sends an HTTP POST with a JSON payload to your webhook URL. This skill sets up:
27
+
28
+ 1. A local HTTP server (Node.js, zero external dependencies) that receives and displays alert payloads
29
+ 2. HMAC-SHA256 signature verification so only authentic Nansen payloads are accepted
30
+ 3. A public tunnel so Nansen's servers can reach your local machine
31
+
32
+ **This skill does NOT create or modify alerts.** It sets up the listener infrastructure and then provides a summary of what the user needs to do to start receiving alerts.
33
+
34
+ **OpenClaw users:** If OpenClaw is running locally on the same machine, the webhook server can forward verified alert payloads to OpenClaw's Gateway (`/hooks/agent`), triggering an agent turn for each alert. Set the `OPENCLAW_GATEWAY_URL` env var to enable this. See the **OpenClaw Integration** section below.
35
+
36
+ ## Security Warning
37
+
38
+ **Before proceeding, inform the user:**
39
+
40
+ > This skill starts an HTTP server on your machine and exposes it to the internet via a tunnel (ngrok or localtunnel). While the server only binds to localhost (`127.0.0.1`) — meaning no one on your local network can access it directly — the tunnel creates a public URL that **anyone on the internet** can send requests to.
41
+ >
42
+ > **Mitigations in place:**
43
+ > - HMAC-SHA256 signature verification rejects all requests not signed by Nansen
44
+ > - 1 MB body size limit prevents memory abuse
45
+ > - Only `POST /webhook` and `GET /health` are accepted; everything else returns 404
46
+ >
47
+ > **You should be aware that:**
48
+ > - The tunnel URL is publicly discoverable (ngrok URLs can be enumerated)
49
+ > - Unsigned requests still reach your machine — they're rejected, but the connection is made
50
+ > - Stop the tunnel when you're done to close the public endpoint
51
+
52
+ Wait for the user to confirm they want to proceed before continuing.
53
+
54
+ ## Execution Plan
55
+
56
+ Follow these steps **in order**. Do not skip signature verification — it is mandatory.
57
+
58
+ ### Step 0: Choose a tunnel provider
59
+
60
+ Before starting, ask the user which tunnel provider they want to use:
61
+
62
+ | | **ngrok** (recommended) | **localtunnel** |
63
+ |---|---|---|
64
+ | Stability | Stable — persistent connections with keepalive | Flaky — free relay drops idle connections without warning, tunnels die randomly |
65
+ | Install | `brew install ngrok` + free account at ngrok.com | Zero install (`npx localtunnel`) |
66
+ | HTTPS | Yes | Yes |
67
+ | Auth required | Yes (free authtoken from ngrok.com) | No |
68
+
69
+ **Recommend ngrok.** localtunnel is convenient but unreliable — in testing, tunnels silently exit after minutes, causing alerts to fail with "503 Tunnel Unavailable". ngrok maintains stable connections.
70
+
71
+ Check if ngrok is available:
72
+ ```bash
73
+ which ngrok && ngrok version
74
+ ```
75
+
76
+ If not installed, tell the user:
77
+ 1. `brew install ngrok` (or download from ngrok.com)
78
+ 2. Create a free account at ngrok.com and copy the authtoken
79
+ 3. `ngrok config add-authtoken <token>`
80
+
81
+ If the user prefers localtunnel or can't install ngrok, proceed with localtunnel but warn them that the tunnel may drop and they'll need to restart it and update their alert's webhook URL.
82
+
83
+ ### Step 1: Generate a webhook secret
84
+
85
+ ```bash
86
+ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
87
+ ```
88
+
89
+ Store the output — you need it for both the server and the alert configuration. **Never log or echo the secret after this point.**
90
+
91
+ ### Step 2: Write the webhook receiver script
92
+
93
+ Create `nansen-webhook-server.mjs` in the current working directory. Use **only** Node.js built-in modules (`node:http`, `node:crypto`). No `npm install` required.
94
+
95
+ **Requirements — do not deviate:**
96
+
97
+ | Requirement | Detail |
98
+ |---|---|
99
+ | Bind address | `127.0.0.1` only — **never** `0.0.0.0` |
100
+ | Default port | `9477` (override via `PORT` env var) |
101
+ | Webhook path | `POST /webhook` — reject all other method/path combos with 404 |
102
+ | Health check | `GET /health` → 200 `{"status":"ok"}` |
103
+ | Signature verification | Verify `x-nansen-signature` header using HMAC-SHA256 with timing-safe comparison. Reject 401 on mismatch. |
104
+ | Secret validation | Exit on startup if `WEBHOOK_SECRET` env var is missing or < 16 chars |
105
+ | Payload logging | Pretty-print valid JSON payloads to stdout with ISO timestamp |
106
+ | Request size limit | Reject bodies > 1 MB (413) to prevent memory abuse |
107
+ | Graceful shutdown | Handle `SIGINT` and `SIGTERM` — close server, then exit |
108
+ | OpenClaw forwarding | If `OPENCLAW_GATEWAY_URL` env var is set, forward verified payloads to `<url>/hooks/agent` via POST. Include `OPENCLAW_AUTH_TOKEN` as Bearer token if set. Log forward success/failure. |
109
+ | No dependencies | Only `node:http`, `node:https`, and `node:crypto` — nothing from npm |
110
+
111
+ **Signature verification — use timing-safe comparison:**
112
+
113
+ ```javascript
114
+ import { createHmac, timingSafeEqual } from 'node:crypto';
115
+
116
+ function verifySignature(rawBody, signatureHeader, secret) {
117
+ if (!signatureHeader || !secret) return false;
118
+ // Nansen sends "sha256=<hex>" — strip the prefix before comparing
119
+ const sig = signatureHeader.startsWith('sha256=') ? signatureHeader.slice(7) : signatureHeader;
120
+ const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
121
+ try {
122
+ return timingSafeEqual(Buffer.from(sig, 'utf8'), Buffer.from(expected, 'utf8'));
123
+ } catch {
124
+ return false; // length mismatch
125
+ }
126
+ }
127
+ ```
128
+
129
+ **Full server template:**
130
+
131
+ ```javascript
132
+ import { createServer } from 'node:http';
133
+ import { createHmac, timingSafeEqual } from 'node:crypto';
134
+
135
+ const PORT = parseInt(process.env.PORT || '9477', 10);
136
+ const SECRET = process.env.WEBHOOK_SECRET;
137
+ const MAX_BODY = 1_048_576; // 1 MB
138
+
139
+ // Optional: forward verified payloads to a local OpenClaw Gateway
140
+ const OPENCLAW_URL = process.env.OPENCLAW_GATEWAY_URL; // e.g. http://localhost:3000
141
+ const OPENCLAW_TOKEN = process.env.OPENCLAW_AUTH_TOKEN;
142
+
143
+ if (!SECRET || SECRET.length < 16) {
144
+ console.error('WEBHOOK_SECRET env var required (minimum 16 characters).');
145
+ console.error('Generate one: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"');
146
+ process.exit(1);
147
+ }
148
+
149
+ function verifySignature(rawBody, signatureHeader) {
150
+ if (!signatureHeader) return false;
151
+ // Nansen sends "sha256=<hex>" — strip the prefix before comparing
152
+ const sig = signatureHeader.startsWith('sha256=') ? signatureHeader.slice(7) : signatureHeader;
153
+ const expected = createHmac('sha256', SECRET).update(rawBody).digest('hex');
154
+ try {
155
+ return timingSafeEqual(Buffer.from(sig, 'utf8'), Buffer.from(expected, 'utf8'));
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ async function forwardToOpenClaw(payload) {
162
+ if (!OPENCLAW_URL) return;
163
+ const url = `${OPENCLAW_URL.replace(/\/+$/, '')}/hooks/agent`;
164
+ const headers = { 'Content-Type': 'application/json' };
165
+ if (OPENCLAW_TOKEN) headers['Authorization'] = `Bearer ${OPENCLAW_TOKEN}`;
166
+ try {
167
+ const res = await fetch(url, {
168
+ method: 'POST',
169
+ headers,
170
+ body: JSON.stringify(payload),
171
+ });
172
+ if (res.ok) {
173
+ console.log(`[${ts()}] Forwarded to OpenClaw (${res.status})`);
174
+ } else {
175
+ console.error(`[${ts()}] OpenClaw forward failed (${res.status})`);
176
+ }
177
+ } catch (err) {
178
+ console.error(`[${ts()}] OpenClaw forward error: ${err.message}`);
179
+ }
180
+ }
181
+
182
+ function ts() { return new Date().toISOString(); }
183
+
184
+ const server = createServer((req, res) => {
185
+ if (req.method === 'GET' && req.url === '/health') {
186
+ res.writeHead(200, { 'Content-Type': 'application/json' });
187
+ return res.end('{"status":"ok"}');
188
+ }
189
+
190
+ if (req.method !== 'POST' || req.url !== '/webhook') {
191
+ res.writeHead(404);
192
+ return res.end();
193
+ }
194
+
195
+ let size = 0;
196
+ const chunks = [];
197
+
198
+ req.on('data', (chunk) => {
199
+ size += chunk.length;
200
+ if (size > MAX_BODY) {
201
+ res.writeHead(413);
202
+ res.end('{"error":"Payload too large"}');
203
+ req.destroy();
204
+ return;
205
+ }
206
+ chunks.push(chunk);
207
+ });
208
+
209
+ req.on('end', () => {
210
+ if (res.writableEnded) return;
211
+
212
+ const rawBody = Buffer.concat(chunks).toString('utf8');
213
+ const signature = req.headers['x-nansen-signature'];
214
+
215
+ if (!verifySignature(rawBody, signature)) {
216
+ console.error(`[${ts()}] REJECTED — invalid signature`);
217
+ res.writeHead(401, { 'Content-Type': 'application/json' });
218
+ return res.end('{"error":"Invalid signature"}');
219
+ }
220
+
221
+ let payload;
222
+ try {
223
+ payload = JSON.parse(rawBody);
224
+ console.log(`\n[${ts()}] Alert received:`);
225
+ console.log(JSON.stringify(payload, null, 2));
226
+ } catch {
227
+ console.error(`[${ts()}] WARNING — valid signature but malformed JSON`);
228
+ }
229
+
230
+ // Forward to OpenClaw if configured (fire-and-forget — don't block response)
231
+ if (payload) forwardToOpenClaw(payload);
232
+
233
+ res.writeHead(200, { 'Content-Type': 'application/json' });
234
+ res.end('{"received":true}');
235
+ });
236
+ });
237
+
238
+ for (const sig of ['SIGINT', 'SIGTERM']) {
239
+ process.on(sig, () => {
240
+ console.log(`\n${sig} — shutting down`);
241
+ server.close(() => process.exit(0));
242
+ });
243
+ }
244
+
245
+ server.listen(PORT, '127.0.0.1', () => {
246
+ console.log(`Webhook listener ready — http://127.0.0.1:${PORT}/webhook`);
247
+ if (OPENCLAW_URL) console.log(`OpenClaw forwarding → ${OPENCLAW_URL}/hooks/agent`);
248
+ console.log('Waiting for alerts… (Ctrl+C to stop)\n');
249
+ });
250
+ ```
251
+
252
+ ### Step 3: Start the server and tunnel
253
+
254
+ Start the server:
255
+
256
+ ```bash
257
+ WEBHOOK_SECRET='<secret>' node nansen-webhook-server.mjs
258
+ ```
259
+
260
+ Then start a public tunnel so Nansen's servers can reach it.
261
+
262
+ **ngrok (recommended):**
263
+ ```bash
264
+ ngrok http 9477
265
+ ```
266
+ Get the public URL from ngrok's output or its local API:
267
+ ```bash
268
+ curl -s http://127.0.0.1:4040/api/tunnels | node -e "process.stdin.on('data',d=>console.log(JSON.parse(d).tunnels[0]?.public_url))"
269
+ ```
270
+ The webhook URL is `https://<subdomain>.ngrok-free.dev/webhook`.
271
+
272
+ **localtunnel (fallback — unreliable):**
273
+ ```bash
274
+ npx localtunnel --port 9477
275
+ ```
276
+ Prints a URL like `https://xxx.loca.lt`. The webhook URL is `https://xxx.loca.lt/webhook`.
277
+
278
+ **Warning:** localtunnel's free relay silently drops connections after minutes. When this happens, all alerts fail with "503 Tunnel Unavailable" until you restart the tunnel and update the alert webhook URL. Use ngrok unless you have a reason not to.
279
+
280
+ **Note:** Tunnel URLs are ephemeral — they change every restart. For permanent setups, deploy the server to a host with a static URL.
281
+
282
+ ### Step 4: Provide a next-steps summary
283
+
284
+ **Do NOT create or modify any alerts.** Instead, print a clear summary for the user explaining what was set up and what they need to do next.
285
+
286
+ The summary MUST include:
287
+ 1. Confirmation of what was created (the server script path and the generated secret)
288
+ 2. The commands to start the server and tunnel (with the actual secret filled in)
289
+ 3. The exact `nansen alerts create` or `nansen alerts update` command they should run, with the `--webhook` and `--webhook-secret` flags filled in with the tunnel URL and secret — but leave the alert-specific flags (`--name`, `--type`, `--chains`, etc.) as placeholders for the user to fill in
290
+ 4. A reminder that the server and tunnel must be running before the alert is created (Nansen validates the webhook endpoint on creation)
291
+ 5. A note that tunnel URLs are ephemeral and will change on restart
292
+
293
+ Example summary format:
294
+
295
+ ```
296
+ ## Webhook listener ready
297
+
298
+ **Server script:** ./nansen-webhook-server.mjs
299
+ **Port:** 9477
300
+
301
+ ### To start receiving alerts:
302
+
303
+ 1. Start the server (keep this terminal open):
304
+ WEBHOOK_SECRET='<actual-secret>' node nansen-webhook-server.mjs
305
+
306
+ 2. In a new terminal, start the tunnel:
307
+ ngrok http 9477 # recommended
308
+ # or: npx localtunnel --port 9477 (unreliable — tunnel drops silently)
309
+
310
+ 3. Create an alert pointing to your webhook (fill in your alert details):
311
+ nansen alerts create \
312
+ --name '<your alert name>' \
313
+ --type <sm-token-flows|common-token-transfer|smart-contract-call> \
314
+ --chains <chains> \
315
+ --webhook 'https://<your-tunnel-url>/webhook' \
316
+ --webhook-secret '<actual-secret>' \
317
+ [type-specific flags...]
318
+
319
+ Or add the webhook to an existing alert:
320
+ nansen alerts update <alert-id> \
321
+ --webhook 'https://<your-tunnel-url>/webhook' \
322
+ --webhook-secret '<actual-secret>'
323
+
324
+ Note: The tunnel URL changes each time you restart. Update the alert
325
+ webhook URL if you restart the tunnel.
326
+
327
+ See `nansen alerts create --help` for full flag reference per alert type.
328
+ ```
329
+
330
+ ## Security Checklist
331
+
332
+ - **Always use a webhook secret** — the server refuses to start without one
333
+ - **Always verify signatures** — never accept unverified payloads
334
+ - **Bind to localhost only** — the tunnel handles public exposure; direct `0.0.0.0` binding exposes you to unauthenticated traffic
335
+ - **Use HTTPS** — both localtunnel and ngrok tunnel via HTTPS by default
336
+ - **Body size limit** — the 1 MB cap prevents memory exhaustion from oversized requests
337
+ - **Timing-safe comparison** — prevents timing side-channel attacks on the signature
338
+
339
+ ## Troubleshooting
340
+
341
+ | Symptom | Fix |
342
+ |---|---|
343
+ | "Invalid signature" on every request | Ensure the **exact same secret** is in `WEBHOOK_SECRET` and `--webhook-secret` |
344
+ | "Failed to send welcome message" on alert create | Start the server and tunnel **before** creating the alert |
345
+ | No alerts arriving | Check `nansen alerts list --table` — is the alert enabled? Is the webhook URL correct (includes `/webhook`)? |
346
+ | Tunnel URL expired / tunnel died | Restart the tunnel, get the new URL, then `nansen alerts update <id> --webhook '<new-url>/webhook'`. If this keeps happening, switch from localtunnel to ngrok. |
347
+ | Port already in use | Set a different port: `PORT=9478 WEBHOOK_SECRET='...' node nansen-webhook-server.mjs` and update the tunnel accordingly |
348
+
349
+ ## OpenClaw Integration
350
+
351
+ If the user is running OpenClaw locally on the same machine, the webhook server can forward verified alert payloads to OpenClaw's Gateway, triggering an agent turn for each alert.
352
+
353
+ **Flow:** `Nansen → ngrok → webhook server (signature check) → OpenClaw /hooks/agent`
354
+
355
+ ### Additional env vars
356
+
357
+ | Var | Required | Purpose |
358
+ |-----|----------|---------|
359
+ | `OPENCLAW_GATEWAY_URL` | Yes | OpenClaw Gateway base URL (e.g. `http://localhost:3000`) |
360
+ | `OPENCLAW_AUTH_TOKEN` | If auth enabled | Bearer token for OpenClaw webhook endpoints |
361
+
362
+ ### Start command (with OpenClaw forwarding)
363
+
364
+ ```bash
365
+ WEBHOOK_SECRET='<secret>' \
366
+ OPENCLAW_GATEWAY_URL='http://localhost:3000' \
367
+ OPENCLAW_AUTH_TOKEN='<token>' \
368
+ node nansen-webhook-server.mjs
369
+ ```
370
+
371
+ The server logs both the alert payload and the OpenClaw forward status. If OpenClaw is unreachable, the forward fails silently (the alert is still logged to stdout).
372
+
373
+ ### Ask the user
374
+
375
+ Before enabling OpenClaw forwarding, ask:
376
+ 1. Is OpenClaw running locally? What port?
377
+ 2. Does their Gateway require auth? If so, what's the Bearer token?
378
+
379
+ If they don't know or aren't running OpenClaw, skip — the server works fine standalone.
380
+
381
+ ## Notes
382
+
383
+ - The server uses zero npm dependencies — only Node.js built-ins
384
+ - One server can receive alerts from multiple Nansen alerts (as long as they share the same webhook secret)
385
+ - For production use, deploy to a cloud host with a static URL and run behind a reverse proxy with TLS
386
+ - The `x-nansen-signature` header format is `sha256=<HMAC-SHA256(secret, rawBody)>` — strip the `sha256=` prefix before comparing
@@ -33,7 +33,18 @@ nansen trade quote \
33
33
  --amount 1000000000
34
34
  ```
35
35
 
36
- Symbols resolve automatically: `SOL`, `ETH`, `USDC`, `USDT`, `WETH`. Raw addresses also work.
36
+ Symbols resolve automatically: `SOL`, `ETH`, `USDC`, `USDT`, `WETH`. Raw addresses also work. Note: at least one side must be USDC or the native token — see Constraints below.
37
+
38
+ ## Constraints
39
+
40
+ **Swap constraint:** At least one side of every swap must be **USDC** or the chain's **native token** (SOL on Solana, ETH on Base). Arbitrary token-to-token swaps (e.g. WETH→USDT, BONK→JUP) are rejected.
41
+
42
+ - USDC (Solana): `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`
43
+ - USDC (Base): `0x833589fcd6edb6e08f4c7c32d4f71b54bda02913`
44
+ - Native SOL: `So11111111111111111111111111111111111111112`
45
+ - Native ETH: `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`
46
+
47
+ For cross-chain swaps, each token is checked against its own chain (from vs `--chain`, to vs `--to-chain`).
37
48
 
38
49
  ## Execute
39
50
 
package/src/api.js CHANGED
@@ -1230,11 +1230,11 @@ export class NansenAPI {
1230
1230
  // ============= Prediction Market Endpoints =============
1231
1231
 
1232
1232
  async pmOhlcv(params = {}) {
1233
- const { marketId, sort, pagination } = params;
1233
+ const { marketId, orderBy, sort, pagination } = params;
1234
1234
  if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1235
1235
  return this.request('/api/v1/prediction-market/ohlcv', {
1236
1236
  market_id: marketId,
1237
- sort,
1237
+ order_by: orderBy || sort,
1238
1238
  pagination
1239
1239
  });
1240
1240
  }
@@ -1249,71 +1249,105 @@ export class NansenAPI {
1249
1249
  }
1250
1250
 
1251
1251
  async pmTopHolders(params = {}) {
1252
- const { marketId, sort, pagination } = params;
1252
+ const { marketId, orderBy, sort, pagination } = params;
1253
1253
  if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1254
1254
  return this.request('/api/v1/prediction-market/top-holders', {
1255
1255
  market_id: marketId,
1256
- sort,
1256
+ order_by: orderBy || sort,
1257
1257
  pagination
1258
1258
  });
1259
1259
  }
1260
1260
 
1261
1261
  async pmTradesByMarket(params = {}) {
1262
- const { marketId, pagination } = params;
1262
+ const { marketId, orderBy, pagination } = params;
1263
1263
  if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1264
1264
  return this.request('/api/v1/prediction-market/trades-by-market', {
1265
1265
  market_id: marketId,
1266
+ order_by: orderBy,
1266
1267
  pagination
1267
1268
  });
1268
1269
  }
1269
1270
 
1270
1271
  async pmTradesByAddress(params = {}) {
1271
- const { address, pagination } = params;
1272
+ const { address, orderBy, pagination } = params;
1272
1273
  // Polymarket runs exclusively on Polygon
1273
1274
  const validation = validateAddress(address, 'polygon');
1274
1275
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
1275
1276
  return this.request('/api/v1/prediction-market/trades-by-address', {
1276
1277
  address,
1278
+ order_by: orderBy,
1277
1279
  pagination
1278
1280
  });
1279
1281
  }
1280
1282
 
1281
1283
  async pmMarketScreener(params = {}) {
1282
- const { sortBy = 'volume_24hr', query = '', status = '', pagination } = params;
1283
- return this.request('/api/v1/prediction-market/market-screener', {
1284
+ const { orderBy, sortBy = 'volume_24hr', query = '', status = '', tags, minLiquidity, maxLiquidity, minUniqueTraders24h, maxUniqueTraders24h, minVolume24hr, maxVolume24hr, negRisk, minOpenInterest, maxOpenInterest, endDateBefore, endDateAfter, minPrice, maxPrice, pagination } = params;
1285
+ const body = {
1284
1286
  sort_by: sortBy,
1285
1287
  query,
1286
1288
  status,
1287
1289
  pagination
1288
- });
1290
+ };
1291
+ if (orderBy) body.order_by = orderBy;
1292
+ if (tags && tags.length) body.tags = tags;
1293
+ if (minLiquidity != null) body.min_liquidity = minLiquidity;
1294
+ if (maxLiquidity != null) body.max_liquidity = maxLiquidity;
1295
+ if (minUniqueTraders24h != null) body.min_unique_traders_24h = minUniqueTraders24h;
1296
+ if (maxUniqueTraders24h != null) body.max_unique_traders_24h = maxUniqueTraders24h;
1297
+ if (minVolume24hr != null) body.min_volume_24hr = minVolume24hr;
1298
+ if (maxVolume24hr != null) body.max_volume_24hr = maxVolume24hr;
1299
+ if (negRisk != null) body.neg_risk = negRisk;
1300
+ if (minOpenInterest != null) body.min_open_interest = minOpenInterest;
1301
+ if (maxOpenInterest != null) body.max_open_interest = maxOpenInterest;
1302
+ if (endDateBefore) body.end_date_before = endDateBefore;
1303
+ if (endDateAfter) body.end_date_after = endDateAfter;
1304
+ if (minPrice != null) body.min_price = minPrice;
1305
+ if (maxPrice != null) body.max_price = maxPrice;
1306
+ return this.request('/api/v1/prediction-market/market-screener', body);
1289
1307
  }
1290
1308
 
1291
1309
  async pmEventScreener(params = {}) {
1292
- const { sortBy = 'volume_24hr', query = '', status = '', pagination } = params;
1293
- return this.request('/api/v1/prediction-market/event-screener', {
1310
+ const { orderBy, sortBy = 'volume_24hr', query = '', status = '', tags, minLiquidity, maxLiquidity, minUniqueTraders24h, maxUniqueTraders24h, minVolume24hr, maxVolume24hr, negRisk, minOpenInterest, maxOpenInterest, endDateBefore, endDateAfter, pagination } = params;
1311
+ const body = {
1294
1312
  sort_by: sortBy,
1295
1313
  query,
1296
1314
  status,
1297
1315
  pagination
1298
- });
1316
+ };
1317
+ if (orderBy) body.order_by = orderBy;
1318
+ if (tags && tags.length) body.tags = tags;
1319
+ if (minLiquidity != null) body.min_liquidity = minLiquidity;
1320
+ if (maxLiquidity != null) body.max_liquidity = maxLiquidity;
1321
+ if (minUniqueTraders24h != null) body.min_unique_traders_24h = minUniqueTraders24h;
1322
+ if (maxUniqueTraders24h != null) body.max_unique_traders_24h = maxUniqueTraders24h;
1323
+ if (minVolume24hr != null) body.min_volume_24hr = minVolume24hr;
1324
+ if (maxVolume24hr != null) body.max_volume_24hr = maxVolume24hr;
1325
+ if (negRisk != null) body.neg_risk = negRisk;
1326
+ if (minOpenInterest != null) body.min_open_interest = minOpenInterest;
1327
+ if (maxOpenInterest != null) body.max_open_interest = maxOpenInterest;
1328
+ if (endDateBefore) body.end_date_before = endDateBefore;
1329
+ if (endDateAfter) body.end_date_after = endDateAfter;
1330
+ return this.request('/api/v1/prediction-market/event-screener', body);
1299
1331
  }
1300
1332
 
1301
1333
  async pmPnlByMarket(params = {}) {
1302
- const { marketId, pagination } = params;
1334
+ const { marketId, orderBy, pagination } = params;
1303
1335
  if (!marketId) throw new NansenError('market_id is required. Run: nansen research pm market-screener --query "your search"', ErrorCode.MISSING_PARAM);
1304
1336
  return this.request('/api/v1/prediction-market/pnl-by-market', {
1305
1337
  market_id: marketId,
1338
+ order_by: orderBy,
1306
1339
  pagination
1307
1340
  });
1308
1341
  }
1309
1342
 
1310
1343
  async pmPnlByAddress(params = {}) {
1311
- const { address, pagination } = params;
1344
+ const { address, orderBy, pagination } = params;
1312
1345
  // Polymarket runs exclusively on Polygon
1313
1346
  const validation = validateAddress(address, 'polygon');
1314
1347
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
1315
1348
  return this.request('/api/v1/prediction-market/pnl-by-address', {
1316
1349
  address,
1350
+ order_by: orderBy,
1317
1351
  pagination
1318
1352
  });
1319
1353
  }
@@ -1334,6 +1368,17 @@ export class NansenAPI {
1334
1368
  });
1335
1369
  }
1336
1370
 
1371
+ async pmAddressSummary(params = {}) {
1372
+ const { address, pagination } = params;
1373
+ // Polymarket runs exclusively on Polygon
1374
+ const validation = validateAddress(address, 'polygon');
1375
+ if (!validation.valid) throw new NansenError(validation.error, validation.code);
1376
+ return this.request('/api/v1/prediction-market/address-summary', {
1377
+ address,
1378
+ pagination
1379
+ });
1380
+ }
1381
+
1337
1382
  // ============= Points Endpoints =============
1338
1383
 
1339
1384
  async pointsLeaderboard(params = {}) {
package/src/cli.js CHANGED
@@ -1407,23 +1407,40 @@ export function buildCommands(deps = {}) {
1407
1407
  const sortBy = options['sort-by'];
1408
1408
  const query = options.query;
1409
1409
  const status = options.status;
1410
- const sort = parseSort(options.sort);
1410
+ const orderBy = parseSort(options.sort, options['order-by']);
1411
1411
  const pagination = buildPagination(options);
1412
1412
 
1413
+ // Screener-specific filter options
1414
+ const tags = options.tags ? options.tags.split(',').map(t => t.trim()) : undefined;
1415
+ const minLiquidity = options['min-liquidity'] != null ? Number(options['min-liquidity']) : undefined;
1416
+ const maxLiquidity = options['max-liquidity'] != null ? Number(options['max-liquidity']) : undefined;
1417
+ const minUniqueTraders24h = options['min-unique-traders-24h'] != null ? Number(options['min-unique-traders-24h']) : undefined;
1418
+ const maxUniqueTraders24h = options['max-unique-traders-24h'] != null ? Number(options['max-unique-traders-24h']) : undefined;
1419
+ const minVolume24hr = options['min-volume-24hr'] != null ? Number(options['min-volume-24hr']) : undefined;
1420
+ const maxVolume24hr = options['max-volume-24hr'] != null ? Number(options['max-volume-24hr']) : undefined;
1421
+ const negRisk = options['neg-risk'] != null ? options['neg-risk'] === 'true' : undefined;
1422
+ const minOpenInterest = options['min-open-interest'] != null ? Number(options['min-open-interest']) : undefined;
1423
+ const maxOpenInterest = options['max-open-interest'] != null ? Number(options['max-open-interest']) : undefined;
1424
+ const endDateBefore = options['end-date-before'];
1425
+ const endDateAfter = options['end-date-after'];
1426
+ const minPrice = options['min-price'] != null ? Number(options['min-price']) : undefined;
1427
+ const maxPrice = options['max-price'] != null ? Number(options['max-price']) : undefined;
1428
+
1413
1429
  const handlers = {
1414
- 'ohlcv': () => apiInstance.pmOhlcv({ marketId, sort, pagination }),
1430
+ 'ohlcv': () => apiInstance.pmOhlcv({ marketId, orderBy, pagination }),
1415
1431
  'orderbook': () => apiInstance.pmOrderbook({ marketId, pagination }),
1416
- 'top-holders': () => apiInstance.pmTopHolders({ marketId, sort, pagination }),
1417
- 'trades-by-market': () => apiInstance.pmTradesByMarket({ marketId, pagination }),
1418
- 'trades-by-address': () => apiInstance.pmTradesByAddress({ address, pagination }),
1419
- 'market-screener': () => apiInstance.pmMarketScreener({ sortBy, query, status, pagination }),
1420
- 'event-screener': () => apiInstance.pmEventScreener({ sortBy, query, status, pagination }),
1421
- 'pnl-by-market': () => apiInstance.pmPnlByMarket({ marketId, pagination }),
1422
- 'pnl-by-address': () => apiInstance.pmPnlByAddress({ address, pagination }),
1432
+ 'top-holders': () => apiInstance.pmTopHolders({ marketId, orderBy, pagination }),
1433
+ 'trades-by-market': () => apiInstance.pmTradesByMarket({ marketId, orderBy, pagination }),
1434
+ 'trades-by-address': () => apiInstance.pmTradesByAddress({ address, orderBy, pagination }),
1435
+ 'market-screener': () => apiInstance.pmMarketScreener({ orderBy, sortBy, query, status, tags, minLiquidity, maxLiquidity, minUniqueTraders24h, maxUniqueTraders24h, minVolume24hr, maxVolume24hr, negRisk, minOpenInterest, maxOpenInterest, endDateBefore, endDateAfter, minPrice, maxPrice, pagination }),
1436
+ 'event-screener': () => apiInstance.pmEventScreener({ orderBy, sortBy, query, status, tags, minLiquidity, maxLiquidity, minUniqueTraders24h, maxUniqueTraders24h, minVolume24hr, maxVolume24hr, negRisk, minOpenInterest, maxOpenInterest, endDateBefore, endDateAfter, pagination }),
1437
+ 'pnl-by-market': () => apiInstance.pmPnlByMarket({ marketId, orderBy, pagination }),
1438
+ 'pnl-by-address': () => apiInstance.pmPnlByAddress({ address, orderBy, pagination }),
1423
1439
  'position-detail': () => apiInstance.pmPositionDetail({ marketId, pagination }),
1424
1440
  'categories': () => apiInstance.pmCategories({ pagination }),
1441
+ 'address-summary': () => apiInstance.pmAddressSummary({ address, pagination }),
1425
1442
  'help': () => ({
1426
- commands: ['ohlcv', 'orderbook', 'top-holders', 'trades-by-market', 'trades-by-address', 'market-screener', 'event-screener', 'pnl-by-market', 'pnl-by-address', 'position-detail', 'categories'],
1443
+ commands: ['ohlcv', 'orderbook', 'top-holders', 'trades-by-market', 'trades-by-address', 'market-screener', 'event-screener', 'pnl-by-market', 'pnl-by-address', 'position-detail', 'categories', 'address-summary'],
1427
1444
  description: 'Polymarket prediction market analytics',
1428
1445
  example: 'nansen research pm market-screener --sort-by volume_24hr --limit 20'
1429
1446
  })
@@ -1488,7 +1505,17 @@ WALLET:
1488
1505
 
1489
1506
  SYMBOLS:
1490
1507
  Common tokens resolve automatically: SOL, ETH, USDC, USDT, WETH
1491
- Raw addresses are also accepted.`);
1508
+ Raw addresses are also accepted.
1509
+
1510
+ CROSS-CHAIN NOTES (when using --to-chain):
1511
+ Supported combos:
1512
+ native → native (ETH <-> SOL) — requires $5+ per trade
1513
+ USDC → USDC (both directions)
1514
+ USDC → native (USDC → ETH or SOL)
1515
+ native → USDC (ETH/SOL → USDC)
1516
+ non-native → non-native — not supported (use USDC as intermediate)
1517
+ Bridge provider: Li.Fi
1518
+ Typical bridge time: 1-5 minutes`);
1492
1519
  return;
1493
1520
  }
1494
1521
  if (!tradingCmds[sub]) {
package/src/index.js CHANGED
@@ -14,4 +14,4 @@
14
14
  import { runCLI } from './cli.js';
15
15
 
16
16
  // Main entry point
17
- runCLI(process.argv.slice(2));
17
+ await runCLI(process.argv.slice(2));
package/src/schema.json CHANGED
@@ -588,18 +588,42 @@
588
588
  "endpoint": "/api/v1/prediction-market/market-screener",
589
589
  "description": "Get Prediction Market Screener",
590
590
  "options": {
591
- "query": {
592
- "default": ""
593
- }
591
+ "query": { "default": "" },
592
+ "sort-by": { "description": "Deprecated: use --sort field:dir instead" },
593
+ "tags": {},
594
+ "min-liquidity": {},
595
+ "max-liquidity": {},
596
+ "min-unique-traders-24h": {},
597
+ "max-unique-traders-24h": {},
598
+ "min-volume-24hr": {},
599
+ "max-volume-24hr": {},
600
+ "neg-risk": {},
601
+ "min-open-interest": {},
602
+ "max-open-interest": {},
603
+ "end-date-before": {},
604
+ "end-date-after": {},
605
+ "min-price": {},
606
+ "max-price": {}
594
607
  }
595
608
  },
596
609
  "event-screener": {
597
610
  "endpoint": "/api/v1/prediction-market/event-screener",
598
611
  "description": "Get Prediction Market Event Screener",
599
612
  "options": {
600
- "query": {
601
- "default": ""
602
- }
613
+ "query": { "default": "" },
614
+ "sort-by": { "description": "Deprecated: use --sort field:dir instead" },
615
+ "tags": {},
616
+ "min-liquidity": {},
617
+ "max-liquidity": {},
618
+ "min-unique-traders-24h": {},
619
+ "max-unique-traders-24h": {},
620
+ "min-volume-24hr": {},
621
+ "max-volume-24hr": {},
622
+ "neg-risk": {},
623
+ "min-open-interest": {},
624
+ "max-open-interest": {},
625
+ "end-date-before": {},
626
+ "end-date-after": {}
603
627
  }
604
628
  },
605
629
  "pnl-by-market": {
@@ -641,6 +665,15 @@
641
665
  "categories": {
642
666
  "endpoint": "/api/v1/prediction-market/categories",
643
667
  "description": "Get Prediction Market Categories"
668
+ },
669
+ "address-summary": {
670
+ "endpoint": "/api/v1/prediction-market/address-summary",
671
+ "description": "Get wallet-level PnL summary for a Polymarket address",
672
+ "options": {
673
+ "address": {
674
+ "required": true
675
+ }
676
+ }
644
677
  }
645
678
  },
646
679
  "description": "Polymarket prediction market analytics"
@@ -806,7 +839,7 @@
806
839
  },
807
840
  "to-chain": {
808
841
  "type": "string",
809
- "description": "Destination blockchain for cross-chain swap (solana or base). Omit for same-chain."
842
+ "description": "Destination blockchain for cross-chain swap (solana or base). Omit for same-chain. At least one side must be USDC or a native token (ETH, SOL). Non-native to non-native is not supported — swap to USDC first, then bridge. Minimum ~$5 per cross-chain trade (Li.Fi bridge)."
810
843
  },
811
844
  "from": {
812
845
  "type": "string",
@@ -61,6 +61,16 @@ export function validateQuoteInput({ chain, toChain, from, to, amount }) {
61
61
  );
62
62
  }
63
63
  }
64
+
65
+ // 5. At least one side must be USDC or the native token
66
+ const fromIsAnchor = isUsdcOrNative(from, normalizedChain);
67
+ const toIsAnchor = isUsdcOrNative(to, normalizedToChain);
68
+ if (!fromIsAnchor && !toIsAnchor) {
69
+ const anchorDesc = normalizedChain === normalizedToChain
70
+ ? `USDC or the native token (${NATIVE_SYMBOLS[normalizedChain] ?? normalizedChain})`
71
+ : `USDC or the native token on either side (${NATIVE_SYMBOLS[normalizedChain] ?? normalizedChain} on ${normalizedChain}, ${NATIVE_SYMBOLS[normalizedToChain] ?? normalizedToChain} on ${normalizedToChain})`;
72
+ throw new Error(`Invalid swap: at least one token must be ${anchorDesc}. Got: ${from} → ${to}.`);
73
+ }
64
74
  }
65
75
 
66
76
  // Native token decimals per chain (for converting balance from base units)
@@ -112,6 +122,12 @@ const NATIVE_TOKEN_ADDRESSES = {
112
122
  base: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
113
123
  };
114
124
 
125
+ // USDC contract addresses per chain.
126
+ const USDC_ADDRESSES = {
127
+ solana: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
128
+ base: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
129
+ };
130
+
115
131
  // Native token symbols for error messages.
116
132
  const NATIVE_SYMBOLS = { solana: 'SOL', base: 'ETH' };
117
133
 
@@ -120,6 +136,21 @@ const FEE_BUFFER = { solana: 0.005, base: 0.00004 };
120
136
  const HIGH_PERCENTAGE_THRESHOLD = 95;
121
137
  const AUTO_ADJUST_THRESHOLD_PERCENT = 2;
122
138
 
139
+ /**
140
+ * Check if an address is USDC or the native token for a chain (case-insensitive for EVM).
141
+ */
142
+ function isUsdcOrNative(address, chain) {
143
+ const usdc = USDC_ADDRESSES[chain];
144
+ const native = NATIVE_TOKEN_ADDRESSES[chain];
145
+ if (!usdc && !native) return false;
146
+ if (chain === 'solana') {
147
+ return address === usdc || address === native;
148
+ }
149
+ // EVM: case-insensitive
150
+ const lower = address.toLowerCase();
151
+ return (usdc && lower === usdc.toLowerCase()) || (native && lower === native.toLowerCase());
152
+ }
153
+
123
154
  /**
124
155
  * Check if an address is the native token for a chain (case-insensitive for EVM).
125
156
  */
package/src/trading.js CHANGED
@@ -994,6 +994,16 @@ EXAMPLES:
994
994
  nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
995
995
  nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
996
996
  nansen trade quote --chain solana --to-chain base --from SOL --to ETH --amount 1000000000
997
+
998
+ CROSS-CHAIN NOTES (when using --to-chain):
999
+ Supported combos:
1000
+ native → native (ETH <-> SOL) — requires $5+ per trade
1001
+ USDC → USDC (both directions)
1002
+ USDC → native (USDC → ETH or SOL)
1003
+ native → USDC (ETH/SOL → USDC)
1004
+ non-native → non-native — not supported (use USDC as intermediate)
1005
+ Bridge provider: Li.Fi
1006
+ Typical bridge time: 1-5 minutes
997
1007
  `, 'MISSING_ARGS');
998
1008
  }
999
1009
 
@@ -1157,6 +1167,10 @@ EXAMPLES:
1157
1167
  };
1158
1168
  if (isCrossChain) {
1159
1169
  params.toChainIndex = toChainConfig.index;
1170
+ // Opt out of Relay aggregator: CLI bypasses backend /execute so the
1171
+ // Redis aggregator hint is never set, and /bridge/status defaults to
1172
+ // LiFi — polling a Relay txHash there returns NOT_FOUND.
1173
+ params.disabledAggregators = 'relay';
1160
1174
  if (toWallet) {
1161
1175
  params.toWalletAddress = toWallet;
1162
1176
  log(` Destination wallet: ${toWallet}`);
package/src/transfer.js CHANGED
@@ -21,6 +21,16 @@ const ATA_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
21
21
  // Alias: buildEvmTransaction uses 'evm' as a generic fallback
22
22
  const CHAIN_IDS = { ...EVM_CHAIN_IDS, evm: 1 };
23
23
 
24
+ // ============= Address Derivation =============
25
+
26
+ function deriveEvmAddress(privateKeyHex) {
27
+ const privBuf = Buffer.from(privateKeyHex, 'hex');
28
+ const ecdh = crypto.createECDH('secp256k1');
29
+ ecdh.setPrivateKey(privBuf);
30
+ const pubKey = ecdh.getPublicKey(null, 'uncompressed');
31
+ return '0x' + keccak256(pubKey.subarray(1)).subarray(12).toString('hex');
32
+ }
33
+
24
34
  // ============= Address Validation =============
25
35
 
26
36
  function validateEvmAddress(address) {
@@ -106,12 +116,9 @@ async function buildEvmTransaction({ to, amount, token, privateKey, chain, max =
106
116
  const rpcUrl = CHAIN_RPCS[chain] || CHAIN_RPCS.evm;
107
117
  const chainId = CHAIN_IDS[chain] || 1;
108
118
 
109
- // Derive address
119
+ // Derive address and buffer for signing
110
120
  const privBuf = Buffer.from(privateKey, 'hex');
111
- const ecdh = crypto.createECDH('secp256k1');
112
- ecdh.setPrivateKey(privBuf);
113
- const pubKey = ecdh.getPublicKey(null, 'uncompressed');
114
- const from = '0x' + keccak256(pubKey.subarray(1)).subarray(12).toString('hex');
121
+ const from = deriveEvmAddress(privateKey);
115
122
 
116
123
  // Nonce
117
124
  const nonceHex = await rpcCall(rpcUrl, 'eth_getTransactionCount', [from, 'latest']);
@@ -735,11 +742,7 @@ export async function sendTokens({ to, amount, chain, token = null, wallet = nul
735
742
 
736
743
  if (max && token) {
737
744
  // Max ERC-20: full token balance
738
- const privBuf = Buffer.from(walletData.evm.privateKey, 'hex');
739
- const ecdh = crypto.createECDH('secp256k1');
740
- ecdh.setPrivateKey(privBuf);
741
- const pubKey = ecdh.getPublicKey(null, 'uncompressed');
742
- const from = '0x' + keccak256(pubKey.subarray(1)).subarray(12).toString('hex');
745
+ const from = deriveEvmAddress(walletData.evm.privateKey);
743
746
  const balResult = await rpcCall(rpcUrl, 'eth_call', [{
744
747
  to: token, data: '0x70a08231' + from.slice(2).padStart(64, '0'),
745
748
  }, 'latest']);
package/src/wallet.js CHANGED
@@ -247,6 +247,14 @@ function getWalletFile(name) {
247
247
  return path.join(getWalletsDir(), `${name}.json`);
248
248
  }
249
249
 
250
+ function requireWalletFile(name) {
251
+ const walletFile = getWalletFile(name);
252
+ if (!fs.existsSync(walletFile)) {
253
+ throw new Error(`Wallet "${name}" not found`);
254
+ }
255
+ return walletFile;
256
+ }
257
+
250
258
  /**
251
259
  * Verify the global password against stored hash.
252
260
  */
@@ -466,10 +474,7 @@ export function createWallet(name, password) {
466
474
  * Show wallet details (addresses only, no keys).
467
475
  */
468
476
  export function showWallet(name) {
469
- const walletFile = getWalletFile(name);
470
- if (!fs.existsSync(walletFile)) {
471
- throw new Error(`Wallet "${name}" not found`);
472
- }
477
+ const walletFile = requireWalletFile(name);
473
478
 
474
479
  const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
475
480
  const config = getWalletConfig();
@@ -495,10 +500,7 @@ export function showWallet(name) {
495
500
  * Export private keys for a wallet (requires password).
496
501
  */
497
502
  export function exportWallet(name, password) {
498
- const walletFile = getWalletFile(name);
499
- if (!fs.existsSync(walletFile)) {
500
- throw new Error(`Wallet "${name}" not found`);
501
- }
503
+ const walletFile = requireWalletFile(name);
502
504
 
503
505
  const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
504
506
  if (data.provider && data.provider !== 'local') {
@@ -527,10 +529,7 @@ export function exportWallet(name, password) {
527
529
  * Set the default wallet.
528
530
  */
529
531
  export function setDefaultWallet(name) {
530
- const walletFile = getWalletFile(name);
531
- if (!fs.existsSync(walletFile)) {
532
- throw new Error(`Wallet "${name}" not found`);
533
- }
532
+ requireWalletFile(name);
534
533
 
535
534
  const config = getWalletConfig();
536
535
  config.defaultWallet = name;
@@ -543,10 +542,7 @@ export function setDefaultWallet(name) {
543
542
  * Delete a wallet.
544
543
  */
545
544
  export async function deleteWallet(name, password) {
546
- const walletFile = getWalletFile(name);
547
- if (!fs.existsSync(walletFile)) {
548
- throw new Error(`Wallet "${name}" not found`);
549
- }
545
+ const walletFile = requireWalletFile(name);
550
546
 
551
547
  const data = JSON.parse(fs.readFileSync(walletFile, 'utf8'));
552
548
  const config = getWalletConfig();