nansen-cli 1.25.0 → 1.26.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 +18 -0
- package/package.json +1 -1
- package/skills/nansen-trading/SKILL.md +26 -6
- package/src/api.js +21 -13
- package/src/cli.js +31 -5
- package/src/schema.json +29 -4
- package/src/trade-validation.js +59 -0
- package/src/trading.js +34 -4
- package/src/transfer.js +1 -2
- package/src/x402-svm.js +2 -54
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.26.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#380](https://github.com/nansen-ai/nansen-cli/pull/380) [`12e4e25`](https://github.com/nansen-ai/nansen-cli/commit/12e4e25d50f50ff1ebbae160ba1016abd1cdbb4d) Thanks [@TimNooren](https://github.com/TimNooren)! - Add `--amount-unit percent` to trade commands, allowing trades as a percentage of wallet balance (e.g. `--amount 100 --amount-unit percent` to sell all)
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#382](https://github.com/nansen-ai/nansen-cli/pull/382) [`d9c87ef`](https://github.com/nansen-ai/nansen-cli/commit/d9c87ef9df51a3e9c53ea59674ad9efe9aa33fb7) Thanks [@kome12](https://github.com/kome12)! - fix: default `profiler balance` chain to `'all'` instead of `'ethereum'`
|
|
12
|
+
|
|
13
|
+
Previously, `nansen profiler balance --address <addr>` without `--chain` defaulted to `ethereum`, returning empty results for wallets with no ETH mainnet holdings (e.g. Base-only or Solana-only wallets). Now defaults to `'all'`, letting the API auto-route based on address format.
|
|
14
|
+
|
|
15
|
+
## 1.25.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#367](https://github.com/nansen-ai/nansen-cli/pull/367) [`9fea10a`](https://github.com/nansen-ai/nansen-cli/commit/9fea10a844acbaccf37a348dfd4efd5c32f76a4a) Thanks [@kome12](https://github.com/kome12)! - add --premium-labels flag to tgm/holders, tgm/pnl-leaderboard, tgm/perp-pnl-leaderboard, and perp-leaderboard endpoints
|
|
20
|
+
|
|
3
21
|
## 1.25.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -85,13 +85,15 @@ nansen trade execute --quote "$quote_id"
|
|
|
85
85
|
|
|
86
86
|
## Amounts
|
|
87
87
|
|
|
88
|
-
By default, `--amount` accepts **integer base units** (lamports, wei, etc). Use `--amount-unit token`
|
|
88
|
+
By default, `--amount` accepts **integer base units** (lamports, wei, etc). Use `--amount-unit token` for human-readable token amounts, or `--amount-unit usd` to specify a USD value — the CLI resolves price and decimals automatically.
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
91
|
# Base units (default)
|
|
92
92
|
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
93
93
|
# Token units (0.5 SOL = 500000000 lamports, resolved automatically)
|
|
94
94
|
nansen trade quote --chain solana --from SOL --to USDC --amount 0.5 --amount-unit token
|
|
95
|
+
# USD amount ($50 worth of SOL, price resolved via Nansen search API)
|
|
96
|
+
nansen trade quote --chain solana --from SOL --to USDC --amount 50 --amount-unit usd
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
| Token | Decimals | 1 token = |
|
|
@@ -100,25 +102,43 @@ nansen trade quote --chain solana --from SOL --to USDC --amount 0.5 --amount-uni
|
|
|
100
102
|
| ETH | 18 | `1000000000000000000` |
|
|
101
103
|
| USDC | 6 | `1000000` |
|
|
102
104
|
|
|
103
|
-
If the user says "$20 worth of X",
|
|
105
|
+
If the user says "$20 worth of X", use `--amount-unit usd` directly — no manual conversion needed. The CLI fetches the current price and converts for you.
|
|
104
106
|
|
|
105
107
|
## Flags
|
|
106
108
|
|
|
109
|
+
### `trade quote` flags
|
|
110
|
+
|
|
107
111
|
| Flag | Purpose |
|
|
108
112
|
|------|---------|
|
|
109
113
|
| `--chain` | Source chain: `solana` or `base` |
|
|
110
114
|
| `--to-chain` | Destination chain for cross-chain swap (omit for same-chain) |
|
|
111
115
|
| `--from` | Source token (symbol or address) |
|
|
112
116
|
| `--to` | Destination token (symbol or address, resolved against destination chain) |
|
|
113
|
-
| `--amount` | Amount in base units (integer), or token units with `--amount-unit
|
|
114
|
-
| `--amount-unit` |
|
|
117
|
+
| `--amount` | Amount in base units (integer), or token/USD units with `--amount-unit` |
|
|
118
|
+
| `--amount-unit` | `token` for token units (e.g. 0.5 SOL), `usd` for USD (e.g. 50), `base` = default |
|
|
115
119
|
| `--wallet` | Wallet name (default: default wallet) |
|
|
116
120
|
| `--to-wallet` | Destination wallet address (auto-derived for cross-chain if omitted) |
|
|
117
121
|
| `--slippage` | Slippage tolerance as decimal (e.g. 0.03) |
|
|
118
|
-
| `--
|
|
122
|
+
| `--auto-slippage` | Enable auto slippage calculation |
|
|
123
|
+
| `--max-auto-slippage` | Max auto slippage when `--auto-slippage` is enabled |
|
|
124
|
+
| `--swap-mode` | `exactIn` (default) or `exactOut` |
|
|
125
|
+
|
|
126
|
+
### `trade execute` flags
|
|
127
|
+
|
|
128
|
+
| Flag | Purpose |
|
|
129
|
+
|------|---------|
|
|
130
|
+
| `--quote` | Quote ID from `trade quote` |
|
|
131
|
+
| `--wallet` | Wallet name (default: default wallet) |
|
|
132
|
+
| `--quote-index` | Pin a specific quote by index (0-based) when multiple quotes were returned |
|
|
119
133
|
| `--no-simulate` | Skip pre-broadcast simulation |
|
|
120
|
-
|
|
134
|
+
|
|
135
|
+
### `trade bridge-status` flags
|
|
136
|
+
|
|
137
|
+
| Flag | Purpose |
|
|
138
|
+
|------|---------|
|
|
139
|
+
| `--tx-hash` | Source tx hash |
|
|
121
140
|
| `--from-chain` | Source chain (for bridge-status) |
|
|
141
|
+
| `--to-chain` | Destination chain (for bridge-status) |
|
|
122
142
|
|
|
123
143
|
## Environment Variables
|
|
124
144
|
|
package/src/api.js
CHANGED
|
@@ -766,7 +766,7 @@ export class NansenAPI {
|
|
|
766
766
|
// ============= Profiler Endpoints =============
|
|
767
767
|
|
|
768
768
|
async addressBalance(params = {}) {
|
|
769
|
-
const { address, entityName, chain = '
|
|
769
|
+
const { address, entityName, chain = 'all', hideSpamToken = true, filters = {}, orderBy } = params;
|
|
770
770
|
if (address) {
|
|
771
771
|
const validation = validateAddress(address, chain);
|
|
772
772
|
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
@@ -971,19 +971,21 @@ export class NansenAPI {
|
|
|
971
971
|
}
|
|
972
972
|
|
|
973
973
|
async tokenHolders(params = {}) {
|
|
974
|
-
const { tokenAddress, chain = 'solana', labelType = 'all_holders', filters = {}, orderBy, pagination } = params;
|
|
974
|
+
const { tokenAddress, chain = 'solana', labelType = 'all_holders', filters = {}, orderBy, pagination, withLabels } = params;
|
|
975
975
|
if (tokenAddress) {
|
|
976
976
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
977
977
|
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
978
978
|
}
|
|
979
|
-
|
|
979
|
+
const body = {
|
|
980
980
|
token_address: tokenAddress,
|
|
981
981
|
chain,
|
|
982
982
|
label_type: labelType,
|
|
983
983
|
filters,
|
|
984
984
|
order_by: orderBy,
|
|
985
985
|
pagination
|
|
986
|
-
}
|
|
986
|
+
};
|
|
987
|
+
if (withLabels !== undefined) body.premium_labels = withLabels;
|
|
988
|
+
return this.request('/api/v1/tgm/holders', body);
|
|
987
989
|
}
|
|
988
990
|
|
|
989
991
|
async tokenFlows(params = {}) {
|
|
@@ -1027,19 +1029,21 @@ export class NansenAPI {
|
|
|
1027
1029
|
}
|
|
1028
1030
|
|
|
1029
1031
|
async tokenPnlLeaderboard(params = {}) {
|
|
1030
|
-
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
1032
|
+
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 30, withLabels } = params;
|
|
1031
1033
|
if (tokenAddress) {
|
|
1032
1034
|
const validation = validateTokenAddress(tokenAddress, chain);
|
|
1033
1035
|
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1034
1036
|
}
|
|
1035
|
-
|
|
1037
|
+
const body = {
|
|
1036
1038
|
token_address: tokenAddress,
|
|
1037
1039
|
chain,
|
|
1038
1040
|
date: buildDateRange(days),
|
|
1039
1041
|
filters,
|
|
1040
1042
|
order_by: orderBy,
|
|
1041
1043
|
pagination
|
|
1042
|
-
}
|
|
1044
|
+
};
|
|
1045
|
+
if (withLabels !== undefined) body.premium_labels = withLabels;
|
|
1046
|
+
return this.request('/api/v1/tgm/pnl-leaderboard', body);
|
|
1043
1047
|
}
|
|
1044
1048
|
|
|
1045
1049
|
async tokenWhoBoughtSold(params = {}) {
|
|
@@ -1126,14 +1130,16 @@ export class NansenAPI {
|
|
|
1126
1130
|
}
|
|
1127
1131
|
|
|
1128
1132
|
async tokenPerpPnlLeaderboard(params = {}) {
|
|
1129
|
-
const { tokenSymbol, filters = {}, orderBy, pagination, days = 30 } = params;
|
|
1130
|
-
|
|
1133
|
+
const { tokenSymbol, filters = {}, orderBy, pagination, days = 30, withLabels } = params;
|
|
1134
|
+
const body = {
|
|
1131
1135
|
token_symbol: tokenSymbol,
|
|
1132
1136
|
date: buildDateRange(days),
|
|
1133
1137
|
filters,
|
|
1134
1138
|
order_by: orderBy,
|
|
1135
1139
|
pagination
|
|
1136
|
-
}
|
|
1140
|
+
};
|
|
1141
|
+
if (withLabels !== undefined) body.premium_labels = withLabels;
|
|
1142
|
+
return this.request('/api/v1/tgm/perp-pnl-leaderboard', body);
|
|
1137
1143
|
}
|
|
1138
1144
|
|
|
1139
1145
|
async tokenIndicators(params = {}) {
|
|
@@ -1187,13 +1193,15 @@ export class NansenAPI {
|
|
|
1187
1193
|
}
|
|
1188
1194
|
|
|
1189
1195
|
async perpLeaderboard(params = {}) {
|
|
1190
|
-
const { filters = {}, orderBy, pagination, days = 30 } = params;
|
|
1191
|
-
|
|
1196
|
+
const { filters = {}, orderBy, pagination, days = 30, withLabels } = params;
|
|
1197
|
+
const body = {
|
|
1192
1198
|
date: buildDateRange(days),
|
|
1193
1199
|
filters,
|
|
1194
1200
|
order_by: orderBy,
|
|
1195
1201
|
pagination
|
|
1196
|
-
}
|
|
1202
|
+
};
|
|
1203
|
+
if (withLabels !== undefined) body.premium_labels = withLabels;
|
|
1204
|
+
return this.request('/api/v1/perp-leaderboard', body);
|
|
1197
1205
|
}
|
|
1198
1206
|
|
|
1199
1207
|
// ============= Prediction Market Endpoints =============
|
package/src/cli.js
CHANGED
|
@@ -30,6 +30,23 @@ export const SCHEMA = { version: VERSION, ...schemaDefinition };
|
|
|
30
30
|
|
|
31
31
|
// ============= Pagination =============
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a boolean CLI option that can be passed as either:
|
|
35
|
+
* --flag (flag=true, options key absent)
|
|
36
|
+
* --flag true (options key = 'true')
|
|
37
|
+
* --flag false (options key = 'false')
|
|
38
|
+
* Returns true/false/undefined (undefined = not supplied).
|
|
39
|
+
*/
|
|
40
|
+
export function resolveBooleanOption(options, flags, key) {
|
|
41
|
+
if (options[key] !== undefined) {
|
|
42
|
+
const val = String(options[key]).toLowerCase();
|
|
43
|
+
if (val === 'true' || val === '1') return true;
|
|
44
|
+
if (val === 'false' || val === '0') return false;
|
|
45
|
+
}
|
|
46
|
+
if (flags[key] !== undefined) return Boolean(flags[key]);
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
export function buildPagination(options) {
|
|
34
51
|
if (!options.limit && !options.page) return undefined;
|
|
35
52
|
return {
|
|
@@ -1093,7 +1110,7 @@ export function buildCommands(deps = {}) {
|
|
|
1093
1110
|
const subcommand = args[0] || 'help';
|
|
1094
1111
|
let address = options.address;
|
|
1095
1112
|
const entityName = options.entity || options['entity-name'];
|
|
1096
|
-
const chain = options.chain || '
|
|
1113
|
+
const chain = options.chain || 'all';
|
|
1097
1114
|
|
|
1098
1115
|
// Resolve ENS names (e.g. vitalik.eth → 0x...)
|
|
1099
1116
|
let ensName;
|
|
@@ -1237,14 +1254,17 @@ export function buildCommands(deps = {}) {
|
|
|
1237
1254
|
}
|
|
1238
1255
|
return result;
|
|
1239
1256
|
},
|
|
1240
|
-
'holders': () => apiInstance.tokenHolders({ tokenAddress, chain, labelType: onlySmartMoney ? 'smart_money' : 'all_holders', filters, orderBy, pagination }),
|
|
1257
|
+
'holders': () => apiInstance.tokenHolders({ tokenAddress, chain, labelType: onlySmartMoney ? 'smart_money' : 'all_holders', filters, orderBy, pagination, withLabels: resolveBooleanOption(options, flags, 'premium-labels') }),
|
|
1241
1258
|
'flows': () => {
|
|
1242
1259
|
const date = parseDateOption(options.date, days);
|
|
1243
1260
|
const label = options.label;
|
|
1244
1261
|
return apiInstance.tokenFlows({ tokenAddress, chain, label, filters, orderBy, pagination, days, date });
|
|
1245
1262
|
},
|
|
1246
1263
|
'dex-trades': () => apiInstance.tokenDexTrades({ tokenAddress, chain, onlySmartMoney, filters, orderBy, pagination, days }),
|
|
1247
|
-
'pnl': () =>
|
|
1264
|
+
'pnl': () => {
|
|
1265
|
+
const withLabels = resolveBooleanOption(options, flags, 'premium-labels');
|
|
1266
|
+
return apiInstance.tokenPnlLeaderboard({ tokenAddress, chain, filters, orderBy, pagination, days, withLabels });
|
|
1267
|
+
},
|
|
1248
1268
|
'who-bought-sold': () => {
|
|
1249
1269
|
const date = parseDateOption(options.date, days);
|
|
1250
1270
|
const buyOrSell = (options['buy-or-sell'] || 'BUY').toUpperCase();
|
|
@@ -1260,7 +1280,10 @@ export function buildCommands(deps = {}) {
|
|
|
1260
1280
|
'jup-dca': () => apiInstance.tokenJupDca({ tokenAddress, filters, orderBy, pagination }),
|
|
1261
1281
|
'perp-trades': () => apiInstance.tokenPerpTrades({ tokenSymbol, filters, orderBy, pagination, days }),
|
|
1262
1282
|
'perp-positions': () => apiInstance.tokenPerpPositions({ tokenSymbol, filters, orderBy, pagination }),
|
|
1263
|
-
'perp-pnl-leaderboard': () =>
|
|
1283
|
+
'perp-pnl-leaderboard': () => {
|
|
1284
|
+
const withLabels = resolveBooleanOption(options, flags, 'premium-labels');
|
|
1285
|
+
return apiInstance.tokenPerpPnlLeaderboard({ tokenSymbol, filters, orderBy, pagination, days, withLabels });
|
|
1286
|
+
},
|
|
1264
1287
|
'help': () => ({
|
|
1265
1288
|
commands: ['info', 'ohlcv', 'screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
|
|
1266
1289
|
description: 'Token God Mode endpoints',
|
|
@@ -1334,7 +1357,10 @@ export function buildCommands(deps = {}) {
|
|
|
1334
1357
|
|
|
1335
1358
|
const handlers = {
|
|
1336
1359
|
'screener': () => apiInstance.perpScreener({ filters, orderBy, pagination, days }),
|
|
1337
|
-
'leaderboard': () =>
|
|
1360
|
+
'leaderboard': () => {
|
|
1361
|
+
const withLabels = resolveBooleanOption(options, flags, 'premium-labels');
|
|
1362
|
+
return apiInstance.perpLeaderboard({ filters, orderBy, pagination, days, withLabels });
|
|
1363
|
+
},
|
|
1338
1364
|
'help': () => ({
|
|
1339
1365
|
commands: ['screener', 'leaderboard'],
|
|
1340
1366
|
description: 'Perpetual futures analytics endpoints',
|
package/src/schema.json
CHANGED
|
@@ -283,7 +283,13 @@
|
|
|
283
283
|
},
|
|
284
284
|
"label": {
|
|
285
285
|
"description": "Holder segment to filter flows by",
|
|
286
|
-
"enum": [
|
|
286
|
+
"enum": [
|
|
287
|
+
"top_100_holders",
|
|
288
|
+
"smart_money",
|
|
289
|
+
"public_figure",
|
|
290
|
+
"whale",
|
|
291
|
+
"exchange"
|
|
292
|
+
],
|
|
287
293
|
"default": "top_100_holders"
|
|
288
294
|
}
|
|
289
295
|
}
|
|
@@ -303,7 +309,10 @@
|
|
|
303
309
|
},
|
|
304
310
|
"buy-or-sell": {
|
|
305
311
|
"description": "Filter by buy or sell side",
|
|
306
|
-
"enum": [
|
|
312
|
+
"enum": [
|
|
313
|
+
"BUY",
|
|
314
|
+
"SELL"
|
|
315
|
+
],
|
|
307
316
|
"default": "BUY"
|
|
308
317
|
}
|
|
309
318
|
}
|
|
@@ -347,6 +356,10 @@
|
|
|
347
356
|
},
|
|
348
357
|
"token": {
|
|
349
358
|
"required": true
|
|
359
|
+
},
|
|
360
|
+
"premium-labels": {
|
|
361
|
+
"description": "Include premium Nansen labels in the response (true=premium, false=free-tier). When omitted, uses API default.",
|
|
362
|
+
"type": "boolean"
|
|
350
363
|
}
|
|
351
364
|
}
|
|
352
365
|
},
|
|
@@ -362,6 +375,10 @@
|
|
|
362
375
|
},
|
|
363
376
|
"days": {
|
|
364
377
|
"default": 30
|
|
378
|
+
},
|
|
379
|
+
"premium-labels": {
|
|
380
|
+
"description": "Include premium Nansen labels in the response (true=premium, false=free-tier). When omitted, uses API default.",
|
|
381
|
+
"type": "boolean"
|
|
365
382
|
}
|
|
366
383
|
}
|
|
367
384
|
},
|
|
@@ -374,6 +391,10 @@
|
|
|
374
391
|
},
|
|
375
392
|
"days": {
|
|
376
393
|
"default": 30
|
|
394
|
+
},
|
|
395
|
+
"premium-labels": {
|
|
396
|
+
"description": "Include premium Nansen labels in the response (true=premium, false=free-tier). When omitted, uses API default.",
|
|
397
|
+
"type": "boolean"
|
|
377
398
|
}
|
|
378
399
|
}
|
|
379
400
|
},
|
|
@@ -515,6 +536,10 @@
|
|
|
515
536
|
"options": {
|
|
516
537
|
"days": {
|
|
517
538
|
"default": 30
|
|
539
|
+
},
|
|
540
|
+
"premium-labels": {
|
|
541
|
+
"description": "Include premium Nansen labels in the response (true=premium, false=free-tier). When omitted, uses API default.",
|
|
542
|
+
"type": "boolean"
|
|
518
543
|
}
|
|
519
544
|
}
|
|
520
545
|
}
|
|
@@ -796,11 +821,11 @@
|
|
|
796
821
|
"amount": {
|
|
797
822
|
"type": "string",
|
|
798
823
|
"required": true,
|
|
799
|
-
"description": "Amount to swap (base units by default, or token units with --amount-unit token, or
|
|
824
|
+
"description": "Amount to swap (base units by default, or token units with --amount-unit token, USD with --amount-unit usd, or percentage of balance with --amount-unit percent)"
|
|
800
825
|
},
|
|
801
826
|
"amount-unit": {
|
|
802
827
|
"type": "string",
|
|
803
|
-
"description": "\"token\" to specify amount in token units (e.g. 0.5 SOL), \"usd\" to specify amount in USD (e.g. 50), or \"base\" for base units (default). The CLI resolves the current token price and
|
|
828
|
+
"description": "\"token\" to specify amount in token units (e.g. 0.5 SOL), \"usd\" to specify amount in USD (e.g. 50), \"percent\" to sell a percentage of your balance (e.g. 100 for all), or \"base\" for base units (default). The CLI resolves the current token price, decimals, and balance locally; the API always receives base units."
|
|
804
829
|
},
|
|
805
830
|
"wallet": {
|
|
806
831
|
"type": "string",
|
package/src/trade-validation.js
CHANGED
|
@@ -210,6 +210,65 @@ export async function validateBalance({ chain, from, amount, amountUnit, walletA
|
|
|
210
210
|
return { adjustedAmount: amount };
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Resolve a percentage amount to a token-unit amount string.
|
|
215
|
+
* Fetches the wallet's balance of the sell token, calculates the percentage,
|
|
216
|
+
* and applies a native-token fee buffer when selling >=95%.
|
|
217
|
+
*
|
|
218
|
+
* Returns the amount in human-readable token units (e.g. "1.5"),
|
|
219
|
+
* ready for convertToBaseUnits().
|
|
220
|
+
*/
|
|
221
|
+
export async function resolvePercentAmount({ chain, from, walletAddress, percentage, decimals }) {
|
|
222
|
+
if (!Number.isFinite(percentage) || percentage <= 0 || percentage > 100) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
percentage > 100
|
|
225
|
+
? `Cannot sell more than 100% of balance. Got: ${percentage}%`
|
|
226
|
+
: `Percentage must be between 0 and 100. Got: ${percentage}%`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const normalizedChain = chain.toLowerCase();
|
|
231
|
+
const isNative = isNativeAddress(from, normalizedChain);
|
|
232
|
+
|
|
233
|
+
let balance;
|
|
234
|
+
if (isNative) {
|
|
235
|
+
balance = await fetchNativeBalance(normalizedChain, walletAddress);
|
|
236
|
+
} else {
|
|
237
|
+
balance = await fetchTokenBalance(normalizedChain, from, walletAddress, decimals);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (balance === null) {
|
|
241
|
+
throw new Error(`Could not fetch balance for ${from} on ${normalizedChain}. Check your RPC connection.`);
|
|
242
|
+
}
|
|
243
|
+
if (balance === 0) {
|
|
244
|
+
const symbol = isNative ? (NATIVE_SYMBOLS[normalizedChain] || from) : from;
|
|
245
|
+
throw new Error(`No ${symbol} balance in wallet. You cannot trade a token you don't own.`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Calculate token amount from percentage.
|
|
249
|
+
// Use exact balance for 100% to avoid floating-point precision loss.
|
|
250
|
+
let tokenAmount = percentage === 100 ? balance : balance * (percentage / 100);
|
|
251
|
+
|
|
252
|
+
// Native token fee buffer: when selling >=95%, cap at balance - reserve.
|
|
253
|
+
if (isNative && percentage >= HIGH_PERCENTAGE_THRESHOLD) {
|
|
254
|
+
const reserve = FEE_BUFFER[normalizedChain] || 0;
|
|
255
|
+
const maxSellable = parseFloat((balance - reserve).toFixed(NATIVE_DECIMALS[normalizedChain]));
|
|
256
|
+
if (maxSellable <= 0) {
|
|
257
|
+
const symbol = NATIVE_SYMBOLS[normalizedChain] || from;
|
|
258
|
+
throw new Error(`Insufficient ${symbol} balance after reserving gas fees.`);
|
|
259
|
+
}
|
|
260
|
+
if (tokenAmount > maxSellable) {
|
|
261
|
+
const symbol = NATIVE_SYMBOLS[normalizedChain] || from;
|
|
262
|
+
process.stderr.write(
|
|
263
|
+
`Warning: Reserving ${reserve} ${symbol} for gas. Adjusted sell amount to ${maxSellable} ${symbol}.\n`
|
|
264
|
+
);
|
|
265
|
+
tokenAmount = maxSellable;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return String(parseFloat(tokenAmount.toFixed(decimals)));
|
|
270
|
+
}
|
|
271
|
+
|
|
213
272
|
/**
|
|
214
273
|
* Fetch an ERC-20 or SPL token balance for a wallet.
|
|
215
274
|
* Returns balance in human-readable token units, or null on RPC failure.
|
package/src/trading.js
CHANGED
|
@@ -13,7 +13,7 @@ import { base58Decode } from './transfer.js';
|
|
|
13
13
|
import { keccak256, signSecp256k1, rlpEncode } from './crypto.js';
|
|
14
14
|
import { getWalletConnectAddress, sendTransactionViaWalletConnect, sendSolanaTransactionViaWalletConnect, sendApprovalViaWalletConnect } from './walletconnect-trading.js';
|
|
15
15
|
import { retrievePassword } from './keychain.js';
|
|
16
|
-
import { validateQuoteInput, validateBalance } from './trade-validation.js';
|
|
16
|
+
import { validateQuoteInput, validateBalance, resolvePercentAmount } from './trade-validation.js';
|
|
17
17
|
import { CHAIN_RPCS } from './rpc-urls.js';
|
|
18
18
|
|
|
19
19
|
// ============= Constants =============
|
|
@@ -975,7 +975,7 @@ OPTIONS:
|
|
|
975
975
|
--from <symbol|address> Input token (symbol like SOL, USDC or address)
|
|
976
976
|
--to <symbol|address> Output token (symbol like USDC, ETH or address)
|
|
977
977
|
--amount <units> Amount in BASE UNITS (e.g. lamports, wei)
|
|
978
|
-
--amount-unit <unit> "token" for token units
|
|
978
|
+
--amount-unit <unit> "token" for token units, "usd" for USD, "percent" for % of balance
|
|
979
979
|
--wallet <name> Wallet name (default: default wallet). Use "walletconnect" or "wc" for WalletConnect.
|
|
980
980
|
--to-wallet <address> Destination wallet address (auto-derived for cross-chain if omitted)
|
|
981
981
|
--slippage <pct> Slippage as decimal (e.g. 0.03 for 3%). Default: 0.03
|
|
@@ -987,6 +987,7 @@ EXAMPLES:
|
|
|
987
987
|
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
988
988
|
nansen trade quote --chain solana --from SOL --to USDC --amount 0.5 --amount-unit token
|
|
989
989
|
nansen trade quote --chain solana --from SOL --to USDC --amount 50 --amount-unit usd
|
|
990
|
+
nansen trade quote --chain solana --from SOL --to USDC --amount 100 --amount-unit percent
|
|
990
991
|
nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
|
|
991
992
|
nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
|
|
992
993
|
nansen trade quote --chain solana --to-chain base --from SOL --to ETH --amount 1000000000
|
|
@@ -996,8 +997,15 @@ EXAMPLES:
|
|
|
996
997
|
}
|
|
997
998
|
|
|
998
999
|
// Validate --amount-unit if provided
|
|
999
|
-
if (amountUnit && amountUnit !== 'token' && amountUnit !== 'base' && amountUnit !== 'usd') {
|
|
1000
|
-
log(`Error: Unknown --amount-unit "${amountUnit}". Supported values: token, base, usd`);
|
|
1000
|
+
if (amountUnit && amountUnit !== 'token' && amountUnit !== 'base' && amountUnit !== 'usd' && amountUnit !== 'percent') {
|
|
1001
|
+
log(`Error: Unknown --amount-unit "${amountUnit}". Supported values: token, base, usd, percent`);
|
|
1002
|
+
exit(1);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// --amount-unit percent is only valid for exactIn (sell-side)
|
|
1007
|
+
if (amountUnit === 'percent' && swapMode === 'exactOut') {
|
|
1008
|
+
log('Error: --amount-unit percent is not supported with --swap-mode exactOut. Percentage is relative to your sell-token balance.');
|
|
1001
1009
|
exit(1);
|
|
1002
1010
|
return;
|
|
1003
1011
|
}
|
|
@@ -1042,6 +1050,8 @@ EXAMPLES:
|
|
|
1042
1050
|
exit(1);
|
|
1043
1051
|
return;
|
|
1044
1052
|
}
|
|
1053
|
+
} else if (amountUnit === 'percent') {
|
|
1054
|
+
// Resolved after wallet address is available — see percent resolution block below.
|
|
1045
1055
|
} else {
|
|
1046
1056
|
const amountError = validateBaseUnitAmount(amount);
|
|
1047
1057
|
if (amountError) {
|
|
@@ -1096,6 +1106,26 @@ EXAMPLES:
|
|
|
1096
1106
|
return;
|
|
1097
1107
|
}
|
|
1098
1108
|
|
|
1109
|
+
// --amount-unit percent: fetch balance, calculate percentage, convert to base units.
|
|
1110
|
+
// Placed after wallet resolution because we need the wallet address to fetch balance.
|
|
1111
|
+
if (amountUnit === 'percent') {
|
|
1112
|
+
try {
|
|
1113
|
+
resolvedDecimals = await resolveTokenDecimals(from, chain);
|
|
1114
|
+
const tokenAmount = await resolvePercentAmount({
|
|
1115
|
+
chain,
|
|
1116
|
+
from,
|
|
1117
|
+
walletAddress,
|
|
1118
|
+
percentage: parseFloat(amount),
|
|
1119
|
+
decimals: resolvedDecimals,
|
|
1120
|
+
});
|
|
1121
|
+
resolvedAmount = convertToBaseUnits(tokenAmount, resolvedDecimals);
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
log(`Error: ${err.message}`);
|
|
1124
|
+
exit(1);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1099
1129
|
// Balance pre-check — catches zero balances and insufficient funds
|
|
1100
1130
|
// before wasting a quote API call. Runs for --amount-unit token and
|
|
1101
1131
|
// usd (after USD→token conversion) in exactIn mode. In exactOut the
|
package/src/transfer.js
CHANGED
|
@@ -559,8 +559,7 @@ async function broadcastTransaction(signedTx, chain) {
|
|
|
559
559
|
|
|
560
560
|
// ============= Public API =============
|
|
561
561
|
|
|
562
|
-
|
|
563
|
-
export { parseAmount, formatAmount, signEd25519, encodeCompactU16, base58Decode, base58DecodePubkey, deriveATA, validateEvmAddress, validateSolanaAddress, bigIntToHex };
|
|
562
|
+
export { parseAmount, formatAmount, signEd25519, encodeCompactU16, base58Decode, base58DecodePubkey, deriveATA, isOnEd25519Curve, validateEvmAddress, validateSolanaAddress, bigIntToHex };
|
|
564
563
|
|
|
565
564
|
/**
|
|
566
565
|
* Send tokens via Privy server wallet. EVM uses Privy's sendTransaction (handles gas/nonce).
|
package/src/x402-svm.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import crypto from 'crypto';
|
|
7
7
|
import { base58Encode, base58DecodePubkey } from './wallet.js';
|
|
8
|
-
import { encodeCompactU16 } from './transfer.js';
|
|
8
|
+
import { encodeCompactU16, isOnEd25519Curve } from './transfer.js';
|
|
9
9
|
|
|
10
10
|
// ============= Constants =============
|
|
11
11
|
|
|
@@ -41,65 +41,13 @@ export function deriveATA(ownerBase58, mintBase58, tokenProgramBase58 = TOKEN_PR
|
|
|
41
41
|
.update(Buffer.concat([owner, tokenProgram, mint, Buffer.from([nonce]), ataProgramKey, Buffer.from('ProgramDerivedAddress')]))
|
|
42
42
|
.digest();
|
|
43
43
|
|
|
44
|
-
if (!
|
|
44
|
+
if (!isOnEd25519Curve(hash)) {
|
|
45
45
|
return base58Encode(hash);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
throw new Error('Could not derive ATA: no valid PDA found');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
* Check if a 32-byte buffer represents a valid ed25519 curve point.
|
|
53
|
-
* Ed25519 curve: -x² + y² = 1 + d*x²*y² over GF(p) where p = 2^255 - 19
|
|
54
|
-
*
|
|
55
|
-
* Decode y from the 32 bytes, compute x² = (y² - 1) / (d*y² + 1),
|
|
56
|
-
* then check if x² is a quadratic residue (QR) mod p.
|
|
57
|
-
*/
|
|
58
|
-
function isOnCurve(bytes) {
|
|
59
|
-
const p = (1n << 255n) - 19n;
|
|
60
|
-
const d = -121665n * modInverse(121666n, p) % p;
|
|
61
|
-
|
|
62
|
-
// Read y-coordinate (little-endian, clear top bit which is sign of x)
|
|
63
|
-
let y = 0n;
|
|
64
|
-
for (let i = 0; i < 32; i++) {
|
|
65
|
-
y |= BigInt(bytes[i]) << (BigInt(i) * 8n);
|
|
66
|
-
}
|
|
67
|
-
y &= (1n << 255n) - 1n; // Clear top bit
|
|
68
|
-
|
|
69
|
-
if (y >= p) return false;
|
|
70
|
-
|
|
71
|
-
// y² mod p
|
|
72
|
-
const y2 = modPow(y, 2n, p);
|
|
73
|
-
|
|
74
|
-
// x² = (y² - 1) * inverse(d*y² + 1) mod p
|
|
75
|
-
const num = ((y2 - 1n) % p + p) % p;
|
|
76
|
-
const den = ((d * y2 + 1n) % p + p) % p;
|
|
77
|
-
const denInv = modInverse(den, p);
|
|
78
|
-
if (denInv === null) return false;
|
|
79
|
-
|
|
80
|
-
const x2 = (num * denInv) % p;
|
|
81
|
-
|
|
82
|
-
// Check if x² is a quadratic residue: x^((p-1)/2) == 1 mod p
|
|
83
|
-
if (x2 === 0n) return true;
|
|
84
|
-
const euler = modPow(x2, (p - 1n) / 2n, p);
|
|
85
|
-
return euler === 1n;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function modPow(base, exp, mod) {
|
|
89
|
-
let result = 1n;
|
|
90
|
-
base = ((base % mod) + mod) % mod;
|
|
91
|
-
while (exp > 0n) {
|
|
92
|
-
if (exp & 1n) result = (result * base) % mod;
|
|
93
|
-
exp >>= 1n;
|
|
94
|
-
base = (base * base) % mod;
|
|
95
|
-
}
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function modInverse(a, mod) {
|
|
100
|
-
return modPow(((a % mod) + mod) % mod, mod - 2n, mod);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
51
|
// ============= MessageV0 Builder =============
|
|
104
52
|
|
|
105
53
|
/**
|