nansen-cli 1.23.1 → 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,20 @@
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
+
3
18
  ## 1.23.1
4
19
 
5
20
  ### Patch 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.1",
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
  };
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
  },
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { validateAddress } from './api.js';
8
+ import { CHAIN_RPCS } from './rpc-urls.js';
8
9
 
9
10
  const SUPPORTED_CHAINS = ['solana', 'base'];
10
11
 
@@ -12,7 +13,7 @@ const SUPPORTED_CHAINS = ['solana', 'base'];
12
13
  * Validate quote inputs before any network call.
13
14
  * Throws on validation failure with an actionable error message.
14
15
  */
15
- export function validateQuoteInput({ chain, from, to, amount }) {
16
+ export function validateQuoteInput({ chain, toChain, from, to, amount }) {
16
17
  // 1. Chain must be supported
17
18
  const normalizedChain = chain?.toLowerCase();
18
19
  if (!SUPPORTED_CHAINS.includes(normalizedChain)) {
@@ -21,6 +22,13 @@ export function validateQuoteInput({ chain, from, to, amount }) {
21
22
  );
22
23
  }
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
+
24
32
  // 2. Amount must be a positive finite number
25
33
  const numAmount = Number(amount);
26
34
  if (!Number.isFinite(numAmount) || numAmount <= 0) {
@@ -36,19 +44,227 @@ export function validateQuoteInput({ chain, from, to, amount }) {
36
44
  `Invalid sell token address for ${normalizedChain}. ${fromResult.error}`
37
45
  );
38
46
  }
39
- const toResult = validateAddress(to, normalizedChain);
47
+ const toResult = validateAddress(to, normalizedToChain);
40
48
  if (!toResult.valid) {
41
49
  throw new Error(
42
- `Invalid buy token address for ${normalizedChain}. ${toResult.error}`
50
+ `Invalid buy token address for ${normalizedToChain}. ${toResult.error}`
43
51
  );
44
52
  }
45
53
 
46
- // 4. Sell and buy tokens must be different
47
- const fromNorm = normalizedChain === 'solana' ? from : from.toLowerCase();
48
- const toNorm = normalizedChain === 'solana' ? to : to.toLowerCase();
49
- if (fromNorm === toNorm) {
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) {
50
164
  throw new Error(
51
- `Cannot swap ${from} for itself. Sell and buy tokens must be different.`
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`
52
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;
53
269
  }
54
270
  }
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 } from './trade-validation.js';
16
+ import { validateQuoteInput, validateBalance } from './trade-validation.js';
17
17
  import { CHAIN_RPCS } from './rpc-urls.js';
18
18
 
19
19
  // ============= Constants =============
@@ -21,8 +21,8 @@ import { CHAIN_RPCS } from './rpc-urls.js';
21
21
  const TRADING_API_URL = process.env.NANSEN_TRADING_API_URL || 'https://trading-api.nansen.ai';
22
22
 
23
23
  const CHAIN_MAP = {
24
- solana: { index: '501', type: 'solana', chainId: 501, name: 'Solana', explorer: 'https://solscan.io/tx/' },
25
- 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' },
26
26
  };
27
27
 
28
28
  // Extend when adding new EVM chains (e.g. arbitrum WETH, polygon WMATIC)
@@ -199,13 +199,93 @@ export async function executeTransaction(params, { retries = 2, retryDelayMs = 1
199
199
  throw lastError;
200
200
  }
201
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
+
202
282
  // ============= Quote Storage =============
203
283
 
204
284
  /**
205
285
  * Save a quote response to disk for later execution.
206
286
  * @returns {string} Quote ID
207
287
  */
208
- export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalletIds = null) {
288
+ export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalletIds = null, toChain = null) {
209
289
  const dir = getQuotesDir();
210
290
  if (!fs.existsSync(dir)) {
211
291
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -216,6 +296,7 @@ export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalle
216
296
  const quoteId = `${timestamp}-${hash}`;
217
297
 
218
298
  const data = { quoteId, chain, timestamp, signerType, response: quoteResponse };
299
+ if (toChain) data.toChain = toChain;
219
300
  if (privyWalletIds) data.privyWalletIds = privyWalletIds;
220
301
 
221
302
  fs.writeFileSync(path.join(dir, `${quoteId}.json`), JSON.stringify(data, null, 2), { mode: 0o600 });
@@ -808,6 +889,15 @@ export function formatQuote(quote, index) {
808
889
  if (quote.tradingFeeInUsd) lines.push(` Trading Fee: $${quote.tradingFeeInUsd}`);
809
890
  if (quote.networkFeeInUsd) lines.push(` Network Fee: $${quote.networkFeeInUsd}`);
810
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
+ }
811
901
  if (quote.priceImpactPct) {
812
902
  const impactAbs = Math.abs(parseFloat(quote.priceImpactPct));
813
903
  if (impactAbs > 5) {
@@ -828,12 +918,15 @@ export function buildTradingCommands(deps = {}) {
828
918
  return {
829
919
  'quote': async (args, apiInstance, flags, options) => {
830
920
  const chain = options.chain || args[0];
921
+ const toChainRaw = options['to-chain'];
831
922
  const fromRaw = options.from || options['from-token'] || args[1];
832
923
  const toRaw = options.to || options['to-token'] || args[2];
924
+ const effectiveToChain = toChainRaw || chain;
833
925
  const from = resolveTokenAddress(fromRaw, chain);
834
- const to = resolveTokenAddress(toRaw, chain);
926
+ const to = resolveTokenAddress(toRaw, effectiveToChain);
835
927
  const amount = options.amount || args[3];
836
928
  const walletName = options.wallet;
929
+ const toWallet = options['to-wallet'];
837
930
  const slippage = options.slippage;
838
931
  const autoSlippage = flags['auto-slippage'];
839
932
  const maxAutoSlippage = options['max-auto-slippage'];
@@ -850,12 +943,14 @@ PREREQUISITE:
850
943
  Set one up with: nansen wallet create
851
944
 
852
945
  OPTIONS:
853
- --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)
854
948
  --from <symbol|address> Input token (symbol like SOL, USDC or address)
855
949
  --to <symbol|address> Output token (symbol like USDC, ETH or address)
856
950
  --amount <units> Amount in BASE UNITS (e.g. lamports, wei)
857
951
  --amount-unit <unit> "token" to specify amount in token units (e.g. 0.5 SOL)
858
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)
859
954
  --slippage <pct> Slippage as decimal (e.g. 0.03 for 3%). Default: 0.03
860
955
  --auto-slippage Enable auto slippage calculation
861
956
  --max-auto-slippage <pct> Max auto slippage when auto-slippage enabled
@@ -865,7 +960,8 @@ EXAMPLES:
865
960
  nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
866
961
  nansen trade quote --chain solana --from SOL --to USDC --amount 0.5 --amount-unit token
867
962
  nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
868
- 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
869
965
  `);
870
966
  exit(1);
871
967
  return;
@@ -881,7 +977,7 @@ EXAMPLES:
881
977
  // Static input validation — catches common agent errors (wrong addresses,
882
978
  // same-token swaps, bad amounts) before any network or wallet call.
883
979
  try {
884
- validateQuoteInput({ chain, from, to, amount });
980
+ validateQuoteInput({ chain, toChain: toChainRaw || null, from, to, amount });
885
981
  } catch (validationErr) {
886
982
  log(`Error: ${validationErr.message}`);
887
983
  exit(1);
@@ -891,11 +987,12 @@ EXAMPLES:
891
987
  // When --amount-unit token is used, resolve decimals and convert to base units.
892
988
  // Otherwise, validate that the amount is already in base units (integer).
893
989
  let resolvedAmount = amount;
990
+ let resolvedDecimals;
894
991
  if (amountUnit === 'token') {
895
992
  try {
896
993
  const tokenForDecimals = swapMode === 'exactOut' ? to : from;
897
- const decimals = await resolveTokenDecimals(tokenForDecimals, chain);
898
- resolvedAmount = convertToBaseUnits(amount, decimals);
994
+ resolvedDecimals = await resolveTokenDecimals(tokenForDecimals, chain);
995
+ resolvedAmount = convertToBaseUnits(amount, resolvedDecimals);
899
996
  } catch (err) {
900
997
  log(`Error resolving token decimals: ${err.message}`);
901
998
  exit(1);
@@ -955,7 +1052,39 @@ EXAMPLES:
955
1052
  return;
956
1053
  }
957
1054
 
958
- 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
+ }
959
1088
  log(` Wallet: ${walletAddress}`);
960
1089
 
961
1090
  const fromWarning = getWrappedNativeFromWarning(from, chain);
@@ -968,6 +1097,20 @@ EXAMPLES:
968
1097
  amount: resolvedAmount,
969
1098
  userWalletAddress: walletAddress,
970
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
+ }
971
1114
  if (slippage) params.slippagePercent = slippage;
972
1115
  if (autoSlippage) params.autoSlippage = true;
973
1116
  if (maxAutoSlippage) params.maxAutoSlippagePercent = maxAutoSlippage;
@@ -988,7 +1131,7 @@ EXAMPLES:
988
1131
  response.quotes.forEach((q, i) => log(formatQuote(q, i)));
989
1132
 
990
1133
  const signerType = isWalletConnect ? 'walletconnect' : walletProvider;
991
- const quoteId = saveQuote(response, chain, signerType, privyWalletIds);
1134
+ const quoteId = saveQuote(response, chain, signerType, privyWalletIds, isCrossChain ? toChainRaw : null);
992
1135
  log(`\n Quote ID: ${quoteId}`);
993
1136
  log(` Execute: nansen trade execute --quote ${quoteId}`);
994
1137
  if (response.quotes.length > 1) {
@@ -1492,6 +1635,23 @@ EXAMPLES:
1492
1635
  log(` Tx Hash: ${wcResult.txHash}`);
1493
1636
  log(` Chain: ${chainConfig.name}`);
1494
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
+
1495
1655
  log('');
1496
1656
  return undefined; // Success
1497
1657
  }
@@ -1678,6 +1838,23 @@ EXAMPLES:
1678
1838
  log(` ${e.inputAmount} ${e.inputMint?.slice(0, 8)}... → ${e.outputAmount} ${e.outputMint?.slice(0, 8)}...`);
1679
1839
  });
1680
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
+
1681
1858
  log('');
1682
1859
  return undefined; // Success — done
1683
1860
  } else {
@@ -1688,8 +1865,12 @@ EXAMPLES:
1688
1865
  }
1689
1866
 
1690
1867
  } catch (quoteErr) {
1691
- log(` ❌ Quote ${quoteName} failed: ${quoteErr.message}`);
1692
- 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}`;
1693
1874
  if (qi + 1 < endIndex) log(` Trying next quote...`);
1694
1875
  }
1695
1876
  }
@@ -1706,5 +1887,55 @@ EXAMPLES:
1706
1887
  exit(1);
1707
1888
  }
1708
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
+ },
1709
1940
  };
1710
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