nansen-cli 1.25.1 → 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 CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## 1.25.1
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.25.1",
3
+ "version": "1.26.0",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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 = 'ethereum', hideSpamToken = true, filters = {}, orderBy } = params;
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);
package/src/cli.js CHANGED
@@ -1110,7 +1110,7 @@ export function buildCommands(deps = {}) {
1110
1110
  const subcommand = args[0] || 'help';
1111
1111
  let address = options.address;
1112
1112
  const entityName = options.entity || options['entity-name'];
1113
- const chain = options.chain || 'ethereum';
1113
+ const chain = options.chain || 'all';
1114
1114
 
1115
1115
  // Resolve ENS names (e.g. vitalik.eth → 0x...)
1116
1116
  let ensName;
package/src/schema.json CHANGED
@@ -821,11 +821,11 @@
821
821
  "amount": {
822
822
  "type": "string",
823
823
  "required": true,
824
- "description": "Amount to swap (base units by default, or token units with --amount-unit token, or USD with --amount-unit usd)"
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)"
825
825
  },
826
826
  "amount-unit": {
827
827
  "type": "string",
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."
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."
829
829
  },
830
830
  "wallet": {
831
831
  "type": "string",
@@ -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 (e.g. 0.5 SOL), "usd" for USD (e.g. 50)
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
- // Exported for testing
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 (!isOnCurve(hash)) {
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
  /**