Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a641192
feat(approve): adjust partial approves for buy orders
shoom3301 Oct 16, 2025
933423f
fix: handle failed approve tx
shoom3301 Oct 16, 2025
e9f8d1f
chore: fix TradeAllowanceDisplay conditions
shoom3301 Oct 16, 2025
2f54b5a
test: add tests for useTradeApproveCallback
shoom3301 Oct 16, 2025
f583a9b
feat(approve): add optimistic allowance amount
shoom3301 Oct 16, 2025
62f2680
fix(approve): do not go to confirm if not enough allowance
shoom3301 Oct 16, 2025
37d07f1
chore: fix optimisticAllowance
shoom3301 Oct 16, 2025
70e6b8e
chore: fix code duple
shoom3301 Oct 16, 2025
fd9ad88
Merge branch 'develop' into fix/partial-approve-buy-order
shoom3301 Oct 16, 2025
c0fd13c
chore: remove mutation
shoom3301 Oct 16, 2025
e2ea1af
Merge remote-tracking branch 'origin/fix/partial-approve-buy-order' i…
shoom3301 Oct 16, 2025
33e4515
chore: fix isApprovedAmountSufficient handling
shoom3301 Oct 16, 2025
dce2fe7
chore: fix tests
shoom3301 Oct 16, 2025
9562f5f
chore: merge develop
shoom3301 Oct 16, 2025
e4f31a9
chore: merge with eth-flow changes
shoom3301 Oct 16, 2025
54ad06d
chore: fix tests
shoom3301 Oct 16, 2025
32829b2
fix: conflicts
limitofzero Oct 19, 2025
87ca67a
fix: conflicts and tests
limitofzero Oct 19, 2025
aa5b50b
Merge branch 'develop' of https://github.com/cowprotocol/cowswap into…
shoom3301 Oct 20, 2025
88466bb
chore: fix build
shoom3301 Oct 20, 2025
bb283e3
Merge branch 'develop' into fix/partial-approve-buy-order
shoom3301 Oct 20, 2025
80b43bc
chore: fix confirm button text
shoom3301 Oct 20, 2025
a4582b9
fix(approve): fix approve button text
shoom3301 Oct 20, 2025
3113335
Merge branch 'develop' into fix/partial-approve-buy-order
shoom3301 Oct 20, 2025
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
72 changes: 44 additions & 28 deletions apps/cowswap-frontend/src/common/hooks/useTokenAllowance.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import { atom, useAtom, useSetAtom } from 'jotai'
import { useCallback, useEffect } from 'react'
import { useAtom } from 'jotai'
import { useEffect, useMemo } from 'react'

import { useTradeSpenderAddress } from '@cowprotocol/balances-and-allowances'
import { SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const'
import { useWalletInfo } from '@cowprotocol/wallet'
import { Token } from '@uniswap/sdk-core'

import { optimisticAllowancesAtom } from 'entities/optimisticAllowance/optimisticAllowancesAtom'
import ms from 'ms.macro'
import useSWR, { SWRConfiguration, SWRResponse } from 'swr'

import { useTokenContract } from 'common/hooks/useContract'

import { getOptimisticAllowanceKey } from '../../entities/optimisticAllowance/getOptimisticAllowanceKey'

interface LastApproveParams {
blockNumber: number
tokenAddress: string
}

const lastApproveTxBlockNumberAtom = atom<Record<string, number>>({})
const OPTIMISTIC_ALLOWANCE_TTL = ms`30s`

const SWR_OPTIONS: SWRConfiguration = {
...SWR_NO_REFRESH_OPTIONS,
Expand All @@ -35,38 +32,57 @@ export function useTokenAllowance(
const { chainId, account } = useWalletInfo()
const { contract: erc20Contract } = useTokenContract(tokenAddress)
const tradeSpender = useTradeSpenderAddress()
const [lastApproveTx, setLastApproveTx] = useAtom(lastApproveTxBlockNumberAtom)
const [optimisticAllowances, setOptimisticAllowances] = useAtom(optimisticAllowancesAtom)

const targetOwner = owner ?? account
const targetSpender = spender ?? tradeSpender
const lastApproveBlockNumber = tokenAddress ? lastApproveTx[tokenAddress.toLowerCase()] : undefined

// Reset lastApproveTxBlockNumberAtom on network changes
useEffect(() => {
setLastApproveTx({})
}, [chainId, setLastApproveTx])
const optimisticAllowanceKey = useMemo(() => {
if (!tokenAddress || !targetOwner || !targetSpender) return null
return getOptimisticAllowanceKey({ chainId, tokenAddress, owner: targetOwner, spender: targetSpender })
}, [chainId, tokenAddress, targetOwner, targetSpender])

return useSWR(
const optimisticAllowance = optimisticAllowanceKey ? optimisticAllowances[optimisticAllowanceKey] : undefined

// Important! Do not add erc20Contract to SWR deps, otherwise it will do unwanted node RPC calls!
const swrResponse = useSWR(
erc20Contract && targetOwner && targetSpender
? [erc20Contract, targetOwner, targetSpender, chainId, lastApproveBlockNumber, 'useTokenAllowance']
? [targetOwner, targetSpender, chainId, tokenAddress, 'useTokenAllowance']
: null,
([erc20Contract, targetOwner, targetSpender]) => {
([targetOwner, targetSpender]) => {
if (!erc20Contract) return undefined

return erc20Contract.allowance(targetOwner, targetSpender).then((result) => result.toBigInt())
},
SWR_OPTIONS,
)
}

export function useUpdateLastApproveTxBlockNumber(): (params: LastApproveParams) => void {
const setState = useSetAtom(lastApproveTxBlockNumberAtom)
// Reset state on network changes
useEffect(() => {
setOptimisticAllowances({})
}, [chainId, setOptimisticAllowances])

return useCallback(
(params: LastApproveParams) => {
setState((state) => ({
...state,
[params.tokenAddress.toLowerCase()]: params.blockNumber,
}))
},
[setState],
// Clean up expired optimistic allowances
useEffect(() => {
const now = Date.now()
const expiredKeys = Object.keys(optimisticAllowances).filter(
(key) => now - optimisticAllowances[key].timestamp > OPTIMISTIC_ALLOWANCE_TTL,
)

if (expiredKeys.length > 0) {
setOptimisticAllowances((state) => {
const newState = { ...state }
expiredKeys.forEach((key) => delete newState[key])
return newState
})
}
}, [optimisticAllowances, setOptimisticAllowances, swrResponse.data])

return useMemo(
() => ({
...swrResponse,
data: optimisticAllowance?.amount ?? swrResponse.data,
}),
[optimisticAllowance?.amount, swrResponse],
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SetOptimisticAllowanceParams } from './useSetOptimisticAllowance'

export function getOptimisticAllowanceKey(
params: Omit<SetOptimisticAllowanceParams, 'amount' | 'blockNumber'>,
): string {
return `${params.chainId}-${params.tokenAddress.toLowerCase()}-${params.owner.toLowerCase()}-${params.spender.toLowerCase()}`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { atom } from 'jotai'

import { OptimisticAllowance } from './types'

export const optimisticAllowancesAtom = atom<Record<string, OptimisticAllowance>>({})
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface OptimisticAllowance {
amount: bigint
blockNumber: number
timestamp: number
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useSetAtom } from 'jotai'
import { useCallback } from 'react'

import { getOptimisticAllowanceKey } from './getOptimisticAllowanceKey'
import { optimisticAllowancesAtom } from './optimisticAllowancesAtom'

export interface SetOptimisticAllowanceParams {
tokenAddress: string
owner: string
spender: string
amount: bigint
blockNumber: number
chainId: number
}

export function useSetOptimisticAllowance(): (params: SetOptimisticAllowanceParams) => void {
const setOptimisticAllowances = useSetAtom(optimisticAllowancesAtom)

return useCallback(
(params: SetOptimisticAllowanceParams) => {
const key = getOptimisticAllowanceKey(params)

// Set optimistic allowance immediately
setOptimisticAllowances((state) => ({
...state,
[key]: {
amount: params.amount,
blockNumber: params.blockNumber,
timestamp: Date.now(),
},
}))
},
[setOptimisticAllowances],
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export function OrderPartialApprove({
<ActiveOrdersWithAffectedPermit orderId={orderId} currency={amountToApprove.currency} />
)}
<TradeApproveButton
ignorePermit
enablePartialApprove
useModals={false}
amountToApprove={amountToApproveFinal}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,16 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core'

import { Trans } from '@lingui/macro'

import { useTokenSupportsPermit } from 'modules/permit'
import { TradeType } from 'modules/trade'
import { useHasCachedPermit } from 'modules/permit'
import { useIsCurrentTradeBridging } from 'modules/trade'

import { useOnApproveClick } from './hooks/useOnApproveClick'
import * as styledEl from './styled'
import { ButtonWrapper } from './styled'

import { MAX_APPROVE_AMOUNT } from '../../constants'
import { useApprovalStateForSpender, useApproveCurrency, useGeneratePermitInAdvanceToTrade } from '../../hooks'
import { useApprovalStateForSpender, useApproveCurrency } from '../../hooks'
import { useApproveAndSwap } from '../../hooks/useApproveAndSwap'
import { LegacyApproveButton } from '../../pure/LegacyApproveButton'
import { useIsPartialApproveSelectedByUser } from '../../state'
import { ApprovalState } from '../../types'

export interface TradeApproveButtonProps {
Expand All @@ -26,8 +25,7 @@ export interface TradeApproveButtonProps {
isDisabled?: boolean
enablePartialApprove?: boolean
onApproveConfirm?: (txHash?: string) => void
ignorePermit?: boolean
label: string
label?: string
buttonSize?: ButtonSize
useModals?: boolean
}
Expand All @@ -37,31 +35,18 @@ export function TradeApproveButton(props: TradeApproveButtonProps): ReactNode {
amountToApprove,
children,
enablePartialApprove,
onApproveConfirm,
label,
ignorePermit,
isDisabled,
buttonSize = ButtonSize.DEFAULT,
useModals = true,
} = props
const isPartialApproveEnabledByUser = useIsPartialApproveSelectedByUser()
const handleApprove = useApproveCurrency(amountToApprove, useModals)

const spender = useTradeSpenderAddress()
const isCurrentTradeBridging = useIsCurrentTradeBridging()
const { approvalState } = useApprovalStateForSpender(amountToApprove, spender)
const isPermitSupported = useTokenSupportsPermit(amountToApprove.currency, TradeType.SWAP) && !ignorePermit
const generatePermitToTrade = useGeneratePermitInAdvanceToTrade(amountToApprove)

const approveAndSwap = useOnApproveClick({
isPermitSupported,
onApproveConfirm,
isPartialApproveEnabledByUser,
amountToApprove,
handleApprove,
generatePermitToTrade,
})

const approveAndSwap = useApproveAndSwap(props)
const approveWithPreventedDoubleExecution = usePreventDoubleExecution(approveAndSwap)
const { data: cachedPermit, isLoading: cachedPermitLoading } = useHasCachedPermit(amountToApprove)

if (!enablePartialApprove) {
return (
Expand All @@ -78,6 +63,10 @@ export function TradeApproveButton(props: TradeApproveButtonProps): ReactNode {
}

const isPending = approvalState === ApprovalState.PENDING
const noCachedPermit = !cachedPermitLoading && !cachedPermit

const label =
props.label || (noCachedPermit ? (isCurrentTradeBridging ? 'Approve, Swap & Bridge' : 'Approve and Swap') : 'Swap')

return (
<ButtonWrapper
Expand All @@ -88,18 +77,20 @@ export function TradeApproveButton(props: TradeApproveButtonProps): ReactNode {
>
<styledEl.ButtonLabelWrapper buttonSize={buttonSize}>
{label}{' '}
<HoverTooltip
wrapInContainer
content={
<Trans>
You must give the CoW Protocol smart contracts permission to use your{' '}
<TokenSymbol token={amountToApprove.currency} />. If you approve the default amount, you will only have to
do this once per token.
</Trans>
}
>
{isPending ? <styledEl.StyledLoader /> : <styledEl.StyledAlert size={24} />}
</HoverTooltip>
{noCachedPermit ? (
<HoverTooltip
wrapInContainer
content={
<Trans>
You must give the CoW Protocol smart contracts permission to use your{' '}
<TokenSymbol token={amountToApprove.currency} />. If you approve the default amount, you will only have
to do this once per token.
</Trans>
}
>
{isPending ? <styledEl.StyledLoader /> : <styledEl.StyledAlert size={24} />}
</HoverTooltip>
) : null}
</styledEl.ButtonLabelWrapper>
</ButtonWrapper>
)
Expand Down
Loading