diff --git a/apps/cowswap-frontend/src/common/hooks/useNeedsApproval.test.ts b/apps/cowswap-frontend/src/common/hooks/useNeedsApproval.test.ts new file mode 100644 index 00000000000..068e9f9527c --- /dev/null +++ b/apps/cowswap-frontend/src/common/hooks/useNeedsApproval.test.ts @@ -0,0 +1,73 @@ +import { useTradeSpenderAddress } from '@cowprotocol/balances-and-allowances' +import { CurrencyAmount, Token } from '@cowprotocol/currency' + +import { renderHook } from '@testing-library/react' +import { SWRResponse } from 'swr' + +import { useNeedsApproval } from './useNeedsApproval' +import { useTokenAllowance } from './useTokenAllowance' + +jest.mock('@cowprotocol/balances-and-allowances', () => ({ + useTradeSpenderAddress: jest.fn(), +})) + +jest.mock('./useTokenAllowance', () => ({ + useTokenAllowance: jest.fn(), +})) + +const mockUseTradeSpenderAddress = useTradeSpenderAddress as jest.MockedFunction +const mockUseTokenAllowance = useTokenAllowance as jest.MockedFunction + +describe('useNeedsApproval', () => { + const spender = '0x0000000000000000000000000000000000000001' + const token = new Token(1, '0x1234567890123456789012345678901234567890', 18, 'TEST', 'Test Token') + const amount = CurrencyAmount.fromRawAmount(token, '100') + + function mockAllowance(data: bigint | undefined): void { + mockUseTokenAllowance.mockReturnValue({ data } as SWRResponse) + } + + beforeEach(() => { + jest.clearAllMocks() + mockUseTradeSpenderAddress.mockReturnValue(spender) + mockAllowance(0n) + }) + + it('returns false when amount is missing', () => { + const { result } = renderHook(() => useNeedsApproval(null)) + + expect(result.current).toBe(false) + }) + + it('returns false when spender is missing', () => { + mockUseTradeSpenderAddress.mockReturnValue(undefined) + + const { result } = renderHook(() => useNeedsApproval(amount)) + + expect(result.current).toBe(false) + }) + + it('returns true when allowance is not loaded yet', () => { + mockAllowance(undefined) + + const { result } = renderHook(() => useNeedsApproval(amount)) + + expect(result.current).toBe(true) + }) + + it('returns true when allowance is insufficient', () => { + mockAllowance(99n) + + const { result } = renderHook(() => useNeedsApproval(amount)) + + expect(result.current).toBe(true) + }) + + it('returns false when allowance is sufficient', () => { + mockAllowance(100n) + + const { result } = renderHook(() => useNeedsApproval(amount)) + + expect(result.current).toBe(false) + }) +}) diff --git a/apps/cowswap-frontend/src/common/hooks/useNeedsApproval.ts b/apps/cowswap-frontend/src/common/hooks/useNeedsApproval.ts index b6148749eb2..1754de025cf 100644 --- a/apps/cowswap-frontend/src/common/hooks/useNeedsApproval.ts +++ b/apps/cowswap-frontend/src/common/hooks/useNeedsApproval.ts @@ -19,18 +19,19 @@ import { useTokenAllowance } from './useTokenAllowance' * @param amount * @returns {boolean} */ -export function useNeedsApproval(amount: Nullish>): boolean { - const spender = useTradeSpenderAddress() +export function useNeedsApproval(amount: Nullish>, spender?: string): boolean { + const tradeSpender = useTradeSpenderAddress() const token = amount ? getWrappedToken(amount.currency) : undefined - const allowance = useTokenAllowance(token) + const approvalSpender = spender ?? tradeSpender + const allowance = useTokenAllowance(token, undefined, approvalSpender) - if (typeof allowance === 'undefined') { - return true + if (!token || !amount || !approvalSpender) { + return false } - if (!token || !amount || !spender) { - return false + if (allowance.data === undefined) { + return true } - return isEnoughAmount(amount, allowance?.data) === false + return isEnoughAmount(amount, allowance.data) === false } diff --git a/apps/cowswap-frontend/src/legacy/state/enhancedTransactions/hooks/TransactionHooksMod.tsx b/apps/cowswap-frontend/src/legacy/state/enhancedTransactions/hooks/TransactionHooksMod.tsx index 5e80444f6f5..e3a2423d294 100644 --- a/apps/cowswap-frontend/src/legacy/state/enhancedTransactions/hooks/TransactionHooksMod.tsx +++ b/apps/cowswap-frontend/src/legacy/state/enhancedTransactions/hooks/TransactionHooksMod.tsx @@ -19,14 +19,15 @@ export function useAllTransactions(): { [txHash: string]: EnhancedTransactionDet } // returns whether a token has a pending approval transaction -export function useHasPendingApproval(tokenAddress: string | undefined): boolean { +export function useHasPendingApproval(tokenAddress: string | undefined, approvalSpender?: string): boolean { const allTransactions = useAllTransactions() const spender = useTradeSpenderAddress() + const targetSpender = approvalSpender ?? spender return useMemo( () => typeof tokenAddress === 'string' && - typeof spender === 'string' && + typeof targetSpender === 'string' && Object.keys(allTransactions).some((hash) => { const tx = allTransactions[hash] if (!tx || tx.receipt || tx.replacementType || tx.errorMessage) return false @@ -34,8 +35,10 @@ export function useHasPendingApproval(tokenAddress: string | undefined): boolean const approval = tx.approval if (!approval) return false - return areAddressesEqual(approval.spender, spender) && areAddressesEqual(approval.tokenAddress, tokenAddress) + return ( + areAddressesEqual(approval.spender, targetSpender) && areAddressesEqual(approval.tokenAddress, tokenAddress) + ) }), - [allTransactions, spender, tokenAddress], + [allTransactions, targetSpender, tokenAddress], ) } diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproval.ts b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproval.ts index 12621fe5542..bec2afe7f59 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproval.ts +++ b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproval.ts @@ -24,7 +24,7 @@ export function useApprovalStateForSpender( const token = currency && !getIsNativeToken(currency) ? currency : undefined const currentAllowance = useTokenAllowance(token, account ?? undefined, spender) - const { state: approvalState } = useApproveState(amountToApprove) + const { state: approvalState } = useApproveState(amountToApprove, spender) return useMemo(() => { return { approvalState, currentAllowance: currentAllowance?.data } diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveCurrency.test.ts b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveCurrency.test.ts index cd628927c67..0434bc9afc2 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveCurrency.test.ts +++ b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveCurrency.test.ts @@ -5,7 +5,7 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { renderHook, waitFor } from '@testing-library/react' import { useTradeApproveCallback } from 'modules/erc20Approve' -import { callWidgetHook } from 'modules/injectedWidget' +import { callOnBeforeApprovalWidgetHook } from 'modules/injectedWidget' import { useShouldZeroApprove, useZeroApprove } from 'modules/zeroApproval' import { useApproveCurrency } from './useApproveCurrency' @@ -23,7 +23,7 @@ jest.mock('modules/erc20Approve', () => ({ })) jest.mock('modules/injectedWidget', () => ({ - callWidgetHook: jest.fn(), + callOnBeforeApprovalWidgetHook: jest.fn(), })) jest.mock('modules/zeroApproval', () => ({ @@ -34,7 +34,9 @@ jest.mock('modules/zeroApproval', () => ({ const mockUseTradeSpenderAddress = useTradeSpenderAddress as jest.MockedFunction const mockUseWalletInfo = useWalletInfo as jest.MockedFunction const mockUseTradeApproveCallback = useTradeApproveCallback as jest.MockedFunction -const mockCallWidgetHook = callWidgetHook as jest.MockedFunction +const mockCallOnBeforeApprovalWidgetHook = callOnBeforeApprovalWidgetHook as jest.MockedFunction< + typeof callOnBeforeApprovalWidgetHook +> const mockUseShouldZeroApprove = useShouldZeroApprove as jest.MockedFunction const mockUseZeroApprove = useZeroApprove as jest.MockedFunction @@ -54,7 +56,7 @@ describe('useApproveCurrency', () => { mockUseTradeSpenderAddress.mockReturnValue(spenderAddress) mockUseWalletInfo.mockReturnValue({ account } as ReturnType) mockUseTradeApproveCallback.mockReturnValue(tradeApproveCallback) - mockCallWidgetHook.mockResolvedValue(true) + mockCallOnBeforeApprovalWidgetHook.mockResolvedValue(true) shouldZeroApprove.mockResolvedValue(false) mockUseShouldZeroApprove.mockReturnValue(shouldZeroApprove) mockUseZeroApprove.mockReturnValue(zeroApprove) @@ -66,18 +68,11 @@ describe('useApproveCurrency', () => { await result.current(approveAmount) await waitFor(() => { - expect(mockCallWidgetHook).toHaveBeenCalledWith('ON_BEFORE_APPROVAL', { - chainId: mockToken.chainId, - sellToken: expect.objectContaining({ - address: mockToken.address, - chainId: mockToken.chainId, - decimals: mockToken.decimals, - name: mockToken.name, - symbol: mockToken.symbol, - }), - sellAmount: approveAmount.toString(), - walletAddress: account, + expect(mockCallOnBeforeApprovalWidgetHook).toHaveBeenCalledWith({ + account, + amountToApprove, spenderAddress, + approvalAmount: approveAmount, }) expect(tradeApproveCallback).toHaveBeenCalledWith(approveAmount, { useModals: true, @@ -87,14 +82,14 @@ describe('useApproveCurrency', () => { }) it('does not run on-chain approval when widget hook blocks it', async () => { - mockCallWidgetHook.mockResolvedValue(false) + mockCallOnBeforeApprovalWidgetHook.mockResolvedValue(false) const { result } = renderHook(() => useApproveCurrency(amountToApprove, true)) await result.current(approveAmount) await waitFor(() => { - expect(mockCallWidgetHook).toHaveBeenCalled() + expect(mockCallOnBeforeApprovalWidgetHook).toHaveBeenCalled() expect(shouldZeroApprove).not.toHaveBeenCalled() expect(zeroApprove).not.toHaveBeenCalled() expect(tradeApproveCallback).not.toHaveBeenCalled() diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveCurrency.ts b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveCurrency.ts index 11ff2070712..9267a69dc97 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveCurrency.ts +++ b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveCurrency.ts @@ -1,15 +1,13 @@ import { useCallback } from 'react' import { useTradeSpenderAddress } from '@cowprotocol/balances-and-allowances' -import { currencyAmountToTokenAmount } from '@cowprotocol/common-utils' import { Currency, CurrencyAmount } from '@cowprotocol/currency' import { Nullish } from '@cowprotocol/types' import { useWalletInfo } from '@cowprotocol/wallet' -import { WidgetHookEvents } from '@cowprotocol/widget-lib' import type { SafeMultisigTransactionResponse } from '@safe-global/types-kit' import { GenerecTradeApproveResult, useTradeApproveCallback } from 'modules/erc20Approve' -import { callWidgetHook } from 'modules/injectedWidget' +import { callOnBeforeApprovalWidgetHook } from 'modules/injectedWidget' import { useShouldZeroApprove, useZeroApprove } from 'modules/zeroApproval' export type ApproveCurrencyCallback = ( @@ -32,17 +30,11 @@ export function useApproveCurrency( async (amount: bigint) => { if (!account || !tradeSpenderAddress || !amountToApprove) return null - const tokenAmount = currencyAmountToTokenAmount(amountToApprove) - const isWidgetHookPassed = await callWidgetHook(WidgetHookEvents.ON_BEFORE_APPROVAL, { - chainId: tokenAmount.currency.chainId, - sellToken: { - ...tokenAmount.currency, - name: tokenAmount.currency.name || '', - symbol: tokenAmount.currency.symbol || '', - }, - sellAmount: amount.toString(), - walletAddress: account, + const isWidgetHookPassed = await callOnBeforeApprovalWidgetHook({ + account, + amountToApprove, spenderAddress: tradeSpenderAddress, + approvalAmount: amount, }) if (!isWidgetHookPassed) return null diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveState.ts b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveState.ts index 3a515020fb0..7c41f9f2379 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveState.ts +++ b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveState.ts @@ -14,14 +14,17 @@ import { useTokenAllowance } from 'common/hooks/useTokenAllowance' import { ApprovalState } from '../types' import { getApprovalState } from '../utils' -export function useApproveState(amountToApprove: Nullish>): { +export function useApproveState( + amountToApprove: Nullish>, + spender?: string, +): { state: ApprovalState currentAllowance: Nullish } { const token = getCurrencyToApprove(amountToApprove) const tokenAddress = token?.address ? getAddressKey(token.address) : undefined - const currentAllowance = useTokenAllowance(token).data - const pendingApproval = useHasPendingApproval(tokenAddress) + const currentAllowance = useTokenAllowance(token, undefined, spender).data + const pendingApproval = useHasPendingApproval(tokenAddress, spender) const approvalStateBase = useSafeMemo(() => { return getApprovalState(amountToApprove, currentAllowance, pendingApproval) diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.test.ts b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.test.ts index 93b0694342e..5c843aebbd8 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.test.ts +++ b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.test.ts @@ -1,9 +1,12 @@ +import { useTradeSpenderAddress } from '@cowprotocol/balances-and-allowances' import { getWrappedToken } from '@cowprotocol/common-utils' import { CurrencyAmount, Token } from '@cowprotocol/currency' +import { DEFAULT_PERMIT_VALUE } from '@cowprotocol/permit-utils' import { useWalletInfo, WalletInfo } from '@cowprotocol/wallet' import { renderHook } from '@testing-library/react' +import { callOnBeforeApprovalWidgetHook } from 'modules/injectedWidget' import { IsTokenPermittableResult, useGeneratePermitHook, usePermitInfo } from 'modules/permit' import { TradeType } from 'modules/trade' @@ -11,6 +14,10 @@ import { useGeneratePermitInAdvanceToTrade } from './useGeneratePermitInAdvanceT import { useResetApproveProgressModalState, useUpdateApproveProgressModalState } from '../' +jest.mock('@cowprotocol/balances-and-allowances', () => ({ + useTradeSpenderAddress: jest.fn(), +})) + jest.mock('@cowprotocol/common-utils', () => ({ ...jest.requireActual('@cowprotocol/common-utils'), getWrappedToken: jest.fn(), @@ -25,6 +32,10 @@ jest.mock('modules/permit', () => ({ usePermitInfo: jest.fn(), })) +jest.mock('modules/injectedWidget', () => ({ + callOnBeforeApprovalWidgetHook: jest.fn(), +})) + jest.mock('modules/trade', () => ({ TradeType: { SWAP: 'SWAP', @@ -36,8 +47,12 @@ jest.mock('../', () => ({ useResetApproveProgressModalState: jest.fn(), })) +const mockUseTradeSpenderAddress = useTradeSpenderAddress as jest.MockedFunction const mockGetWrappedToken = getWrappedToken as jest.MockedFunction const mockUseWalletInfo = useWalletInfo as jest.MockedFunction +const mockCallOnBeforeApprovalWidgetHook = callOnBeforeApprovalWidgetHook as jest.MockedFunction< + typeof callOnBeforeApprovalWidgetHook +> const mockUseGeneratePermitHook = useGeneratePermitHook as jest.MockedFunction const mockUsePermitInfo = usePermitInfo as jest.MockedFunction const mockUseUpdateApproveProgressModalState = useUpdateApproveProgressModalState as jest.MockedFunction< @@ -47,6 +62,7 @@ const mockUseResetApproveProgressModalState = useResetApproveProgressModalState typeof useResetApproveProgressModalState > +// eslint-disable-next-line max-lines-per-function describe('useGeneratePermitInAdvanceToTrade', () => { const mockToken = new Token(1, '0x1234567890123456789012345678901234567890', 18, 'TEST', 'Test Token') const mockWrappedToken = new Token(1, '0x0987654321098765432109876543210987654321', 18, 'WETH', 'Wrapped Ether') @@ -55,14 +71,17 @@ describe('useGeneratePermitInAdvanceToTrade', () => { const mockPermitInfo = { type: 'eip-2612' as const } const mockUpdateApproveProgressModalState = jest.fn() const mockResetApproveProgressModalState = jest.fn() + const mockSpenderAddress = '0x9008D19f58AAbD9eD0D60971565AA8510560ab41' const mockGeneratePermit = jest.fn() beforeEach(() => { jest.clearAllMocks() + mockUseTradeSpenderAddress.mockReturnValue(mockSpenderAddress) mockGetWrappedToken.mockReturnValue(mockWrappedToken as unknown as ReturnType) mockUseWalletInfo.mockReturnValue({ account: mockAccount, chainId: 1 } as WalletInfo) + mockCallOnBeforeApprovalWidgetHook.mockResolvedValue(true) mockUseGeneratePermitHook.mockReturnValue(mockGeneratePermit) mockUsePermitInfo.mockReturnValue(mockPermitInfo) mockUseUpdateApproveProgressModalState.mockReturnValue(mockUpdateApproveProgressModalState) @@ -85,7 +104,7 @@ describe('useGeneratePermitInAdvanceToTrade', () => { it('should call usePermitInfo with wrapped token and SWAP trade type', () => { renderHook(() => useGeneratePermitInAdvanceToTrade(mockAmountToApprove)) - expect(mockUsePermitInfo).toHaveBeenCalledWith(mockWrappedToken, TradeType.SWAP) + expect(mockUsePermitInfo).toHaveBeenCalledWith(mockWrappedToken, TradeType.SWAP, mockSpenderAddress) }) }) @@ -98,6 +117,7 @@ describe('useGeneratePermitInAdvanceToTrade', () => { const result_value = await generatePermit() expect(result_value).toBe(false) + expect(mockCallOnBeforeApprovalWidgetHook).not.toHaveBeenCalled() expect(mockGeneratePermit).not.toHaveBeenCalled() }) @@ -109,6 +129,7 @@ describe('useGeneratePermitInAdvanceToTrade', () => { const result_value = await generatePermit() expect(result_value).toBe(false) + expect(mockCallOnBeforeApprovalWidgetHook).not.toHaveBeenCalled() expect(mockGeneratePermit).not.toHaveBeenCalled() }) @@ -120,6 +141,7 @@ describe('useGeneratePermitInAdvanceToTrade', () => { const result_value = await generatePermit() expect(result_value).toBe(false) + expect(mockCallOnBeforeApprovalWidgetHook).not.toHaveBeenCalled() expect(mockGeneratePermit).not.toHaveBeenCalled() }) @@ -133,6 +155,12 @@ describe('useGeneratePermitInAdvanceToTrade', () => { const result_value = await generatePermit() expect(result_value).toBe(true) + expect(mockCallOnBeforeApprovalWidgetHook).toHaveBeenCalledWith({ + account: mockAccount, + amountToApprove: mockAmountToApprove, + approvalAmount: BigInt(mockAmountToApprove.quotient.toString()), + spenderAddress: mockSpenderAddress, + }) expect(mockGeneratePermit).toHaveBeenCalledWith({ inputToken: { name: mockWrappedToken.name || '', @@ -141,11 +169,47 @@ describe('useGeneratePermitInAdvanceToTrade', () => { account: mockAccount, permitInfo: mockPermitInfo, amount: BigInt(mockAmountToApprove.quotient.toString()), + customSpender: mockSpenderAddress, preSignCallback: expect.any(Function), postSignCallback: expect.any(Function), }) }) + it('should stop before generating a permit when widget approval hook blocks it', async () => { + mockCallOnBeforeApprovalWidgetHook.mockResolvedValue(false) + + const { result } = renderHook(() => useGeneratePermitInAdvanceToTrade(mockAmountToApprove)) + + const generatePermit = result.current + const result_value = await generatePermit() + + expect(result_value).toBe(false) + expect(mockCallOnBeforeApprovalWidgetHook).toHaveBeenCalled() + expect(mockGeneratePermit).not.toHaveBeenCalled() + }) + + it('should report max approval amount to the widget hook for DAI-like permits', async () => { + mockUsePermitInfo.mockReturnValue({ type: 'dai-like' }) + mockGeneratePermit.mockResolvedValue({ signature: '0x123' }) + + const { result } = renderHook(() => useGeneratePermitInAdvanceToTrade(mockAmountToApprove)) + + await result.current() + + expect(mockCallOnBeforeApprovalWidgetHook).toHaveBeenCalledWith({ + account: mockAccount, + amountToApprove: mockAmountToApprove, + approvalAmount: DEFAULT_PERMIT_VALUE, + spenderAddress: mockSpenderAddress, + }) + expect(mockGeneratePermit).toHaveBeenCalledWith( + expect.objectContaining({ + amount: BigInt(mockAmountToApprove.quotient.toString()), + permitInfo: { type: 'dai-like' }, + }), + ) + }) + it('should return false when generatePermit returns null', async () => { mockGeneratePermit.mockResolvedValue(null) diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.ts b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.ts index 95bc25ddfa5..395f7dbfec0 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.ts +++ b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.ts @@ -1,9 +1,12 @@ import { useCallback } from 'react' +import { useTradeSpenderAddress } from '@cowprotocol/balances-and-allowances' import { getWrappedToken, isRejectRequestProviderError } from '@cowprotocol/common-utils' import { Currency, CurrencyAmount } from '@cowprotocol/currency' +import { DEFAULT_PERMIT_VALUE } from '@cowprotocol/permit-utils' import { useWalletInfo } from '@cowprotocol/wallet' +import { callOnBeforeApprovalWidgetHook } from 'modules/injectedWidget' import { useGeneratePermitHook, usePermitInfo } from 'modules/permit' import { TradeType } from 'modules/trade' @@ -14,12 +17,27 @@ export function useGeneratePermitInAdvanceToTrade(amountToApprove: CurrencyAmoun const updateApproveProgressModalState = useUpdateApproveProgressModalState() const resetApproveProgressModalState = useResetApproveProgressModalState() const { account } = useWalletInfo() + const tradeSpenderAddress = useTradeSpenderAddress() const token = getWrappedToken(amountToApprove.currency) - const permitInfo = usePermitInfo(token, TradeType.SWAP) + const permitInfo = usePermitInfo(token, TradeType.SWAP, tradeSpenderAddress) return useCallback(async () => { - if (!account || !permitInfo) return false + if (!account || !permitInfo || !tradeSpenderAddress) return false + + const permitAmount = BigInt(amountToApprove.quotient.toString()) + const hookApprovalAmount = permitInfo.type === 'dai-like' ? DEFAULT_PERMIT_VALUE : permitAmount + + const isWidgetHookPassed = await callOnBeforeApprovalWidgetHook({ + account, + amountToApprove, + spenderAddress: tradeSpenderAddress, + approvalAmount: hookApprovalAmount, + }) + + if (!isWidgetHookPassed) { + return false + } const preSignCallback = (): void => updateApproveProgressModalState({ @@ -33,7 +51,8 @@ export function useGeneratePermitInAdvanceToTrade(amountToApprove: CurrencyAmoun inputToken: { name: token.name || '', address: token.address as `0x${string}` }, account, permitInfo, - amount: BigInt(amountToApprove.quotient.toString()), + amount: permitAmount, + customSpender: tradeSpenderAddress, preSignCallback, postSignCallback: resetApproveProgressModalState, }) @@ -52,6 +71,7 @@ export function useGeneratePermitInAdvanceToTrade(amountToApprove: CurrencyAmoun generatePermit, permitInfo, resetApproveProgressModalState, + tradeSpenderAddress, token.address, token.name, updateApproveProgressModalState, diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/index.ts b/apps/cowswap-frontend/src/modules/injectedWidget/index.ts index 3838fb7c76b..746b7d8be6b 100644 --- a/apps/cowswap-frontend/src/modules/injectedWidget/index.ts +++ b/apps/cowswap-frontend/src/modules/injectedWidget/index.ts @@ -7,6 +7,7 @@ export { useInjectedWidgetPalette } from './hooks/useInjectedWidgetPalette' export { WidgetMarkdownContent } from './pure/WidgetMarkdownContent' export { callWidgetHook } from './services/callWidgetHook' +export { callOnBeforeApprovalWidgetHook } from './services/callOnBeforeApprovalWidgetHook' export { buildOrderWidgetHookPayload, buildOrdersWidgetHookPayload, diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/services/callOnBeforeApprovalWidgetHook.ts b/apps/cowswap-frontend/src/modules/injectedWidget/services/callOnBeforeApprovalWidgetHook.ts new file mode 100644 index 00000000000..8d3ca828617 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/injectedWidget/services/callOnBeforeApprovalWidgetHook.ts @@ -0,0 +1,33 @@ +import { currencyAmountToTokenAmount } from '@cowprotocol/common-utils' +import { Currency, CurrencyAmount } from '@cowprotocol/currency' +import { WidgetHookEvents } from '@cowprotocol/widget-lib' + +import { callWidgetHook } from './callWidgetHook' + +interface CallOnBeforeApprovalWidgetHookParams { + amountToApprove: CurrencyAmount + account: string + spenderAddress: string + approvalAmount?: bigint +} + +export function callOnBeforeApprovalWidgetHook({ + amountToApprove, + account, + spenderAddress, + approvalAmount, +}: CallOnBeforeApprovalWidgetHookParams): Promise { + const tokenAmount = currencyAmountToTokenAmount(amountToApprove) + + return callWidgetHook(WidgetHookEvents.ON_BEFORE_APPROVAL, { + chainId: tokenAmount.currency.chainId, + sellToken: { + ...tokenAmount.currency, + name: tokenAmount.currency.name || '', + symbol: tokenAmount.currency.symbol || '', + }, + sellAmount: (approvalAmount ?? BigInt(amountToApprove.quotient.toString())).toString(), + walletAddress: account, + spenderAddress, + }) +}