nansen-cli 1.14.0 → 1.15.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/src/schema.json CHANGED
@@ -1831,6 +1831,48 @@
1831
1831
  }
1832
1832
  }
1833
1833
  }
1834
+ },
1835
+ "wallet": {
1836
+ "description": "Wallet management (local or Privy server wallets)",
1837
+ "options": {
1838
+ "provider": {
1839
+ "type": "string",
1840
+ "enum": ["local", "privy"],
1841
+ "description": "Wallet provider (default: local). Use 'privy' for server-managed wallets."
1842
+ }
1843
+ },
1844
+ "subcommands": {
1845
+ "create": {
1846
+ "description": "Create a new wallet",
1847
+ "options": {
1848
+ "name": { "type": "string", "description": "Wallet name (default: 'default')" }
1849
+ }
1850
+ },
1851
+ "list": { "description": "List all wallets" },
1852
+ "show": {
1853
+ "description": "Show wallet details",
1854
+ "options": {
1855
+ "name": { "type": "string", "description": "Wallet name" }
1856
+ }
1857
+ },
1858
+ "delete": {
1859
+ "description": "Delete a wallet",
1860
+ "options": {
1861
+ "name": { "type": "string", "description": "Wallet name" }
1862
+ }
1863
+ },
1864
+ "send": {
1865
+ "description": "Send tokens or native currency",
1866
+ "options": {
1867
+ "to": { "type": "string", "required": true, "description": "Recipient address" },
1868
+ "amount": { "type": "string", "description": "Amount to send" },
1869
+ "chain": { "type": "string", "required": true, "description": "Blockchain to use" }
1870
+ }
1871
+ },
1872
+ "export": { "description": "Export private keys (local only, requires password)" },
1873
+ "default": { "description": "Set default wallet (local only)" },
1874
+ "help": { "description": "Show wallet help" }
1875
+ }
1834
1876
  }
1835
1877
  },
1836
1878
  "globalOptions": {
@@ -1879,13 +1921,11 @@
1879
1921
  "avalanche",
1880
1922
  "linea",
1881
1923
  "scroll",
1882
- "zksync",
1883
1924
  "mantle",
1884
1925
  "ronin",
1885
1926
  "sei",
1886
1927
  "plasma",
1887
1928
  "sonic",
1888
- "unichain",
1889
1929
  "monad",
1890
1930
  "hyperevm",
1891
1931
  "iotaevm"
package/src/trading.js CHANGED
@@ -8,7 +8,7 @@
8
8
  import crypto from 'crypto';
9
9
  import fs from 'fs';
10
10
  import path from 'path';
11
- import { exportWallet, getDefaultAddress, showWallet, listWallets, getWalletConfig } from './wallet.js';
11
+ import { exportWallet, getWalletConfig, showWallet, listWallets } from './wallet.js';
12
12
  import { base58Decode } from './transfer.js';
13
13
  import { keccak256, signSecp256k1, rlpEncode } from './crypto.js';
14
14
  import { getWalletConnectAddress, sendTransactionViaWalletConnect, sendApprovalViaWalletConnect } from './walletconnect-trading.js';
@@ -68,6 +68,33 @@ const EVM_RPC_URLS = {
68
68
  base: process.env.NANSEN_RPC_BASE || 'https://mainnet.base.org',
69
69
  };
70
70
 
71
+ /**
72
+ * Make a JSON-RPC call to an EVM RPC endpoint.
73
+ * @param {string} chain - Chain name (key into EVM_RPC_URLS)
74
+ * @param {string} method - JSON-RPC method name
75
+ * @param {Array} params - Method parameters
76
+ * @returns {Promise<*>} Parsed result value
77
+ * @throws {Error} If chain has no configured RPC or the RPC returns an error
78
+ */
79
+ async function evmRpcCall(chain, method, params = []) {
80
+ const rpcUrl = EVM_RPC_URLS[chain];
81
+ if (!rpcUrl) throw new Error(`No RPC URL configured for chain: ${chain}`);
82
+ const res = await fetch(rpcUrl, {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
86
+ });
87
+ const text = await res.text();
88
+ let body;
89
+ try {
90
+ body = JSON.parse(text);
91
+ } catch {
92
+ throw new Error(`RPC endpoint returned non-JSON response (HTTP ${res.status}) for ${method}: ${text.slice(0, 100)}`);
93
+ }
94
+ if (body.error) throw new Error(`RPC error (${method}): ${body.error.message}`);
95
+ return body.result;
96
+ }
97
+
71
98
  function getQuotesDir() {
72
99
  const configDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.nansen');
73
100
  return path.join(configDir, 'quotes');
@@ -187,7 +214,7 @@ export async function executeTransaction(params, { retries = 2, retryDelayMs = 1
187
214
  * Save a quote response to disk for later execution.
188
215
  * @returns {string} Quote ID
189
216
  */
190
- export function saveQuote(quoteResponse, chain, signerType = 'local') {
217
+ export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalletIds = null) {
191
218
  const dir = getQuotesDir();
192
219
  if (!fs.existsSync(dir)) {
193
220
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -198,6 +225,7 @@ export function saveQuote(quoteResponse, chain, signerType = 'local') {
198
225
  const quoteId = `${timestamp}-${hash}`;
199
226
 
200
227
  const data = { quoteId, chain, timestamp, signerType, response: quoteResponse };
228
+ if (privyWalletIds) data.privyWalletIds = privyWalletIds;
201
229
 
202
230
  fs.writeFileSync(path.join(dir, `${quoteId}.json`), JSON.stringify(data, null, 2), { mode: 0o600 });
203
231
  cleanupQuotes();
@@ -336,22 +364,8 @@ export function signEvmTransaction(txData, privateKeyHex, chain, nonce) {
336
364
  * @returns {Promise<number>} Nonce
337
365
  */
338
366
  export async function getEvmNonce(chain, address) {
339
- const rpcUrl = EVM_RPC_URLS[chain];
340
- if (!rpcUrl) throw new Error(`No RPC URL configured for chain: ${chain}`);
341
-
342
- const res = await fetch(rpcUrl, {
343
- method: 'POST',
344
- headers: { 'Content-Type': 'application/json' },
345
- body: JSON.stringify({
346
- jsonrpc: '2.0',
347
- id: 1,
348
- method: 'eth_getTransactionCount',
349
- params: [address, 'pending'],
350
- }),
351
- });
352
- const body = await res.json();
353
- if (body.error) throw new Error(`RPC error: ${body.error.message}`);
354
- return parseInt(body.result, 16);
367
+ const result = await evmRpcCall(chain, 'eth_getTransactionCount', [address, 'pending']);
368
+ return parseInt(result, 16);
355
369
  }
356
370
 
357
371
  /**
@@ -365,28 +379,21 @@ export async function getEvmNonce(chain, address) {
365
379
  * @returns {Promise<object>} Transaction receipt
366
380
  */
367
381
  export async function waitForReceipt(chain, txHash, timeoutMs = 30000, pollMs = 2000) {
368
- const rpcUrl = EVM_RPC_URLS[chain];
369
- if (!rpcUrl) throw new Error(`No RPC URL configured for chain: ${chain}`);
370
-
371
382
  const start = Date.now();
372
383
  while (Date.now() - start < timeoutMs) {
373
- const res = await fetch(rpcUrl, {
374
- method: 'POST',
375
- headers: { 'Content-Type': 'application/json' },
376
- body: JSON.stringify({
377
- jsonrpc: '2.0',
378
- id: 1,
379
- method: 'eth_getTransactionReceipt',
380
- params: [txHash],
381
- }),
382
- });
383
- const body = await res.json();
384
- if (body.result) {
385
- const status = parseInt(body.result.status, 16);
386
- if (status !== 1) {
387
- throw new Error(`Transaction reverted on-chain (status: ${body.result.status}). Tx: ${txHash}`);
384
+ try {
385
+ const receipt = await evmRpcCall(chain, 'eth_getTransactionReceipt', [txHash]);
386
+ if (receipt) {
387
+ const status = parseInt(receipt.status, 16);
388
+ if (status !== 1) {
389
+ throw new Error(`Transaction reverted on-chain (status: ${receipt.status}). Tx: ${txHash}`);
390
+ }
391
+ return receipt;
388
392
  }
389
- return body.result;
393
+ } catch (e) {
394
+ // Re-throw confirmed on-chain reverts immediately; swallow transient RPC/network errors
395
+ if (e.message?.startsWith('Transaction reverted')) throw e;
396
+ // else: continue polling (pending tx, transient network error, etc.)
390
397
  }
391
398
  // Receipt not yet available — wait and retry
392
399
  await new Promise(r => setTimeout(r, pollMs));
@@ -399,30 +406,21 @@ export async function waitForReceipt(chain, txHash, timeoutMs = 30000, pollMs =
399
406
  * Returns { success: true } or { success: false, reason: string }.
400
407
  */
401
408
  export async function simulateEvmCall(chain, { from, to, data, value, gas }) {
402
- const rpcUrl = EVM_RPC_URLS[chain];
403
- if (!rpcUrl) return { success: true }; // Can't simulate, skip
409
+ if (!EVM_RPC_URLS[chain]) return { success: true }; // Can't simulate, skip
404
410
 
405
411
  try {
406
412
  const callObj = { from, to, data, value: value || '0x0' };
407
413
  if (gas) callObj.gas = gas; // Pass gas limit to catch under-gassed quotes
408
- const res = await fetch(rpcUrl, {
409
- method: 'POST',
410
- headers: { 'Content-Type': 'application/json' },
411
- body: JSON.stringify({
412
- jsonrpc: '2.0',
413
- id: 1,
414
- method: 'eth_call',
415
- params: [callObj, 'latest'],
416
- }),
417
- });
418
- const body = await res.json();
419
- if (body.error) {
420
- const reason = body.error.message || 'unknown';
421
- return { success: false, reason };
414
+ await evmRpcCall(chain, 'eth_call', [callObj, 'latest']);
415
+ return { success: true };
416
+ } catch (e) {
417
+ const msg = e.message || 'unknown';
418
+ // Only block on actual contract-level revert errors from the RPC
419
+ if (msg.startsWith('RPC error (eth_call):')) {
420
+ return { success: false, reason: msg.replace(/^RPC error \(eth_call\): /, '') };
422
421
  }
422
+ // Network/infrastructure errors (fetch failure, rate limit, non-JSON response) → non-blocking
423
423
  return { success: true };
424
- } catch {
425
- return { success: true }; // Network error — don't block, let broadcast decide
426
424
  }
427
425
  }
428
426
 
@@ -431,23 +429,11 @@ export async function simulateEvmCall(chain, { from, to, data, value, gas }) {
431
429
  * Used to fix under-gassed quotes from aggregators.
432
430
  */
433
431
  export async function estimateEvmGas(chain, { from, to, data, value }) {
434
- const rpcUrl = EVM_RPC_URLS[chain];
435
- if (!rpcUrl) return null;
432
+ if (!EVM_RPC_URLS[chain]) return null;
436
433
 
437
434
  try {
438
- const res = await fetch(rpcUrl, {
439
- method: 'POST',
440
- headers: { 'Content-Type': 'application/json' },
441
- body: JSON.stringify({
442
- jsonrpc: '2.0',
443
- id: 1,
444
- method: 'eth_estimateGas',
445
- params: [{ from, to, data, value: value || '0x0' }],
446
- }),
447
- });
448
- const body = await res.json();
449
- if (body.error) return null;
450
- return parseInt(body.result, 16);
435
+ const result = await evmRpcCall(chain, 'eth_estimateGas', [{ from, to, data, value: value || '0x0' }]);
436
+ return parseInt(result, 16);
451
437
  } catch {
452
438
  return null;
453
439
  }
@@ -458,27 +444,16 @@ export async function estimateEvmGas(chain, { from, to, data, value }) {
458
444
  * Returns the allowance as a BigInt, or 0n on failure.
459
445
  */
460
446
  export async function checkErc20Allowance(chain, tokenAddress, ownerAddress, spenderAddress) {
461
- const rpcUrl = EVM_RPC_URLS[chain];
462
- if (!rpcUrl) return 0n;
447
+ if (!EVM_RPC_URLS[chain]) return 0n;
463
448
 
464
449
  try {
465
450
  // allowance(address,address) selector = 0xdd62ed3e
466
451
  const data = '0xdd62ed3e'
467
452
  + ownerAddress.slice(2).toLowerCase().padStart(64, '0')
468
453
  + spenderAddress.slice(2).toLowerCase().padStart(64, '0');
469
- const res = await fetch(rpcUrl, {
470
- method: 'POST',
471
- headers: { 'Content-Type': 'application/json' },
472
- body: JSON.stringify({
473
- jsonrpc: '2.0',
474
- id: 1,
475
- method: 'eth_call',
476
- params: [{ to: tokenAddress, data }, 'latest'],
477
- }),
478
- });
479
- const body = await res.json();
480
- if (body.error || !body.result) return 0n;
481
- return BigInt(body.result);
454
+ const result = await evmRpcCall(chain, 'eth_call', [{ to: tokenAddress, data }, 'latest']);
455
+ if (!result) return 0n;
456
+ return BigInt(result);
482
457
  } catch {
483
458
  return 0n;
484
459
  }
@@ -800,6 +775,8 @@ EXAMPLES:
800
775
  const isWalletConnect = walletName === 'walletconnect' || walletName === 'wc';
801
776
 
802
777
  let walletAddress;
778
+ let walletProvider = 'local';
779
+ let privyWalletIds = null;
803
780
  if (isWalletConnect) {
804
781
  if (chainType !== 'evm') {
805
782
  log('WalletConnect is only supported for EVM chains');
@@ -815,9 +792,21 @@ EXAMPLES:
815
792
  } else if (walletName) {
816
793
  const wallet = showWallet(walletName);
817
794
  walletAddress = chainType === 'solana' ? wallet.solana : wallet.evm;
795
+ if (wallet.provider === 'privy') {
796
+ walletProvider = 'privy';
797
+ privyWalletIds = wallet.privyWalletIds;
798
+ }
818
799
  } else {
819
800
  try {
820
- walletAddress = getDefaultAddress(chainType);
801
+ const config = getWalletConfig();
802
+ if (config.defaultWallet) {
803
+ const wallet = showWallet(config.defaultWallet);
804
+ walletAddress = chainType === 'solana' ? wallet.solana : wallet.evm;
805
+ if (wallet.provider === 'privy') {
806
+ walletProvider = 'privy';
807
+ privyWalletIds = wallet.privyWalletIds;
808
+ }
809
+ }
821
810
  } catch {
822
811
  // No wallet configured — fall through to the check below
823
812
  }
@@ -861,7 +850,8 @@ EXAMPLES:
861
850
  log('');
862
851
  response.quotes.forEach((q, i) => log(formatQuote(q, i)));
863
852
 
864
- const quoteId = saveQuote(response, chain, isWalletConnect ? 'walletconnect' : 'local');
853
+ const signerType = isWalletConnect ? 'walletconnect' : walletProvider;
854
+ const quoteId = saveQuote(response, chain, signerType, privyWalletIds);
865
855
  log(`\n Quote ID: ${quoteId}`);
866
856
  log(` Execute: nansen trade execute --quote ${quoteId}`);
867
857
  if (response.quotes.length > 1) {
@@ -935,12 +925,18 @@ EXAMPLES:
935
925
  return;
936
926
  }
937
927
 
938
- // Determine if this is a WalletConnect-signed quote
928
+ // Determine if this is a WalletConnect or Privy-signed quote
939
929
  const isWalletConnect = quoteData.signerType === 'walletconnect'
940
930
  || walletName === 'walletconnect' || walletName === 'wc';
931
+ const isPrivy = quoteData.signerType === 'privy';
941
932
 
942
933
  let exported = null;
943
- if (!isWalletConnect) {
934
+ let privyClient = null;
935
+ if (isPrivy) {
936
+ // Privy signing -- import + instantiate once for all quotes
937
+ const { PrivyClient } = await import('./privy.js');
938
+ privyClient = new PrivyClient(process.env.PRIVY_APP_ID, process.env.PRIVY_APP_SECRET);
939
+ } else if (!isWalletConnect) {
944
940
  // Get wallet credentials once (before the loop)
945
941
  const walletConfig = getWalletConfig();
946
942
  let password = null;
@@ -1021,7 +1017,159 @@ EXAMPLES:
1021
1017
  let signedTransaction;
1022
1018
  let requestId;
1023
1019
 
1024
- if (chainType === 'solana') {
1020
+ if (chainType === 'solana' && isPrivy) {
1021
+ // Solana via Privy: sign the serialized transaction
1022
+ let txBase64 = currentQuote.transaction;
1023
+ if (typeof txBase64 === 'object' && txBase64.data) {
1024
+ txBase64 = base58Decode(txBase64.data).toString('base64');
1025
+ }
1026
+ log(' Signing Solana transaction via Privy...');
1027
+ const solWalletId = quoteData.privyWalletIds?.solana;
1028
+ if (!solWalletId) throw new Error('No Solana Privy wallet ID in quote');
1029
+ const signResult = await privyClient.signSolanaTransaction(solWalletId, txBase64);
1030
+ signedTransaction = signResult.data?.signed_transaction || signResult.signed_transaction;
1031
+ requestId = currentQuote.metadata?.requestId;
1032
+
1033
+ } else if (chainType === 'evm' && isPrivy) {
1034
+ // EVM via Privy: sign-only, then broadcast via Trading API
1035
+ const evmWalletId = quoteData.privyWalletIds?.evm;
1036
+ if (!evmWalletId) throw new Error('No EVM Privy wallet ID in quote');
1037
+
1038
+ const walletResult = await privyClient.getWallet(evmWalletId);
1039
+ const walletAddress = walletResult.address;
1040
+
1041
+ // Validate transaction.value (same checks as local wallet)
1042
+ const isNative = isNativeToken(currentQuote.inputMint);
1043
+ const txValue = BigInt(currentQuote.transaction.value || '0');
1044
+ if (isNative) {
1045
+ const expectedValue = BigInt(currentQuote.inAmount || currentQuote.inputAmount || '0');
1046
+ if (txValue !== expectedValue) {
1047
+ log(` ❌ Transaction value mismatch for ${quoteName}: tx.value=${txValue}, expected=${expectedValue}`);
1048
+ if (qi + 1 < endIndex) log(` Trying next quote...`);
1049
+ lastQuoteError = `${quoteName} transaction value mismatch`;
1050
+ continue;
1051
+ }
1052
+ } else {
1053
+ if (txValue > 0n) {
1054
+ log(` ❌ ERC-20 swap has non-zero tx.value (${txValue}) for ${quoteName} — aborting`);
1055
+ if (qi + 1 < endIndex) log(` Trying next quote...`);
1056
+ lastQuoteError = `${quoteName} unexpected tx.value`;
1057
+ continue;
1058
+ }
1059
+ }
1060
+
1061
+ // Handle approval if needed
1062
+ if (currentQuote.approvalAddress && !isNative) {
1063
+ const inputAmount = BigInt(currentQuote.inputAmount || currentQuote.inAmount || '0');
1064
+ const existingAllowance = await checkErc20Allowance(
1065
+ chain, currentQuote.inputMint, walletAddress, currentQuote.approvalAddress
1066
+ );
1067
+
1068
+ if (existingAllowance >= inputAmount && existingAllowance > 0n) {
1069
+ log(` ✓ Sufficient allowance exists for ${quoteName}, skipping approval`);
1070
+ } else {
1071
+ log(` ⚠ Approval required → ${currentQuote.approvalAddress}`);
1072
+ const approvalNonce = await getEvmNonce(chain, walletAddress);
1073
+ const MAX_UINT256 = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
1074
+ const approvalData = '0x095ea7b3'
1075
+ + currentQuote.approvalAddress.slice(2).toLowerCase().padStart(64, '0')
1076
+ + MAX_UINT256;
1077
+ const approvalMaxFee = currentQuote.transaction?.maxFeePerGas || currentQuote.transaction?.gasPrice || '1000000';
1078
+ const approvalPriorityFee = currentQuote.transaction?.maxPriorityFeePerGas || '1000000';
1079
+ const approvalSignResult = await privyClient.signEvmTransaction(evmWalletId, {
1080
+ to: currentQuote.inputMint,
1081
+ data: approvalData,
1082
+ value: '0x0',
1083
+ chain_id: chainConfig.chainId,
1084
+ nonce: toHex(approvalNonce),
1085
+ gas_limit: toHex(100000),
1086
+ max_fee_per_gas: toHex(approvalMaxFee),
1087
+ max_priority_fee_per_gas: toHex(approvalPriorityFee),
1088
+ });
1089
+ const signedApproval = approvalSignResult.data?.signed_transaction || approvalSignResult.signed_transaction;
1090
+ const approvalResult = await executeTransaction({ signedTransaction: signedApproval, chain, simulate: !noSimulate });
1091
+ if (approvalResult.status !== 'Success') {
1092
+ log(` ❌ Approval failed for ${quoteName}: ${approvalResult.error || 'unknown'}`);
1093
+ if (qi + 1 < endIndex) log(` Trying next quote...`);
1094
+ lastQuoteError = `${quoteName} approval failed`;
1095
+ continue;
1096
+ }
1097
+ log(` Waiting for approval confirmation...`);
1098
+ try {
1099
+ const receipt = await waitForReceipt(chain, approvalResult.txHash);
1100
+ log(` ✓ Approval confirmed in block ${parseInt(receipt.blockNumber, 16)}: ${approvalResult.txHash}`);
1101
+ } catch (receiptErr) {
1102
+ log(` ❌ Approval may not have confirmed: ${receiptErr.message}`);
1103
+ if (qi + 1 < endIndex) log(` Trying next quote...`);
1104
+ lastQuoteError = `${quoteName} approval unconfirmed`;
1105
+ continue;
1106
+ }
1107
+ await new Promise(r => setTimeout(r, 2000));
1108
+ }
1109
+ }
1110
+
1111
+ // Pre-flight simulation
1112
+ if (!noSimulate) {
1113
+ const sim = await simulateEvmCall(chain, {
1114
+ from: walletAddress,
1115
+ to: currentQuote.transaction.to,
1116
+ data: currentQuote.transaction.data,
1117
+ value: currentQuote.transaction.value ? '0x' + BigInt(currentQuote.transaction.value).toString(16) : '0x0',
1118
+ });
1119
+ if (!sim.success) {
1120
+ log(` ⚠ Simulation failed for ${quoteName}: ${sim.reason}`);
1121
+ if (qi + 1 < endIndex) log(` Trying next quote...`);
1122
+ lastQuoteError = `${quoteName} simulation failed: ${sim.reason}`;
1123
+ continue;
1124
+ }
1125
+ }
1126
+
1127
+ // Gas resolution — fall back to eth_estimateGas if quote has no gas
1128
+ const txData = currentQuote.transaction;
1129
+ const apiGas = parseInt(currentQuote.gas || '0');
1130
+ const txGas = parseInt(txData.gas || txData.gasLimit || '0');
1131
+ let finalGas = apiGas > 0 ? apiGas : txGas;
1132
+ if (finalGas === 0) {
1133
+ try {
1134
+ const rpcUrl = EVM_RPC_URLS[chain];
1135
+ const estRes = await fetch(rpcUrl, {
1136
+ method: 'POST',
1137
+ headers: { 'Content-Type': 'application/json' },
1138
+ body: JSON.stringify({
1139
+ jsonrpc: '2.0', id: 1, method: 'eth_estimateGas',
1140
+ params: [{
1141
+ from: walletAddress, to: txData.to, data: txData.data || '0x',
1142
+ value: txData.value ? '0x' + BigInt(txData.value).toString(16) : '0x0',
1143
+ }],
1144
+ }),
1145
+ });
1146
+ const estBody = await estRes.json();
1147
+ if (estBody.result) finalGas = Math.ceil(parseInt(estBody.result, 16) * 1.5);
1148
+ } catch { /* ignore */ }
1149
+ if (finalGas === 0) finalGas = 210000;
1150
+ }
1151
+
1152
+ log(' Fetching nonce...');
1153
+ const nonce = await getEvmNonce(chain, walletAddress);
1154
+
1155
+ // Privy signs EIP-1559 (type 2) transactions, so convert gasPrice to EIP-1559 fields
1156
+ const maxFee = txData.maxFeePerGas || txData.gasPrice || '1000000';
1157
+ const priorityFee = txData.maxPriorityFeePerGas || '1000000';
1158
+
1159
+ log(' Signing EVM transaction via Privy...');
1160
+ const signResult = await privyClient.signEvmTransaction(evmWalletId, {
1161
+ to: txData.to,
1162
+ data: txData.data || '0x',
1163
+ value: txData.value ? '0x' + BigInt(txData.value).toString(16) : '0x0',
1164
+ chain_id: chainConfig.chainId,
1165
+ nonce: toHex(nonce),
1166
+ gas_limit: toHex(finalGas),
1167
+ max_fee_per_gas: toHex(maxFee),
1168
+ max_priority_fee_per_gas: toHex(priorityFee),
1169
+ });
1170
+ signedTransaction = signResult.data?.signed_transaction || signResult.signed_transaction;
1171
+
1172
+ } else if (chainType === 'solana') {
1025
1173
  // Solana: transaction is either a base64 string (Jupiter) or an object
1026
1174
  // with a base58-encoded `data` field (OKX). Normalize to base64.
1027
1175
  let txBase64 = currentQuote.transaction;