nansen-cli 1.10.0 → 1.11.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 +34 -0
- package/README.md +2 -2
- package/package.json +3 -2
- package/src/api.js +13 -9
- package/src/cli.js +31 -164
- package/src/index.js +4 -4
- package/src/schema.json +1684 -0
- package/src/trading.js +121 -134
- package/src/wallet.js +32 -39
package/src/trading.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Nansen CLI - Trading Commands
|
|
3
3
|
* Quote and execute DEX swaps via the Nansen Trading API.
|
|
4
|
-
* Supports Solana and
|
|
4
|
+
* Supports Solana and Base.
|
|
5
5
|
* Zero external dependencies — uses Node.js built-in crypto only.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -19,21 +19,17 @@ const TRADING_API_URL = process.env.NANSEN_TRADING_API_URL || 'https://trading-a
|
|
|
19
19
|
|
|
20
20
|
const CHAIN_MAP = {
|
|
21
21
|
solana: { index: '501', type: 'solana', chainId: 501, name: 'Solana', explorer: 'https://solscan.io/tx/' },
|
|
22
|
-
ethereum: { index: '1', type: 'evm', chainId: 1, name: 'Ethereum', explorer: 'https://etherscan.io/tx/' },
|
|
23
22
|
base: { index: '8453', type: 'evm', chainId: 8453, name: 'Base', explorer: 'https://basescan.org/tx/' },
|
|
24
|
-
bsc: { index: '56', type: 'evm', chainId: 56, name: 'BSC', explorer: 'https://bscscan.com/tx/' },
|
|
25
23
|
};
|
|
26
24
|
|
|
27
25
|
// Extend when adding new EVM chains (e.g. arbitrum WETH, polygon WMATIC)
|
|
28
26
|
const WRAPPED_NATIVE_TOKENS = {
|
|
29
|
-
ethereum: { address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', symbol: 'WETH', nativeSymbol: 'ETH' },
|
|
30
27
|
base: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH', nativeSymbol: 'ETH' },
|
|
31
|
-
bsc: { address: '0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c', symbol: 'WBNB', nativeSymbol: 'BNB' },
|
|
32
28
|
};
|
|
33
29
|
|
|
34
30
|
// Common token symbol → address lookup per chain.
|
|
35
31
|
// Native sentinels: Solana uses native mint, EVM uses 0xeee…eee.
|
|
36
|
-
// Wrapped-native addresses (WETH
|
|
32
|
+
// Wrapped-native addresses (WETH) are derived from WRAPPED_NATIVE_TOKENS
|
|
37
33
|
// to avoid duplication — keep that map as the single source of truth.
|
|
38
34
|
const EVM_NATIVE = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
|
|
39
35
|
const TOKEN_SYMBOLS = {
|
|
@@ -43,12 +39,6 @@ const TOKEN_SYMBOLS = {
|
|
|
43
39
|
USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
44
40
|
USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
|
|
45
41
|
},
|
|
46
|
-
ethereum: {
|
|
47
|
-
ETH: EVM_NATIVE,
|
|
48
|
-
WETH: WRAPPED_NATIVE_TOKENS.ethereum.address,
|
|
49
|
-
USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
|
50
|
-
USDT: '0xdac17f958d2ee523a2206206994597c13d831ec7',
|
|
51
|
-
},
|
|
52
42
|
base: {
|
|
53
43
|
ETH: EVM_NATIVE,
|
|
54
44
|
WETH: WRAPPED_NATIVE_TOKENS.base.address,
|
|
@@ -57,12 +47,6 @@ const TOKEN_SYMBOLS = {
|
|
|
57
47
|
// (like Circle did with USDC), this address will need updating.
|
|
58
48
|
USDT: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2',
|
|
59
49
|
},
|
|
60
|
-
bsc: {
|
|
61
|
-
BNB: EVM_NATIVE,
|
|
62
|
-
WBNB: WRAPPED_NATIVE_TOKENS.bsc.address,
|
|
63
|
-
USDC: '0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d',
|
|
64
|
-
USDT: '0x55d398326f99059ff775485246999027b3197955',
|
|
65
|
-
},
|
|
66
50
|
};
|
|
67
51
|
|
|
68
52
|
/**
|
|
@@ -80,9 +64,7 @@ export function resolveTokenAddress(symbolOrAddress, chainName) {
|
|
|
80
64
|
|
|
81
65
|
// Default public RPC endpoints (used for nonce fetching)
|
|
82
66
|
const EVM_RPC_URLS = {
|
|
83
|
-
ethereum: process.env.NANSEN_RPC_ETHEREUM || 'https://eth.llamarpc.com',
|
|
84
67
|
base: process.env.NANSEN_RPC_BASE || 'https://mainnet.base.org',
|
|
85
|
-
bsc: process.env.NANSEN_RPC_BSC || 'https://bsc-dataseed.binance.org',
|
|
86
68
|
};
|
|
87
69
|
|
|
88
70
|
function getQuotesDir() {
|
|
@@ -321,7 +303,7 @@ export function signSolanaTransaction(transactionBase64, privateKeyHex) {
|
|
|
321
303
|
*
|
|
322
304
|
* @param {object} txData - Transaction fields from quote.transaction { to, data, value, gas, gasPrice }
|
|
323
305
|
* @param {string} privateKeyHex - 64-char hex (32-byte secp256k1 private key)
|
|
324
|
-
* @param {string} chain - Chain name
|
|
306
|
+
* @param {string} chain - Chain name
|
|
325
307
|
* @param {number} nonce - Account nonce
|
|
326
308
|
* @returns {string} 0x-prefixed signed transaction hex
|
|
327
309
|
*/
|
|
@@ -774,7 +756,7 @@ export function formatQuote(quote, index) {
|
|
|
774
756
|
* Build trading command handlers for CLI integration.
|
|
775
757
|
*/
|
|
776
758
|
export function buildTradingCommands(deps = {}) {
|
|
777
|
-
const {
|
|
759
|
+
const { log = console.log, exit = process.exit } = deps;
|
|
778
760
|
|
|
779
761
|
return {
|
|
780
762
|
'quote': async (args, apiInstance, flags, options) => {
|
|
@@ -791,15 +773,20 @@ export function buildTradingCommands(deps = {}) {
|
|
|
791
773
|
const swapMode = options['swap-mode'] || 'exactIn';
|
|
792
774
|
|
|
793
775
|
if (!chain || !from || !to || !amount) {
|
|
794
|
-
|
|
776
|
+
log(`
|
|
795
777
|
Usage: nansen trade quote --chain <chain> --from <token> --to <token> --amount <baseUnits>
|
|
796
778
|
|
|
779
|
+
PREREQUISITE:
|
|
780
|
+
A wallet must be configured before using this command (the trading API builds
|
|
781
|
+
a transaction specific to your sender address).
|
|
782
|
+
Set one up with: nansen wallet create
|
|
783
|
+
|
|
797
784
|
OPTIONS:
|
|
798
|
-
--chain <chain> Chain: solana,
|
|
785
|
+
--chain <chain> Chain: solana, base
|
|
799
786
|
--from <symbol|address> Input token (symbol like SOL, USDC or address)
|
|
800
787
|
--to <symbol|address> Output token (symbol like USDC, ETH or address)
|
|
801
788
|
--amount <units> Amount in BASE UNITS (e.g. lamports, wei)
|
|
802
|
-
--wallet <name> Wallet name (default: default wallet)
|
|
789
|
+
--wallet <name> Wallet name (default: default wallet). Use "walletconnect" or "wc" for WalletConnect (EVM only).
|
|
803
790
|
--slippage <pct> Slippage as decimal (e.g. 0.03 for 3%). Default: 0.03
|
|
804
791
|
--auto-slippage Enable auto slippage calculation
|
|
805
792
|
--max-auto-slippage <pct> Max auto slippage when auto-slippage enabled
|
|
@@ -816,7 +803,7 @@ EXAMPLES:
|
|
|
816
803
|
|
|
817
804
|
const amountError = validateBaseUnitAmount(amount);
|
|
818
805
|
if (amountError) {
|
|
819
|
-
|
|
806
|
+
log(`Error: ${amountError}`);
|
|
820
807
|
exit(1);
|
|
821
808
|
return;
|
|
822
809
|
}
|
|
@@ -830,13 +817,13 @@ EXAMPLES:
|
|
|
830
817
|
let walletAddress;
|
|
831
818
|
if (isWalletConnect) {
|
|
832
819
|
if (chainType !== 'evm') {
|
|
833
|
-
|
|
820
|
+
log('WalletConnect is only supported for EVM chains');
|
|
834
821
|
exit(1);
|
|
835
822
|
return;
|
|
836
823
|
}
|
|
837
824
|
walletAddress = await getWalletConnectAddress();
|
|
838
825
|
if (!walletAddress) {
|
|
839
|
-
|
|
826
|
+
log('No WalletConnect session active. Run: walletconnect connect');
|
|
840
827
|
exit(1);
|
|
841
828
|
return;
|
|
842
829
|
}
|
|
@@ -852,16 +839,16 @@ EXAMPLES:
|
|
|
852
839
|
}
|
|
853
840
|
|
|
854
841
|
if (!walletAddress) {
|
|
855
|
-
|
|
842
|
+
log('No wallet found. A wallet address is required for quotes because the trading API builds a transaction specific to the sender.\nCreate one with: nansen wallet create');
|
|
856
843
|
exit(1);
|
|
857
844
|
return;
|
|
858
845
|
}
|
|
859
846
|
|
|
860
|
-
|
|
861
|
-
|
|
847
|
+
log(`\nFetching quote on ${chainConfig.name}...`);
|
|
848
|
+
log(` Wallet: ${walletAddress}`);
|
|
862
849
|
|
|
863
850
|
const fromWarning = getWrappedNativeFromWarning(from, chain);
|
|
864
|
-
if (fromWarning)
|
|
851
|
+
if (fromWarning) log(` ${fromWarning}`);
|
|
865
852
|
|
|
866
853
|
const params = {
|
|
867
854
|
chainIndex: chainConfig.index,
|
|
@@ -878,30 +865,30 @@ EXAMPLES:
|
|
|
878
865
|
const response = await getQuote(params);
|
|
879
866
|
|
|
880
867
|
if (!response.success || !response.quotes?.length) {
|
|
881
|
-
|
|
868
|
+
log('No quotes available');
|
|
882
869
|
if (response.warnings?.length) {
|
|
883
|
-
response.warnings.forEach(w =>
|
|
870
|
+
response.warnings.forEach(w => log(` Warning: ${w}`));
|
|
884
871
|
}
|
|
885
872
|
exit(1);
|
|
886
873
|
return;
|
|
887
874
|
}
|
|
888
875
|
|
|
889
|
-
|
|
890
|
-
response.quotes.forEach((q, i) =>
|
|
876
|
+
log('');
|
|
877
|
+
response.quotes.forEach((q, i) => log(formatQuote(q, i)));
|
|
891
878
|
|
|
892
879
|
const quoteId = saveQuote(response, chain, isWalletConnect ? 'walletconnect' : 'local');
|
|
893
|
-
|
|
894
|
-
|
|
880
|
+
log(`\n Quote ID: ${quoteId}`);
|
|
881
|
+
log(` Execute: nansen trade execute --quote ${quoteId}`);
|
|
895
882
|
if (response.quotes.length > 1) {
|
|
896
|
-
|
|
883
|
+
log(` Pin #1: nansen trade execute --quote ${quoteId} --quote-index 0`);
|
|
897
884
|
}
|
|
898
885
|
|
|
899
886
|
if (response.quotes[0]?.approvalAddress && !isNativeToken(response.quotes[0]?.inputMint)) {
|
|
900
|
-
|
|
901
|
-
|
|
887
|
+
log(`\n Warning: This token swap requires an ERC-20 approval step.`);
|
|
888
|
+
log(` The execute command will handle this automatically.`);
|
|
902
889
|
}
|
|
903
890
|
|
|
904
|
-
|
|
891
|
+
log('');
|
|
905
892
|
return undefined; // Output already printed above
|
|
906
893
|
|
|
907
894
|
} catch (err) {
|
|
@@ -909,8 +896,8 @@ EXAMPLES:
|
|
|
909
896
|
if (err.code === 'INVALID_AMOUNT' || /amount/i.test(err.message)) {
|
|
910
897
|
message += '. Amounts must be in base units (e.g., 1000000000 lamports for 1 SOL, 1000000000000000000 wei for 1 ETH)';
|
|
911
898
|
}
|
|
912
|
-
|
|
913
|
-
if (err.details)
|
|
899
|
+
log(`Error: ${message}`);
|
|
900
|
+
if (err.details) log(` Details: ${JSON.stringify(err.details)}`);
|
|
914
901
|
exit(1);
|
|
915
902
|
}
|
|
916
903
|
},
|
|
@@ -921,7 +908,7 @@ EXAMPLES:
|
|
|
921
908
|
const noSimulate = flags['no-simulate'] || flags.noSimulate;
|
|
922
909
|
|
|
923
910
|
if (!quoteId) {
|
|
924
|
-
|
|
911
|
+
log(`
|
|
925
912
|
Usage: nansen trade execute --quote <quoteId> [options]
|
|
926
913
|
|
|
927
914
|
OPTIONS:
|
|
@@ -944,7 +931,7 @@ EXAMPLES:
|
|
|
944
931
|
|
|
945
932
|
const allQuotes = quoteData.response.quotes || [];
|
|
946
933
|
if (!allQuotes.length) {
|
|
947
|
-
|
|
934
|
+
log('❌ No quote data found');
|
|
948
935
|
exit(1);
|
|
949
936
|
return;
|
|
950
937
|
}
|
|
@@ -957,8 +944,8 @@ EXAMPLES:
|
|
|
957
944
|
// Check if any quote in range has transaction data before prompting for password
|
|
958
945
|
const hasAnyTransaction = allQuotes.slice(startIndex, endIndex).some(q => q?.transaction);
|
|
959
946
|
if (!hasAnyTransaction) {
|
|
960
|
-
|
|
961
|
-
|
|
947
|
+
log('❌ No quotes contain transaction data.');
|
|
948
|
+
log(' Ensure userWalletAddress was provided when fetching the quote.');
|
|
962
949
|
exit(1);
|
|
963
950
|
return;
|
|
964
951
|
}
|
|
@@ -978,7 +965,7 @@ EXAMPLES:
|
|
|
978
965
|
effectiveWalletName = list.defaultWallet;
|
|
979
966
|
}
|
|
980
967
|
if (!effectiveWalletName) {
|
|
981
|
-
|
|
968
|
+
log('No wallet found. Create one with: nansen wallet create');
|
|
982
969
|
exit(1);
|
|
983
970
|
return;
|
|
984
971
|
}
|
|
@@ -987,13 +974,13 @@ EXAMPLES:
|
|
|
987
974
|
} else {
|
|
988
975
|
// Verify WalletConnect session is still active and address matches quote
|
|
989
976
|
if (chainType !== 'evm') {
|
|
990
|
-
|
|
977
|
+
log('WalletConnect is only supported for EVM chains');
|
|
991
978
|
exit(1);
|
|
992
979
|
return;
|
|
993
980
|
}
|
|
994
981
|
const wcAddress = await getWalletConnectAddress();
|
|
995
982
|
if (!wcAddress) {
|
|
996
|
-
|
|
983
|
+
log('No WalletConnect session active. Run: walletconnect connect');
|
|
997
984
|
exit(1);
|
|
998
985
|
return;
|
|
999
986
|
}
|
|
@@ -1001,7 +988,7 @@ EXAMPLES:
|
|
|
1001
988
|
const quoteWallet = quoteData.response?.quotes?.[0]?.transaction?.from
|
|
1002
989
|
|| quoteData.response?.metadata?.userWalletAddress;
|
|
1003
990
|
if (quoteWallet && wcAddress.toLowerCase() !== quoteWallet.toLowerCase()) {
|
|
1004
|
-
|
|
991
|
+
log(`Connected wallet (${wcAddress}) doesn't match quote. Get a new quote with --wallet walletconnect`);
|
|
1005
992
|
exit(1);
|
|
1006
993
|
return;
|
|
1007
994
|
}
|
|
@@ -1017,17 +1004,17 @@ EXAMPLES:
|
|
|
1017
1004
|
|
|
1018
1005
|
// Verify transaction data exists
|
|
1019
1006
|
if (!currentQuote.transaction) {
|
|
1020
|
-
|
|
1007
|
+
log(` ⚠ Quote ${quoteName}: no transaction data, skipping...`);
|
|
1021
1008
|
lastQuoteError = `Quote ${quoteName} has no transaction data`;
|
|
1022
1009
|
continue;
|
|
1023
1010
|
}
|
|
1024
1011
|
|
|
1025
|
-
|
|
1012
|
+
log(`\nExecuting trade on ${chainConfig.name}...`);
|
|
1026
1013
|
if (endIndex - startIndex > 1) {
|
|
1027
|
-
|
|
1014
|
+
log(` Trying quote ${qi + 1}/${allQuotes.length} (${quoteName})...`);
|
|
1028
1015
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1016
|
+
log(formatQuote(currentQuote));
|
|
1017
|
+
log('');
|
|
1031
1018
|
|
|
1032
1019
|
try {
|
|
1033
1020
|
let signedTransaction;
|
|
@@ -1040,7 +1027,7 @@ EXAMPLES:
|
|
|
1040
1027
|
if (typeof txBase64 === 'object' && txBase64.data) {
|
|
1041
1028
|
txBase64 = base58Decode(txBase64.data).toString('base64');
|
|
1042
1029
|
}
|
|
1043
|
-
|
|
1030
|
+
log(' Signing Solana transaction...');
|
|
1044
1031
|
signedTransaction = signSolanaTransaction(txBase64, exported.solana.privateKey);
|
|
1045
1032
|
requestId = currentQuote.metadata?.requestId;
|
|
1046
1033
|
|
|
@@ -1054,15 +1041,15 @@ EXAMPLES:
|
|
|
1054
1041
|
if (isNative) {
|
|
1055
1042
|
const expectedValue = BigInt(currentQuote.inAmount || currentQuote.inputAmount || '0');
|
|
1056
1043
|
if (txValue !== expectedValue) {
|
|
1057
|
-
|
|
1058
|
-
if (qi + 1 < endIndex)
|
|
1044
|
+
log(` ❌ Transaction value mismatch for ${quoteName}: tx.value=${txValue}, expected=${expectedValue}`);
|
|
1045
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1059
1046
|
lastQuoteError = `${quoteName} transaction value mismatch`;
|
|
1060
1047
|
continue;
|
|
1061
1048
|
}
|
|
1062
1049
|
} else {
|
|
1063
1050
|
if (txValue > 0n) {
|
|
1064
|
-
|
|
1065
|
-
if (qi + 1 < endIndex)
|
|
1051
|
+
log(` ❌ ERC-20 swap has non-zero tx.value (${txValue}) for ${quoteName} — aborting`);
|
|
1052
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1066
1053
|
lastQuoteError = `${quoteName} unexpected tx.value`;
|
|
1067
1054
|
continue;
|
|
1068
1055
|
}
|
|
@@ -1076,10 +1063,10 @@ EXAMPLES:
|
|
|
1076
1063
|
);
|
|
1077
1064
|
|
|
1078
1065
|
if (existingAllowance >= inputAmount && existingAllowance > 0n) {
|
|
1079
|
-
|
|
1066
|
+
log(` ✓ Sufficient allowance exists for ${quoteName}, skipping approval`);
|
|
1080
1067
|
} else {
|
|
1081
|
-
|
|
1082
|
-
|
|
1068
|
+
log(` ⚠ Approval required → ${currentQuote.approvalAddress}`);
|
|
1069
|
+
log(` Sending approval via WalletConnect...`);
|
|
1083
1070
|
try {
|
|
1084
1071
|
const approvalResult = await sendApprovalViaWalletConnect(
|
|
1085
1072
|
currentQuote.inputMint,
|
|
@@ -1089,7 +1076,7 @@ EXAMPLES:
|
|
|
1089
1076
|
let approvalTxHash = approvalResult.txHash;
|
|
1090
1077
|
if (!approvalTxHash && approvalResult.signedTransaction) {
|
|
1091
1078
|
// Wallet returned a signed tx instead of broadcasting — broadcast via Trading API
|
|
1092
|
-
|
|
1079
|
+
log(` Broadcasting approval via Trading API...`);
|
|
1093
1080
|
const broadcastResult = await executeTransaction({
|
|
1094
1081
|
signedTransaction: approvalResult.signedTransaction,
|
|
1095
1082
|
chain,
|
|
@@ -1101,18 +1088,18 @@ EXAMPLES:
|
|
|
1101
1088
|
approvalTxHash = broadcastResult.txHash;
|
|
1102
1089
|
}
|
|
1103
1090
|
if (approvalTxHash) {
|
|
1104
|
-
|
|
1091
|
+
log(` Waiting for approval confirmation...`);
|
|
1105
1092
|
const receipt = await waitForReceipt(chain, approvalTxHash);
|
|
1106
|
-
|
|
1093
|
+
log(` ✓ Approval confirmed in block ${parseInt(receipt.blockNumber, 16)}: ${approvalTxHash}`);
|
|
1107
1094
|
}
|
|
1108
1095
|
} catch (approvalErr) {
|
|
1109
|
-
|
|
1110
|
-
if (qi + 1 < endIndex)
|
|
1096
|
+
log(` ❌ Approval failed for ${quoteName}: ${approvalErr.message}`);
|
|
1097
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1111
1098
|
lastQuoteError = `${quoteName} approval failed`;
|
|
1112
1099
|
continue;
|
|
1113
1100
|
}
|
|
1114
1101
|
await new Promise(r => setTimeout(r, 2000));
|
|
1115
|
-
|
|
1102
|
+
log('');
|
|
1116
1103
|
}
|
|
1117
1104
|
}
|
|
1118
1105
|
|
|
@@ -1126,8 +1113,8 @@ EXAMPLES:
|
|
|
1126
1113
|
value: txData.value ? '0x' + BigInt(txData.value).toString(16) : '0x0',
|
|
1127
1114
|
});
|
|
1128
1115
|
if (!sim.success) {
|
|
1129
|
-
|
|
1130
|
-
if (qi + 1 < endIndex)
|
|
1116
|
+
log(` ⚠ Simulation failed for ${quoteName}: ${sim.reason}`);
|
|
1117
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1131
1118
|
lastQuoteError = `${quoteName} simulation failed: ${sim.reason}`;
|
|
1132
1119
|
continue;
|
|
1133
1120
|
}
|
|
@@ -1140,7 +1127,7 @@ EXAMPLES:
|
|
|
1140
1127
|
const finalGas = apiGas > 0 ? apiGas : txGas;
|
|
1141
1128
|
|
|
1142
1129
|
// Send transaction via WalletConnect
|
|
1143
|
-
|
|
1130
|
+
log(' Sending transaction via WalletConnect...');
|
|
1144
1131
|
let wcResult;
|
|
1145
1132
|
try {
|
|
1146
1133
|
wcResult = await sendTransactionViaWalletConnect({
|
|
@@ -1151,24 +1138,24 @@ EXAMPLES:
|
|
|
1151
1138
|
chainId: chainConfig.chainId,
|
|
1152
1139
|
});
|
|
1153
1140
|
} catch (wcErr) {
|
|
1154
|
-
|
|
1155
|
-
if (qi + 1 < endIndex)
|
|
1141
|
+
log(` ❌ WalletConnect transaction failed for ${quoteName}: ${wcErr.message}`);
|
|
1142
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1156
1143
|
lastQuoteError = `${quoteName}: ${wcErr.message}`;
|
|
1157
1144
|
continue;
|
|
1158
1145
|
}
|
|
1159
1146
|
|
|
1160
1147
|
if (wcResult.txHash) {
|
|
1161
1148
|
// Wallet broadcast — verify on-chain
|
|
1162
|
-
|
|
1149
|
+
log(' Verifying on-chain status...');
|
|
1163
1150
|
try {
|
|
1164
1151
|
await waitForReceipt(chain, wcResult.txHash);
|
|
1165
1152
|
} catch (receiptErr) {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1153
|
+
log(`\n ⚠ Transaction was broadcast but REVERTED on-chain!`);
|
|
1154
|
+
log(` Tx Hash: ${wcResult.txHash}`);
|
|
1155
|
+
log(` Explorer: ${chainConfig.explorer}${wcResult.txHash}`);
|
|
1156
|
+
log(` Error: ${receiptErr.message}`);
|
|
1170
1157
|
if (qi + 1 < endIndex) {
|
|
1171
|
-
|
|
1158
|
+
log(` Trying next quote...`);
|
|
1172
1159
|
lastQuoteError = `${quoteName} reverted on-chain`;
|
|
1173
1160
|
continue;
|
|
1174
1161
|
}
|
|
@@ -1176,11 +1163,11 @@ EXAMPLES:
|
|
|
1176
1163
|
return;
|
|
1177
1164
|
}
|
|
1178
1165
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1166
|
+
log(`\n ✓ Transaction successful!`);
|
|
1167
|
+
log(` Tx Hash: ${wcResult.txHash}`);
|
|
1168
|
+
log(` Chain: ${chainConfig.name}`);
|
|
1169
|
+
log(` Explorer: ${chainConfig.explorer}${wcResult.txHash}`);
|
|
1170
|
+
log('');
|
|
1184
1171
|
return undefined; // Success
|
|
1185
1172
|
}
|
|
1186
1173
|
|
|
@@ -1204,15 +1191,15 @@ EXAMPLES:
|
|
|
1204
1191
|
if (isNative) {
|
|
1205
1192
|
const expectedValue = BigInt(currentQuote.inAmount || currentQuote.inputAmount || '0');
|
|
1206
1193
|
if (txValue !== expectedValue) {
|
|
1207
|
-
|
|
1208
|
-
if (qi + 1 < endIndex)
|
|
1194
|
+
log(` ❌ Transaction value mismatch for ${quoteName}: tx.value=${txValue}, expected=${expectedValue}`);
|
|
1195
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1209
1196
|
lastQuoteError = `${quoteName} transaction value mismatch`;
|
|
1210
1197
|
continue;
|
|
1211
1198
|
}
|
|
1212
1199
|
} else {
|
|
1213
1200
|
if (txValue > 0n) {
|
|
1214
|
-
|
|
1215
|
-
if (qi + 1 < endIndex)
|
|
1201
|
+
log(` ❌ ERC-20 swap has non-zero tx.value (${txValue}) for ${quoteName} — aborting`);
|
|
1202
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1216
1203
|
lastQuoteError = `${quoteName} unexpected tx.value`;
|
|
1217
1204
|
continue;
|
|
1218
1205
|
}
|
|
@@ -1226,10 +1213,10 @@ EXAMPLES:
|
|
|
1226
1213
|
);
|
|
1227
1214
|
|
|
1228
1215
|
if (existingAllowance >= inputAmount && existingAllowance > 0n) {
|
|
1229
|
-
|
|
1216
|
+
log(` ✓ Sufficient allowance exists for ${quoteName}, skipping approval`);
|
|
1230
1217
|
} else {
|
|
1231
|
-
|
|
1232
|
-
|
|
1218
|
+
log(` ⚠ Approval required → ${currentQuote.approvalAddress}`);
|
|
1219
|
+
log(` Sending approval tx...`);
|
|
1233
1220
|
const approvalNonce = await getEvmNonce(chain, walletAddress);
|
|
1234
1221
|
|
|
1235
1222
|
const approvalGasPrice = currentQuote.transaction?.gasPrice || currentQuote.transaction?.maxFeePerGas || '1000000';
|
|
@@ -1249,25 +1236,25 @@ EXAMPLES:
|
|
|
1249
1236
|
});
|
|
1250
1237
|
|
|
1251
1238
|
if (approvalResult.status !== 'Success') {
|
|
1252
|
-
|
|
1253
|
-
if (qi + 1 < endIndex)
|
|
1239
|
+
log(` ❌ Approval failed for ${quoteName}: ${approvalResult.error || 'unknown error'}`);
|
|
1240
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1254
1241
|
lastQuoteError = `${quoteName} approval failed`;
|
|
1255
1242
|
continue;
|
|
1256
1243
|
}
|
|
1257
1244
|
|
|
1258
|
-
|
|
1245
|
+
log(` Waiting for approval confirmation...`);
|
|
1259
1246
|
try {
|
|
1260
1247
|
const receipt = await waitForReceipt(chain, approvalResult.txHash);
|
|
1261
|
-
|
|
1248
|
+
log(` ✓ Approval confirmed in block ${parseInt(receipt.blockNumber, 16)}: ${approvalResult.txHash}`);
|
|
1262
1249
|
} catch (receiptErr) {
|
|
1263
|
-
|
|
1264
|
-
if (qi + 1 < endIndex)
|
|
1250
|
+
log(` ❌ Approval may not have confirmed: ${receiptErr.message}`);
|
|
1251
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1265
1252
|
lastQuoteError = `${quoteName} approval unconfirmed`;
|
|
1266
1253
|
continue;
|
|
1267
1254
|
}
|
|
1268
1255
|
// Wait for RPC state propagation after approval
|
|
1269
1256
|
await new Promise(r => setTimeout(r, 2000));
|
|
1270
|
-
|
|
1257
|
+
log('');
|
|
1271
1258
|
}
|
|
1272
1259
|
}
|
|
1273
1260
|
|
|
@@ -1283,8 +1270,8 @@ EXAMPLES:
|
|
|
1283
1270
|
value: txData.value ? '0x' + BigInt(txData.value).toString(16) : '0x0',
|
|
1284
1271
|
});
|
|
1285
1272
|
if (!sim.success) {
|
|
1286
|
-
|
|
1287
|
-
if (qi + 1 < endIndex)
|
|
1273
|
+
log(` ⚠ Simulation failed for ${quoteName}: ${sim.reason}`);
|
|
1274
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1288
1275
|
lastQuoteError = `${quoteName} simulation failed: ${sim.reason}`;
|
|
1289
1276
|
continue;
|
|
1290
1277
|
}
|
|
@@ -1298,17 +1285,17 @@ EXAMPLES:
|
|
|
1298
1285
|
const txGas = parseInt(txData.gas || txData.gasLimit || "0");
|
|
1299
1286
|
const finalGas = apiGas > 0 ? apiGas : txGas;
|
|
1300
1287
|
if (finalGas !== txGas) {
|
|
1301
|
-
|
|
1288
|
+
log(` ℹ Using API gas ${finalGas} (tx.gas was ${txGas})`);
|
|
1302
1289
|
}
|
|
1303
1290
|
if (txData.gasLimit) txData.gasLimit = String(finalGas);
|
|
1304
1291
|
else txData.gas = String(finalGas);
|
|
1305
1292
|
|
|
1306
|
-
|
|
1293
|
+
log(' Fetching nonce...');
|
|
1307
1294
|
await new Promise(r => setTimeout(r, 1000));
|
|
1308
1295
|
const nonce = await getEvmNonce(chain, walletAddress);
|
|
1309
|
-
|
|
1296
|
+
log(` Nonce: ${nonce}`);
|
|
1310
1297
|
|
|
1311
|
-
|
|
1298
|
+
log(' Signing EVM transaction...');
|
|
1312
1299
|
signedTransaction = signEvmTransaction(
|
|
1313
1300
|
currentQuote.transaction,
|
|
1314
1301
|
exported.evm.privateKey,
|
|
@@ -1317,7 +1304,7 @@ EXAMPLES:
|
|
|
1317
1304
|
);
|
|
1318
1305
|
}
|
|
1319
1306
|
|
|
1320
|
-
|
|
1307
|
+
log(' Broadcasting...');
|
|
1321
1308
|
const execParams = {
|
|
1322
1309
|
signedTransaction,
|
|
1323
1310
|
chain,
|
|
@@ -1333,64 +1320,64 @@ EXAMPLES:
|
|
|
1333
1320
|
|
|
1334
1321
|
// For EVM: verify the tx actually succeeded on-chain
|
|
1335
1322
|
if (chainType === 'evm' && result.txHash) {
|
|
1336
|
-
|
|
1323
|
+
log(' Verifying on-chain status...');
|
|
1337
1324
|
try {
|
|
1338
1325
|
await waitForReceipt(chain, result.txHash);
|
|
1339
1326
|
} catch (receiptErr) {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1327
|
+
log(`\n ⚠ Transaction was broadcast but REVERTED on-chain!`);
|
|
1328
|
+
log(` Tx Hash: ${result.txHash}`);
|
|
1329
|
+
log(` Explorer: ${explorerUrl}`);
|
|
1330
|
+
log(` Error: ${receiptErr.message}`);
|
|
1344
1331
|
if (qi + 1 < endIndex) {
|
|
1345
|
-
|
|
1332
|
+
log(` Trying next quote...`);
|
|
1346
1333
|
lastQuoteError = `${quoteName} reverted on-chain`;
|
|
1347
1334
|
continue;
|
|
1348
1335
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1336
|
+
log(`\n The trading API reported success, but the contract execution failed.`);
|
|
1337
|
+
log(` This can happen due to: stale quotes, insufficient gas, or liquidity changes.`);
|
|
1351
1338
|
exit(1);
|
|
1352
1339
|
return;
|
|
1353
1340
|
}
|
|
1354
1341
|
}
|
|
1355
1342
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1343
|
+
log(`\n ✓ Transaction successful!`);
|
|
1344
|
+
log(` Status: ${result.status}`);
|
|
1345
|
+
log(` ${result.signature ? 'Signature' : 'Tx Hash'}: ${txId}`);
|
|
1346
|
+
log(` Chain: ${chainConfig.name} (${result.chainType})`);
|
|
1347
|
+
log(` Broadcaster: ${result.broadcaster}`);
|
|
1348
|
+
log(` Explorer: ${explorerUrl}`);
|
|
1362
1349
|
|
|
1363
1350
|
if (result.swapEvents?.length) {
|
|
1364
|
-
|
|
1351
|
+
log(` Swaps:`);
|
|
1365
1352
|
result.swapEvents.forEach(e => {
|
|
1366
|
-
|
|
1353
|
+
log(` ${e.inputAmount} ${e.inputMint?.slice(0, 8)}... → ${e.outputAmount} ${e.outputMint?.slice(0, 8)}...`);
|
|
1367
1354
|
});
|
|
1368
1355
|
}
|
|
1369
|
-
|
|
1356
|
+
log('');
|
|
1370
1357
|
return undefined; // Success — done
|
|
1371
1358
|
} else {
|
|
1372
|
-
|
|
1373
|
-
if (result.error)
|
|
1359
|
+
log(`\n ✗ Quote ${quoteName} failed: ${result.status}`);
|
|
1360
|
+
if (result.error) log(` Error: ${result.error}`);
|
|
1374
1361
|
lastQuoteError = `${quoteName}: ${result.error || result.status}`;
|
|
1375
|
-
if (qi + 1 < endIndex)
|
|
1362
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1376
1363
|
}
|
|
1377
1364
|
|
|
1378
1365
|
} catch (quoteErr) {
|
|
1379
|
-
|
|
1366
|
+
log(` ❌ Quote ${quoteName} failed: ${quoteErr.message}`);
|
|
1380
1367
|
lastQuoteError = `${quoteName}: ${quoteErr.message}`;
|
|
1381
|
-
if (qi + 1 < endIndex)
|
|
1368
|
+
if (qi + 1 < endIndex) log(` Trying next quote...`);
|
|
1382
1369
|
}
|
|
1383
1370
|
}
|
|
1384
1371
|
|
|
1385
1372
|
// All quotes exhausted
|
|
1386
|
-
|
|
1387
|
-
|
|
1373
|
+
log(`\n❌ All quotes failed. Last error: ${lastQuoteError || 'unknown'}`);
|
|
1374
|
+
log('');
|
|
1388
1375
|
exit(1);
|
|
1389
1376
|
return undefined;
|
|
1390
1377
|
|
|
1391
1378
|
} catch (err) {
|
|
1392
|
-
|
|
1393
|
-
if (err.details)
|
|
1379
|
+
log(`Error: ${err.message}`);
|
|
1380
|
+
if (err.details) log(` Details: ${JSON.stringify(err.details)}`);
|
|
1394
1381
|
exit(1);
|
|
1395
1382
|
}
|
|
1396
1383
|
},
|