nansen-cli 1.24.0 → 1.25.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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.25.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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
8
+
9
+ ## 1.25.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#363](https://github.com/nansen-ai/nansen-cli/pull/363) [`6ae402e`](https://github.com/nansen-ai/nansen-cli/commit/6ae402ef1e5bdeacf83fe04bcf6e8e0c9f9c91b7) Thanks [@TimNooren](https://github.com/TimNooren)! - Add `--amount-unit usd` to trade commands — specify swap amounts in USD
14
+
15
+ ### Patch Changes
16
+
17
+ - [#374](https://github.com/nansen-ai/nansen-cli/pull/374) [`0f14803`](https://github.com/nansen-ai/nansen-cli/commit/0f148031ef7590f3405c1dba9f31ad83768a7141) Thanks [@TimNooren](https://github.com/TimNooren)! - Fix cross-chain quote display: show adaptive precision for sub-cent bridge fees, "< 1 min" for fast bridges, and echo --to-wallet address in output
18
+
19
+ - [#366](https://github.com/nansen-ai/nansen-cli/pull/366) [`f358fff`](https://github.com/nansen-ai/nansen-cli/commit/f358fffcc80136c4e609f2e056f2c5ffb052d626) Thanks [@kome12](https://github.com/kome12)! - fix(token): replace dead `--days` param with working `--timeframe` for `token flow-intelligence`
20
+
21
+ The `--days` option was accepted but never sent to the API, resulting in always fetching `1d` data. This replaces it with `--timeframe` (enum: `1h | 6h | 12h | 1d | 7d`, default `1d`) which maps correctly to the API parameter.
22
+
3
23
  ## 1.24.0
4
24
 
5
25
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.24.0",
3
+ "version": "1.25.1",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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` to specify human-readable token amounts instead — the CLI resolves decimals locally and sends base units to the API.
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", 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.
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 token` |
114
- | `--amount-unit` | Set to `token` to specify amount in token units (e.g. 0.5 SOL) |
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
- | `--quote` | Quote ID for execute |
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
- | `--tx-hash` | Source tx hash (for bridge-status) |
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
@@ -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
- return this.request('/api/v1/tgm/holders', {
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
- return this.request('/api/v1/tgm/pnl-leaderboard', {
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 = {}) {
@@ -1061,14 +1065,15 @@ export class NansenAPI {
1061
1065
  }
1062
1066
 
1063
1067
  async tokenFlowIntelligence(params = {}) {
1064
- const { tokenAddress, chain = 'solana' } = params;
1068
+ const { tokenAddress, chain = 'solana', timeframe = '1d' } = params;
1065
1069
  if (tokenAddress) {
1066
1070
  const validation = validateTokenAddress(tokenAddress, chain);
1067
1071
  if (!validation.valid) throw new NansenError(validation.error, validation.code);
1068
1072
  }
1069
1073
  return this.request('/api/v1/tgm/flow-intelligence', {
1070
1074
  token_address: tokenAddress,
1071
- chain
1075
+ chain,
1076
+ timeframe
1072
1077
  });
1073
1078
  }
1074
1079
 
@@ -1125,14 +1130,16 @@ export class NansenAPI {
1125
1130
  }
1126
1131
 
1127
1132
  async tokenPerpPnlLeaderboard(params = {}) {
1128
- const { tokenSymbol, filters = {}, orderBy, pagination, days = 30 } = params;
1129
- return this.request('/api/v1/tgm/perp-pnl-leaderboard', {
1133
+ const { tokenSymbol, filters = {}, orderBy, pagination, days = 30, withLabels } = params;
1134
+ const body = {
1130
1135
  token_symbol: tokenSymbol,
1131
1136
  date: buildDateRange(days),
1132
1137
  filters,
1133
1138
  order_by: orderBy,
1134
1139
  pagination
1135
- });
1140
+ };
1141
+ if (withLabels !== undefined) body.premium_labels = withLabels;
1142
+ return this.request('/api/v1/tgm/perp-pnl-leaderboard', body);
1136
1143
  }
1137
1144
 
1138
1145
  async tokenIndicators(params = {}) {
@@ -1186,13 +1193,15 @@ export class NansenAPI {
1186
1193
  }
1187
1194
 
1188
1195
  async perpLeaderboard(params = {}) {
1189
- const { filters = {}, orderBy, pagination, days = 30 } = params;
1190
- return this.request('/api/v1/perp-leaderboard', {
1196
+ const { filters = {}, orderBy, pagination, days = 30, withLabels } = params;
1197
+ const body = {
1191
1198
  date: buildDateRange(days),
1192
1199
  filters,
1193
1200
  order_by: orderBy,
1194
1201
  pagination
1195
- });
1202
+ };
1203
+ if (withLabels !== undefined) body.premium_labels = withLabels;
1204
+ return this.request('/api/v1/perp-leaderboard', body);
1196
1205
  }
1197
1206
 
1198
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 {
@@ -1237,20 +1254,23 @@ 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': () => apiInstance.tokenPnlLeaderboard({ tokenAddress, chain, filters, orderBy, pagination, days }),
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();
1251
1271
  return apiInstance.tokenWhoBoughtSold({ tokenAddress, chain, buyOrSell, filters, orderBy, pagination, days, date });
1252
1272
  },
1253
- 'flow-intelligence': () => apiInstance.tokenFlowIntelligence({ tokenAddress, chain, days }),
1273
+ 'flow-intelligence': () => apiInstance.tokenFlowIntelligence({ tokenAddress, chain, timeframe: options.timeframe || '1d' }),
1254
1274
  'transfers': () => {
1255
1275
  // Inject --from/--to into filters
1256
1276
  if (options.from) filters.from_address = options.from;
@@ -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': () => apiInstance.tokenPerpPnlLeaderboard({ tokenSymbol, filters, orderBy, pagination, days }),
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': () => apiInstance.perpLeaderboard({ filters, orderBy, pagination, days }),
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": ["top_100_holders", "smart_money", "public_figure", "whale", "exchange"],
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": ["BUY", "SELL"],
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
  },
@@ -408,8 +429,8 @@
408
429
  "token": {
409
430
  "required": true
410
431
  },
411
- "days": {
412
- "default": 30
432
+ "timeframe": {
433
+ "default": "1d"
413
434
  }
414
435
  }
415
436
  },
@@ -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)"
824
+ "description": "Amount to swap (base units by default, or token units with --amount-unit token, or USD with --amount-unit usd)"
800
825
  },
801
826
  "amount-unit": {
802
827
  "type": "string",
803
- "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."
828
+ "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 decimals locally; the API always receives base units."
804
829
  },
805
830
  "wallet": {
806
831
  "type": "string",
package/src/trading.js CHANGED
@@ -853,6 +853,27 @@ export function convertToBaseUnits(amount, decimals) {
853
853
  return raw;
854
854
  }
855
855
 
856
+ /**
857
+ * Fetch the current USD price for a token via the Nansen search API.
858
+ * Used by --amount-unit usd to convert dollar amounts to token amounts.
859
+ */
860
+ export async function resolveUsdPrice(apiInstance, tokenAddress, chain) {
861
+ const result = await apiInstance.generalSearch({
862
+ query: tokenAddress,
863
+ resultType: 'token',
864
+ chain,
865
+ limit: 1,
866
+ });
867
+ const isEvm = tokenAddress.startsWith('0x');
868
+ const token = result.tokens?.find(t =>
869
+ isEvm ? t.address?.toLowerCase() === tokenAddress.toLowerCase() : t.address === tokenAddress
870
+ );
871
+ if (!token?.price) {
872
+ throw new Error(`Could not resolve USD price for ${tokenAddress} on ${chain}. The token may not have pricing data.`);
873
+ }
874
+ return token.price;
875
+ }
876
+
856
877
  /**
857
878
  * Check if amount contains a decimal point (i.e. not in base units).
858
879
  * Returns an error string if invalid, or null if OK. Pure function.
@@ -892,10 +913,16 @@ export function formatQuote(quote, index) {
892
913
  const meta = quote.metadata || {};
893
914
  if (meta.isCrossChain) {
894
915
  if (meta.bridgeTool) lines.push(` Bridge: ${meta.bridgeTool}`);
895
- if (meta.executionDuration) lines.push(` Est. Time: ~${Math.round(meta.executionDuration / 60)} min`);
916
+ if (meta.executionDuration) {
917
+ const mins = Math.round(meta.executionDuration / 60);
918
+ lines.push(` Est. Time: ${mins < 1 ? '< 1 min' : `~${mins} min`}`);
919
+ }
896
920
  if (meta.feeCosts?.length) {
897
921
  const totalFees = meta.feeCosts.reduce((sum, f) => sum + parseFloat(f.amountUSD || 0), 0);
898
- if (totalFees > 0) lines.push(` Bridge Fees: $${totalFees.toFixed(2)}`);
922
+ if (totalFees > 0) {
923
+ const feeStr = totalFees < 0.01 ? totalFees.toPrecision(1) : totalFees.toFixed(2);
924
+ lines.push(` Bridge Fees: $${feeStr}`);
925
+ }
899
926
  }
900
927
  }
901
928
  if (quote.priceImpactPct) {
@@ -948,7 +975,7 @@ OPTIONS:
948
975
  --from <symbol|address> Input token (symbol like SOL, USDC or address)
949
976
  --to <symbol|address> Output token (symbol like USDC, ETH or address)
950
977
  --amount <units> Amount in BASE UNITS (e.g. lamports, wei)
951
- --amount-unit <unit> "token" to specify amount in token units (e.g. 0.5 SOL)
978
+ --amount-unit <unit> "token" for token units (e.g. 0.5 SOL), "usd" for USD (e.g. 50)
952
979
  --wallet <name> Wallet name (default: default wallet). Use "walletconnect" or "wc" for WalletConnect.
953
980
  --to-wallet <address> Destination wallet address (auto-derived for cross-chain if omitted)
954
981
  --slippage <pct> Slippage as decimal (e.g. 0.03 for 3%). Default: 0.03
@@ -959,6 +986,7 @@ OPTIONS:
959
986
  EXAMPLES:
960
987
  nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
961
988
  nansen trade quote --chain solana --from SOL --to USDC --amount 0.5 --amount-unit token
989
+ nansen trade quote --chain solana --from SOL --to USDC --amount 50 --amount-unit usd
962
990
  nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
963
991
  nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
964
992
  nansen trade quote --chain solana --to-chain base --from SOL --to ETH --amount 1000000000
@@ -968,8 +996,8 @@ EXAMPLES:
968
996
  }
969
997
 
970
998
  // Validate --amount-unit if provided
971
- if (amountUnit && amountUnit !== 'token' && amountUnit !== 'base') {
972
- log(`Error: Unknown --amount-unit "${amountUnit}". Supported values: token, base`);
999
+ if (amountUnit && amountUnit !== 'token' && amountUnit !== 'base' && amountUnit !== 'usd') {
1000
+ log(`Error: Unknown --amount-unit "${amountUnit}". Supported values: token, base, usd`);
973
1001
  exit(1);
974
1002
  return;
975
1003
  }
@@ -988,7 +1016,23 @@ EXAMPLES:
988
1016
  // Otherwise, validate that the amount is already in base units (integer).
989
1017
  let resolvedAmount = amount;
990
1018
  let resolvedDecimals;
991
- if (amountUnit === 'token') {
1019
+ let usdTokenAmount; // token-unit amount after USD conversion (for balance pre-check)
1020
+ if (amountUnit === 'usd') {
1021
+ try {
1022
+ const tokenForPrice = swapMode === 'exactOut' ? to : from;
1023
+ const price = await resolveUsdPrice(apiInstance, tokenForPrice, chain);
1024
+ resolvedDecimals = await resolveTokenDecimals(tokenForPrice, chain);
1025
+ // Convert USD to token amount, then to base units via string math.
1026
+ // Use toFixed() instead of String() to avoid scientific notation for small values.
1027
+ const tokenAmount = parseFloat(amount) / price;
1028
+ usdTokenAmount = tokenAmount.toFixed(resolvedDecimals);
1029
+ resolvedAmount = convertToBaseUnits(usdTokenAmount, resolvedDecimals);
1030
+ } catch (err) {
1031
+ log(`Error converting USD amount: ${err.message}`);
1032
+ exit(1);
1033
+ return;
1034
+ }
1035
+ } else if (amountUnit === 'token') {
992
1036
  try {
993
1037
  const tokenForDecimals = swapMode === 'exactOut' ? to : from;
994
1038
  resolvedDecimals = await resolveTokenDecimals(tokenForDecimals, chain);
@@ -1053,21 +1097,24 @@ EXAMPLES:
1053
1097
  }
1054
1098
 
1055
1099
  // Balance pre-check — catches zero balances and insufficient funds
1056
- // before wasting a quote API call. Only runs for --amount-unit token
1057
- // in exactIn mode (in exactOut, the amount is the buy amount so
1058
- // comparing it against the sell token balance is meaningless).
1059
- if (amountUnit === 'token' && swapMode !== 'exactOut') {
1100
+ // before wasting a quote API call. Runs for --amount-unit token and
1101
+ // usd (after USD→token conversion) in exactIn mode. In exactOut the
1102
+ // amount is the buy amount so comparing against sell balance is meaningless.
1103
+ if ((amountUnit === 'token' || amountUnit === 'usd') && swapMode !== 'exactOut') {
1060
1104
  try {
1105
+ // For USD, pass the converted token-unit amount so validateBalance
1106
+ // can compare against the wallet balance in token units.
1107
+ const tokenUnitAmount = amountUnit === 'usd' ? usdTokenAmount : amount;
1061
1108
  const { adjustedAmount: balanceAdjusted } = await validateBalance({
1062
1109
  chain,
1063
1110
  from,
1064
- amount,
1065
- amountUnit,
1111
+ amount: tokenUnitAmount,
1112
+ amountUnit: 'token',
1066
1113
  walletAddress,
1067
1114
  decimals: resolvedDecimals,
1068
1115
  symbol: fromRaw,
1069
1116
  });
1070
- if (balanceAdjusted !== amount) {
1117
+ if (balanceAdjusted !== tokenUnitAmount) {
1071
1118
  resolvedAmount = convertToBaseUnits(balanceAdjusted, resolvedDecimals);
1072
1119
  }
1073
1120
  } catch (balanceErr) {
@@ -1101,6 +1148,7 @@ EXAMPLES:
1101
1148
  params.toChainIndex = toChainConfig.index;
1102
1149
  if (toWallet) {
1103
1150
  params.toWalletAddress = toWallet;
1151
+ log(` Destination wallet: ${toWallet}`);
1104
1152
  } else if (chainConfig.type !== toChainConfig.type) {
1105
1153
  // Solana↔Base: auto-derive the destination address from the same wallet
1106
1154
  const effectiveWalletName = walletName || getWalletConfig()?.defaultWallet;
package/src/x402-svm.js CHANGED
@@ -5,6 +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
9
 
9
10
  // ============= Constants =============
10
11
 
@@ -18,22 +19,6 @@ const _SYSTEM_PROGRAM = '11111111111111111111111111111111';
18
19
  const DEFAULT_COMPUTE_UNIT_LIMIT = 20000;
19
20
  const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
20
21
 
21
- // ============= Compact-u16 Encoding =============
22
- // (Solana's variable-length integer format, from trading.js pattern)
23
-
24
- export function encodeCompactU16(value) {
25
- if (value < 0x80) return Buffer.from([value]);
26
- if (value < 0x4000) return Buffer.from([
27
- (value & 0x7f) | 0x80,
28
- (value >> 7) & 0x7f,
29
- ]);
30
- return Buffer.from([
31
- (value & 0x7f) | 0x80,
32
- ((value >> 7) & 0x7f) | 0x80,
33
- (value >> 14) & 0x03,
34
- ]);
35
- }
36
-
37
22
  // ============= PDA Derivation =============
38
23
 
39
24
  /**