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 +12 -0
- package/package.json +1 -1
- package/src/api.js +1 -1
- package/src/cli.js +1 -1
- package/src/schema.json +2 -2
- 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,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
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);
|
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 || '
|
|
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
|
|
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
|
|
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",
|
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
|
/**
|