nansen-cli 1.23.0 → 1.24.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,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.24.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#333](https://github.com/nansen-ai/nansen-cli/pull/333) [`c8fe79c`](https://github.com/nansen-ai/nansen-cli/commit/c8fe79c4ec5e1ba6acf2498d9bfbe14c726a913c) Thanks [@imhta](https://github.com/imhta)! - Add cross-chain swap support between Solana and Base via Li.Fi bridge.
8
+
9
+ `nansen trade quote --chain base --to-chain solana --from ETH --to SOL --amount 0.01 --amount-unit token`
10
+ `nansen trade execute --quote <id>`
11
+
12
+ Bridge status can be checked with `nansen trade bridge-status`.
13
+
14
+ ### Patch Changes
15
+
16
+ - [#365](https://github.com/nansen-ai/nansen-cli/pull/365) [`56335af`](https://github.com/nansen-ai/nansen-cli/commit/56335af600e51436b30ad2fc1530aa754bac2a2f) Thanks [@TimNooren](https://github.com/TimNooren)! - Add balance pre-check before quote API calls. Validates sell token balance, auto-adjusts near-full-balance trades (≤2% over), and reserves gas fees for native token swaps (SOL/ETH).
17
+
18
+ ## 1.23.1
19
+
20
+ ### Patch Changes
21
+
22
+ - [#361](https://github.com/nansen-ai/nansen-cli/pull/361) [`ff22da3`](https://github.com/nansen-ai/nansen-cli/commit/ff22da376c1072ef06e84054f6ee31c058e6e08c) Thanks [@TimNooren](https://github.com/TimNooren)! - fix(alerts): error when --webhook-secret is passed without --webhook
23
+
24
+ Previously, passing --webhook-secret with a non-webhook channel (e.g. --telegram)
25
+ silently discarded the secret with no warning. The alert was created successfully
26
+ but without any signing, giving the false impression that the secret was active.
27
+
28
+ Now throws an actionable error: "--webhook-secret requires --webhook".
29
+
30
+ - [#358](https://github.com/nansen-ai/nansen-cli/pull/358) [`70ee712`](https://github.com/nansen-ai/nansen-cli/commit/70ee71205343fb2d003eb5f50144e266bdc6109e) Thanks [@TimNooren](https://github.com/TimNooren)! - Add pre-quote trade input validation: rejects same-token swaps, invalid address formats, and non-positive amounts before any network call.
31
+
3
32
  ## 1.23.0
4
33
 
5
34
  ### Minor Changes
package/README.md CHANGED
@@ -36,7 +36,7 @@ nansen schema [command] [--pretty] # full command reference (no API key neede
36
36
 
37
37
  **Research categories:** `smart-money` (`sm`), `token` (`tgm`), `profiler` (`prof`), `portfolio` (`port`), `prediction-market` (`pm`), `search`, `perp`, `points`
38
38
 
39
- **Trade:** `quote`, `execute` — DEX swaps on Solana and Base.
39
+ **Trade:** `quote`, `execute`, `bridge-status` — DEX swaps on Solana and Base, including cross-chain bridges.
40
40
 
41
41
  **Wallet:** `create`, `list`, `show`, `export`, `default`, `delete`, `send` — local or Privy server-side wallets (EVM + Solana).
42
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: nansen-trading
3
- description: Execute DEX swaps on Solana or Base. Use when buying or selling a token, getting a swap quote, or executing a trade.
3
+ description: Execute DEX swaps on Solana or Base, including cross-chain bridges. Use when buying or selling a token, getting a swap quote, or executing a trade.
4
4
  metadata:
5
5
  openclaw:
6
6
  requires:
@@ -41,6 +41,31 @@ Symbols resolve automatically: `SOL`, `ETH`, `USDC`, `USDT`, `WETH`. Raw address
41
41
  nansen trade execute --quote <quote-id>
42
42
  ```
43
43
 
44
+ ## Cross-Chain Swap
45
+
46
+ Bridge tokens between Solana and Base using `--to-chain`:
47
+
48
+ ```bash
49
+ nansen trade quote \
50
+ --chain base \
51
+ --to-chain solana \
52
+ --from USDC \
53
+ --to USDC \
54
+ --amount 1000000
55
+ ```
56
+
57
+ For Solana↔Base bridges, the destination wallet address is auto-derived from your wallet (which stores both EVM and Solana keys). Override with `--to-wallet <address>` if needed.
58
+
59
+ Note: you need gas on the **source** chain to submit the initial transaction (e.g. SOL for Solana→Base, ETH for Base→Solana).
60
+
61
+ ## Bridge Status
62
+
63
+ After executing a cross-chain swap, the CLI polls bridge status automatically. To check manually:
64
+
65
+ ```bash
66
+ nansen trade bridge-status --tx-hash <hash> --from-chain base --to-chain solana
67
+ ```
68
+
44
69
  ## Agent pattern
45
70
 
46
71
  ```bash
@@ -81,15 +106,19 @@ If the user says "$20 worth of X", you must convert USD → token amount, then e
81
106
 
82
107
  | Flag | Purpose |
83
108
  |------|---------|
84
- | `--chain` | `solana` or `base` |
109
+ | `--chain` | Source chain: `solana` or `base` |
110
+ | `--to-chain` | Destination chain for cross-chain swap (omit for same-chain) |
85
111
  | `--from` | Source token (symbol or address) |
86
- | `--to` | Destination token (symbol or address) |
112
+ | `--to` | Destination token (symbol or address, resolved against destination chain) |
87
113
  | `--amount` | Amount in base units (integer), or token units with `--amount-unit token` |
88
114
  | `--amount-unit` | Set to `token` to specify amount in token units (e.g. 0.5 SOL) |
89
115
  | `--wallet` | Wallet name (default: default wallet) |
116
+ | `--to-wallet` | Destination wallet address (auto-derived for cross-chain if omitted) |
90
117
  | `--slippage` | Slippage tolerance as decimal (e.g. 0.03) |
91
118
  | `--quote` | Quote ID for execute |
92
119
  | `--no-simulate` | Skip pre-broadcast simulation |
120
+ | `--tx-hash` | Source tx hash (for bridge-status) |
121
+ | `--from-chain` | Source chain (for bridge-status) |
93
122
 
94
123
  ## Environment Variables
95
124
 
package/src/cli.js CHANGED
@@ -1447,18 +1447,22 @@ export function buildCommands(deps = {}) {
1447
1447
  log(`nansen trade — DEX trading commands
1448
1448
 
1449
1449
  SUBCOMMANDS:
1450
- quote Get a swap quote (price, route, fees)
1451
- execute Sign and broadcast a quoted swap
1450
+ quote Get a swap quote (price, route, fees)
1451
+ execute Sign and broadcast a quoted swap
1452
+ bridge-status Check cross-chain bridge transaction status
1452
1453
 
1453
1454
  USAGE:
1454
1455
  nansen trade quote --chain <chain> --from <token> --to <token> --amount <units> [--wallet <name>]
1456
+ nansen trade quote --chain <chain> --to-chain <chain> --from <token> --to <token> --amount <units>
1455
1457
  nansen trade execute --quote <quoteId> [--wallet <name>]
1458
+ nansen trade bridge-status --tx-hash <hash> --from-chain <chain> --to-chain <chain>
1456
1459
 
1457
1460
  EXAMPLES:
1458
1461
  nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
1459
1462
  nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
1460
- nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000 --wallet walletconnect
1463
+ nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
1461
1464
  nansen trade execute --quote 1708900000000-abc123
1465
+ nansen trade bridge-status --tx-hash 0xabc... --from-chain base --to-chain solana
1462
1466
 
1463
1467
  WALLET:
1464
1468
  --wallet <name> Use a named wallet, or "walletconnect" / "wc" for WalletConnect (EVM only).
@@ -1470,7 +1474,7 @@ SYMBOLS:
1470
1474
  return;
1471
1475
  }
1472
1476
  if (!tradingCmds[sub]) {
1473
- throw new NansenError(`Unknown trade subcommand: ${sub}. Available: quote, execute`, ErrorCode.UNKNOWN);
1477
+ throw new NansenError(`Unknown trade subcommand: ${sub}. Available: quote, execute, bridge-status`, ErrorCode.UNKNOWN);
1474
1478
  }
1475
1479
  return tradingCmds[sub](args.slice(1), apiInstance, flags, options);
1476
1480
  };
@@ -560,8 +560,11 @@ USAGE:
560
560
  return;
561
561
  }
562
562
 
563
- // Build channels array from --telegram/--slack/--discord flags
563
+ // Build channels array from --telegram/--slack/--discord/--webhook flags
564
564
  function buildChannels() {
565
+ if (options["webhook-secret"] && !options.webhook) {
566
+ throw new NansenError('--webhook-secret requires --webhook', ErrorCode.INVALID_PARAMS);
567
+ }
565
568
  const channels = [];
566
569
  if (options.telegram) channels.push({ type: 'telegram', data: { chatId: String(options.telegram) } });
567
570
  if (options.slack) channels.push({ type: 'slack', data: { webhookUrl: options.slack } });
package/src/schema.json CHANGED
@@ -772,12 +772,16 @@
772
772
  "description": "DEX trading commands",
773
773
  "subcommands": {
774
774
  "quote": {
775
- "description": "Get a DEX swap quote (chain, tokens, amount)",
775
+ "description": "Get a DEX swap quote (same-chain or cross-chain)",
776
776
  "options": {
777
777
  "chain": {
778
778
  "type": "string",
779
779
  "default": "base",
780
- "description": "Blockchain (solana or base)"
780
+ "description": "Source blockchain (solana or base)"
781
+ },
782
+ "to-chain": {
783
+ "type": "string",
784
+ "description": "Destination blockchain for cross-chain swap (solana or base). Omit for same-chain."
781
785
  },
782
786
  "from": {
783
787
  "type": "string",
@@ -787,7 +791,7 @@
787
791
  "to": {
788
792
  "type": "string",
789
793
  "required": true,
790
- "description": "Token to buy (address or symbol)"
794
+ "description": "Token to buy (address or symbol, resolved against destination chain for cross-chain)"
791
795
  },
792
796
  "amount": {
793
797
  "type": "string",
@@ -801,6 +805,10 @@
801
805
  "wallet": {
802
806
  "type": "string",
803
807
  "description": "Wallet name (or \"walletconnect\"/\"wc\" for WalletConnect, EVM only). A configured wallet is required \u2014 run `nansen wallet create` if you haven't set one up yet."
808
+ },
809
+ "to-wallet": {
810
+ "type": "string",
811
+ "description": "Destination wallet address for cross-chain swaps. Auto-derived from wallet if omitted."
804
812
  }
805
813
  },
806
814
  "prerequisites": [
@@ -820,6 +828,26 @@
820
828
  "description": "Wallet name, or \"walletconnect\"/\"wc\" for WalletConnect (EVM only)"
821
829
  }
822
830
  }
831
+ },
832
+ "bridge-status": {
833
+ "description": "Check cross-chain bridge transaction status",
834
+ "options": {
835
+ "tx-hash": {
836
+ "type": "string",
837
+ "required": true,
838
+ "description": "Source chain transaction hash"
839
+ },
840
+ "from-chain": {
841
+ "type": "string",
842
+ "required": true,
843
+ "description": "Source chain (solana or base)"
844
+ },
845
+ "to-chain": {
846
+ "type": "string",
847
+ "required": true,
848
+ "description": "Destination chain (solana or base)"
849
+ }
850
+ }
823
851
  }
824
852
  }
825
853
  },
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Trade input validation for the Nansen CLI.
3
+ * Catches common agent errors (wrong addresses, same-token swaps,
4
+ * bad amounts) before any network call.
5
+ */
6
+
7
+ import { validateAddress } from './api.js';
8
+ import { CHAIN_RPCS } from './rpc-urls.js';
9
+
10
+ const SUPPORTED_CHAINS = ['solana', 'base'];
11
+
12
+ /**
13
+ * Validate quote inputs before any network call.
14
+ * Throws on validation failure with an actionable error message.
15
+ */
16
+ export function validateQuoteInput({ chain, toChain, from, to, amount }) {
17
+ // 1. Chain must be supported
18
+ const normalizedChain = chain?.toLowerCase();
19
+ if (!SUPPORTED_CHAINS.includes(normalizedChain)) {
20
+ throw new Error(
21
+ `Unsupported chain "${chain}". Supported chains: ${SUPPORTED_CHAINS.join(', ')}.`
22
+ );
23
+ }
24
+
25
+ const normalizedToChain = toChain ? toChain.toLowerCase() : normalizedChain;
26
+ if (toChain && !SUPPORTED_CHAINS.includes(normalizedToChain)) {
27
+ throw new Error(
28
+ `Unsupported destination chain "${toChain}". Supported chains: ${SUPPORTED_CHAINS.join(', ')}.`
29
+ );
30
+ }
31
+
32
+ // 2. Amount must be a positive finite number
33
+ const numAmount = Number(amount);
34
+ if (!Number.isFinite(numAmount) || numAmount <= 0) {
35
+ throw new Error(
36
+ `Invalid amount "${amount}". Must be a positive number.`
37
+ );
38
+ }
39
+
40
+ // 3. Token address format must match the chain (reuses api.js validateAddress)
41
+ const fromResult = validateAddress(from, normalizedChain);
42
+ if (!fromResult.valid) {
43
+ throw new Error(
44
+ `Invalid sell token address for ${normalizedChain}. ${fromResult.error}`
45
+ );
46
+ }
47
+ const toResult = validateAddress(to, normalizedToChain);
48
+ if (!toResult.valid) {
49
+ throw new Error(
50
+ `Invalid buy token address for ${normalizedToChain}. ${toResult.error}`
51
+ );
52
+ }
53
+
54
+ // 4. Sell and buy token must be different (only applies to same-chain swaps)
55
+ if (normalizedChain === normalizedToChain) {
56
+ const fromNorm = normalizedChain === 'solana' ? from : from.toLowerCase();
57
+ const toNorm = normalizedChain === 'solana' ? to : to.toLowerCase();
58
+ if (fromNorm === toNorm) {
59
+ throw new Error(
60
+ `Cannot swap ${from} for itself. Sell and buy tokens must be different.`
61
+ );
62
+ }
63
+ }
64
+ }
65
+
66
+ // Native token decimals per chain (for converting balance from base units)
67
+ const NATIVE_DECIMALS = { solana: 9, base: 18 };
68
+
69
+ /**
70
+ * Fetch the native token balance (ETH or SOL) for a wallet.
71
+ * Returns balance in human-readable token units (e.g. 1.5 ETH), or null on RPC failure.
72
+ *
73
+ * Uses Number (not BigInt) for the result — acceptable precision loss for a
74
+ * best-effort pre-check with 2% tolerance. Transaction amounts use BigInt elsewhere.
75
+ */
76
+ export async function fetchNativeBalance(chain, walletAddress) {
77
+ try {
78
+ const rpcUrl = CHAIN_RPCS[chain];
79
+ if (!rpcUrl) return null;
80
+
81
+ const chainType = chain === 'solana' ? 'solana' : 'evm';
82
+
83
+ if (chainType === 'evm') {
84
+ const res = await fetch(rpcUrl, {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBalance', params: [walletAddress, 'latest'] }),
88
+ });
89
+ const body = await res.json();
90
+ if (body.error || body.result === undefined) return null;
91
+ const wei = BigInt(body.result);
92
+ return Number(wei) / 10 ** NATIVE_DECIMALS[chain];
93
+ }
94
+
95
+ // Solana — getBalance returns { value: <lamports> }
96
+ const res = await fetch(rpcUrl, {
97
+ method: 'POST',
98
+ headers: { 'Content-Type': 'application/json' },
99
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'getBalance', params: [walletAddress] }),
100
+ });
101
+ const body = await res.json();
102
+ if (body.error || body.result?.value === undefined) return null;
103
+ return body.result.value / 10 ** NATIVE_DECIMALS[chain];
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ // Addresses that represent native tokens (SOL, ETH) — not ERC-20/SPL contracts.
110
+ const NATIVE_TOKEN_ADDRESSES = {
111
+ solana: 'So11111111111111111111111111111111111111112',
112
+ base: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
113
+ };
114
+
115
+ // Native token symbols for error messages.
116
+ const NATIVE_SYMBOLS = { solana: 'SOL', base: 'ETH' };
117
+
118
+ const FEE_BUFFER = { solana: 0.005, base: 0.00004 };
119
+ const HIGH_PERCENTAGE_THRESHOLD = 95;
120
+ const AUTO_ADJUST_THRESHOLD_PERCENT = 2;
121
+
122
+ /**
123
+ * Check if an address is the native token for a chain (case-insensitive for EVM).
124
+ */
125
+ function isNativeAddress(address, chain) {
126
+ const native = NATIVE_TOKEN_ADDRESSES[chain];
127
+ if (!native) return false;
128
+ if (chain === 'solana') return address === native;
129
+ return address.toLowerCase() === native.toLowerCase();
130
+ }
131
+
132
+ /**
133
+ * Validate that the wallet has sufficient balance of the sell token.
134
+ * Only applies when amountUnit is 'token' (human-readable amounts).
135
+ *
136
+ * Returns { adjustedAmount } — may differ from input if auto-adjusted
137
+ * to 100% of balance (when amount exceeds balance by ≤2%).
138
+ *
139
+ * Throws on validation failure. Returns without action if RPC fails (best-effort).
140
+ */
141
+ export async function validateBalance({ chain, from, amount, amountUnit, walletAddress, decimals, symbol: callerSymbol }) {
142
+ // Only validate when amount is in token units — we can compare directly.
143
+ if (amountUnit !== 'token') return { adjustedAmount: amount };
144
+
145
+ const normalizedChain = chain.toLowerCase();
146
+ const isNative = isNativeAddress(from, normalizedChain);
147
+ const symbol = callerSymbol
148
+ || (isNative ? NATIVE_SYMBOLS[normalizedChain] : null)
149
+ || from;
150
+
151
+ let balance;
152
+ if (isNative) {
153
+ balance = await fetchNativeBalance(normalizedChain, walletAddress);
154
+ } else {
155
+ if (decimals === undefined) return { adjustedAmount: amount };
156
+ balance = await fetchTokenBalance(normalizedChain, from, walletAddress, decimals);
157
+ }
158
+
159
+ // Best-effort: if RPC failed, proceed without validation.
160
+ if (balance === null) return { adjustedAmount: amount };
161
+
162
+ // Check 1: wallet must hold the token
163
+ if (balance === 0) {
164
+ throw new Error(
165
+ `No ${symbol} balance in wallet. You cannot trade a token you don't own.`
166
+ );
167
+ }
168
+
169
+ // Check 2: amount vs balance
170
+ let numAmount = Number(amount);
171
+ if (numAmount > balance) {
172
+ const excessPercent = ((numAmount - balance) / balance) * 100;
173
+ if (excessPercent > AUTO_ADJUST_THRESHOLD_PERCENT) {
174
+ throw new Error(
175
+ `Insufficient balance. You have ${balance} ${symbol} but the trade requires ${amount} ${symbol}.`
176
+ );
177
+ }
178
+ // Auto-adjust to 100% of balance
179
+ const adjustedAmount = String(balance);
180
+ numAmount = balance;
181
+ process.stderr.write(
182
+ `Warning: Amount ${amount} exceeds balance ${balance}. Auto-adjusting to ${adjustedAmount} ${symbol}.\n`
183
+ );
184
+ if (!isNative) return { adjustedAmount };
185
+ // Native tokens fall through to the fee buffer check below — selling 100%
186
+ // of a native balance still needs a gas reserve applied.
187
+ }
188
+
189
+ // Check 3: native token fee buffer when selling ≥95% of balance
190
+ if (isNative) {
191
+ const percentOfBalance = (numAmount / balance) * 100;
192
+ if (percentOfBalance >= HIGH_PERCENTAGE_THRESHOLD) {
193
+ const reserve = FEE_BUFFER[normalizedChain] || 0;
194
+ const maxSellable = parseFloat((balance - reserve).toFixed(NATIVE_DECIMALS[normalizedChain]));
195
+ if (maxSellable <= 0) {
196
+ throw new Error(
197
+ `Insufficient ${symbol} balance after reserving gas fees.`
198
+ );
199
+ }
200
+ if (numAmount > maxSellable) {
201
+ const adjustedAmount = String(maxSellable);
202
+ process.stderr.write(
203
+ `Warning: Reserving ${reserve} ${symbol} for gas. Adjusted sell amount to ${adjustedAmount} ${symbol}.\n`
204
+ );
205
+ return { adjustedAmount };
206
+ }
207
+ }
208
+ }
209
+
210
+ return { adjustedAmount: amount };
211
+ }
212
+
213
+ /**
214
+ * Fetch an ERC-20 or SPL token balance for a wallet.
215
+ * Returns balance in human-readable token units, or null on RPC failure.
216
+ * Requires `decimals` to convert from base units.
217
+ *
218
+ * Uses Number (not BigInt) for the result — see fetchNativeBalance note on precision.
219
+ */
220
+ export async function fetchTokenBalance(chain, tokenAddress, walletAddress, decimals) {
221
+ try {
222
+ const rpcUrl = CHAIN_RPCS[chain];
223
+ if (!rpcUrl) return null;
224
+
225
+ const chainType = chain === 'solana' ? 'solana' : 'evm';
226
+
227
+ if (chainType === 'evm') {
228
+ // balanceOf(address) selector = 0x70a08231, address padded to 32 bytes
229
+ const paddedAddress = walletAddress.replace('0x', '').toLowerCase().padStart(64, '0');
230
+ const data = '0x70a08231' + paddedAddress;
231
+ const res = await fetch(rpcUrl, {
232
+ method: 'POST',
233
+ headers: { 'Content-Type': 'application/json' },
234
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: tokenAddress, data }, 'latest'] }),
235
+ });
236
+ const body = await res.json();
237
+ if (body.error || !body.result) return null;
238
+ const raw = BigInt(body.result);
239
+ return Number(raw) / 10 ** decimals;
240
+ }
241
+
242
+ // Solana — getTokenAccountsByOwner with the mint filter
243
+ const res = await fetch(rpcUrl, {
244
+ method: 'POST',
245
+ headers: { 'Content-Type': 'application/json' },
246
+ body: JSON.stringify({
247
+ jsonrpc: '2.0', id: 1,
248
+ method: 'getTokenAccountsByOwner',
249
+ params: [
250
+ walletAddress,
251
+ { mint: tokenAddress },
252
+ { encoding: 'jsonParsed' },
253
+ ],
254
+ }),
255
+ });
256
+ const body = await res.json();
257
+ if (body.error) return null;
258
+ const accounts = body.result?.value || [];
259
+ if (accounts.length === 0) return 0;
260
+ // Sum across all token accounts for this mint (rare but possible)
261
+ let total = 0n;
262
+ for (const acct of accounts) {
263
+ const amount = acct.account?.data?.parsed?.info?.tokenAmount?.amount;
264
+ if (amount) total += BigInt(amount);
265
+ }
266
+ return Number(total) / 10 ** decimals;
267
+ } catch {
268
+ return null;
269
+ }
270
+ }
package/src/trading.js CHANGED
@@ -13,6 +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
17
  import { CHAIN_RPCS } from './rpc-urls.js';
17
18
 
18
19
  // ============= Constants =============
@@ -20,8 +21,8 @@ import { CHAIN_RPCS } from './rpc-urls.js';
20
21
  const TRADING_API_URL = process.env.NANSEN_TRADING_API_URL || 'https://trading-api.nansen.ai';
21
22
 
22
23
  const CHAIN_MAP = {
23
- solana: { index: '501', type: 'solana', chainId: 501, name: 'Solana', explorer: 'https://solscan.io/tx/' },
24
- base: { index: '8453', type: 'evm', chainId: 8453, name: 'Base', explorer: 'https://basescan.org/tx/' },
24
+ solana: { index: '501', type: 'solana', chainId: 501, name: 'Solana', explorer: 'https://solscan.io/tx/', lifiChainId: '1151111081099710' },
25
+ base: { index: '8453', type: 'evm', chainId: 8453, name: 'Base', explorer: 'https://basescan.org/tx/', lifiChainId: '8453' },
25
26
  };
26
27
 
27
28
  // Extend when adding new EVM chains (e.g. arbitrum WETH, polygon WMATIC)
@@ -198,13 +199,93 @@ export async function executeTransaction(params, { retries = 2, retryDelayMs = 1
198
199
  throw lastError;
199
200
  }
200
201
 
202
+ // ============= Bridge Status =============
203
+
204
+ /**
205
+ * Check the status of a cross-chain bridge transaction.
206
+ * @param {string} txHash - Source chain transaction hash
207
+ * @param {string} fromChain - Source chain name (e.g. 'base')
208
+ * @param {string} toChain - Destination chain name (e.g. 'solana')
209
+ * @returns {Promise<object>} Bridge status
210
+ */
211
+ export async function getBridgeStatus(txHash, fromChain, toChain) {
212
+ const fromConfig = resolveChain(fromChain);
213
+ const toConfig = resolveChain(toChain);
214
+ const url = new URL('/bridge/status', TRADING_API_URL);
215
+ url.searchParams.set('txHash', txHash);
216
+ url.searchParams.set('fromChain', fromConfig.lifiChainId || fromConfig.index);
217
+ url.searchParams.set('toChain', toConfig.lifiChainId || toConfig.index);
218
+
219
+ const res = await fetch(url.toString(), { headers: { 'Accept': 'application/json' } });
220
+ const text = await res.text();
221
+ let body;
222
+ try {
223
+ body = JSON.parse(text);
224
+ } catch {
225
+ throw Object.assign(
226
+ new Error(`Bridge status API returned non-JSON response (status ${res.status}).`),
227
+ { code: 'BRIDGE_STATUS_ERROR', status: res.status, details: text.slice(0, 200) }
228
+ );
229
+ }
230
+ if (!res.ok) {
231
+ throw Object.assign(
232
+ new Error(body.message || `Bridge status check failed with status ${res.status}`),
233
+ { code: body.code || 'BRIDGE_STATUS_ERROR', status: res.status, details: body.details }
234
+ );
235
+ }
236
+ return body;
237
+ }
238
+
239
+ /**
240
+ * Poll bridge status until completion or timeout.
241
+ * @param {string} txHash - Source chain transaction hash
242
+ * @param {string} fromChain - Source chain name
243
+ * @param {string} toChain - Destination chain name
244
+ * @param {object} [opts]
245
+ * @param {number} [opts.timeoutMs=600000] - Timeout (default 10 min)
246
+ * @param {number} [opts.pollMs=10000] - Poll interval (default 10s)
247
+ * @param {Function} [opts.log=console.log] - Logger
248
+ * @returns {Promise<object>} Final bridge status
249
+ */
250
+ export async function pollBridgeStatus(txHash, fromChain, toChain, { timeoutMs = 600000, pollMs = 10000, log = console.log } = {}) {
251
+ const start = Date.now();
252
+ while (Date.now() - start < timeoutMs) {
253
+ let status;
254
+ try {
255
+ status = await getBridgeStatus(txHash, fromChain, toChain);
256
+ } catch (err) {
257
+ // Transient errors (502, 503, network failures) — retry after poll interval.
258
+ log(` Bridge: poll error (${err.status || err.code || 'unknown'}) — retrying...`);
259
+ await new Promise(r => setTimeout(r, pollMs));
260
+ continue;
261
+ }
262
+ const sending = status.sending?.status || status.status || 'pending';
263
+ const receiving = status.receiving?.status || 'pending';
264
+ log(` Bridge: ${sending} → ${receiving}`);
265
+
266
+ if (status.status === 'DONE' || status.receiving?.status === 'DONE') return status;
267
+ if (status.status === 'FAILED') {
268
+ throw Object.assign(
269
+ new Error(`Bridge failed: ${status.substatusMessage || 'unknown error'}`),
270
+ { code: 'BRIDGE_FAILED', details: status }
271
+ );
272
+ }
273
+
274
+ await new Promise(r => setTimeout(r, pollMs));
275
+ }
276
+ throw Object.assign(
277
+ new Error(`Bridge status polling timed out after ${timeoutMs / 1000}s. Check manually with: nansen trade bridge-status --tx-hash ${txHash} --from-chain ${fromChain} --to-chain ${toChain}`),
278
+ { code: 'BRIDGE_TIMEOUT' }
279
+ );
280
+ }
281
+
201
282
  // ============= Quote Storage =============
202
283
 
203
284
  /**
204
285
  * Save a quote response to disk for later execution.
205
286
  * @returns {string} Quote ID
206
287
  */
207
- export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalletIds = null) {
288
+ export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalletIds = null, toChain = null) {
208
289
  const dir = getQuotesDir();
209
290
  if (!fs.existsSync(dir)) {
210
291
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -215,6 +296,7 @@ export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalle
215
296
  const quoteId = `${timestamp}-${hash}`;
216
297
 
217
298
  const data = { quoteId, chain, timestamp, signerType, response: quoteResponse };
299
+ if (toChain) data.toChain = toChain;
218
300
  if (privyWalletIds) data.privyWalletIds = privyWalletIds;
219
301
 
220
302
  fs.writeFileSync(path.join(dir, `${quoteId}.json`), JSON.stringify(data, null, 2), { mode: 0o600 });
@@ -807,6 +889,15 @@ export function formatQuote(quote, index) {
807
889
  if (quote.tradingFeeInUsd) lines.push(` Trading Fee: $${quote.tradingFeeInUsd}`);
808
890
  if (quote.networkFeeInUsd) lines.push(` Network Fee: $${quote.networkFeeInUsd}`);
809
891
  if (quote.approvalAddress && !isNativeToken(quote.inputMint)) lines.push(` ⚠ Requires token approval to: ${quote.approvalAddress}`);
892
+ const meta = quote.metadata || {};
893
+ if (meta.isCrossChain) {
894
+ if (meta.bridgeTool) lines.push(` Bridge: ${meta.bridgeTool}`);
895
+ if (meta.executionDuration) lines.push(` Est. Time: ~${Math.round(meta.executionDuration / 60)} min`);
896
+ if (meta.feeCosts?.length) {
897
+ const totalFees = meta.feeCosts.reduce((sum, f) => sum + parseFloat(f.amountUSD || 0), 0);
898
+ if (totalFees > 0) lines.push(` Bridge Fees: $${totalFees.toFixed(2)}`);
899
+ }
900
+ }
810
901
  if (quote.priceImpactPct) {
811
902
  const impactAbs = Math.abs(parseFloat(quote.priceImpactPct));
812
903
  if (impactAbs > 5) {
@@ -827,12 +918,15 @@ export function buildTradingCommands(deps = {}) {
827
918
  return {
828
919
  'quote': async (args, apiInstance, flags, options) => {
829
920
  const chain = options.chain || args[0];
921
+ const toChainRaw = options['to-chain'];
830
922
  const fromRaw = options.from || options['from-token'] || args[1];
831
923
  const toRaw = options.to || options['to-token'] || args[2];
924
+ const effectiveToChain = toChainRaw || chain;
832
925
  const from = resolveTokenAddress(fromRaw, chain);
833
- const to = resolveTokenAddress(toRaw, chain);
926
+ const to = resolveTokenAddress(toRaw, effectiveToChain);
834
927
  const amount = options.amount || args[3];
835
928
  const walletName = options.wallet;
929
+ const toWallet = options['to-wallet'];
836
930
  const slippage = options.slippage;
837
931
  const autoSlippage = flags['auto-slippage'];
838
932
  const maxAutoSlippage = options['max-auto-slippage'];
@@ -849,12 +943,14 @@ PREREQUISITE:
849
943
  Set one up with: nansen wallet create
850
944
 
851
945
  OPTIONS:
852
- --chain <chain> Chain: solana, base
946
+ --chain <chain> Source chain: solana, base
947
+ --to-chain <chain> Destination chain for cross-chain swap (e.g. solana, base)
853
948
  --from <symbol|address> Input token (symbol like SOL, USDC or address)
854
949
  --to <symbol|address> Output token (symbol like USDC, ETH or address)
855
950
  --amount <units> Amount in BASE UNITS (e.g. lamports, wei)
856
951
  --amount-unit <unit> "token" to specify amount in token units (e.g. 0.5 SOL)
857
952
  --wallet <name> Wallet name (default: default wallet). Use "walletconnect" or "wc" for WalletConnect.
953
+ --to-wallet <address> Destination wallet address (auto-derived for cross-chain if omitted)
858
954
  --slippage <pct> Slippage as decimal (e.g. 0.03 for 3%). Default: 0.03
859
955
  --auto-slippage Enable auto slippage calculation
860
956
  --max-auto-slippage <pct> Max auto slippage when auto-slippage enabled
@@ -864,7 +960,8 @@ EXAMPLES:
864
960
  nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
865
961
  nansen trade quote --chain solana --from SOL --to USDC --amount 0.5 --amount-unit token
866
962
  nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
867
- nansen trade quote --chain solana --from So11111111111111111111111111111111111111112 --to EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v --amount 1000000000
963
+ nansen trade quote --chain base --to-chain solana --from USDC --to USDC --amount 1000000
964
+ nansen trade quote --chain solana --to-chain base --from SOL --to ETH --amount 1000000000
868
965
  `);
869
966
  exit(1);
870
967
  return;
@@ -877,14 +974,25 @@ EXAMPLES:
877
974
  return;
878
975
  }
879
976
 
977
+ // Static input validation — catches common agent errors (wrong addresses,
978
+ // same-token swaps, bad amounts) before any network or wallet call.
979
+ try {
980
+ validateQuoteInput({ chain, toChain: toChainRaw || null, from, to, amount });
981
+ } catch (validationErr) {
982
+ log(`Error: ${validationErr.message}`);
983
+ exit(1);
984
+ return;
985
+ }
986
+
880
987
  // When --amount-unit token is used, resolve decimals and convert to base units.
881
988
  // Otherwise, validate that the amount is already in base units (integer).
882
989
  let resolvedAmount = amount;
990
+ let resolvedDecimals;
883
991
  if (amountUnit === 'token') {
884
992
  try {
885
993
  const tokenForDecimals = swapMode === 'exactOut' ? to : from;
886
- const decimals = await resolveTokenDecimals(tokenForDecimals, chain);
887
- resolvedAmount = convertToBaseUnits(amount, decimals);
994
+ resolvedDecimals = await resolveTokenDecimals(tokenForDecimals, chain);
995
+ resolvedAmount = convertToBaseUnits(amount, resolvedDecimals);
888
996
  } catch (err) {
889
997
  log(`Error resolving token decimals: ${err.message}`);
890
998
  exit(1);
@@ -944,7 +1052,39 @@ EXAMPLES:
944
1052
  return;
945
1053
  }
946
1054
 
947
- log(`\nFetching quote on ${chainConfig.name}...`);
1055
+ // 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') {
1060
+ try {
1061
+ const { adjustedAmount: balanceAdjusted } = await validateBalance({
1062
+ chain,
1063
+ from,
1064
+ amount,
1065
+ amountUnit,
1066
+ walletAddress,
1067
+ decimals: resolvedDecimals,
1068
+ symbol: fromRaw,
1069
+ });
1070
+ if (balanceAdjusted !== amount) {
1071
+ resolvedAmount = convertToBaseUnits(balanceAdjusted, resolvedDecimals);
1072
+ }
1073
+ } catch (balanceErr) {
1074
+ log(`Error: ${balanceErr.message}`);
1075
+ exit(1);
1076
+ return;
1077
+ }
1078
+ }
1079
+
1080
+ const toChainConfig = toChainRaw ? resolveChain(toChainRaw) : null;
1081
+ const isCrossChain = toChainConfig && toChainConfig.index !== chainConfig.index;
1082
+
1083
+ if (isCrossChain) {
1084
+ log(`\nFetching cross-chain quote: ${chainConfig.name} → ${toChainConfig.name}...`);
1085
+ } else {
1086
+ log(`\nFetching quote on ${chainConfig.name}...`);
1087
+ }
948
1088
  log(` Wallet: ${walletAddress}`);
949
1089
 
950
1090
  const fromWarning = getWrappedNativeFromWarning(from, chain);
@@ -957,6 +1097,20 @@ EXAMPLES:
957
1097
  amount: resolvedAmount,
958
1098
  userWalletAddress: walletAddress,
959
1099
  };
1100
+ if (isCrossChain) {
1101
+ params.toChainIndex = toChainConfig.index;
1102
+ if (toWallet) {
1103
+ params.toWalletAddress = toWallet;
1104
+ } else if (chainConfig.type !== toChainConfig.type) {
1105
+ // Solana↔Base: auto-derive the destination address from the same wallet
1106
+ const effectiveWalletName = walletName || getWalletConfig()?.defaultWallet;
1107
+ if (effectiveWalletName) {
1108
+ const walletData = showWallet(effectiveWalletName);
1109
+ params.toWalletAddress = toChainConfig.type === 'solana' ? walletData.solana : walletData.evm;
1110
+ log(` Destination wallet: ${params.toWalletAddress}`);
1111
+ }
1112
+ }
1113
+ }
960
1114
  if (slippage) params.slippagePercent = slippage;
961
1115
  if (autoSlippage) params.autoSlippage = true;
962
1116
  if (maxAutoSlippage) params.maxAutoSlippagePercent = maxAutoSlippage;
@@ -977,7 +1131,7 @@ EXAMPLES:
977
1131
  response.quotes.forEach((q, i) => log(formatQuote(q, i)));
978
1132
 
979
1133
  const signerType = isWalletConnect ? 'walletconnect' : walletProvider;
980
- const quoteId = saveQuote(response, chain, signerType, privyWalletIds);
1134
+ const quoteId = saveQuote(response, chain, signerType, privyWalletIds, isCrossChain ? toChainRaw : null);
981
1135
  log(`\n Quote ID: ${quoteId}`);
982
1136
  log(` Execute: nansen trade execute --quote ${quoteId}`);
983
1137
  if (response.quotes.length > 1) {
@@ -1481,6 +1635,23 @@ EXAMPLES:
1481
1635
  log(` Tx Hash: ${wcResult.txHash}`);
1482
1636
  log(` Chain: ${chainConfig.name}`);
1483
1637
  log(` Explorer: ${chainConfig.explorer}${wcResult.txHash}`);
1638
+
1639
+ // Cross-chain: poll bridge status after source tx success
1640
+ if (quoteData.toChain && quoteData.toChain !== quoteData.chain) {
1641
+ log(`\n Cross-chain bridge in progress (${chainConfig.name} → ${resolveChain(quoteData.toChain).name})...`);
1642
+ try {
1643
+ const bridgeResult = await pollBridgeStatus(wcResult.txHash, quoteData.chain, quoteData.toChain, { log });
1644
+ log(`\n ✓ Bridge completed!`);
1645
+ if (bridgeResult.receiving?.txHash) {
1646
+ const toChainConfig = resolveChain(quoteData.toChain);
1647
+ log(` Destination tx: ${toChainConfig.explorer}${bridgeResult.receiving.txHash}`);
1648
+ }
1649
+ } catch (bridgeErr) {
1650
+ log(`\n Bridge status: ${bridgeErr.message}`);
1651
+ log(` Check later with: nansen trade bridge-status --tx-hash ${wcResult.txHash} --from-chain ${quoteData.chain} --to-chain ${quoteData.toChain}`);
1652
+ }
1653
+ }
1654
+
1484
1655
  log('');
1485
1656
  return undefined; // Success
1486
1657
  }
@@ -1667,6 +1838,23 @@ EXAMPLES:
1667
1838
  log(` ${e.inputAmount} ${e.inputMint?.slice(0, 8)}... → ${e.outputAmount} ${e.outputMint?.slice(0, 8)}...`);
1668
1839
  });
1669
1840
  }
1841
+
1842
+ // Cross-chain: poll bridge status after source tx success
1843
+ if (quoteData.toChain && quoteData.toChain !== quoteData.chain) {
1844
+ log(`\n Cross-chain bridge in progress (${chainConfig.name} → ${resolveChain(quoteData.toChain).name})...`);
1845
+ try {
1846
+ const bridgeResult = await pollBridgeStatus(txId, quoteData.chain, quoteData.toChain, { log });
1847
+ log(`\n ✓ Bridge completed!`);
1848
+ if (bridgeResult.receiving?.txHash) {
1849
+ const toChainConfig = resolveChain(quoteData.toChain);
1850
+ log(` Destination tx: ${toChainConfig.explorer}${bridgeResult.receiving.txHash}`);
1851
+ }
1852
+ } catch (bridgeErr) {
1853
+ log(`\n Bridge status: ${bridgeErr.message}`);
1854
+ log(` Check later with: nansen trade bridge-status --tx-hash ${txId} --from-chain ${quoteData.chain} --to-chain ${quoteData.toChain}`);
1855
+ }
1856
+ }
1857
+
1670
1858
  log('');
1671
1859
  return undefined; // Success — done
1672
1860
  } else {
@@ -1677,8 +1865,12 @@ EXAMPLES:
1677
1865
  }
1678
1866
 
1679
1867
  } catch (quoteErr) {
1680
- log(` ❌ Quote ${quoteName} failed: ${quoteErr.message}`);
1681
- lastQuoteError = `${quoteName}: ${quoteErr.message}`;
1868
+ const msg = quoteErr.message || '';
1869
+ log(` ❌ Quote ${quoteName} failed: ${msg}`);
1870
+ if (msg.includes('AccountNotFound') && chainType === 'solana') {
1871
+ log(` Hint: Your Solana wallet may not have enough SOL to cover transaction fees (~0.005 SOL minimum).`);
1872
+ }
1873
+ lastQuoteError = `${quoteName}: ${msg}`;
1682
1874
  if (qi + 1 < endIndex) log(` Trying next quote...`);
1683
1875
  }
1684
1876
  }
@@ -1695,5 +1887,55 @@ EXAMPLES:
1695
1887
  exit(1);
1696
1888
  }
1697
1889
  },
1890
+
1891
+ 'bridge-status': async (args, _apiInstance, _flags, options) => {
1892
+ const txHash = options['tx-hash'] || args[0];
1893
+ const fromChain = options['from-chain'] || args[1];
1894
+ const toChain = options['to-chain'] || args[2];
1895
+
1896
+ if (!txHash || !fromChain || !toChain) {
1897
+ log(`
1898
+ Usage: nansen trade bridge-status --tx-hash <hash> --from-chain <chain> --to-chain <chain>
1899
+
1900
+ Check the status of a cross-chain bridge transaction.
1901
+
1902
+ OPTIONS:
1903
+ --tx-hash <hash> Source chain transaction hash
1904
+ --from-chain <chain> Source chain (solana or base)
1905
+ --to-chain <chain> Destination chain (solana or base)
1906
+
1907
+ EXAMPLES:
1908
+ nansen trade bridge-status --tx-hash 0xabc... --from-chain base --to-chain solana
1909
+ `);
1910
+ exit(1);
1911
+ return;
1912
+ }
1913
+
1914
+ try {
1915
+ const status = await getBridgeStatus(txHash, fromChain, toChain);
1916
+ log(`\nBridge Status: ${status.status || 'unknown'}`);
1917
+ if (status.substatus) log(` Substatus: ${status.substatus}`);
1918
+ if (status.substatusMessage) log(` Message: ${status.substatusMessage}`);
1919
+ if (status.tool) log(` Bridge: ${status.tool}`);
1920
+ if (status.sending?.txHash) {
1921
+ log(` Sending:`);
1922
+ log(` Tx: ${status.sending.txHash}`);
1923
+ if (status.sending.amount) log(` Amount: ${status.sending.amount}`);
1924
+ if (status.sending.txLink) log(` Explorer: ${status.sending.txLink}`);
1925
+ }
1926
+ if (status.receiving?.txHash) {
1927
+ log(` Receiving:`);
1928
+ log(` Tx: ${status.receiving.txHash}`);
1929
+ if (status.receiving.amount) log(` Amount: ${status.receiving.amount}`);
1930
+ if (status.receiving.txLink) log(` Explorer: ${status.receiving.txLink}`);
1931
+ }
1932
+ if (status.lifiExplorerLink) log(` Li.Fi: ${status.lifiExplorerLink}`);
1933
+ log('');
1934
+ } catch (err) {
1935
+ log(`Error: ${err.message}`);
1936
+ if (err.details) log(` Details: ${JSON.stringify(err.details)}`);
1937
+ exit(1);
1938
+ }
1939
+ },
1698
1940
  };
1699
1941
  }
package/src/transfer.js CHANGED
@@ -4,8 +4,7 @@
4
4
  */
5
5
 
6
6
  import crypto from 'crypto';
7
- import { base58 } from '@scure/base';
8
- import { base58Encode, exportWallet, getWalletConfig, verifyPassword, showWallet } from './wallet.js';
7
+ import { base58Encode, base58Decode, base58DecodePubkey, exportWallet, getWalletConfig, verifyPassword, showWallet } from './wallet.js';
9
8
  import { keccak256, signSecp256k1, rlpEncode } from './crypto.js';
10
9
  import { getWalletConnectAddress, sendTransactionViaWalletConnect } from './walletconnect-trading.js';
11
10
  import { EVM_CHAIN_IDS } from './chain-ids.js';
@@ -22,19 +21,6 @@ const ATA_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
22
21
  // Alias: buildEvmTransaction uses 'evm' as a generic fallback
23
22
  const CHAIN_IDS = { ...EVM_CHAIN_IDS, evm: 1 };
24
23
 
25
- // ============= Base58 =============
26
-
27
- function base58Decode(str) {
28
- return Buffer.from(base58.decode(str));
29
- }
30
-
31
- function base58DecodePubkey(str) {
32
- const raw = base58Decode(str);
33
- if (raw.length === 32) return raw;
34
- if (raw.length < 32) return Buffer.concat([Buffer.alloc(32 - raw.length), raw]);
35
- return raw.subarray(raw.length - 32);
36
- }
37
-
38
24
  // ============= Address Validation =============
39
25
 
40
26
  function validateEvmAddress(address) {
@@ -825,13 +811,7 @@ async function sendTokensViaWalletConnect({ to, amount, chain, token, max, dryRu
825
811
  let txTo, txValue, txData;
826
812
 
827
813
  if (token) {
828
- // Validate ERC-20 contract
829
- const code = await rpcCall(rpcUrl, 'eth_getCode', [token, 'latest']);
830
- if (!code || code === '0x' || code === '0x0') {
831
- throw new Error(`Address ${token} is not a contract — not a valid ERC-20 token`);
832
- }
833
- const decResult = await rpcCall(rpcUrl, 'eth_call', [{ to: token, data: '0x313ce567' }, 'latest']);
834
- const decimals = parseInt(decResult, 16);
814
+ const decimals = await validateErc20Token(rpcUrl, token);
835
815
 
836
816
  if (max) {
837
817
  // Max ERC-20: full token balance
package/src/wallet.js CHANGED
@@ -40,6 +40,17 @@ export function base58Encode(buf) {
40
40
  return base58.encode(buf instanceof Uint8Array ? buf : Uint8Array.from(buf));
41
41
  }
42
42
 
43
+ export function base58Decode(str) {
44
+ return Buffer.from(base58.decode(str));
45
+ }
46
+
47
+ export function base58DecodePubkey(str) {
48
+ const raw = base58Decode(str);
49
+ if (raw.length === 32) return raw;
50
+ if (raw.length < 32) return Buffer.concat([Buffer.alloc(32 - raw.length), raw]);
51
+ return raw.subarray(raw.length - 32);
52
+ }
53
+
43
54
  // ============= Encryption =============
44
55
 
45
56
  /**
package/src/x402-svm.js CHANGED
@@ -4,13 +4,7 @@
4
4
  */
5
5
 
6
6
  import crypto from 'crypto';
7
- import { base58 } from '@scure/base';
8
-
9
- // ============= Base58 Encode =============
10
-
11
- export function base58Encode(buf) {
12
- return base58.encode(buf instanceof Uint8Array ? buf : Uint8Array.from(buf));
13
- }
7
+ import { base58Encode, base58DecodePubkey } from './wallet.js';
14
8
 
15
9
  // ============= Constants =============
16
10
 
@@ -24,25 +18,6 @@ const _SYSTEM_PROGRAM = '11111111111111111111111111111111';
24
18
  const DEFAULT_COMPUTE_UNIT_LIMIT = 20000;
25
19
  const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
26
20
 
27
- // ============= Base58 Decode =============
28
-
29
- export function base58Decode(str) {
30
- return Buffer.from(base58.decode(str));
31
- }
32
-
33
- /**
34
- * Decode a base58 string to exactly 32 bytes (left-pad with zeros).
35
- * Use for Solana public keys and hashes.
36
- */
37
- export function base58DecodePubkey(str) {
38
- const raw = base58Decode(str);
39
- if (raw.length === 32) return raw;
40
- if (raw.length < 32) {
41
- return Buffer.concat([Buffer.alloc(32 - raw.length), raw]);
42
- }
43
- return raw.subarray(raw.length - 32);
44
- }
45
-
46
21
  // ============= Compact-u16 Encoding =============
47
22
  // (Solana's variable-length integer format, from trading.js pattern)
48
23