diff --git a/src/components/transactions/ClaimRewards/ClaimRewardsActions.tsx b/src/components/transactions/ClaimRewards/ClaimRewardsActions.tsx index be711da755..43c21583ab 100644 --- a/src/components/transactions/ClaimRewards/ClaimRewardsActions.tsx +++ b/src/components/transactions/ClaimRewards/ClaimRewardsActions.tsx @@ -5,7 +5,7 @@ import { } from '@aave/contract-helpers'; import { chainId, evmAddress, useUserMeritRewards } from '@aave/react'; import { Trans } from '@lingui/macro'; -import { BigNumber, PopulatedTransaction, utils } from 'ethers'; +import { BigNumber, PopulatedTransaction } from 'ethers'; import { Reward } from 'src/helpers/types'; import { useTransactionHandler } from 'src/helpers/useTransactionHandler'; import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider'; @@ -70,9 +70,8 @@ export const ClaimRewardsActions = ({ }); } - // Use complex multicall logic only when needed + // Claim protocol and Merit rewards separately so each claim is sent by the wallet account. if (isClaimingAll && hasProtocolRewards && hasMeritRewards) { - // Get protocol rewards transaction const protocolTxns = await claimRewards({ isWrongNetwork, blocked, @@ -84,15 +83,13 @@ export const ClaimRewardsActions = ({ if (!meritClaimRewards?.transaction) { throw new Error('Merit rewards transaction not available'); } - const multicallTx = await createMulticallTransaction( - protocolTxns, - meritClaimRewards.transaction as unknown as PopulatedTransaction - ); - // Check if there are approval transactions that need to be handled separately - const approvalTxns = protocolTxns.filter((tx) => tx.txType === 'ERC20_APPROVAL'); - - return approvalTxns.length > 0 ? [...approvalTxns, multicallTx] : [multicallTx]; + return [ + ...protocolTxns, + convertMeritTransactionToEthereum( + meritClaimRewards.transaction as unknown as PopulatedTransaction + ), + ]; } else if ((isClaimingAll && !hasProtocolRewards && hasMeritRewards) || isClaimingMeritAll) { // Only merit rewards - use merit transaction directly if (!meritClaimRewards?.transaction) { @@ -120,89 +117,6 @@ export const ClaimRewardsActions = ({ deps: [selectedReward, meritClaimRewards], }); - // Helper function to create multicall transaction - const createMulticallTransaction = async ( - protocolTxns: EthereumTransactionTypeExtended[], - meritTransaction: PopulatedTransaction - ): Promise => { - // Multicall3 contract address (same across chains) - const multicallAddress = '0xcA11bde05977b3631167028862bE2a173976CA11'; - - const calls = []; - - for (const txExt of protocolTxns) { - if (txExt.txType === 'ERC20_APPROVAL') continue; // Skip approvals for multicall - - const tx = await txExt.tx(); - calls.push({ - target: tx.to, - callData: tx.data, - value: tx.value ? (BigNumber.isBigNumber(tx.value) ? tx.value.toString() : tx.value) : '0', - }); - } - - calls.push({ - target: meritTransaction.to, - callData: meritTransaction.data, - value: meritTransaction.value - ? BigNumber.isBigNumber(meritTransaction.value) - ? meritTransaction.value.toString() - : meritTransaction.value - : '0', - }); - - const multicallInterface = new utils.Interface([ - 'function aggregate3Value((address target, bool allowFailure, uint256 value, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[])', - ]); - - const callsWithFailure = calls.map((call) => [ - call.target, - false, // allowFailure = false - call.value, - call.callData, - ]); - - const data = multicallInterface.encodeFunctionData('aggregate3Value', [callsWithFailure]); - - // Calculate total value needed for multicall by summing individual call values - const totalValue = calls.reduce((sum, call) => { - return BigNumber.from(sum).add(BigNumber.from(call.value)).toString(); - }, '0'); - - return { - txType: eEthereumTxType.DLP_ACTION, - tx: async () => ({ - to: multicallAddress, - from: currentAccount, - data, - value: totalValue, - }), - gas: async () => { - try { - const tx = { - to: multicallAddress, - from: currentAccount, - data, - value: BigNumber.from(totalValue), - }; - - const estimatedTx = await estimateGasLimit(tx, currentMarketData.chainId); - - return { - gasLimit: estimatedTx.gasLimit?.toString(), - gasPrice: '0', // Legacy field - actual gas pricing handled by wallet (EIP-1559) - }; - } catch (error) { - console.warn('Gas estimation failed for multicall, using fallback:', error); - return { - gasLimit: '800000', // Conservative fallback - gasPrice: '0', // Legacy field - actual gas pricing handled by wallet (EIP-1559) - }; - } - }, - }; - }; - // Helper function to convert merit transaction to Ethereum format const convertMeritTransactionToEthereum = ( meritTx: PopulatedTransaction diff --git a/src/helpers/useTransactionHandler.tsx b/src/helpers/useTransactionHandler.tsx index 64dc14413b..f7a6e55663 100644 --- a/src/helpers/useTransactionHandler.tsx +++ b/src/helpers/useTransactionHandler.tsx @@ -18,6 +18,17 @@ import { useShallow } from 'zustand/shallow'; export const MOCK_SIGNED_HASH = 'Signed correctly'; +const MAIN_ACTION_TX_TYPES = [ + 'DLP_ACTION', + 'REWARD_ACTION', + 'FAUCET_V2_MINT', + 'FAUCET_MINT', + 'STAKE_ACTION', + 'GOV_DELEGATION_ACTION', + 'GOVERNANCE_ACTION', + 'V3_MIGRATION_ACTION', +]; + interface UseTransactionHandlerProps { handleGetTxns: () => Promise; handleGetPermitTxns?: ( @@ -84,7 +95,7 @@ export const useTransactionHandler = ({ ); const [approvalTxes, setApprovalTxes] = useState(); - const [actionTx, setActionTx] = useState(); + const [actionTxes, setActionTxes] = useState(); const [usePermit, setUsePermit] = useState(false); const mounted = useRef(false); @@ -313,31 +324,44 @@ export const useTransactionHandler = ({ }); } } - if ((!usePermit || !approvalTxes) && actionTx) { + if ((!usePermit || !approvalTxes) && actionTxes?.length) { + let errorHandled = false; try { setMainTxState({ ...mainTxState, loading: true }); - const params = await actionTx.tx(); - delete params.gasPrice; - return processTx({ - tx: () => sendTx(params), - successCallback: (txnResponse: TransactionResponse) => { - setMainTxState({ - txHash: txnResponse.hash, - loading: false, - success: true, - }); - setTxError(undefined); - }, - errorCallback: (error, hash) => { - const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION); - setTxError(parsedError); - setMainTxState({ - txHash: hash, - loading: false, + + for (let index = 0; index < actionTxes.length; index++) { + const actionTx = actionTxes[index]; + const params = await actionTx.tx(); + const isLastAction = index === actionTxes.length - 1; + delete params.gasPrice; + + await new Promise((resolve, reject) => { + processTx({ + tx: () => sendTx(params), + successCallback: (txnResponse: TransactionResponse) => { + setMainTxState({ + txHash: txnResponse.hash, + loading: !isLastAction, + success: isLastAction, + }); + setTxError(undefined); + resolve(); + }, + errorCallback: (error, hash) => { + errorHandled = true; + const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION); + setTxError(parsedError); + setMainTxState({ + txHash: hash, + loading: false, + }); + reject(error); + }, }); - }, - }); + }); + } } catch (error) { + if (errorHandled) return; const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false); console.error(error, parsedError); setTxError(parsedError); @@ -388,20 +412,7 @@ export const useTransactionHandler = ({ } else { setApprovalTxes(undefined); } - setActionTx( - txs.find((tx) => - [ - 'DLP_ACTION', - 'REWARD_ACTION', - 'FAUCET_V2_MINT', - 'FAUCET_MINT', - 'STAKE_ACTION', - 'GOV_DELEGATION_ACTION', - 'GOVERNANCE_ACTION', - 'V3_MIGRATION_ACTION', - ].includes(tx.txType) - ) - ); + setActionTxes(txs.filter((tx) => MAIN_ACTION_TX_TYPES.includes(tx.txType))); setMainTxState({ txHash: undefined, }); @@ -437,7 +448,7 @@ export const useTransactionHandler = ({ return () => clearTimeout(timeout); } else { setApprovalTxes(undefined); - setActionTx(undefined); + setActionTxes(undefined); } }, [skip, ...deps, tryPermit, walletApprovalMethodPreference]);