Skip to content

Commit 593ad71

Browse files
committed
fix: gate approval entrypoints
1 parent b05c1b3 commit 593ad71

12 files changed

Lines changed: 160 additions & 27 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { getApprovalState } from './getApprovalState'
2+
export { getIsTradeApproveResult } from './getIsTradeApproveResult'
23
export * from './isMaxAmountToApprove'

apps/cowswap-frontend/src/modules/ethFlow/containers/EthFlow/hooks/useEthFlowActions.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import { useWalletInfo } from '@cowprotocol/wallet'
88
import { WrapUnwrapCallback } from 'legacy/hooks/useWrapCallback'
99
import { Field } from 'legacy/state/types'
1010

11-
import { MAX_APPROVE_AMOUNT, TradeApproveCallback } from 'modules/erc20Approve'
11+
import { ApproveCurrencyCallback, getIsTradeApproveResult, MAX_APPROVE_AMOUNT } from 'modules/erc20Approve'
1212
import { useIsInfiniteApproveDisabledInWidget } from 'modules/injectedWidget'
13-
import { useSwapPartialApprovalToggleState } from 'modules/swap/hooks/useSwapSettings'
13+
import { useSwapPartialApprovalToggleState } from 'modules/swap'
1414
import { useOnCurrencySelection, useTradeConfirmActions } from 'modules/trade'
1515

1616
import { updateEthFlowContextAtom } from '../../../state/ethFlowContextAtom'
1717

1818
export interface EthFlowActionCallbacks {
19-
approve: TradeApproveCallback
19+
approve: ApproveCurrencyCallback
2020
wrap: WrapUnwrapCallback | null
2121
directSwap: Command
2222
dismiss: Command
@@ -77,10 +77,15 @@ export function useEthFlowActions(callbacks: EthFlowActionCallbacks, amountToApp
7777

7878
return sendTransaction('approve', () => {
7979
return callbacks.approve(unitsToApprove).then((res): string | undefined => {
80-
const tx = res?.txResponse
81-
return (tx && 'transactionHash' in tx ? tx.transactionHash : (tx as { hash?: string })?.hash) as
82-
| string
83-
| undefined
80+
if (!res) return undefined
81+
82+
if (getIsTradeApproveResult(res)) {
83+
const tx = res.txResponse
84+
85+
return 'transactionHash' in tx ? tx.transactionHash : tx.hash
86+
}
87+
88+
return res.transactionHash || undefined
8489
})
8590
})
8691
}

apps/cowswap-frontend/src/modules/ethFlow/containers/EthFlow/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import { useSingleActivityDescriptor } from 'legacy/hooks/useRecentActivity'
1111
import { WrapUnwrapCallback } from 'legacy/hooks/useWrapCallback'
1212

1313
import {
14+
useApproveCurrency,
1415
useApproveState,
1516
useIsPartialApproveSelectedByUser,
1617
usePartialApproveAmountModalState,
17-
useTradeApproveCallback,
1818
useUpdatePartialApproveAmountModalState,
1919
} from 'modules/erc20Approve'
2020
import { useWrappedToken } from 'modules/trade'
@@ -25,6 +25,7 @@ import { useEthFlowActions } from './hooks/useEthFlowActions'
2525
import useRemainingNativeTxsAndCosts from './hooks/useRemainingNativeTxsAndCosts'
2626
import { useSetupEthFlow } from './hooks/useSetupEthFlow'
2727
import { getDerivedEthFlowState } from './utils/getDerivedEthFlowState'
28+
import { getEthFlowCurrencyToApprove } from './utils/getEthFlowCurrencyToApprove'
2829

2930
import { EthFlowModalContent } from '../../pure/EthFlowModalContent'
3031
import { WrappingPreviewProps } from '../../pure/WrappingPreview'
@@ -61,9 +62,13 @@ export function EthFlowModal({
6162
const { amountSetByUser } = usePartialApproveAmountModalState() || {}
6263
const updatePartialApproveAmountModalState = useUpdatePartialApproveAmountModalState()
6364
const isPartialApproveSelectedByUser = useIsPartialApproveSelectedByUser()
64-
const currencyToApprove = isPartialApproveSelectedByUser ? (amountSetByUser ?? wrappedAmount) : undefined
65+
const currencyToApprove = getEthFlowCurrencyToApprove({
66+
amountSetByUser,
67+
isPartialApproveSelectedByUser,
68+
wrappedAmount,
69+
})
6570

66-
const approveCallback = useTradeApproveCallback(wrapped)
71+
const approveCallback = useApproveCurrency(wrappedAmount ?? undefined, true)
6772

6873
const ethFlowActions = useEthFlowActions(
6974
{
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { CurrencyAmount, Token } from '@cowprotocol/currency'
2+
3+
import { getEthFlowCurrencyToApprove } from './getEthFlowCurrencyToApprove'
4+
5+
describe('getEthFlowCurrencyToApprove', () => {
6+
const wrappedNative = new Token(1, '0x1111111111111111111111111111111111111111', 18, 'WETH', 'Wrapped Ether')
7+
const otherToken = new Token(1, '0x2222222222222222222222222222222222222222', 18, 'TEST', 'Test Token')
8+
9+
const wrappedAmount = CurrencyAmount.fromRawAmount(wrappedNative, '100')
10+
const sameCurrencyAmountSetByUser = CurrencyAmount.fromRawAmount(wrappedNative, '25')
11+
const staleAmountSetByUser = CurrencyAmount.fromRawAmount(otherToken, '25')
12+
13+
it('returns undefined when partial approval is not selected', () => {
14+
expect(
15+
getEthFlowCurrencyToApprove({
16+
amountSetByUser: sameCurrencyAmountSetByUser,
17+
isPartialApproveSelectedByUser: false,
18+
wrappedAmount,
19+
}),
20+
).toBeUndefined()
21+
})
22+
23+
it('uses the current wrapped amount when the user has not set a custom amount', () => {
24+
expect(
25+
getEthFlowCurrencyToApprove({
26+
amountSetByUser: undefined,
27+
isPartialApproveSelectedByUser: true,
28+
wrappedAmount,
29+
}),
30+
).toBe(wrappedAmount)
31+
})
32+
33+
it('uses the user amount when it belongs to the current wrapped token', () => {
34+
expect(
35+
getEthFlowCurrencyToApprove({
36+
amountSetByUser: sameCurrencyAmountSetByUser,
37+
isPartialApproveSelectedByUser: true,
38+
wrappedAmount,
39+
}),
40+
).toBe(sameCurrencyAmountSetByUser)
41+
})
42+
43+
it('ignores a stale user amount from a different token', () => {
44+
expect(
45+
getEthFlowCurrencyToApprove({
46+
amountSetByUser: staleAmountSetByUser,
47+
isPartialApproveSelectedByUser: true,
48+
wrappedAmount,
49+
}),
50+
).toBe(wrappedAmount)
51+
})
52+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Currency, CurrencyAmount } from '@cowprotocol/currency'
2+
3+
interface GetEthFlowCurrencyToApproveParams {
4+
amountSetByUser: CurrencyAmount<Currency> | undefined
5+
isPartialApproveSelectedByUser: boolean
6+
wrappedAmount: CurrencyAmount<Currency> | null
7+
}
8+
9+
export function getEthFlowCurrencyToApprove({
10+
amountSetByUser,
11+
isPartialApproveSelectedByUser,
12+
wrappedAmount,
13+
}: GetEthFlowCurrencyToApproveParams): CurrencyAmount<Currency> | undefined {
14+
if (!isPartialApproveSelectedByUser) return undefined
15+
if (!wrappedAmount) return undefined
16+
if (!amountSetByUser) return wrappedAmount
17+
18+
return amountSetByUser.currency.equals(wrappedAmount.currency) ? amountSetByUser : wrappedAmount
19+
}

apps/cowswap-frontend/src/modules/limitOrders/services/safeBundleFlow/index.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { partialOrderUpdate } from 'legacy/state/orders/utils'
1313
import { mapUnsignedOrderToOrder, wrapErrorInOperatorError } from 'legacy/utils/trade'
1414

1515
import { removePermitHookFromAppData } from 'modules/appData'
16+
import { callOnBeforeApprovalWidgetHook } from 'modules/injectedWidget'
1617
import { LOW_RATE_THRESHOLD_PERCENT } from 'modules/limitOrders/const/trade'
1718
import { PriceImpactDeclineError, SafeBundleFlowContext } from 'modules/limitOrders/services/types'
1819
import { LimitOrdersSettingsState } from 'modules/limitOrders/state/limitOrdersSettingsAtom'
@@ -47,7 +48,7 @@ export async function safeBundleFlow({
4748
analytics: TradeFlowAnalytics
4849
beforeTrade?: Command
4950
config: Config
50-
}): Promise<string> {
51+
}): Promise<string | undefined> {
5152
logTradeFlow(LOG_PREFIX, 'STEP 1: confirm price impact')
5253
const isTooLowRate = params.rateImpact < LOW_RATE_THRESHOLD_PERCENT
5354

@@ -68,15 +69,26 @@ export async function safeBundleFlow({
6869
orderType: UiOrderType.LIMIT,
6970
}
7071

71-
logTradeFlow(LOG_PREFIX, 'STEP 2: send transaction')
72-
analytics.approveAndPresign(swapFlowAnalyticsContext)
73-
beforeTrade?.()
74-
7572
const { chainId, postOrderParams, spender, dispatch, sendBatchTransactions } = params
7673

7774
const validTo = calculateLimitOrdersDeadline(settingsState, params.quoteState)
7875

7976
try {
77+
const isWidgetHookPassed = await callOnBeforeApprovalWidgetHook({
78+
account,
79+
amountToApprove: inputAmount,
80+
spenderAddress: spender,
81+
approvalAmount: maxUint256,
82+
})
83+
84+
if (!isWidgetHookPassed) {
85+
return undefined
86+
}
87+
88+
logTradeFlow(LOG_PREFIX, 'STEP 2: send transaction')
89+
analytics.approveAndPresign(swapFlowAnalyticsContext)
90+
beforeTrade?.()
91+
8092
// For now, bundling ALWAYS includes 2 steps: approve and presign.
8193
// In the feature users will be able to sort/add steps as they see fit
8294
logTradeFlow(LOG_PREFIX, 'STEP 2: build approval tx')

apps/cowswap-frontend/src/modules/swap/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { useSwapRawState } from './hooks/useSwapRawState'
44
export { useSwapFlowContext } from './hooks/useSwapFlowContext'
55
export { useUpdateSwapRawState } from './hooks/useUpdateSwapRawState'
66
export { useSwapDerivedStateToFill } from './hooks/useSwapDerivedState'
7+
export { useSwapPartialApprovalToggleState } from './hooks/useSwapSettings'
78
export { SwapUpdaters } from './updaters'
89
export { swapDerivedStateAtom } from './state/swapRawStateAtom'
910
export { DeprecatedNetworkBanner } from './containers/DeprecatedNetworkBanner/DeprecatedNetworkBanner.container'

apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function useSafeBundleFlowContext(): SafeBundleFlowContext | null {
2222
// todo check for safe wallet
2323
const { maximumSendSellAmount } = useAmountsToSignFromQuote() || {}
2424

25-
const needsApproval = useNeedsApproval(maximumSendSellAmount)
25+
const needsApproval = useNeedsApproval(maximumSendSellAmount, spender)
2626
const tokenAddress = useMemo(() => {
2727
return maximumSendSellAmount ? getCurrencyAddress(maximumSendSellAmount.currency) : undefined
2828
}, [maximumSendSellAmount])

apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { partialOrderUpdate } from 'legacy/state/orders/utils'
1212
import { mapUnsignedOrderToOrder, wrapErrorInOperatorError } from 'legacy/utils/trade'
1313

1414
import { removePermitHookFromAppData } from 'modules/appData'
15+
import { callOnBeforeApprovalWidgetHook } from 'modules/injectedWidget'
1516
import { buildApproveTx } from 'modules/operations/bundle/buildApproveTx'
1617
import { buildZeroApproveTx } from 'modules/operations/bundle/buildZeroApproveTx'
1718
import { emitPostedOrderEvent } from 'modules/orders'
@@ -68,10 +69,20 @@ export async function safeBundleApprovalFlow({
6869
const tradeAmounts = { inputAmount, outputAmount }
6970
const isBridgingOrder = inputAmount.currency.chainId !== outputAmount.currency.chainId
7071

71-
analytics.approveAndPresign(swapFlowAnalyticsContext)
72-
tradeConfirmActions.onSign(tradeAmounts)
73-
7472
try {
73+
const isWidgetHookPassed = await callOnBeforeApprovalWidgetHook({
74+
account,
75+
amountToApprove,
76+
spenderAddress: spender,
77+
})
78+
79+
if (!isWidgetHookPassed) {
80+
return false
81+
}
82+
83+
analytics.approveAndPresign(swapFlowAnalyticsContext)
84+
tradeConfirmActions.onSign(tradeAmounts)
85+
7586
// For now, bundling ALWAYS includes 2 steps: approve and presign.
7687
// In the feature users will be able to sort/add steps as they see fit
7788
logTradeFlow(LOG_PREFIX, 'STEP 2: build approval tx')

apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { partialOrderUpdate } from 'legacy/state/orders/utils'
1212
import { mapUnsignedOrderToOrder, type PostOrderParams, wrapErrorInOperatorError } from 'legacy/utils/trade'
1313

1414
import { removePermitHookFromAppData } from 'modules/appData'
15+
import { callOnBeforeApprovalWidgetHook } from 'modules/injectedWidget'
1516
import { buildApproveTx } from 'modules/operations/bundle/buildApproveTx'
1617
import { buildWrapTx } from 'modules/operations/bundle/buildWrapTx'
1718
import { emitPostedOrderEvent } from 'modules/orders'
@@ -27,7 +28,7 @@ import { SafeBundleFlowContext, TradeFlowContext } from '../../types/TradeFlowCo
2728
const LOG_PREFIX = 'SAFE BUNDLE ETH FLOW'
2829

2930
// TODO: Break down this large function into smaller functions
30-
// eslint-disable-next-line max-lines-per-function
31+
// eslint-disable-next-line complexity, max-lines-per-function
3132
export async function safeBundleEthFlow(
3233
tradeContext: TradeFlowContext,
3334
safeBundleContext: SafeBundleFlowContext,
@@ -70,12 +71,9 @@ export async function safeBundleEthFlow(
7071
const { account, recipientAddressOrName, kind } = orderParams
7172
const isBridgingOrder = inputAmount.currency.chainId !== outputAmount.currency.chainId
7273

73-
analytics.wrapApproveAndPresign(swapFlowAnalyticsContext)
7474
// Wrap the max sell amount (slippage-adjusted for buy orders); inputAmount alone underwraps buy orders and makes them unfillable.
7575
const nativeAmountInWei = maximumSendSellAmount.quotient.toString()
7676
const tradeAmounts = { inputAmount, outputAmount }
77-
78-
tradeConfirmActions.onSign(tradeAmounts)
7977
try {
8078
const txs: MetaTransactionData[] = []
8179

@@ -92,6 +90,19 @@ export async function safeBundleEthFlow(
9290
logTradeFlow(LOG_PREFIX, 'STEP 3: [optional] build approval tx')
9391

9492
if (needsApproval) {
93+
const isWidgetHookPassed = await callOnBeforeApprovalWidgetHook({
94+
account,
95+
amountToApprove,
96+
spenderAddress: spender,
97+
})
98+
99+
if (!isWidgetHookPassed) {
100+
return false
101+
}
102+
103+
analytics.wrapApproveAndPresign(swapFlowAnalyticsContext)
104+
tradeConfirmActions.onSign(tradeAmounts)
105+
95106
const approveTx = await buildApproveTx({
96107
tokenAddress: wrappedNativeContract.address,
97108
spender,
@@ -106,6 +117,11 @@ export async function safeBundleEthFlow(
106117
})
107118
}
108119

120+
if (!needsApproval) {
121+
analytics.wrapApproveAndPresign(swapFlowAnalyticsContext)
122+
tradeConfirmActions.onSign(tradeAmounts)
123+
}
124+
109125
orderParams.appData = await removePermitHookFromAppData(orderParams.appData, typedHooks)
110126

111127
logTradeFlow(LOG_PREFIX, 'STEP 4: post order')

0 commit comments

Comments
 (0)