nansen-cli 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/wallet.js CHANGED
@@ -629,45 +629,47 @@ export function buildWalletCommands(deps = {}) {
629
629
 
630
630
  'send': async () => {
631
631
  const { sendTokens } = await import('./transfer.js');
632
-
632
+
633
633
  if (!options.to) {
634
634
  log('❌ --to <address> is required');
635
635
  exit(1);
636
636
  return;
637
637
  }
638
-
638
+
639
639
  const isMax = flags.max || options.amount === 'max';
640
640
  if (!options.amount && !isMax) {
641
641
  log('❌ --amount <number> or --max is required');
642
642
  exit(1);
643
643
  return;
644
644
  }
645
-
645
+
646
646
  if (!options.chain) {
647
647
  log('❌ --chain <evm|solana> is required');
648
648
  exit(1);
649
649
  return;
650
650
  }
651
-
651
+
652
652
  if (!['evm', 'solana', 'ethereum', 'base'].includes(options.chain)) {
653
653
  log('❌ --chain must be one of: evm, solana, ethereum, base');
654
654
  exit(1);
655
655
  return;
656
656
  }
657
-
658
- const password = process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps);
657
+
658
+ const isWalletConnect = options.wallet === 'walletconnect' || options.wallet === 'wc';
659
+ const password = isWalletConnect ? null : (process.env.NANSEN_WALLET_PASSWORD || await promptPassword('Enter wallet password: ', deps));
659
660
  const dryRun = flags['dry-run'] || flags.dryRun;
660
-
661
+
661
662
  try {
662
663
  const sendOpts = {
663
664
  to: options.to,
664
665
  amount: isMax ? '0' : String(options.amount),
665
666
  chain: options.chain,
666
667
  token: options.token || null,
667
- wallet: options.wallet || null,
668
+ wallet: isWalletConnect ? null : (options.wallet || null),
668
669
  max: isMax,
669
670
  password,
670
671
  dryRun,
672
+ walletconnect: isWalletConnect,
671
673
  };
672
674
 
673
675
  if (dryRun) {
@@ -731,7 +733,7 @@ OPTIONS:
731
733
  --amount <number> Amount to send in human-readable format (required unless --max)
732
734
  --chain <evm|solana> Blockchain to use (required for send)
733
735
  --token <address> Token contract/mint address (optional, sends native if omitted)
734
- --wallet <name> Wallet to use (optional, uses default if omitted)
736
+ --wallet <name> Wallet to use (optional, uses default if omitted; use "walletconnect" or "wc" for WalletConnect, EVM only)
735
737
  --max Send entire balance (deducts gas for native transfers)
736
738
 
737
739
  ENVIRONMENT:
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shared subprocess helper for WalletConnect CLI calls.
3
+ *
4
+ * Used by walletconnect-x402.js and walletconnect-trading.js.
5
+ */
6
+
7
+ import { execFile } from 'child_process';
8
+
9
+ /**
10
+ * Execute a walletconnect CLI command and return stdout.
11
+ */
12
+ export function wcExec(cmd, args, timeoutMs = 10000) {
13
+ return new Promise((resolve, reject) => {
14
+ execFile(cmd, args, { timeout: timeoutMs }, (err, stdout, stderr) => {
15
+ if (err) {
16
+ reject(new Error(err.message));
17
+ return;
18
+ }
19
+ resolve(stdout.trim());
20
+ });
21
+ });
22
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * WalletConnect Trading & Transfer Support
3
+ *
4
+ * Allows signing and broadcasting transactions via a WalletConnect-connected wallet
5
+ * (hardware wallets, mobile wallets) instead of local key storage.
6
+ * Uses the walletconnect CLI binary (subprocess-based, same as x402).
7
+ *
8
+ * EVM only — Solana via WalletConnect is not supported.
9
+ */
10
+
11
+ import { wcExec } from './walletconnect-exec.js';
12
+
13
+ /**
14
+ * Get the address of the connected WalletConnect wallet.
15
+ * Returns the first account address, or null if not connected / binary missing.
16
+ */
17
+ export async function getWalletConnectAddress() {
18
+ try {
19
+ const output = await wcExec('walletconnect', ['whoami', '--json'], 3000);
20
+ const data = JSON.parse(output);
21
+ if (data.connected === false) return null;
22
+ return data.accounts?.[0]?.address || null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Send a transaction via WalletConnect.
30
+ *
31
+ * The connected wallet signs and may broadcast the transaction.
32
+ * Returns either { txHash } (wallet broadcast) or { signedTransaction } (we broadcast).
33
+ *
34
+ * @param {object} txData - Transaction data: { to, data, value, gas, chainId }
35
+ * @param {number} [timeoutMs=120000] - Timeout for user approval
36
+ * @returns {{ txHash?: string, signedTransaction?: string }}
37
+ */
38
+ export async function sendTransactionViaWalletConnect(txData, timeoutMs = 120000) {
39
+ // The walletconnect CLI expects chainId as "eip155:<id>" string format
40
+ const chainId = txData.chainId
41
+ ? (String(txData.chainId).startsWith('eip155:') ? txData.chainId : `eip155:${txData.chainId}`)
42
+ : undefined;
43
+
44
+ const payload = {
45
+ to: txData.to,
46
+ data: txData.data || '0x',
47
+ value: txData.value ? '0x' + BigInt(txData.value).toString(16) : '0x0',
48
+ gas: txData.gas ? '0x' + BigInt(txData.gas).toString(16) : undefined,
49
+ chainId,
50
+ };
51
+
52
+ const output = await wcExec('walletconnect', ['send-transaction', JSON.stringify(payload)], timeoutMs);
53
+
54
+ // walletconnect may print status messages before the JSON line — extract JSON only
55
+ const jsonLine = output.split('\n').find(line => line.startsWith('{'));
56
+ if (!jsonLine) throw new Error('No JSON output from walletconnect send-transaction');
57
+ const result = JSON.parse(jsonLine);
58
+
59
+ // The CLI returns { transactionHash: "0x..." }
60
+ if (result.transactionHash) return { txHash: result.transactionHash };
61
+ if (result.txHash) return { txHash: result.txHash };
62
+ if (result.signedTransaction) return { signedTransaction: result.signedTransaction };
63
+
64
+ throw new Error('Unexpected response from walletconnect send-transaction');
65
+ }
66
+
67
+ /**
68
+ * Send an ERC-20 approval via WalletConnect.
69
+ *
70
+ * Builds approve(spender, MAX_UINT256) calldata and delegates to sendTransactionViaWalletConnect.
71
+ *
72
+ * @param {string} tokenAddress - ERC-20 token contract
73
+ * @param {string} spenderAddress - Approval target (e.g. DEX router)
74
+ * @param {number} chainId - EIP-155 chain ID
75
+ * @returns {{ txHash?: string, signedTransaction?: string }}
76
+ */
77
+ export async function sendApprovalViaWalletConnect(tokenAddress, spenderAddress, chainId) {
78
+ // ERC-20 approve(address spender, uint256 amount) selector = 0x095ea7b3
79
+ const MAX_UINT256_HEX = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
80
+ const data = '0x095ea7b3'
81
+ + spenderAddress.slice(2).toLowerCase().padStart(64, '0')
82
+ + MAX_UINT256_HEX;
83
+
84
+ return sendTransactionViaWalletConnect({
85
+ to: tokenAddress,
86
+ data,
87
+ value: '0',
88
+ gas: '100000',
89
+ chainId,
90
+ });
91
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * x402 Auto-Payment via WalletConnect
3
+ *
4
+ * Handles automatic payment signing when the API returns HTTP 402.
5
+ * Uses the walletconnect CLI to check wallet connection and sign EIP-712 typed data.
6
+ */
7
+
8
+ import crypto from 'crypto';
9
+ import { NansenError, ErrorCode } from './api.js';
10
+ import { wcExec } from './walletconnect-exec.js';
11
+ import { EVM_CHAIN_IDS } from './chain-ids.js';
12
+
13
+ /**
14
+ * Check if a WalletConnect wallet session is active.
15
+ * Returns { wallet, accounts, expires } or null.
16
+ */
17
+ export async function checkWalletConnection() {
18
+ try {
19
+ const output = await wcExec('walletconnect', ['whoami', '--json'], 3000);
20
+ const data = JSON.parse(output);
21
+ if (data.connected === false) return null;
22
+ return data;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Select a compatible payment requirement from the accepts array.
30
+ * Requires scheme=exact and EIP-3009 TransferWithAuthorization support (extra.name + extra.version).
31
+ */
32
+ export function selectPaymentRequirement(accepts) {
33
+ if (!Array.isArray(accepts) || accepts.length === 0) return null;
34
+
35
+ return accepts.find(req =>
36
+ req.scheme === 'exact' &&
37
+ req.extra?.name &&
38
+ req.extra?.version
39
+ ) || null;
40
+ }
41
+
42
+ /**
43
+ * Parse chain ID from network string (e.g., "eip155:8453" → 8453)
44
+ */
45
+ function parseChainId(network) {
46
+ if (!network) return null;
47
+ const match = network.match(/^eip155:(\d+)$/);
48
+ return match ? Number(match[1]) : null;
49
+ }
50
+
51
+ /**
52
+ * Build EIP-712 typed data for TransferWithAuthorization (EIP-3009).
53
+ */
54
+ export function buildEIP712TypedData({ fromAddress, requirement }) {
55
+ const { asset, payTo, extra, maxTimeoutSeconds } = requirement;
56
+ // x402 uses "amount", fall back to "maxAmountRequired" for compatibility
57
+ const amount = requirement.amount || requirement.maxAmountRequired;
58
+
59
+ // Determine chain ID: extra.chainId > parsed from network > fallback map > base
60
+ const chainId = extra.chainId || parseChainId(requirement.network) || EVM_CHAIN_IDS[requirement.chain] || EVM_CHAIN_IDS.base;
61
+
62
+ const now = Math.floor(Date.now() / 1000);
63
+ const nonce = '0x' + crypto.randomBytes(32).toString('hex');
64
+
65
+ const typedData = {
66
+ types: {
67
+ EIP712Domain: [
68
+ { name: 'name', type: 'string' },
69
+ { name: 'version', type: 'string' },
70
+ { name: 'chainId', type: 'uint256' },
71
+ { name: 'verifyingContract', type: 'address' },
72
+ ],
73
+ TransferWithAuthorization: [
74
+ { name: 'from', type: 'address' },
75
+ { name: 'to', type: 'address' },
76
+ { name: 'value', type: 'uint256' },
77
+ { name: 'validAfter', type: 'uint256' },
78
+ { name: 'validBefore', type: 'uint256' },
79
+ { name: 'nonce', type: 'bytes32' },
80
+ ],
81
+ },
82
+ primaryType: 'TransferWithAuthorization',
83
+ domain: {
84
+ name: extra.name,
85
+ version: extra.version,
86
+ chainId,
87
+ verifyingContract: asset,
88
+ },
89
+ message: {
90
+ from: fromAddress,
91
+ to: payTo,
92
+ value: amount,
93
+ validAfter: now - 600, // 10 min in the past to tolerate clock skew between client and verifier
94
+ validBefore: now + (maxTimeoutSeconds || 120),
95
+ nonce,
96
+ },
97
+ };
98
+
99
+ return typedData;
100
+ }
101
+
102
+ /**
103
+ * Build the base64-encoded Payment-Signature header value.
104
+ * Follows x402 v2 spec: { x402Version, resource, accepted, payload }
105
+ */
106
+ export function buildPaymentSignatureHeader({ signature, authorization, resource, accepted }) {
107
+ const paymentPayload = {
108
+ x402Version: 2,
109
+ resource: resource || { url: '', description: '', mimeType: '' },
110
+ accepted: accepted || {},
111
+ payload: {
112
+ signature,
113
+ authorization,
114
+ },
115
+ };
116
+ return btoa(JSON.stringify(paymentPayload));
117
+ }
118
+
119
+ /**
120
+ * Format amount for human-readable display (e.g., "0.01 USDC")
121
+ */
122
+ function formatPaymentAmount(requirement) {
123
+ const { extra } = requirement;
124
+ const rawAmount = requirement.amount || requirement.maxAmountRequired;
125
+ const symbol = extra.symbol || extra.name || 'tokens';
126
+ const decimals = extra.decimals || 6;
127
+ const amount = Number(rawAmount) / Math.pow(10, decimals);
128
+ const chain = requirement.network || requirement.chain || 'unknown';
129
+ return `${amount} ${symbol} on ${chain}`;
130
+ }
131
+
132
+ /**
133
+ * Handle x402 payment: check wallet, sign, return Payment-Signature header.
134
+ *
135
+ * @param {Object} paymentRequirements - Decoded payment requirements from 402 response
136
+ * @param {string} requestUrl - The original request URL (for context in errors)
137
+ * @returns {string} Base64-encoded Payment-Signature header value
138
+ * @throws {NansenError} On failure
139
+ */
140
+ export async function handleX402Payment(paymentRequirements) {
141
+ // 1. Check wallet connection
142
+ const wallet = await checkWalletConnection();
143
+ if (!wallet) {
144
+ throw new NansenError(
145
+ 'x402 payment required but no wallet connected. Run `walletconnect connect` first.',
146
+ ErrorCode.PAYMENT_REQUIRED,
147
+ 402
148
+ );
149
+ }
150
+
151
+ const fromAddress = wallet.accounts[0]?.address;
152
+ if (!fromAddress) {
153
+ throw new NansenError(
154
+ 'x402 payment required but wallet has no accounts.',
155
+ ErrorCode.PAYMENT_REQUIRED,
156
+ 402
157
+ );
158
+ }
159
+
160
+ // 2. Select compatible payment requirement
161
+ const accepts = paymentRequirements.accepts || paymentRequirements;
162
+ const requirement = selectPaymentRequirement(Array.isArray(accepts) ? accepts : [accepts]);
163
+ if (!requirement) {
164
+ const available = (Array.isArray(accepts) ? accepts : []).map(r => r.scheme).join(', ');
165
+ throw new NansenError(
166
+ `x402 payment required but no compatible payment method found. Available: ${available || 'none'}. Need scheme=exact with EIP-3009 support.`,
167
+ ErrorCode.PAYMENT_REQUIRED,
168
+ 402
169
+ );
170
+ }
171
+
172
+ // 3. Build EIP-712 typed data
173
+ const typedData = buildEIP712TypedData({ fromAddress, requirement });
174
+ const typedDataJson = JSON.stringify(typedData);
175
+
176
+ // 4. Log payment info to stderr (stdout is for JSON output)
177
+ const amountStr = formatPaymentAmount(requirement);
178
+ process.stderr.write(`x402: Requesting payment approval (${amountStr})...\n`);
179
+
180
+ // 5. Sign via walletconnect CLI (120s timeout for user approval)
181
+ let signResult;
182
+ try {
183
+ const output = await wcExec('walletconnect', ['sign-typed-data', typedDataJson], 120000);
184
+ // walletconnect may print status messages before the JSON line — extract JSON only
185
+ const jsonLine = output.split('\n').find(line => line.startsWith('{'));
186
+ if (!jsonLine) throw new Error('No JSON output from walletconnect sign-typed-data');
187
+ signResult = JSON.parse(jsonLine);
188
+ } catch (err) {
189
+ throw new NansenError(
190
+ `x402 payment signing failed: ${err.message}`,
191
+ ErrorCode.PAYMENT_REQUIRED,
192
+ 402
193
+ );
194
+ }
195
+
196
+ // 6. Build Payment-Signature header (authorization values must be strings per x402 spec)
197
+ const authorization = {
198
+ from: fromAddress,
199
+ to: requirement.payTo,
200
+ value: (requirement.amount || requirement.maxAmountRequired).toString(),
201
+ validAfter: typedData.message.validAfter.toString(),
202
+ validBefore: typedData.message.validBefore.toString(),
203
+ nonce: typedData.message.nonce,
204
+ };
205
+
206
+ const headerValue = buildPaymentSignatureHeader({
207
+ signature: signResult.signature,
208
+ authorization,
209
+ resource: paymentRequirements.resource || { url: '', description: '', mimeType: '' },
210
+ accepted: requirement,
211
+ });
212
+
213
+ process.stderr.write(`x402: Payment signed successfully.\n`);
214
+ return headerValue;
215
+ }
@@ -1,8 +0,0 @@
1
- # Changesets
2
-
3
- Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4
- with multi-package repos, or single-package repos to help you version and publish your code. You can
5
- find the full documentation for it [in the repository](https://github.com/changesets/changesets)
6
-
7
- We have a quick list of common questions to get you started engaging with this project in
8
- [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
package/AGENTS.md DELETED
@@ -1,176 +0,0 @@
1
- # AGENTS.md — Contributor Guide
2
-
3
- Guidance for AI coding agents (Claude Code, Codex, Copilot, etc.) working on this repository. If you're an agent **using** the CLI, see [README.md](README.md).
4
-
5
- ## Architecture
6
-
7
- ```
8
- src/
9
- ├── index.js # Entry point (shebang, calls runCLI)
10
- ├── cli.js # Command router, arg parsing, schema, help text
11
- ├── api.js # NansenAPI client (REST, retry, cache, x402 auto-pay)
12
- ├── wallet.js # Wallet CRUD (create/list/show/export/delete/send)
13
- ├── trading.js # Quote + execute swaps (OKX router via API)
14
- ├── transfer.js # Token/native transfers (EVM + Solana)
15
- ├── x402.js # x402 payment orchestration (picks network, signs)
16
- ├── x402-evm.js # EVM payment signing (EIP-3009 transferWithAuthorization)
17
- ├── x402-svm.js # Solana payment signing (SPL transfer)
18
- ├── crypto.js # Key encryption/decryption (AES-256-GCM or plaintext)
19
- └── update-check.js # Version upgrade notice
20
- ```
21
-
22
- ### Command routing
23
-
24
- `src/index.js` → `runCLI()` in `src/cli.js`
25
-
26
- Commands are built by three functions, merged in `runCLI()`:
27
- - `buildCommands()` in cli.js — analytics commands (smart-money, profiler, token, etc.)
28
- - `buildWalletCommands()` in wallet.js — wallet subcommands
29
- - `buildTradingCommands()` in trading.js — quote/execute
30
-
31
- Commands listed in `NO_AUTH_COMMANDS` skip API initialization. Everything else instantiates `NansenAPI` with retry, cache, and x402 config.
32
-
33
- ### Data flow: trade
34
-
35
- ```
36
- CLI args → api.js GET /defi/quote → quote response
37
- → wallet.js decrypt key → trading.js sign tx → api.js POST /defi/execute → broadcast
38
- ```
39
-
40
- ### Data flow: x402 auto-pay
41
-
42
- ```
43
- api.js (any call) → 402 response with payment requirements
44
- → x402.js rankRequirements() → picks cheapest network (EVM first)
45
- → x402-evm.js or x402-svm.js → sign USDC payment
46
- → api.js retries original request with Payment-Signature header
47
- ```
48
-
49
- If EVM payment fails (insufficient funds), the async generator yields a Solana signature as fallback.
50
-
51
- ### Output convention
52
-
53
- Core functions return data objects. The CLI layer formats via `formatOutput()`. Never `console.log` in core functions — use the `log` dependency injection for CLI output.
54
-
55
- ## Development
56
-
57
- ```bash
58
- npm install # Install dependencies
59
- npm test # Run tests (vitest)
60
- npm run test:watch # Watch mode
61
- npm run test:coverage # With coverage
62
- ```
63
-
64
- ### Running locally
65
-
66
- ```bash
67
- node src/index.js <command> [options]
68
-
69
- # Examples
70
- node src/index.js wallet create my-wallet
71
- node src/index.js smart-money --chain solana --limit 5
72
- ```
73
-
74
- ## Testing
75
-
76
- - **Framework:** Vitest
77
- - **Test files:** `src/__tests__/*.test.js`
78
- - **Current:** 577 tests across 13 test files
79
- - **All new code must have tests**
80
- - **Mock all RPC/API calls** — never hit real networks in tests
81
-
82
- ### Test structure
83
-
84
- ```js
85
- import { describe, it, expect, vi, beforeEach } from 'vitest';
86
-
87
- global.fetch = vi.fn();
88
-
89
- describe('featureName', () => {
90
- beforeEach(() => {
91
- fetch.mockReset();
92
- });
93
-
94
- it('should do the thing', async () => {
95
- fetch.mockResolvedValueOnce({
96
- ok: true,
97
- json: async () => ({ jsonrpc: '2.0', result: '0x...', id: 1 })
98
- });
99
- // test logic
100
- });
101
- });
102
- ```
103
-
104
- ### Required RPC mocks by code path
105
-
106
- **EVM transfers:** `eth_getBalance`, `eth_gasPrice`, `eth_maxPriorityFeePerGas`, `eth_getTransactionCount`, `eth_estimateGas`, `eth_getCode`, `eth_sendRawTransaction`, `eth_getTransactionReceipt`
107
-
108
- **Solana transfers:** `getBalance`, `getLatestBlockhash`, `sendTransaction`, `getSignatureStatuses`
109
-
110
- **SPL token transfers** (additionally): `getTokenAccountsByOwner`, `getAccountInfo`
111
-
112
- **Wallet operations:** No RPC mocks needed (file I/O only). Mock `fs` if testing file paths.
113
-
114
- **API calls:** Mock `fetch` to return `{ ok: true, json: () => ({...}) }` or `{ ok: false, status: 402, headers: new Headers({...}) }` for x402 paths.
115
-
116
- ## Style Guide
117
-
118
- - **ESM only** (`import`/`export`). No TypeScript, no transpilation.
119
- - **No interactive prompts in core functions.** Use env vars: `NANSEN_WALLET_PASSWORD`, `NANSEN_API_KEY`.
120
- - **Error handling:** `throw new Error('descriptive message')` in core. CLI catches and formats.
121
- - **Actionable error messages** — tell the user what to do:
122
- - ❌ `"Authentication failed"`
123
- - ✅ `"Not logged in. Run: nansen login"`
124
- - **BigInt for token amounts.** Never use floating point. Parse to BigInt with decimals.
125
- - **Chain branching:** Use `chain === 'solana'` checks, not inheritance/polymorphism.
126
- - **Minimal dependencies.** Prefer Node.js built-in APIs (crypto, fs, path, http).
127
-
128
- ## PR Checklist
129
-
130
- - [ ] `npm test` passes (all tests)
131
- - [ ] New code paths have test coverage
132
- - [ ] No hardcoded secrets, API keys, or private keys
133
- - [ ] No `console.log` in core functions (use `log` dep injection)
134
- - [ ] Error messages are actionable (tell user what to do)
135
- - [ ] CLI help text updated if adding/changing commands
136
- - [ ] RPC mocks cover all methods in the code path
137
- - [ ] Wallet flows work both with and without `NANSEN_WALLET_PASSWORD`
138
- - [ ] Changeset added if changing user-facing behavior (add a `.changeset/<name>.md` file — `npm test` will warn if missing)
139
-
140
- ## Chains & Networks
141
-
142
- **EVM:** Ethereum (chain ID 1), Base (8453). `CHAIN_IDS` in transfer.js only maps these two — other EVM chains will fail for transfers.
143
-
144
- **Solana:** mainnet-beta. Supports native SOL, standard SPL tokens, and Token-2022 (Token Extensions).
145
-
146
- **RPC endpoints:** Hardcoded in `CHAIN_RPCS` (transfer.js). Nansen API handles RPC for trading.
147
-
148
- ## Key Constants
149
-
150
- | Constant | Value |
151
- |----------|-------|
152
- | USDC (Base) | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
153
- | USDC (Solana) | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |
154
- | x402 payment | $0.05 USDC per API call |
155
- | Gas buffer | API provides `quote.gas` with 1.5x multiplier — use directly |
156
-
157
- ## Endpoint Quirks
158
-
159
- These are internal details agents should know when writing or debugging tests:
160
-
161
- - **`token holders --smart-money`** — Returns `UNSUPPORTED_FILTER` for tokens without smart money tracking. Not all tokens have this data.
162
- - **`token flow-intelligence`** — May return all-zero flows for illiquid tokens. Normal, not an error.
163
- - **`token screener --search`** — Client-side filtering. The CLI fetches up to 500 results, then filters locally.
164
- - **`--fields`** — Applies to the entire response tree, including the `success`/`data` wrapper.
165
- - **Profiler beta endpoints** use `recordsPerPage` instead of `per_page`. The CLI handles this automatically.
166
- - **`profiler perp-positions`** — No pagination support; the API ignores the pagination parameter.
167
-
168
- ## Known Gotchas
169
-
170
- 1. **EIP-7702 delegated accounts** on Base have contract code. Always use `eth_estimateGas`, never hardcode 21000 gas.
171
- 2. **Solana SPL account ordering:** Writable accounts (destATA) must precede readonly (mint) in the transaction message.
172
- 3. **`getSignatureStatuses`** over `confirmTransaction` — the latter is deprecated and unreliable on public RPCs.
173
- 4. **`--max` native SOL:** Reserve 5000 lamports for fee. On EVM L2s, reserve 3x estimated gas for L1 data posting fees.
174
- 5. **Token-2022:** Use `TOKEN_2022_PROGRAM_ID` and `TransferCheckedInstruction` (not plain `Transfer`).
175
- 6. **CreateATA path:** When recipient doesn't have a token account, the sender creates it. This path in transfer.js has limited test coverage — add tests if modifying.
176
- 7. **`CHAIN_IDS` is incomplete:** Only ethereum and base are mapped. Adding new EVM chain support requires updating this map.