Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/cowswap-frontend/src/common/hooks/useNeedsApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ import { useTokenAllowance } from './useTokenAllowance'
* @param amount
* @returns {boolean}
*/
export function useNeedsApproval(amount: Nullish<CurrencyAmount<Currency>>): boolean {
const spender = useTradeSpenderAddress()
export function useNeedsApproval(amount: Nullish<CurrencyAmount<Currency>>, spender?: string): boolean {
const tradeSpender = useTradeSpenderAddress()
const token = amount ? getWrappedToken(amount.currency) : undefined
const allowance = useTokenAllowance(token)
const allowance = useTokenAllowance(token, undefined, spender ?? tradeSpender)

if (typeof allowance === 'undefined') {
return true
}

if (!token || !amount || !spender) {
if (!token || !amount || !(spender ?? tradeSpender)) {
return false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -23,7 +23,7 @@ jest.mock('modules/erc20Approve', () => ({
}))

jest.mock('modules/injectedWidget', () => ({
callWidgetHook: jest.fn(),
callOnBeforeApprovalWidgetHook: jest.fn(),
}))

jest.mock('modules/zeroApproval', () => ({
Expand All @@ -34,7 +34,9 @@ jest.mock('modules/zeroApproval', () => ({
const mockUseTradeSpenderAddress = useTradeSpenderAddress as jest.MockedFunction<typeof useTradeSpenderAddress>
const mockUseWalletInfo = useWalletInfo as jest.MockedFunction<typeof useWalletInfo>
const mockUseTradeApproveCallback = useTradeApproveCallback as jest.MockedFunction<typeof useTradeApproveCallback>
const mockCallWidgetHook = callWidgetHook as jest.MockedFunction<typeof callWidgetHook>
const mockCallOnBeforeApprovalWidgetHook = callOnBeforeApprovalWidgetHook as jest.MockedFunction<
typeof callOnBeforeApprovalWidgetHook
>
const mockUseShouldZeroApprove = useShouldZeroApprove as jest.MockedFunction<typeof useShouldZeroApprove>
const mockUseZeroApprove = useZeroApprove as jest.MockedFunction<typeof useZeroApprove>

Expand All @@ -54,7 +56,7 @@ describe('useApproveCurrency', () => {
mockUseTradeSpenderAddress.mockReturnValue(spenderAddress)
mockUseWalletInfo.mockReturnValue({ account } as ReturnType<typeof useWalletInfo>)
mockUseTradeApproveCallback.mockReturnValue(tradeApproveCallback)
mockCallWidgetHook.mockResolvedValue(true)
mockCallOnBeforeApprovalWidgetHook.mockResolvedValue(true)
shouldZeroApprove.mockResolvedValue(false)
mockUseShouldZeroApprove.mockReturnValue(shouldZeroApprove)
mockUseZeroApprove.mockReturnValue(zeroApprove)
Expand All @@ -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,
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = (
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import { useTokenAllowance } from 'common/hooks/useTokenAllowance'
import { ApprovalState } from '../types'
import { getApprovalState } from '../utils'

export function useApproveState(amountToApprove: Nullish<CurrencyAmount<Currency>>): {
export function useApproveState(
amountToApprove: Nullish<CurrencyAmount<Currency>>,
spender?: string,
): {
state: ApprovalState
currentAllowance: Nullish<bigint>
} {
const token = getCurrencyToApprove(amountToApprove)
const tokenAddress = token?.address ? getAddressKey(token.address) : undefined
const currentAllowance = useTokenAllowance(token).data
const currentAllowance = useTokenAllowance(token, undefined, spender).data
const pendingApproval = useHasPendingApproval(tokenAddress)

const approvalStateBase = useSafeMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { useTradeSpenderAddress } from '@cowprotocol/balances-and-allowances'
import { getWrappedToken } from '@cowprotocol/common-utils'
import { CurrencyAmount, Token } from '@cowprotocol/currency'
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'

import { useGeneratePermitInAdvanceToTrade } from './useGeneratePermitInAdvanceToTrade'

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(),
Expand All @@ -25,6 +31,10 @@ jest.mock('modules/permit', () => ({
usePermitInfo: jest.fn(),
}))

jest.mock('modules/injectedWidget', () => ({
callOnBeforeApprovalWidgetHook: jest.fn(),
}))

jest.mock('modules/trade', () => ({
TradeType: {
SWAP: 'SWAP',
Expand All @@ -36,8 +46,12 @@ jest.mock('../', () => ({
useResetApproveProgressModalState: jest.fn(),
}))

const mockUseTradeSpenderAddress = useTradeSpenderAddress as jest.MockedFunction<typeof useTradeSpenderAddress>
const mockGetWrappedToken = getWrappedToken as jest.MockedFunction<typeof getWrappedToken>
const mockUseWalletInfo = useWalletInfo as jest.MockedFunction<typeof useWalletInfo>
const mockCallOnBeforeApprovalWidgetHook = callOnBeforeApprovalWidgetHook as jest.MockedFunction<
typeof callOnBeforeApprovalWidgetHook
>
const mockUseGeneratePermitHook = useGeneratePermitHook as jest.MockedFunction<typeof useGeneratePermitHook>
const mockUsePermitInfo = usePermitInfo as jest.MockedFunction<typeof usePermitInfo>
const mockUseUpdateApproveProgressModalState = useUpdateApproveProgressModalState as jest.MockedFunction<
Expand All @@ -47,6 +61,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')
Expand All @@ -55,14 +70,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<typeof getWrappedToken>)
mockUseWalletInfo.mockReturnValue({ account: mockAccount, chainId: 1 } as WalletInfo)
mockCallOnBeforeApprovalWidgetHook.mockResolvedValue(true)
mockUseGeneratePermitHook.mockReturnValue(mockGeneratePermit)
mockUsePermitInfo.mockReturnValue(mockPermitInfo)
mockUseUpdateApproveProgressModalState.mockReturnValue(mockUpdateApproveProgressModalState)
Expand All @@ -85,7 +103,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)
})
})

Expand All @@ -98,6 +116,7 @@ describe('useGeneratePermitInAdvanceToTrade', () => {
const result_value = await generatePermit()

expect(result_value).toBe(false)
expect(mockCallOnBeforeApprovalWidgetHook).not.toHaveBeenCalled()
expect(mockGeneratePermit).not.toHaveBeenCalled()
})

Expand All @@ -109,6 +128,7 @@ describe('useGeneratePermitInAdvanceToTrade', () => {
const result_value = await generatePermit()

expect(result_value).toBe(false)
expect(mockCallOnBeforeApprovalWidgetHook).not.toHaveBeenCalled()
expect(mockGeneratePermit).not.toHaveBeenCalled()
})

Expand All @@ -120,6 +140,7 @@ describe('useGeneratePermitInAdvanceToTrade', () => {
const result_value = await generatePermit()

expect(result_value).toBe(false)
expect(mockCallOnBeforeApprovalWidgetHook).not.toHaveBeenCalled()
expect(mockGeneratePermit).not.toHaveBeenCalled()
})

Expand All @@ -133,6 +154,11 @@ describe('useGeneratePermitInAdvanceToTrade', () => {
const result_value = await generatePermit()

expect(result_value).toBe(true)
expect(mockCallOnBeforeApprovalWidgetHook).toHaveBeenCalledWith({
account: mockAccount,
amountToApprove: mockAmountToApprove,
spenderAddress: mockSpenderAddress,
})
expect(mockGeneratePermit).toHaveBeenCalledWith({
inputToken: {
name: mockWrappedToken.name || '',
Expand All @@ -141,11 +167,25 @@ 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 return false when generatePermit returns null', async () => {
mockGeneratePermit.mockResolvedValue(null)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
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 { useWalletInfo } from '@cowprotocol/wallet'

import { callOnBeforeApprovalWidgetHook } from 'modules/injectedWidget'
import { useGeneratePermitHook, usePermitInfo } from 'modules/permit'
import { TradeType } from 'modules/trade'

Expand All @@ -14,12 +16,23 @@ 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 isWidgetHookPassed = await callOnBeforeApprovalWidgetHook({
account,
amountToApprove,
spenderAddress: tradeSpenderAddress,
})

if (!isWidgetHookPassed) {
return false
}

const preSignCallback = (): void =>
updateApproveProgressModalState({
Expand All @@ -34,6 +47,7 @@ export function useGeneratePermitInAdvanceToTrade(amountToApprove: CurrencyAmoun
account,
permitInfo,
amount: BigInt(amountToApprove.quotient.toString()),
customSpender: tradeSpenderAddress,
preSignCallback,
postSignCallback: resetApproveProgressModalState,
})
Expand All @@ -52,6 +66,7 @@ export function useGeneratePermitInAdvanceToTrade(amountToApprove: CurrencyAmoun
generatePermit,
permitInfo,
resetApproveProgressModalState,
tradeSpenderAddress,
token.address,
token.name,
updateApproveProgressModalState,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { getApprovalState } from './getApprovalState'
export { getIsTradeApproveResult } from './getIsTradeApproveResult'
export * from './isMaxAmountToApprove'
Loading
Loading