diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/components/TransactionHistory.tsx b/apps/kyberswap-interface/src/pages/CrossChainSwap/components/TransactionHistory.tsx index 3fbc32330c..e148f8ecb3 100644 --- a/apps/kyberswap-interface/src/pages/CrossChainSwap/components/TransactionHistory.tsx +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/components/TransactionHistory.tsx @@ -59,17 +59,17 @@ export const TransactionHistory = () => { // Track ongoing API calls to prevent duplicates const ongoingCallsRef = useRef(new Set()) const intervalRef = useRef | null>(null) + const latestTransactionsRef = useRef(transactions) + + useEffect(() => { + latestTransactionsRef.current = transactions + }, [transactions]) const pendingTxs = useMemo(() => { - return transactions.filter( - tx => - (!tx.targetTxHash || !tx.status || tx.status === 'Processing') && - tx.status !== 'Refunded' && - // hardcode to update status for a failed tx, if user checked again, we can remove this in next release - (tx.sourceTxHash.toLowerCase() !== '0xb321c90b203b9641cc6b7039eade7a6d212a9e133b6817b593f4b9408ca55d87' - ? tx.status !== 'Failed' - : true), - ) + return transactions.filter(tx => { + const isProcessing = !tx.targetTxHash || !tx.status || tx.status === 'Processing' + return isProcessing && tx.status !== 'Refunded' && tx.status !== 'Failed' + }) }, [transactions]) useEffect(() => { @@ -83,7 +83,7 @@ export const TransactionHistory = () => { txsToCheck.forEach(tx => ongoingCallsRef.current.add(tx.id)) try { - const updatedTransactions = [...transactions] + const updatesById = new Map>() let hasUpdates = false // Use Promise.allSettled to handle individual failures gracefully @@ -102,88 +102,84 @@ export const TransactionHistory = () => { // Only update if we have meaningful changes // Check if actual amountOut is available and different from estimated - const hasActualAmountOut = amountOut && amountOut !== '0' && amountOut !== tx.outputAmount - if ((txHash && txHash !== tx.targetTxHash) || status !== tx.status || hasActualAmountOut) { - const txIndex = updatedTransactions.findIndex(t => t.id === tx.id) - - if (txIndex !== -1) { - const oldStatus = updatedTransactions[txIndex].status - updatedTransactions[txIndex] = { - ...updatedTransactions[txIndex], - targetTxHash: txHash || updatedTransactions[txIndex].targetTxHash, - status: status || updatedTransactions[txIndex].status, - // Update outputAmount with actual amount if available - // Store original outputAmount as estimatedAmountOut for debugging (in local storage) - ...(hasActualAmountOut && { - outputAmount: amountOut, - estimatedAmountOut: - updatedTransactions[txIndex].estimatedAmountOut || updatedTransactions[txIndex].outputAmount, - }), + const currentTx = latestTransactionsRef.current.find(t => t.id === tx.id) || tx + const hasActualAmountOut = amountOut && amountOut !== '0' && amountOut !== currentTx.outputAmount + if ((txHash && txHash !== currentTx.targetTxHash) || status !== currentTx.status || hasActualAmountOut) { + const oldStatus = currentTx.status + + updatesById.set(tx.id, { + targetTxHash: txHash || currentTx.targetTxHash, + status: status || currentTx.status, + // Update outputAmount with actual amount if available + // Store original outputAmount as estimatedAmountOut for debugging (in local storage) + ...(hasActualAmountOut && { + outputAmount: amountOut, + estimatedAmountOut: currentTx.estimatedAmountOut || currentTx.outputAmount, + }), + }) + + // Fire specific GA events for success/failure + if (status && status !== oldStatus) { + const swapDetails = { + amount_in: tx.inputAmount, + amount_in_usd: tx.amountInUsd, + amount_out: tx.outputAmount, + amount_out_usd: tx.amountOutUsd, + currency: 'USD', + fee_percent: tx.platformFeePercent, + from_chain: tx.sourceChain, + from_chain_name: getChainName(tx.sourceChain), + from_token: + tx.sourceChain === NonEvmChain.Bitcoin + ? tx.sourceToken.symbol + : tx.sourceChain === NonEvmChain.Solana + ? (tx.sourceToken as any).id + : tx.sourceChain === NonEvmChain.Near + ? (tx.sourceToken as any).assetId + : (tx.sourceToken as any)?.address || + (tx.sourceToken as any)?.wrapped?.address || + tx.sourceToken?.symbol, + from_token_symbol: tx.sourceToken?.symbol, + from_token_decimals: tx.sourceToken?.decimals, + to_chain: tx.targetChain, + to_chain_name: getChainName(tx.targetChain), + to_token: + tx.targetChain === NonEvmChain.Bitcoin + ? tx.targetToken.symbol + : tx.targetChain === NonEvmChain.Solana + ? (tx.targetToken as any).id + : tx.targetChain === NonEvmChain.Near + ? (tx.targetToken as any).assetId + : (tx.targetToken as any)?.address || + (tx.targetToken as any)?.wrapped?.address || + tx.targetToken?.symbol, + to_token_symbol: tx.targetToken?.symbol, + to_token_decimals: tx.targetToken?.decimals, + partner: tx.adapter, + platform: 'KyberSwap Cross-Chain', + source_tx_hash: tx.sourceTxHash, + target_tx_hash: txHash || tx.targetTxHash, + recipient: tx.recipient, + sender: tx.sender, + status: status, + time: Date.now(), + timestamp: tx.timestamp, } - // Fire specific GA events for success/failure - if (status && status !== oldStatus) { - const swapDetails = { - amount_in: tx.inputAmount, - amount_in_usd: tx.amountInUsd, - amount_out: tx.outputAmount, - amount_out_usd: tx.amountOutUsd, - currency: 'USD', - fee_percent: tx.platformFeePercent, - from_chain: tx.sourceChain, - from_chain_name: getChainName(tx.sourceChain), - from_token: - tx.sourceChain === NonEvmChain.Bitcoin - ? tx.sourceToken.symbol - : tx.sourceChain === NonEvmChain.Solana - ? (tx.sourceToken as any).id - : tx.sourceChain === NonEvmChain.Near - ? (tx.sourceToken as any).assetId - : (tx.sourceToken as any)?.address || - (tx.sourceToken as any)?.wrapped?.address || - tx.sourceToken?.symbol, - from_token_symbol: tx.sourceToken?.symbol, - from_token_decimals: tx.sourceToken?.decimals, - to_chain: tx.targetChain, - to_chain_name: getChainName(tx.targetChain), - to_token: - tx.targetChain === NonEvmChain.Bitcoin - ? tx.targetToken.symbol - : tx.targetChain === NonEvmChain.Solana - ? (tx.targetToken as any).id - : tx.targetChain === NonEvmChain.Near - ? (tx.targetToken as any).assetId - : (tx.targetToken as any)?.address || - (tx.targetToken as any)?.wrapped?.address || - tx.targetToken?.symbol, - to_token_symbol: tx.targetToken?.symbol, - to_token_decimals: tx.targetToken?.decimals, - partner: tx.adapter, - platform: 'KyberSwap Cross-Chain', - source_tx_hash: tx.sourceTxHash, - target_tx_hash: txHash || tx.targetTxHash, - recipient: tx.recipient, - sender: tx.sender, - status: status, - time: Date.now(), - timestamp: tx.timestamp, - } - - if (status === 'Success') { - crossChainMixpanelHandler(CROSS_CHAIN_MIXPANEL_TYPE.CROSS_CHAIN_SWAP_SUCCESS, { - ...swapDetails, - status: 'succeed', - }) - } else if (status === 'Failed') { - crossChainMixpanelHandler(CROSS_CHAIN_MIXPANEL_TYPE.CROSS_CHAIN_SWAP_FAILED, { - ...swapDetails, - status: 'failed', - }) - } + if (status === 'Success') { + crossChainMixpanelHandler(CROSS_CHAIN_MIXPANEL_TYPE.CROSS_CHAIN_SWAP_SUCCESS, { + ...swapDetails, + status: 'succeed', + }) + } else if (status === 'Failed') { + crossChainMixpanelHandler(CROSS_CHAIN_MIXPANEL_TYPE.CROSS_CHAIN_SWAP_FAILED, { + ...swapDetails, + status: 'failed', + }) } - - hasUpdates = true } + + hasUpdates = true } return { success: true, txId: tx.id } } catch (error) { @@ -203,9 +199,17 @@ export const TransactionHistory = () => { } }) + // Ensure all in-flight ids are released even if adapter lookup failed + txsToCheck.forEach(tx => ongoingCallsRef.current.delete(tx.id)) + // Update transactions if we have changes if (hasUpdates) { - setTransactions(updatedTransactions) + const baseTransactions = latestTransactionsRef.current + const nextTransactions = baseTransactions.map(tx => { + const patch = updatesById.get(tx.id) + return patch ? { ...tx, ...patch } : tx + }) + setTransactions(nextTransactions) } } catch (error) { console.error('Error in checkTransactions:', error)