Skip to content

Commit 1380176

Browse files
committed
fix: route approvals through shared gate
1 parent 9a7cc91 commit 1380176

11 files changed

Lines changed: 235 additions & 50 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { useTradeSpenderAddress } from '@cowprotocol/balances-and-allowances'
2+
import { CurrencyAmount, Token } from '@cowprotocol/currency'
3+
4+
import { renderHook } from '@testing-library/react'
5+
import { SWRResponse } from 'swr'
6+
7+
import { useNeedsApproval } from './useNeedsApproval'
8+
import { useTokenAllowance } from './useTokenAllowance'
9+
10+
jest.mock('@cowprotocol/balances-and-allowances', () => ({
11+
useTradeSpenderAddress: jest.fn(),
12+
}))
13+
14+
jest.mock('./useTokenAllowance', () => ({
15+
useTokenAllowance: jest.fn(),
16+
}))
17+
18+
const mockUseTradeSpenderAddress = useTradeSpenderAddress as jest.MockedFunction<typeof useTradeSpenderAddress>
19+
const mockUseTokenAllowance = useTokenAllowance as jest.MockedFunction<typeof useTokenAllowance>
20+
21+
describe('useNeedsApproval', () => {
22+
const spender = '0x0000000000000000000000000000000000000001'
23+
const token = new Token(1, '0x1234567890123456789012345678901234567890', 18, 'TEST', 'Test Token')
24+
const amount = CurrencyAmount.fromRawAmount(token, '100')
25+
26+
function mockAllowance(data: bigint | undefined): void {
27+
mockUseTokenAllowance.mockReturnValue({ data } as SWRResponse<bigint | undefined>)
28+
}
29+
30+
beforeEach(() => {
31+
jest.clearAllMocks()
32+
mockUseTradeSpenderAddress.mockReturnValue(spender)
33+
mockAllowance(0n)
34+
})
35+
36+
it('returns false when amount is missing', () => {
37+
const { result } = renderHook(() => useNeedsApproval(null))
38+
39+
expect(result.current).toBe(false)
40+
})
41+
42+
it('returns false when spender is missing', () => {
43+
mockUseTradeSpenderAddress.mockReturnValue(undefined)
44+
45+
const { result } = renderHook(() => useNeedsApproval(amount))
46+
47+
expect(result.current).toBe(false)
48+
})
49+
50+
it('returns true when allowance is not loaded yet', () => {
51+
mockAllowance(undefined)
52+
53+
const { result } = renderHook(() => useNeedsApproval(amount))
54+
55+
expect(result.current).toBe(true)
56+
})
57+
58+
it('returns true when allowance is insufficient', () => {
59+
mockAllowance(99n)
60+
61+
const { result } = renderHook(() => useNeedsApproval(amount))
62+
63+
expect(result.current).toBe(true)
64+
})
65+
66+
it('returns false when allowance is sufficient', () => {
67+
mockAllowance(100n)
68+
69+
const { result } = renderHook(() => useNeedsApproval(amount))
70+
71+
expect(result.current).toBe(false)
72+
})
73+
})

apps/cowswap-frontend/src/common/hooks/useNeedsApproval.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,19 @@ import { useTokenAllowance } from './useTokenAllowance'
1919
* @param amount
2020
* @returns {boolean}
2121
*/
22-
export function useNeedsApproval(amount: Nullish<CurrencyAmount<Currency>>): boolean {
23-
const spender = useTradeSpenderAddress()
22+
export function useNeedsApproval(amount: Nullish<CurrencyAmount<Currency>>, spender?: string): boolean {
23+
const tradeSpender = useTradeSpenderAddress()
2424
const token = amount ? getWrappedToken(amount.currency) : undefined
25-
const allowance = useTokenAllowance(token)
25+
const approvalSpender = spender ?? tradeSpender
26+
const allowance = useTokenAllowance(token, undefined, approvalSpender)
2627

27-
if (typeof allowance === 'undefined') {
28-
return true
28+
if (!token || !amount || !approvalSpender) {
29+
return false
2930
}
3031

31-
if (!token || !amount || !spender) {
32-
return false
32+
if (allowance.data === undefined) {
33+
return true
3334
}
3435

35-
return isEnoughAmount(amount, allowance?.data) === false
36+
return isEnoughAmount(amount, allowance.data) === false
3637
}

apps/cowswap-frontend/src/legacy/state/enhancedTransactions/hooks/TransactionHooksMod.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,26 @@ export function useAllTransactions(): { [txHash: string]: EnhancedTransactionDet
1919
}
2020

2121
// returns whether a token has a pending approval transaction
22-
export function useHasPendingApproval(tokenAddress: string | undefined): boolean {
22+
export function useHasPendingApproval(tokenAddress: string | undefined, approvalSpender?: string): boolean {
2323
const allTransactions = useAllTransactions()
2424
const spender = useTradeSpenderAddress()
25+
const targetSpender = approvalSpender ?? spender
2526

2627
return useMemo(
2728
() =>
2829
typeof tokenAddress === 'string' &&
29-
typeof spender === 'string' &&
30+
typeof targetSpender === 'string' &&
3031
Object.keys(allTransactions).some((hash) => {
3132
const tx = allTransactions[hash]
3233
if (!tx || tx.receipt || tx.replacementType || tx.errorMessage) return false
3334

3435
const approval = tx.approval
3536
if (!approval) return false
3637

37-
return areAddressesEqual(approval.spender, spender) && areAddressesEqual(approval.tokenAddress, tokenAddress)
38+
return (
39+
areAddressesEqual(approval.spender, targetSpender) && areAddressesEqual(approval.tokenAddress, tokenAddress)
40+
)
3841
}),
39-
[allTransactions, spender, tokenAddress],
42+
[allTransactions, targetSpender, tokenAddress],
4043
)
4144
}

apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproval.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function useApprovalStateForSpender(
2424
const token = currency && !getIsNativeToken(currency) ? currency : undefined
2525

2626
const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
27-
const { state: approvalState } = useApproveState(amountToApprove)
27+
const { state: approvalState } = useApproveState(amountToApprove, spender)
2828

2929
return useMemo(() => {
3030
return { approvalState, currentAllowance: currentAllowance?.data }

apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveCurrency.test.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useWalletInfo } from '@cowprotocol/wallet'
55
import { renderHook, waitFor } from '@testing-library/react'
66

77
import { useTradeApproveCallback } from 'modules/erc20Approve'
8-
import { callWidgetHook } from 'modules/injectedWidget'
8+
import { callOnBeforeApprovalWidgetHook } from 'modules/injectedWidget'
99
import { useShouldZeroApprove, useZeroApprove } from 'modules/zeroApproval'
1010

1111
import { useApproveCurrency } from './useApproveCurrency'
@@ -23,7 +23,7 @@ jest.mock('modules/erc20Approve', () => ({
2323
}))
2424

2525
jest.mock('modules/injectedWidget', () => ({
26-
callWidgetHook: jest.fn(),
26+
callOnBeforeApprovalWidgetHook: jest.fn(),
2727
}))
2828

2929
jest.mock('modules/zeroApproval', () => ({
@@ -34,7 +34,9 @@ jest.mock('modules/zeroApproval', () => ({
3434
const mockUseTradeSpenderAddress = useTradeSpenderAddress as jest.MockedFunction<typeof useTradeSpenderAddress>
3535
const mockUseWalletInfo = useWalletInfo as jest.MockedFunction<typeof useWalletInfo>
3636
const mockUseTradeApproveCallback = useTradeApproveCallback as jest.MockedFunction<typeof useTradeApproveCallback>
37-
const mockCallWidgetHook = callWidgetHook as jest.MockedFunction<typeof callWidgetHook>
37+
const mockCallOnBeforeApprovalWidgetHook = callOnBeforeApprovalWidgetHook as jest.MockedFunction<
38+
typeof callOnBeforeApprovalWidgetHook
39+
>
3840
const mockUseShouldZeroApprove = useShouldZeroApprove as jest.MockedFunction<typeof useShouldZeroApprove>
3941
const mockUseZeroApprove = useZeroApprove as jest.MockedFunction<typeof useZeroApprove>
4042

@@ -54,7 +56,7 @@ describe('useApproveCurrency', () => {
5456
mockUseTradeSpenderAddress.mockReturnValue(spenderAddress)
5557
mockUseWalletInfo.mockReturnValue({ account } as ReturnType<typeof useWalletInfo>)
5658
mockUseTradeApproveCallback.mockReturnValue(tradeApproveCallback)
57-
mockCallWidgetHook.mockResolvedValue(true)
59+
mockCallOnBeforeApprovalWidgetHook.mockResolvedValue(true)
5860
shouldZeroApprove.mockResolvedValue(false)
5961
mockUseShouldZeroApprove.mockReturnValue(shouldZeroApprove)
6062
mockUseZeroApprove.mockReturnValue(zeroApprove)
@@ -66,18 +68,11 @@ describe('useApproveCurrency', () => {
6668
await result.current(approveAmount)
6769

6870
await waitFor(() => {
69-
expect(mockCallWidgetHook).toHaveBeenCalledWith('ON_BEFORE_APPROVAL', {
70-
chainId: mockToken.chainId,
71-
sellToken: expect.objectContaining({
72-
address: mockToken.address,
73-
chainId: mockToken.chainId,
74-
decimals: mockToken.decimals,
75-
name: mockToken.name,
76-
symbol: mockToken.symbol,
77-
}),
78-
sellAmount: approveAmount.toString(),
79-
walletAddress: account,
71+
expect(mockCallOnBeforeApprovalWidgetHook).toHaveBeenCalledWith({
72+
account,
73+
amountToApprove,
8074
spenderAddress,
75+
approvalAmount: approveAmount,
8176
})
8277
expect(tradeApproveCallback).toHaveBeenCalledWith(approveAmount, {
8378
useModals: true,
@@ -87,14 +82,14 @@ describe('useApproveCurrency', () => {
8782
})
8883

8984
it('does not run on-chain approval when widget hook blocks it', async () => {
90-
mockCallWidgetHook.mockResolvedValue(false)
85+
mockCallOnBeforeApprovalWidgetHook.mockResolvedValue(false)
9186

9287
const { result } = renderHook(() => useApproveCurrency(amountToApprove, true))
9388

9489
await result.current(approveAmount)
9590

9691
await waitFor(() => {
97-
expect(mockCallWidgetHook).toHaveBeenCalled()
92+
expect(mockCallOnBeforeApprovalWidgetHook).toHaveBeenCalled()
9893
expect(shouldZeroApprove).not.toHaveBeenCalled()
9994
expect(zeroApprove).not.toHaveBeenCalled()
10095
expect(tradeApproveCallback).not.toHaveBeenCalled()

apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveCurrency.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { useCallback } from 'react'
22

33
import { useTradeSpenderAddress } from '@cowprotocol/balances-and-allowances'
4-
import { currencyAmountToTokenAmount } from '@cowprotocol/common-utils'
54
import { Currency, CurrencyAmount } from '@cowprotocol/currency'
65
import { Nullish } from '@cowprotocol/types'
76
import { useWalletInfo } from '@cowprotocol/wallet'
8-
import { WidgetHookEvents } from '@cowprotocol/widget-lib'
97
import type { SafeMultisigTransactionResponse } from '@safe-global/types-kit'
108

119
import { GenerecTradeApproveResult, useTradeApproveCallback } from 'modules/erc20Approve'
12-
import { callWidgetHook } from 'modules/injectedWidget'
10+
import { callOnBeforeApprovalWidgetHook } from 'modules/injectedWidget'
1311
import { useShouldZeroApprove, useZeroApprove } from 'modules/zeroApproval'
1412

1513
export type ApproveCurrencyCallback = (
@@ -32,17 +30,11 @@ export function useApproveCurrency(
3230
async (amount: bigint) => {
3331
if (!account || !tradeSpenderAddress || !amountToApprove) return null
3432

35-
const tokenAmount = currencyAmountToTokenAmount(amountToApprove)
36-
const isWidgetHookPassed = await callWidgetHook(WidgetHookEvents.ON_BEFORE_APPROVAL, {
37-
chainId: tokenAmount.currency.chainId,
38-
sellToken: {
39-
...tokenAmount.currency,
40-
name: tokenAmount.currency.name || '',
41-
symbol: tokenAmount.currency.symbol || '',
42-
},
43-
sellAmount: amount.toString(),
44-
walletAddress: account,
33+
const isWidgetHookPassed = await callOnBeforeApprovalWidgetHook({
34+
account,
35+
amountToApprove,
4536
spenderAddress: tradeSpenderAddress,
37+
approvalAmount: amount,
4638
})
4739

4840
if (!isWidgetHookPassed) return null

apps/cowswap-frontend/src/modules/erc20Approve/hooks/useApproveState.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ import { useTokenAllowance } from 'common/hooks/useTokenAllowance'
1414
import { ApprovalState } from '../types'
1515
import { getApprovalState } from '../utils'
1616

17-
export function useApproveState(amountToApprove: Nullish<CurrencyAmount<Currency>>): {
17+
export function useApproveState(
18+
amountToApprove: Nullish<CurrencyAmount<Currency>>,
19+
spender?: string,
20+
): {
1821
state: ApprovalState
1922
currentAllowance: Nullish<bigint>
2023
} {
2124
const token = getCurrencyToApprove(amountToApprove)
2225
const tokenAddress = token?.address ? getAddressKey(token.address) : undefined
23-
const currentAllowance = useTokenAllowance(token).data
24-
const pendingApproval = useHasPendingApproval(tokenAddress)
26+
const currentAllowance = useTokenAllowance(token, undefined, spender).data
27+
const pendingApproval = useHasPendingApproval(tokenAddress, spender)
2528

2629
const approvalStateBase = useSafeMemo(() => {
2730
return getApprovalState(amountToApprove, currentAllowance, pendingApproval)

0 commit comments

Comments
 (0)