Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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