nansen-cli 1.21.0 → 1.23.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 +2 -0
- package/package.json +1 -1
- package/skills/nansen-agent-guide/SKILL.md +58 -0
- package/skills/nansen-smart-alerts/SKILL.md +4 -1
- package/skills/nansen-trading/SKILL.md +12 -4
- package/src/api.js +17 -2
- package/src/cli.js +15 -8
- package/src/commands/alerts.js +14 -3
- package/src/schema.json +35 -1
- package/src/trading.js +131 -14
- package/src/wallet.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.23.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#341](https://github.com/nansen-ai/nansen-cli/pull/341) [`4b60056`](https://github.com/nansen-ai/nansen-cli/commit/4b6005697d52b5d432b9b32bcd1d36422f9166cc) Thanks [@gulshngill](https://github.com/gulshngill)! - Add `--webhook <url>` and `--webhook-secret <secret>` flags to `alerts create` and `alerts update`.
|
|
8
|
+
|
|
9
|
+
Allows alerts to be delivered to any HTTP/HTTPS endpoint via POST, alongside
|
|
10
|
+
the existing `--telegram`, `--slack`, and `--discord` channels. The optional
|
|
11
|
+
`--webhook-secret` enables HMAC payload signing for verification.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#344](https://github.com/nansen-ai/nansen-cli/pull/344) [`3dc09cc`](https://github.com/nansen-ai/nansen-cli/commit/3dc09cc8aa38cd4da4ae305f83e9599efb3b9ff9) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Add nansen-agent-guide skill — routing guide for when to use `nansen agent` vs direct CLI data commands
|
|
16
|
+
|
|
17
|
+
- [#347](https://github.com/nansen-ai/nansen-cli/pull/347) [`a243c7a`](https://github.com/nansen-ai/nansen-cli/commit/a243c7a33f28057949d2060c732083642422eb18) Thanks [@kome12](https://github.com/kome12)! - Add --buy-or-sell option to `token who-bought-sold` command — allows filtering by buy or sell side (BUY | SELL, defaults to BUY)
|
|
18
|
+
|
|
19
|
+
## 1.22.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- [#336](https://github.com/nansen-ai/nansen-cli/pull/336) [`c3b1fbd`](https://github.com/nansen-ai/nansen-cli/commit/c3b1fbdee46c15326a1656bf27a314f6c55dddf8) Thanks [@kome12](https://github.com/kome12)! - Add --label option to `token flows` command to filter by holder segment (top_100_holders, smart_money, public_figure, whale, exchange).
|
|
24
|
+
|
|
25
|
+
- [#334](https://github.com/nansen-ai/nansen-cli/pull/334) [`83244c6`](https://github.com/nansen-ai/nansen-cli/commit/83244c658d4ece2072dea0c6ed405a088c98aa4f) Thanks [@kome12](https://github.com/kome12)! - Add `--include-stablecoins` flag to `token screener` command. Pass `--include-stablecoins false` to exclude stablecoins from screener results (API default is `true`). Supports combined usage with `--smart-money`.
|
|
26
|
+
|
|
27
|
+
- [#339](https://github.com/nansen-ai/nansen-cli/pull/339) [`27ebcfc`](https://github.com/nansen-ai/nansen-cli/commit/27ebcfc2a3afd836db595df6d5a2a5f9242b624c) Thanks [@TimNooren](https://github.com/TimNooren)! - Add --amount-unit token flag to trade quote for human-readable amounts
|
|
28
|
+
|
|
3
29
|
## 1.21.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -27,6 +27,8 @@ Get your API key at [app.nansen.ai/auth/agent-setup](https://app.nansen.ai/auth/
|
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
nansen research <category> <subcommand> [options]
|
|
30
|
+
nansen agent "<question>" # AI research agent (200 credits, Pro)
|
|
31
|
+
nansen agent "<question>" --expert # deeper analysis (750 credits, Pro)
|
|
30
32
|
nansen trade <subcommand> [options]
|
|
31
33
|
nansen wallet <subcommand> [options]
|
|
32
34
|
nansen schema [command] [--pretty] # full command reference (no API key needed)
|
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nansen-agent-guide
|
|
3
|
+
description: Routing guide -- when to use `nansen agent` (AI research) vs direct CLI data commands. Use when deciding how to answer a user's research question with Nansen tools.
|
|
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
|
+
# Agent vs CLI Routing
|
|
20
|
+
|
|
21
|
+
| Need a... | Use |
|
|
22
|
+
|-----------|-----|
|
|
23
|
+
| **take** (analysis, interpretation) | `nansen agent` |
|
|
24
|
+
| **table** (raw data, specific metrics) | Direct CLI commands |
|
|
25
|
+
| **report** (both) | Agent for narrative + CLI for data |
|
|
26
|
+
|
|
27
|
+
## Use `nansen agent` when
|
|
28
|
+
|
|
29
|
+
- Question requires interpretation or synthesis across multiple data sources
|
|
30
|
+
- Open-ended research: "analyse this wallet", "what's happening with ETH smart money?"
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
nansen agent "What are top smart money tokens on Solana today and why?"
|
|
34
|
+
nansen agent "Analyse wallet 0x123... -- is this a smart trader?"
|
|
35
|
+
nansen agent "..." --expert # deeper analysis, 750 credits
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Cost: 200 credits (fast) / 750 credits (expert)
|
|
39
|
+
|
|
40
|
+
## Use direct CLI commands when
|
|
41
|
+
|
|
42
|
+
- You need specific structured data -- prices, volumes, holders, flows
|
|
43
|
+
- Deterministic question: "top 10 tokens by netflow on ethereum"
|
|
44
|
+
- Piping output or building a data table
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
nansen research token screener --chain ethereum --smart-money --limit 10
|
|
48
|
+
nansen research smart-money netflow --chain solana
|
|
49
|
+
nansen research profiler balance --address 0x123... --chain ethereum
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Cost: 5-50 credits per call
|
|
53
|
+
|
|
54
|
+
## Anti-patterns
|
|
55
|
+
|
|
56
|
+
- Don't use `nansen agent` for simple data fetches -- 40x more expensive
|
|
57
|
+
- Don't use raw CLI for open-ended analysis -- returns data, not interpretation
|
|
58
|
+
- Don't chain 3+ agent calls -- get raw data via CLI, call agent once for synthesis
|
|
@@ -41,6 +41,8 @@ nansen alerts delete <id>
|
|
|
41
41
|
| `--telegram` | chat ID | optional | | |
|
|
42
42
|
| `--slack` | webhook URL | optional | | |
|
|
43
43
|
| `--discord` | webhook URL | optional | | |
|
|
44
|
+
| `--webhook` | endpoint URL | optional | optional | |
|
|
45
|
+
| `--webhook-secret` | optional (webhook only) | optional | | |
|
|
44
46
|
| `--description` | optional | optional | | |
|
|
45
47
|
| `--enabled` | | flag | flag | |
|
|
46
48
|
| `--disabled` | flag | flag | flag | |
|
|
@@ -131,7 +133,8 @@ nansen alerts create \
|
|
|
131
133
|
## Notes
|
|
132
134
|
|
|
133
135
|
- Chain aliases: Hyperliquid = `hyperevm`, BSC = `bnb`.
|
|
134
|
-
- Multiple channels can be combined: `--telegram 123 --slack https://...`
|
|
136
|
+
- Multiple channels can be combined: `--telegram 123 --slack https://... --webhook https://...`
|
|
137
|
+
- `--webhook <url>` sends a POST request with the alert payload to any HTTP/HTTPS endpoint. Useful for server deployments, Zapier, n8n, or custom integrations. The endpoint must be publicly reachable and return a 2xx response.
|
|
135
138
|
- `--data '<json>'` merges raw JSON on top of named flags (escape hatch for fields without named flags).
|
|
136
139
|
- Alert endpoints are internal-only. Non-internal users receive 404.
|
|
137
140
|
- Use single quotes for names with `$` or special characters: `--name 'SM >$1M'`
|
|
@@ -58,9 +58,16 @@ nansen trade execute --quote "$quote_id"
|
|
|
58
58
|
| ETH | Base | `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` |
|
|
59
59
|
| USDC | Base | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
|
|
60
60
|
|
|
61
|
-
## Amounts
|
|
61
|
+
## Amounts
|
|
62
62
|
|
|
63
|
-
`--amount` accepts **integer base units
|
|
63
|
+
By default, `--amount` accepts **integer base units** (lamports, wei, etc). Use `--amount-unit token` to specify human-readable token amounts instead — the CLI resolves decimals locally and sends base units to the API.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Base units (default)
|
|
67
|
+
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
68
|
+
# Token units (0.5 SOL = 500000000 lamports, resolved automatically)
|
|
69
|
+
nansen trade quote --chain solana --from SOL --to USDC --amount 0.5 --amount-unit token
|
|
70
|
+
```
|
|
64
71
|
|
|
65
72
|
| Token | Decimals | 1 token = |
|
|
66
73
|
|-------|----------|-----------|
|
|
@@ -68,7 +75,7 @@ nansen trade execute --quote "$quote_id"
|
|
|
68
75
|
| ETH | 18 | `1000000000000000000` |
|
|
69
76
|
| USDC | 6 | `1000000` |
|
|
70
77
|
|
|
71
|
-
If the user says "$20 worth of X", you must convert USD → token amount
|
|
78
|
+
If the user says "$20 worth of X", you must convert USD → token amount, then either pass base units or use `--amount-unit token`. For example, to buy $20 of SOL at $150/SOL: $20 ÷ $150 = 0.1333 SOL → `--amount 0.1333 --amount-unit token`. Use a price lookup (e.g. `nansen research token info`) to get the current price first.
|
|
72
79
|
|
|
73
80
|
## Flags
|
|
74
81
|
|
|
@@ -77,7 +84,8 @@ If the user says "$20 worth of X", you must convert USD → token amount → bas
|
|
|
77
84
|
| `--chain` | `solana` or `base` |
|
|
78
85
|
| `--from` | Source token (symbol or address) |
|
|
79
86
|
| `--to` | Destination token (symbol or address) |
|
|
80
|
-
| `--amount` | Amount in base units (integer) |
|
|
87
|
+
| `--amount` | Amount in base units (integer), or token units with `--amount-unit token` |
|
|
88
|
+
| `--amount-unit` | Set to `token` to specify amount in token units (e.g. 0.5 SOL) |
|
|
81
89
|
| `--wallet` | Wallet name (default: default wallet) |
|
|
82
90
|
| `--slippage` | Slippage tolerance as decimal (e.g. 0.03) |
|
|
83
91
|
| `--quote` | Quote ID for execute |
|
package/src/api.js
CHANGED
|
@@ -293,6 +293,19 @@ export function validateAddress(address, chain = 'ethereum') {
|
|
|
293
293
|
return { valid: true };
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Normalize EVM address to lowercase for API compatibility.
|
|
298
|
+
* The API should handle case-insensitive addresses server-side, but this is
|
|
299
|
+
* a defensive client-side measure since checksummed addresses currently
|
|
300
|
+
* return empty results.
|
|
301
|
+
*/
|
|
302
|
+
export function normalizeAddress(address, chain = 'ethereum') {
|
|
303
|
+
if (address && typeof address === 'string' && address.startsWith('0x') && EVM_CHAINS.includes(chain)) {
|
|
304
|
+
return address.toLowerCase();
|
|
305
|
+
}
|
|
306
|
+
return address;
|
|
307
|
+
}
|
|
308
|
+
|
|
296
309
|
/**
|
|
297
310
|
* Validate token address (same rules as wallet address)
|
|
298
311
|
*/
|
|
@@ -974,7 +987,7 @@ export class NansenAPI {
|
|
|
974
987
|
}
|
|
975
988
|
|
|
976
989
|
async tokenFlows(params = {}) {
|
|
977
|
-
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 30, date } = params;
|
|
990
|
+
const { tokenAddress, chain = 'solana', label, filters = {}, orderBy, pagination, days = 30, date } = params;
|
|
978
991
|
if (tokenAddress) {
|
|
979
992
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
980
993
|
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
@@ -984,6 +997,7 @@ export class NansenAPI {
|
|
|
984
997
|
token_address: tokenAddress,
|
|
985
998
|
chain,
|
|
986
999
|
date: dateRange,
|
|
1000
|
+
label,
|
|
987
1001
|
filters,
|
|
988
1002
|
order_by: orderBy,
|
|
989
1003
|
pagination
|
|
@@ -1029,7 +1043,7 @@ export class NansenAPI {
|
|
|
1029
1043
|
}
|
|
1030
1044
|
|
|
1031
1045
|
async tokenWhoBoughtSold(params = {}) {
|
|
1032
|
-
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 30, date } = params;
|
|
1046
|
+
const { tokenAddress, chain = 'solana', buyOrSell = 'BUY', filters = {}, orderBy, pagination, days = 30, date } = params;
|
|
1033
1047
|
if (tokenAddress) {
|
|
1034
1048
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1035
1049
|
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
@@ -1038,6 +1052,7 @@ export class NansenAPI {
|
|
|
1038
1052
|
return this.request('/api/v1/tgm/who-bought-sold', {
|
|
1039
1053
|
token_address: tokenAddress,
|
|
1040
1054
|
chain,
|
|
1055
|
+
buy_or_sell: buyOrSell,
|
|
1041
1056
|
date: dateRange,
|
|
1042
1057
|
filters,
|
|
1043
1058
|
order_by: orderBy,
|
package/src/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Extracted from index.js for coverage
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { NansenAPI, NansenError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, sleep } from './api.js';
|
|
6
|
+
import { NansenAPI, NansenError, ErrorCode, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir, validateAddress, normalizeAddress, sleep } from './api.js';
|
|
7
7
|
import { buildWalletCommands } from './wallet.js';
|
|
8
8
|
import { buildTradingCommands } from './trading.js';
|
|
9
9
|
import { formatAlertsTable, buildAlertsCommands } from './commands/alerts.js';
|
|
@@ -870,7 +870,7 @@ export function buildCommands(deps = {}) {
|
|
|
870
870
|
return;
|
|
871
871
|
}
|
|
872
872
|
|
|
873
|
-
let apiKey = options['api-key']
|
|
873
|
+
let apiKey = options['api-key'];
|
|
874
874
|
|
|
875
875
|
if (!apiKey) {
|
|
876
876
|
apiKey = process.env.NANSEN_API_KEY;
|
|
@@ -1187,9 +1187,9 @@ export function buildCommands(deps = {}) {
|
|
|
1187
1187
|
|
|
1188
1188
|
'token': async (args, apiInstance, flags, options) => {
|
|
1189
1189
|
const subcommand = args[0] || 'help';
|
|
1190
|
-
const tokenAddress = options.token || options['token-address'];
|
|
1191
|
-
const tokenSymbol = options.symbol || options['token-symbol'];
|
|
1192
1190
|
const chain = options.chain || 'solana';
|
|
1191
|
+
const tokenAddress = normalizeAddress(options.token || options['token-address'], chain);
|
|
1192
|
+
const tokenSymbol = options.symbol || options['token-symbol'];
|
|
1193
1193
|
const chains = options.chains || [chain];
|
|
1194
1194
|
const timeframe = options.timeframe || '24h';
|
|
1195
1195
|
const filters = options.filters || {};
|
|
@@ -1200,10 +1200,15 @@ export function buildCommands(deps = {}) {
|
|
|
1200
1200
|
// Convenience filter for smart money only
|
|
1201
1201
|
const onlySmartMoney = options['smart-money'] || flags['smart-money'] || false;
|
|
1202
1202
|
if (onlySmartMoney) {
|
|
1203
|
-
filters.include_smart_money_labels = filters.include_smart_money_labels ||
|
|
1203
|
+
filters.include_smart_money_labels = filters.include_smart_money_labels ||
|
|
1204
1204
|
['Fund', 'Smart Trader', '30D Smart Trader', '90D Smart Trader', '180D Smart Trader'];
|
|
1205
1205
|
}
|
|
1206
1206
|
|
|
1207
|
+
const includeStablecoins = options['include-stablecoins'] ?? flags['include-stablecoins'];
|
|
1208
|
+
if (includeStablecoins !== undefined) {
|
|
1209
|
+
filters.include_stablecoins = includeStablecoins;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1207
1212
|
const handlers = {
|
|
1208
1213
|
'indicators': () => apiInstance.tokenIndicators({ tokenAddress, chain }),
|
|
1209
1214
|
'ohlcv': () => apiInstance.tokenOhlcv({ tokenAddress, chain, timeframe: options.timeframe || '1d' }),
|
|
@@ -1235,13 +1240,15 @@ export function buildCommands(deps = {}) {
|
|
|
1235
1240
|
'holders': () => apiInstance.tokenHolders({ tokenAddress, chain, labelType: onlySmartMoney ? 'smart_money' : 'all_holders', filters, orderBy, pagination }),
|
|
1236
1241
|
'flows': () => {
|
|
1237
1242
|
const date = parseDateOption(options.date, days);
|
|
1238
|
-
|
|
1243
|
+
const label = options.label;
|
|
1244
|
+
return apiInstance.tokenFlows({ tokenAddress, chain, label, filters, orderBy, pagination, days, date });
|
|
1239
1245
|
},
|
|
1240
1246
|
'dex-trades': () => apiInstance.tokenDexTrades({ tokenAddress, chain, onlySmartMoney, filters, orderBy, pagination, days }),
|
|
1241
1247
|
'pnl': () => apiInstance.tokenPnlLeaderboard({ tokenAddress, chain, filters, orderBy, pagination, days }),
|
|
1242
1248
|
'who-bought-sold': () => {
|
|
1243
1249
|
const date = parseDateOption(options.date, days);
|
|
1244
|
-
|
|
1250
|
+
const buyOrSell = (options['buy-or-sell'] || 'BUY').toUpperCase();
|
|
1251
|
+
return apiInstance.tokenWhoBoughtSold({ tokenAddress, chain, buyOrSell, filters, orderBy, pagination, days, date });
|
|
1245
1252
|
},
|
|
1246
1253
|
'flow-intelligence': () => apiInstance.tokenFlowIntelligence({ tokenAddress, chain, days }),
|
|
1247
1254
|
'transfers': () => {
|
|
@@ -1257,7 +1264,7 @@ export function buildCommands(deps = {}) {
|
|
|
1257
1264
|
'help': () => ({
|
|
1258
1265
|
commands: ['info', 'ohlcv', 'screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
|
|
1259
1266
|
description: 'Token God Mode endpoints',
|
|
1260
|
-
example: 'nansen token screener --chain solana --timeframe 24h --smart-money'
|
|
1267
|
+
example: 'nansen token screener --chain solana --timeframe 24h --smart-money --include-stablecoins false'
|
|
1261
1268
|
})
|
|
1262
1269
|
};
|
|
1263
1270
|
|
package/src/commands/alerts.js
CHANGED
|
@@ -484,10 +484,11 @@ USAGE:
|
|
|
484
484
|
REQUIRED:
|
|
485
485
|
--name <name> Alert name
|
|
486
486
|
--type <type> sm-token-flows | common-token-transfer | smart-contract-call
|
|
487
|
-
At least one channel: --telegram <chatId> | --slack <url> | --discord <url>
|
|
487
|
+
At least one channel: --telegram <chatId> | --slack <url> | --discord <url> | --webhook <url>
|
|
488
488
|
|
|
489
489
|
OPTIONS (all types):
|
|
490
490
|
--chains <chains> Comma-separated chains (e.g. ethereum,solana)
|
|
491
|
+
--webhook-secret <secret> Signing secret for webhook payload verification (webhook only)
|
|
491
492
|
--token <address:chain> Include token (repeatable)
|
|
492
493
|
--exclude-token <addr:chain> Exclude token (repeatable)
|
|
493
494
|
--description '<text>' Alert description
|
|
@@ -565,6 +566,14 @@ USAGE:
|
|
|
565
566
|
if (options.telegram) channels.push({ type: 'telegram', data: { chatId: String(options.telegram) } });
|
|
566
567
|
if (options.slack) channels.push({ type: 'slack', data: { webhookUrl: options.slack } });
|
|
567
568
|
if (options.discord) channels.push({ type: 'discord', data: { webhookUrl: options.discord } });
|
|
569
|
+
if (options.webhook) {
|
|
570
|
+
const webhookData = { webhookUrl: options.webhook };
|
|
571
|
+
if (options["webhook-secret"]) {
|
|
572
|
+
if (options["webhook-secret"].length < 16) throw new NansenError('--webhook-secret must be at least 16 characters', ErrorCode.INVALID_PARAMS);
|
|
573
|
+
webhookData.secret = options["webhook-secret"];
|
|
574
|
+
}
|
|
575
|
+
channels.push({ type: 'webhook', data: webhookData });
|
|
576
|
+
}
|
|
568
577
|
return channels.length > 0 ? channels : null;
|
|
569
578
|
}
|
|
570
579
|
|
|
@@ -608,7 +617,7 @@ USAGE:
|
|
|
608
617
|
if (!name) missing.push('--name');
|
|
609
618
|
if (!type) missing.push('--type');
|
|
610
619
|
if (!options.chains) missing.push('--chains');
|
|
611
|
-
if (!channels) missing.push('a channel (--telegram, --slack, or --
|
|
620
|
+
if (!channels) missing.push('a channel (--telegram, --slack, --discord, or --webhook)');
|
|
612
621
|
if (missing.length > 0) {
|
|
613
622
|
throw new NansenError(`Required: ${missing.join(', ')}`, ErrorCode.MISSING_PARAM);
|
|
614
623
|
}
|
|
@@ -707,7 +716,9 @@ USAGE:
|
|
|
707
716
|
? `Invalid Slack webhook URL. Check the URL and try again.`
|
|
708
717
|
: ch?.type === 'discord'
|
|
709
718
|
? `Invalid Discord webhook URL. Check the URL and try again.`
|
|
710
|
-
:
|
|
719
|
+
: ch?.type === 'webhook'
|
|
720
|
+
? `Invalid webhook URL (${ch.data.webhookUrl}). Ensure the endpoint is reachable and returns 2xx.`
|
|
721
|
+
: err.message;
|
|
711
722
|
throw new NansenError(hint, err.code ?? ErrorCode.INVALID_PARAMS, err.status);
|
|
712
723
|
}
|
|
713
724
|
throw err;
|
package/src/schema.json
CHANGED
|
@@ -280,6 +280,11 @@
|
|
|
280
280
|
},
|
|
281
281
|
"days": {
|
|
282
282
|
"default": 30
|
|
283
|
+
},
|
|
284
|
+
"label": {
|
|
285
|
+
"description": "Holder segment to filter flows by",
|
|
286
|
+
"enum": ["top_100_holders", "smart_money", "public_figure", "whale", "exchange"],
|
|
287
|
+
"default": "top_100_holders"
|
|
283
288
|
}
|
|
284
289
|
}
|
|
285
290
|
},
|
|
@@ -295,6 +300,11 @@
|
|
|
295
300
|
},
|
|
296
301
|
"days": {
|
|
297
302
|
"default": 30
|
|
303
|
+
},
|
|
304
|
+
"buy-or-sell": {
|
|
305
|
+
"description": "Filter by buy or sell side",
|
|
306
|
+
"enum": ["BUY", "SELL"],
|
|
307
|
+
"default": "BUY"
|
|
298
308
|
}
|
|
299
309
|
}
|
|
300
310
|
},
|
|
@@ -463,6 +473,10 @@
|
|
|
463
473
|
},
|
|
464
474
|
"chain": {
|
|
465
475
|
"default": "solana"
|
|
476
|
+
},
|
|
477
|
+
"include-stablecoins": {
|
|
478
|
+
"description": "Whether to include stablecoins in screener results (default true on API side)",
|
|
479
|
+
"default": true
|
|
466
480
|
}
|
|
467
481
|
}
|
|
468
482
|
}
|
|
@@ -660,6 +674,14 @@
|
|
|
660
674
|
"type": "string",
|
|
661
675
|
"description": "Discord webhook URL for notifications"
|
|
662
676
|
},
|
|
677
|
+
"webhook": {
|
|
678
|
+
"type": "string",
|
|
679
|
+
"description": "HTTP/HTTPS endpoint URL to POST alert payloads to"
|
|
680
|
+
},
|
|
681
|
+
"webhook-secret": {
|
|
682
|
+
"type": "string",
|
|
683
|
+
"description": "Signing secret for webhook payload verification (optional, webhook only)"
|
|
684
|
+
},
|
|
663
685
|
"data": {
|
|
664
686
|
"type": "string",
|
|
665
687
|
"description": "Alert config JSON. --chains is merged on top."
|
|
@@ -701,6 +723,14 @@
|
|
|
701
723
|
"type": "string",
|
|
702
724
|
"description": "Discord webhook URL"
|
|
703
725
|
},
|
|
726
|
+
"webhook": {
|
|
727
|
+
"type": "string",
|
|
728
|
+
"description": "HTTP/HTTPS endpoint URL to POST alert payloads to"
|
|
729
|
+
},
|
|
730
|
+
"webhook-secret": {
|
|
731
|
+
"type": "string",
|
|
732
|
+
"description": "Signing secret for webhook payload verification (optional, webhook only)"
|
|
733
|
+
},
|
|
704
734
|
"data": {
|
|
705
735
|
"type": "string",
|
|
706
736
|
"description": "Alert config JSON. --chains merged on top."
|
|
@@ -762,7 +792,11 @@
|
|
|
762
792
|
"amount": {
|
|
763
793
|
"type": "string",
|
|
764
794
|
"required": true,
|
|
765
|
-
"description": "Amount to swap"
|
|
795
|
+
"description": "Amount to swap (base units by default, or token units with --amount-unit token)"
|
|
796
|
+
},
|
|
797
|
+
"amount-unit": {
|
|
798
|
+
"type": "string",
|
|
799
|
+
"description": "\"token\" to specify amount in token units (e.g. 0.5 SOL), or \"base\" for base units (default). Decimals are resolved locally and the API always receives base units."
|
|
766
800
|
},
|
|
767
801
|
"wallet": {
|
|
768
802
|
"type": "string",
|
package/src/trading.js
CHANGED
|
@@ -115,9 +115,6 @@ export async function getQuote(params) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
const headers = { 'Accept': 'application/json' };
|
|
118
|
-
if (process.env.NANSEN_API_KEY) {
|
|
119
|
-
headers['Authorization'] = `Bearer ${process.env.NANSEN_API_KEY}`;
|
|
120
|
-
}
|
|
121
118
|
|
|
122
119
|
const res = await fetch(url.toString(), { headers });
|
|
123
120
|
|
|
@@ -156,10 +153,6 @@ export async function executeTransaction(params, { retries = 2, retryDelayMs = 1
|
|
|
156
153
|
'Content-Type': 'application/json',
|
|
157
154
|
'Accept': 'application/json',
|
|
158
155
|
};
|
|
159
|
-
if (process.env.NANSEN_API_KEY) {
|
|
160
|
-
headers['Authorization'] = `Bearer ${process.env.NANSEN_API_KEY}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
156
|
let lastError;
|
|
164
157
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
165
158
|
if (attempt > 0) {
|
|
@@ -683,6 +676,101 @@ export function getWrappedNativeFromWarning(tokenAddress, chain) {
|
|
|
683
676
|
return null;
|
|
684
677
|
}
|
|
685
678
|
|
|
679
|
+
// ============= Token Decimal Resolution =============
|
|
680
|
+
|
|
681
|
+
// Hardcoded decimals for well-known tokens — avoids RPC calls in the common case.
|
|
682
|
+
const KNOWN_DECIMALS = {
|
|
683
|
+
// Solana
|
|
684
|
+
'So11111111111111111111111111111111111111112': 9, // SOL/WSOL
|
|
685
|
+
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': 6, // USDC
|
|
686
|
+
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': 6, // USDT
|
|
687
|
+
// Base (EVM) — lowercase for case-insensitive matching
|
|
688
|
+
'0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee': 18, // ETH native
|
|
689
|
+
'0x4200000000000000000000000000000000000006': 18, // WETH
|
|
690
|
+
'0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': 6, // USDC
|
|
691
|
+
'0xfde4c96c8593536e31f229ea8f37b2ada2699bb2': 6, // USDT
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Resolve the number of decimals for a token.
|
|
696
|
+
* Checks a hardcoded map first, then falls back to an RPC call.
|
|
697
|
+
* Solana: getAccountInfo with jsonParsed encoding.
|
|
698
|
+
* EVM: eth_call to decimals() selector 0x313ce567.
|
|
699
|
+
*/
|
|
700
|
+
export async function resolveTokenDecimals(tokenAddress, chainName) {
|
|
701
|
+
// Normalise for map lookup (EVM addresses are case-insensitive)
|
|
702
|
+
const key = tokenAddress.startsWith('0x') ? tokenAddress.toLowerCase() : tokenAddress;
|
|
703
|
+
if (KNOWN_DECIMALS[key] !== undefined) return KNOWN_DECIMALS[key];
|
|
704
|
+
|
|
705
|
+
const chain = chainName.toLowerCase();
|
|
706
|
+
const chainConfig = CHAIN_MAP[chain];
|
|
707
|
+
if (!chainConfig) throw new Error(`Unknown chain: ${chain}`);
|
|
708
|
+
|
|
709
|
+
// Validate address format before making RPC calls.
|
|
710
|
+
// A bare symbol like "SOL" that didn't resolve means it's not recognized on this chain.
|
|
711
|
+
if (chainConfig.type === 'solana') {
|
|
712
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(tokenAddress)) {
|
|
713
|
+
throw new Error(`"${tokenAddress}" is not a recognized token on ${chainName}. Use a valid Solana address (base58, 32-44 chars).`);
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(tokenAddress)) {
|
|
717
|
+
throw new Error(`"${tokenAddress}" is not a recognized token on ${chainName}. Use a valid EVM address (0x + 40 hex chars).`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (chainConfig.type === 'solana') {
|
|
722
|
+
const rpcUrl = CHAIN_RPCS.solana;
|
|
723
|
+
const res = await fetch(rpcUrl, {
|
|
724
|
+
method: 'POST',
|
|
725
|
+
headers: { 'Content-Type': 'application/json' },
|
|
726
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'getAccountInfo', params: [tokenAddress, { encoding: 'jsonParsed' }] }),
|
|
727
|
+
});
|
|
728
|
+
const body = await res.json();
|
|
729
|
+
const decimals = body.result?.value?.data?.parsed?.info?.decimals;
|
|
730
|
+
if (decimals === undefined) throw new Error(`Could not resolve decimals for Solana token ${tokenAddress}`);
|
|
731
|
+
return decimals;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// EVM — eth_call to decimals()
|
|
735
|
+
const result = await evmRpcCall(chain, 'eth_call', [{ to: tokenAddress, data: '0x313ce567' }, 'latest']);
|
|
736
|
+
const decimals = parseInt(result, 16);
|
|
737
|
+
if (isNaN(decimals) || decimals > 255) throw new Error(`Could not resolve decimals for EVM token ${tokenAddress}`);
|
|
738
|
+
return decimals;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Convert a human-readable token amount to base units using string math.
|
|
743
|
+
* Avoids floating-point precision issues by operating on digit strings.
|
|
744
|
+
* Example: convertToBaseUnits('0.5', 9) => '500000000'
|
|
745
|
+
*/
|
|
746
|
+
export function convertToBaseUnits(amount, decimals) {
|
|
747
|
+
const str = String(amount);
|
|
748
|
+
if (!/^\d+(\.\d+)?$/.test(str)) {
|
|
749
|
+
throw new Error(`Invalid amount: "${str}". Must be a non-negative number (e.g. "0.5", "100").`);
|
|
750
|
+
}
|
|
751
|
+
const dotIndex = str.indexOf('.');
|
|
752
|
+
if (dotIndex === -1) {
|
|
753
|
+
// Whole number — append zeros and strip leading zeros
|
|
754
|
+
const raw = str + '0'.repeat(decimals);
|
|
755
|
+
return raw.replace(/^0+/, '') || '0';
|
|
756
|
+
}
|
|
757
|
+
const whole = str.slice(0, dotIndex);
|
|
758
|
+
let frac = str.slice(dotIndex + 1);
|
|
759
|
+
if (frac.length > decimals) {
|
|
760
|
+
// Reject if meaningful (non-zero) digits would be lost
|
|
761
|
+
const excess = frac.slice(decimals);
|
|
762
|
+
if (/[1-9]/.test(excess)) {
|
|
763
|
+
throw new Error(`Amount "${str}" has more fractional digits than the token supports (${decimals} decimals). The smallest unit is ${decimals === 0 ? '1 token' : '0.' + '0'.repeat(decimals - 1) + '1'}.`);
|
|
764
|
+
}
|
|
765
|
+
frac = frac.slice(0, decimals);
|
|
766
|
+
} else {
|
|
767
|
+
frac = frac.padEnd(decimals, '0');
|
|
768
|
+
}
|
|
769
|
+
// Strip leading zeros from the combined result
|
|
770
|
+
const raw = (whole + frac).replace(/^0+/, '') || '0';
|
|
771
|
+
return raw;
|
|
772
|
+
}
|
|
773
|
+
|
|
686
774
|
/**
|
|
687
775
|
* Check if amount contains a decimal point (i.e. not in base units).
|
|
688
776
|
* Returns an error string if invalid, or null if OK. Pure function.
|
|
@@ -690,8 +778,12 @@ export function getWrappedNativeFromWarning(tokenAddress, chain) {
|
|
|
690
778
|
export function validateBaseUnitAmount(amount) {
|
|
691
779
|
if (!amount) return null;
|
|
692
780
|
const str = String(amount);
|
|
781
|
+
if (str.startsWith('-')) {
|
|
782
|
+
return 'Amount cannot be negative. Got: ' + str;
|
|
783
|
+
}
|
|
693
784
|
if (str.includes('.')) {
|
|
694
|
-
return 'Amount must be in base units (integer)
|
|
785
|
+
return 'Amount must be in base units (integer). ' +
|
|
786
|
+
'Use --amount-unit token to specify token amounts (e.g. --amount 0.5 --amount-unit token). ' +
|
|
695
787
|
'Examples: 1000000000 lamports = 1 SOL, 1000000000000000000 wei = 1 ETH, ' +
|
|
696
788
|
'1000000 = 1 USDC. Got: ' + str;
|
|
697
789
|
}
|
|
@@ -742,9 +834,10 @@ export function buildTradingCommands(deps = {}) {
|
|
|
742
834
|
const amount = options.amount || args[3];
|
|
743
835
|
const walletName = options.wallet;
|
|
744
836
|
const slippage = options.slippage;
|
|
745
|
-
const autoSlippage = flags['auto-slippage']
|
|
837
|
+
const autoSlippage = flags['auto-slippage'];
|
|
746
838
|
const maxAutoSlippage = options['max-auto-slippage'];
|
|
747
839
|
const swapMode = options['swap-mode'] || 'exactIn';
|
|
840
|
+
const amountUnit = options['amount-unit'];
|
|
748
841
|
|
|
749
842
|
if (!chain || !from || !to || !amount) {
|
|
750
843
|
log(`
|
|
@@ -760,6 +853,7 @@ OPTIONS:
|
|
|
760
853
|
--from <symbol|address> Input token (symbol like SOL, USDC or address)
|
|
761
854
|
--to <symbol|address> Output token (symbol like USDC, ETH or address)
|
|
762
855
|
--amount <units> Amount in BASE UNITS (e.g. lamports, wei)
|
|
856
|
+
--amount-unit <unit> "token" to specify amount in token units (e.g. 0.5 SOL)
|
|
763
857
|
--wallet <name> Wallet name (default: default wallet). Use "walletconnect" or "wc" for WalletConnect.
|
|
764
858
|
--slippage <pct> Slippage as decimal (e.g. 0.03 for 3%). Default: 0.03
|
|
765
859
|
--auto-slippage Enable auto slippage calculation
|
|
@@ -768,6 +862,7 @@ OPTIONS:
|
|
|
768
862
|
|
|
769
863
|
EXAMPLES:
|
|
770
864
|
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
865
|
+
nansen trade quote --chain solana --from SOL --to USDC --amount 0.5 --amount-unit token
|
|
771
866
|
nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
|
|
772
867
|
nansen trade quote --chain solana --from So11111111111111111111111111111111111111112 --to EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v --amount 1000000000
|
|
773
868
|
`);
|
|
@@ -775,13 +870,35 @@ EXAMPLES:
|
|
|
775
870
|
return;
|
|
776
871
|
}
|
|
777
872
|
|
|
778
|
-
|
|
779
|
-
if (
|
|
780
|
-
log(`Error: ${
|
|
873
|
+
// Validate --amount-unit if provided
|
|
874
|
+
if (amountUnit && amountUnit !== 'token' && amountUnit !== 'base') {
|
|
875
|
+
log(`Error: Unknown --amount-unit "${amountUnit}". Supported values: token, base`);
|
|
781
876
|
exit(1);
|
|
782
877
|
return;
|
|
783
878
|
}
|
|
784
879
|
|
|
880
|
+
// When --amount-unit token is used, resolve decimals and convert to base units.
|
|
881
|
+
// Otherwise, validate that the amount is already in base units (integer).
|
|
882
|
+
let resolvedAmount = amount;
|
|
883
|
+
if (amountUnit === 'token') {
|
|
884
|
+
try {
|
|
885
|
+
const tokenForDecimals = swapMode === 'exactOut' ? to : from;
|
|
886
|
+
const decimals = await resolveTokenDecimals(tokenForDecimals, chain);
|
|
887
|
+
resolvedAmount = convertToBaseUnits(amount, decimals);
|
|
888
|
+
} catch (err) {
|
|
889
|
+
log(`Error resolving token decimals: ${err.message}`);
|
|
890
|
+
exit(1);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
} else {
|
|
894
|
+
const amountError = validateBaseUnitAmount(amount);
|
|
895
|
+
if (amountError) {
|
|
896
|
+
log(`Error: ${amountError}`);
|
|
897
|
+
exit(1);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
785
902
|
try {
|
|
786
903
|
const chainConfig = resolveChain(chain);
|
|
787
904
|
const chainType = chainConfig.type === 'evm' ? 'evm' : 'solana';
|
|
@@ -837,7 +954,7 @@ EXAMPLES:
|
|
|
837
954
|
chainIndex: chainConfig.index,
|
|
838
955
|
fromTokenAddress: from,
|
|
839
956
|
toTokenAddress: to,
|
|
840
|
-
amount,
|
|
957
|
+
amount: resolvedAmount,
|
|
841
958
|
userWalletAddress: walletAddress,
|
|
842
959
|
};
|
|
843
960
|
if (slippage) params.slippagePercent = slippage;
|
|
@@ -889,7 +1006,7 @@ EXAMPLES:
|
|
|
889
1006
|
'execute': async (args, apiInstance, flags, options) => {
|
|
890
1007
|
const quoteId = options.quote || options['quote-id'] || args[0];
|
|
891
1008
|
const walletName = options.wallet;
|
|
892
|
-
const noSimulate = flags['no-simulate']
|
|
1009
|
+
const noSimulate = flags['no-simulate'];
|
|
893
1010
|
|
|
894
1011
|
if (!quoteId) {
|
|
895
1012
|
log(`
|