Skip to content

Commit 4c3f41a

Browse files
authored
feat: claim rewards (#2598)
1 parent ff2fbcf commit 4c3f41a

14 files changed

Lines changed: 946 additions & 118 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"dependencies": {
3434
"@aave/contract-helpers": "1.36.1",
3535
"@aave/math-utils": "1.36.1",
36-
"@aave/react": "^0.4.0",
36+
"@aave/react": "0.6.1",
3737
"@amplitude/analytics-browser": "^2.13.0",
3838
"@bgd-labs/aave-address-book": "^4.25.1",
3939
"@cowprotocol/app-data": "^3.1.0",
@@ -156,4 +156,4 @@
156156
"budgetPercentIncreaseRed": 20,
157157
"showDetails": true
158158
}
159-
}
159+
}

src/components/transactions/ClaimRewards/ClaimRewardsActions.tsx

Lines changed: 218 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import { ProtocolAction } from '@aave/contract-helpers';
1+
import {
2+
eEthereumTxType,
3+
EthereumTransactionTypeExtended,
4+
ProtocolAction,
5+
} from '@aave/contract-helpers';
6+
import { chainId, evmAddress, useUserMeritRewards } from '@aave/react';
27
import { Trans } from '@lingui/macro';
8+
import { BigNumber, PopulatedTransaction, utils } from 'ethers';
39
import { Reward } from 'src/helpers/types';
410
import { useTransactionHandler } from 'src/helpers/useTransactionHandler';
511
import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider';
12+
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
613
import { useRootStore } from 'src/store/root';
14+
import { useShallow } from 'zustand/shallow';
715

816
import { TxActionsWrapper } from '../TxActionsWrapper';
17+
import { ControllerIdentifier, RewardSymbol } from './constants';
918

1019
export type ClaimRewardsActionsProps = {
1120
isWrongNetwork: boolean;
@@ -21,6 +30,17 @@ export const ClaimRewardsActions = ({
2130
const claimRewards = useRootStore((state) => state.claimRewards);
2231
const { reserves } = useAppDataContext();
2332

33+
const { currentAccount } = useWeb3Context();
34+
35+
const [currentMarketData, estimateGasLimit] = useRootStore(
36+
useShallow((store) => [store.currentMarketData, store.estimateGasLimit])
37+
);
38+
39+
const { data: meritClaimRewards } = useUserMeritRewards({
40+
user: evmAddress(currentAccount),
41+
chainId: chainId(currentMarketData.chainId),
42+
});
43+
2444
const { action, loadingTxns, mainTxState, requiresApproval } = useTransactionHandler({
2545
protocolAction: ProtocolAction.claimRewards,
2646
eventTxInfo: {
@@ -29,12 +49,202 @@ export const ClaimRewardsActions = ({
2949
},
3050
tryPermit: false,
3151
handleGetTxns: async () => {
32-
return claimRewards({ isWrongNetwork, blocked, selectedReward, formattedReserves: reserves });
52+
// Check if we need to claim both protocol and merit rewards
53+
const isClaimingAll = selectedReward.symbol === RewardSymbol.ALL;
54+
const isClaimingMeritAll = selectedReward.symbol === RewardSymbol.MERIT_ALL;
55+
const isClaimingProtocolAll = selectedReward.symbol === RewardSymbol.PROTOCOL_ALL;
56+
const hasProtocolRewards =
57+
selectedReward.incentiveControllerAddress !== ControllerIdentifier.MERIT_REWARD;
58+
const hasMeritRewards =
59+
meritClaimRewards?.claimable && meritClaimRewards.claimable.length > 0;
60+
const isIndividualProtocolReward =
61+
hasProtocolRewards && !isClaimingAll && !isClaimingProtocolAll && !isClaimingMeritAll;
62+
63+
// Use simple approach for individual protocol rewards (most common case)
64+
if (isIndividualProtocolReward) {
65+
return claimRewards({
66+
isWrongNetwork,
67+
blocked,
68+
selectedReward,
69+
formattedReserves: reserves,
70+
});
71+
}
72+
73+
// Use complex multicall logic only when needed
74+
if (isClaimingAll && hasProtocolRewards && hasMeritRewards) {
75+
// Get protocol rewards transaction
76+
const protocolTxns = await claimRewards({
77+
isWrongNetwork,
78+
blocked,
79+
selectedReward,
80+
formattedReserves: reserves,
81+
});
82+
83+
// Create multicall transaction that includes both protocol and merit claims
84+
if (!meritClaimRewards?.transaction) {
85+
throw new Error('Merit rewards transaction not available');
86+
}
87+
const multicallTx = await createMulticallTransaction(
88+
protocolTxns,
89+
meritClaimRewards.transaction as unknown as PopulatedTransaction
90+
);
91+
92+
// Check if there are approval transactions that need to be handled separately
93+
const approvalTxns = protocolTxns.filter((tx) => tx.txType === 'ERC20_APPROVAL');
94+
95+
return approvalTxns.length > 0 ? [...approvalTxns, multicallTx] : [multicallTx];
96+
} else if ((isClaimingAll && !hasProtocolRewards && hasMeritRewards) || isClaimingMeritAll) {
97+
// Only merit rewards - use merit transaction directly
98+
if (!meritClaimRewards?.transaction) {
99+
throw new Error('Merit rewards transaction not available');
100+
}
101+
102+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
103+
// @ts-ignore
104+
return [
105+
convertMeritTransactionToEthereum(
106+
meritClaimRewards.transaction as unknown as PopulatedTransaction
107+
),
108+
];
109+
} else {
110+
// Protocol-all or other cases - use existing protocol logic
111+
return claimRewards({
112+
isWrongNetwork,
113+
blocked,
114+
selectedReward,
115+
formattedReserves: reserves,
116+
});
117+
}
33118
},
34119
skip: Object.keys(selectedReward).length === 0 || blocked,
35-
deps: [selectedReward],
120+
deps: [selectedReward, meritClaimRewards],
36121
});
37122

123+
// Helper function to create multicall transaction
124+
const createMulticallTransaction = async (
125+
protocolTxns: EthereumTransactionTypeExtended[],
126+
meritTransaction: PopulatedTransaction
127+
): Promise<EthereumTransactionTypeExtended> => {
128+
// Multicall3 contract address (same across chains)
129+
const multicallAddress = '0xcA11bde05977b3631167028862bE2a173976CA11';
130+
131+
const calls = [];
132+
133+
for (const txExt of protocolTxns) {
134+
if (txExt.txType === 'ERC20_APPROVAL') continue; // Skip approvals for multicall
135+
136+
const tx = await txExt.tx();
137+
calls.push({
138+
target: tx.to,
139+
callData: tx.data,
140+
value: tx.value ? (BigNumber.isBigNumber(tx.value) ? tx.value.toString() : tx.value) : '0',
141+
});
142+
}
143+
144+
calls.push({
145+
target: meritTransaction.to,
146+
callData: meritTransaction.data,
147+
value: meritTransaction.value
148+
? BigNumber.isBigNumber(meritTransaction.value)
149+
? meritTransaction.value.toString()
150+
: meritTransaction.value
151+
: '0',
152+
});
153+
154+
const multicallInterface = new utils.Interface([
155+
'function aggregate3Value((address target, bool allowFailure, uint256 value, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[])',
156+
]);
157+
158+
const callsWithFailure = calls.map((call) => [
159+
call.target,
160+
false, // allowFailure = false
161+
call.value,
162+
call.callData,
163+
]);
164+
165+
const data = multicallInterface.encodeFunctionData('aggregate3Value', [callsWithFailure]);
166+
167+
// Calculate total value needed for multicall by summing individual call values
168+
const totalValue = calls.reduce((sum, call) => {
169+
return BigNumber.from(sum).add(BigNumber.from(call.value)).toString();
170+
}, '0');
171+
172+
return {
173+
txType: eEthereumTxType.DLP_ACTION,
174+
tx: async () => ({
175+
to: multicallAddress,
176+
from: currentAccount,
177+
data,
178+
value: totalValue,
179+
}),
180+
gas: async () => {
181+
try {
182+
const tx = {
183+
to: multicallAddress,
184+
from: currentAccount,
185+
data,
186+
value: BigNumber.from(totalValue),
187+
};
188+
189+
const estimatedTx = await estimateGasLimit(tx, currentMarketData.chainId);
190+
191+
return {
192+
gasLimit: estimatedTx.gasLimit?.toString(),
193+
gasPrice: '0', // Legacy field - actual gas pricing handled by wallet (EIP-1559)
194+
};
195+
} catch (error) {
196+
console.warn('Gas estimation failed for multicall, using fallback:', error);
197+
return {
198+
gasLimit: '800000', // Conservative fallback
199+
gasPrice: '0', // Legacy field - actual gas pricing handled by wallet (EIP-1559)
200+
};
201+
}
202+
},
203+
};
204+
};
205+
206+
// Helper function to convert merit transaction to Ethereum format
207+
const convertMeritTransactionToEthereum = (
208+
meritTx: PopulatedTransaction
209+
): EthereumTransactionTypeExtended => {
210+
return {
211+
txType: eEthereumTxType.DLP_ACTION,
212+
tx: async () => ({
213+
to: meritTx.to,
214+
from: meritTx.from || currentAccount,
215+
data: meritTx.data,
216+
value: meritTx.value
217+
? BigNumber.isBigNumber(meritTx.value)
218+
? meritTx.value.toString()
219+
: meritTx.value
220+
: '0',
221+
}),
222+
gas: async () => {
223+
try {
224+
const tx = {
225+
to: meritTx.to,
226+
from: meritTx.from || currentAccount,
227+
data: meritTx.data,
228+
value: meritTx.value,
229+
};
230+
231+
const estimatedTx = await estimateGasLimit(tx, currentMarketData.chainId);
232+
233+
return {
234+
gasLimit: estimatedTx.gasLimit?.toString(),
235+
gasPrice: '0', // Legacy field - actual gas pricing handled by wallet (EIP-1559)
236+
};
237+
} catch (error) {
238+
console.warn('Gas estimation failed for merit transaction, using fallback:', error);
239+
return {
240+
gasLimit: '400000',
241+
gasPrice: '0', // Legacy field - actual gas pricing handled by wallet (EIP-1559)
242+
};
243+
}
244+
},
245+
};
246+
};
247+
38248
return (
39249
<TxActionsWrapper
40250
requiresApproval={requiresApproval}
@@ -43,8 +253,12 @@ export const ClaimRewardsActions = ({
43253
mainTxState={mainTxState}
44254
handleAction={action}
45255
actionText={
46-
selectedReward.symbol === 'all' ? (
256+
selectedReward.symbol === RewardSymbol.ALL ? (
47257
<Trans>Claim all</Trans>
258+
) : selectedReward.symbol === RewardSymbol.MERIT_ALL ? (
259+
<Trans>Claim all merit rewards</Trans>
260+
) : selectedReward.symbol === RewardSymbol.PROTOCOL_ALL ? (
261+
<Trans>Claim all protocol rewards</Trans>
48262
) : (
49263
<Trans>Claim {selectedReward.symbol}</Trans>
50264
)

0 commit comments

Comments
 (0)