diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/containers/Erc20ApproveWidget/index.tsx b/apps/cowswap-frontend/src/modules/erc20Approve/containers/Erc20ApproveWidget/index.tsx new file mode 100644 index 0000000000..0fa0e5ffb7 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/erc20Approve/containers/Erc20ApproveWidget/index.tsx @@ -0,0 +1,18 @@ +import { useSetAtom } from 'jotai' +import { useEffect } from 'react' + +import { isPartialApproveEnabledAtom } from '../../state/isPartialApproveEnabledAtom' + +interface Erc20ApproveProps { + isPartialApprovalEnabled: boolean +} + +export function Erc20ApproveWidget({ isPartialApprovalEnabled }: Erc20ApproveProps): null { + const setIsPartialApproveEnabled = useSetAtom(isPartialApproveEnabledAtom) + + useEffect(() => { + setIsPartialApproveEnabled(isPartialApprovalEnabled) + }, [setIsPartialApproveEnabled, isPartialApprovalEnabled]) + + return null +} diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/containers/TradeApproveWithAffectedOrderList/TradeApproveWithAffectedOrderList.tsx b/apps/cowswap-frontend/src/modules/erc20Approve/containers/TradeApproveWithAffectedOrderList/TradeApproveWithAffectedOrderList.tsx index 4026275822..94f8fc5ffe 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/containers/TradeApproveWithAffectedOrderList/TradeApproveWithAffectedOrderList.tsx +++ b/apps/cowswap-frontend/src/modules/erc20Approve/containers/TradeApproveWithAffectedOrderList/TradeApproveWithAffectedOrderList.tsx @@ -1,3 +1,4 @@ +import { useAtomValue } from 'jotai' import { ReactNode } from 'react' import { @@ -8,15 +9,16 @@ import { } from '../../hooks' import { TradeAllowanceDisplay } from '../../pure/TradeAllowanceDisplay' import { useSetUserApproveAmountModalState } from '../../state' +import { isPartialApproveEnabledAtom } from '../../state/isPartialApproveEnabledAtom' import { isMaxAmountToApprove } from '../../utils' import { ActiveOrdersWithAffectedPermit } from '../ActiveOrdersWithAffectedPermit' import { TradeApproveToggle } from '../TradeApproveToggle' export function TradeApproveWithAffectedOrderList(): ReactNode { - const { - reason: isApproveRequired, - currentAllowance - } = useIsApprovalOrPermitRequired({ isBundlingSupportedOrEnabledForContext: false }) + const { reason: isApproveRequired, currentAllowance } = useIsApprovalOrPermitRequired({ + isBundlingSupportedOrEnabledForContext: false, + }) + const isPartialApprovalEnabledInSettings = useAtomValue(isPartialApproveEnabledAtom) const setUserApproveAmountModalState = useSetUserApproveAmountModalState() @@ -30,7 +32,7 @@ export function TradeApproveWithAffectedOrderList(): ReactNode { isApproveRequired === ApproveRequiredReason.Required || isApproveRequired === ApproveRequiredReason.Eip2612PermitRequired - if (!partialAmountToApprove) return null + if (!partialAmountToApprove || !isPartialApprovalEnabledInSettings) return null const currencyToApprove = partialAmountToApprove.currency diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/containers/index.ts b/apps/cowswap-frontend/src/modules/erc20Approve/containers/index.ts index 9a99689faa..9c8f1f9478 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/containers/index.ts +++ b/apps/cowswap-frontend/src/modules/erc20Approve/containers/index.ts @@ -7,3 +7,4 @@ export * from './TradeApproveWithAffectedOrderList' export * from './TradeChangeApproveAmountModal' export * from './PartialApproveAmountModal' export * from './PartialApproveContainer' +export * from './Erc20ApproveWidget' diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGetAmountToSignApprove.test.ts b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGetAmountToSignApprove.test.ts index b8029419f8..ea0eb22280 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGetAmountToSignApprove.test.ts +++ b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGetAmountToSignApprove.test.ts @@ -1,3 +1,5 @@ +import { useAtomValue } from 'jotai' + import { useFeatureFlags } from '@cowprotocol/common-hooks' import { CurrencyAmount, Token } from '@uniswap/sdk-core' @@ -8,10 +10,14 @@ import { useNeedsApproval } from 'common/hooks/useNeedsApproval' import { useGetAmountToSignApprove } from './useGetAmountToSignApprove' import { useGetPartialAmountToSignApprove } from './useGetPartialAmountToSignApprove' -import { useSwapPartialApprovalToggleState } from '../../swap/hooks/useSwapSettings' import { MAX_APPROVE_AMOUNT } from '../constants' import { useIsPartialApproveSelectedByUser } from '../state' +jest.mock('jotai', () => ({ + ...jest.requireActual('jotai'), + useAtomValue: jest.fn(), +})) + jest.mock('@cowprotocol/common-hooks', () => ({ useFeatureFlags: jest.fn(), })) @@ -24,22 +30,16 @@ jest.mock('./useGetPartialAmountToSignApprove', () => ({ useGetPartialAmountToSignApprove: jest.fn(), })) -jest.mock('../../swap/hooks/useSwapSettings', () => ({ - useSwapPartialApprovalToggleState: jest.fn(), -})) - jest.mock('../state', () => ({ useIsPartialApproveSelectedByUser: jest.fn(), })) +const mockUseAtomValue = useAtomValue as jest.MockedFunction const mockUseFeatureFlags = useFeatureFlags as jest.MockedFunction const mockUseNeedsApproval = useNeedsApproval as jest.MockedFunction const mockUseGetPartialAmountToSignApprove = useGetPartialAmountToSignApprove as jest.MockedFunction< typeof useGetPartialAmountToSignApprove > -const mockUseSwapPartialApprovalToggleState = useSwapPartialApprovalToggleState as jest.MockedFunction< - typeof useSwapPartialApprovalToggleState -> const mockUseIsPartialApproveSelectedByUser = useIsPartialApproveSelectedByUser as jest.MockedFunction< typeof useIsPartialApproveSelectedByUser > @@ -57,7 +57,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(false) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: true }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([true, jest.fn()]) + mockUseAtomValue.mockReturnValue(true) }) describe('when partialAmountToSign is null', () => { @@ -82,7 +82,7 @@ describe('useGetAmountToSignApprove', () => { it('should return zero amount when approval is not needed regardless of partial approval settings', () => { mockUseNeedsApproval.mockReturnValue(false) mockUseIsPartialApproveSelectedByUser.mockReturnValue(true) - mockUseSwapPartialApprovalToggleState.mockReturnValue([true, jest.fn()]) + mockUseAtomValue.mockReturnValue(true) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -95,7 +95,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: true }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([true, jest.fn()]) + mockUseAtomValue.mockReturnValue(true) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -106,7 +106,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(false) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: true }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([true, jest.fn()]) + mockUseAtomValue.mockReturnValue(true) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -117,7 +117,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: true }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([false, jest.fn()]) + mockUseAtomValue.mockReturnValue(false) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -129,7 +129,7 @@ describe('useGetAmountToSignApprove', () => { it('should return max amount when isPartialApproveEnabled is false', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: false }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([null, null]) + mockUseAtomValue.mockReturnValue(false) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -140,7 +140,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: false }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([null, null]) + mockUseAtomValue.mockReturnValue(false) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -153,7 +153,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: false }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([null, null]) + mockUseAtomValue.mockReturnValue(false) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -164,7 +164,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: true }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([false, jest.fn()]) + mockUseAtomValue.mockReturnValue(false) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -175,7 +175,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(false) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: true }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([true, jest.fn()]) + mockUseAtomValue.mockReturnValue(true) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -250,7 +250,7 @@ describe('useGetAmountToSignApprove', () => { const firstResult = result.current - mockUseSwapPartialApprovalToggleState.mockReturnValue([false, jest.fn()]) + mockUseAtomValue.mockReturnValue(false) rerender() expect(result.current).not.toBe(firstResult) @@ -263,7 +263,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: true }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([true, jest.fn()]) + mockUseAtomValue.mockReturnValue(true) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -293,7 +293,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: scenario.isPartialApproveEnabled }) mockUseIsPartialApproveSelectedByUser.mockReturnValue(scenario.isPartialApprovalSelectedByUser) - mockUseSwapPartialApprovalToggleState.mockReturnValue([scenario.isPartialApprovalEnabledInSettings, jest.fn()]) + mockUseAtomValue.mockReturnValue(scenario.isPartialApprovalEnabledInSettings) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -305,7 +305,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: true }) mockUseIsPartialApproveSelectedByUser.mockReturnValue(true) - mockUseSwapPartialApprovalToggleState.mockReturnValue([false, jest.fn()]) + mockUseAtomValue.mockReturnValue(false) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -337,7 +337,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: true }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([true, jest.fn()]) + mockUseAtomValue.mockReturnValue(true) const { result } = renderHook(() => useGetAmountToSignApprove()) @@ -351,7 +351,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: true }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([true, jest.fn()]) + mockUseAtomValue.mockReturnValue(true) const { result, rerender } = renderHook(() => useGetAmountToSignApprove()) @@ -368,7 +368,7 @@ describe('useGetAmountToSignApprove', () => { mockUseNeedsApproval.mockReturnValue(true) mockUseIsPartialApproveSelectedByUser.mockReturnValue(true) mockUseFeatureFlags.mockReturnValue({ isPartialApproveEnabled: true }) - mockUseSwapPartialApprovalToggleState.mockReturnValue([true, jest.fn()]) + mockUseAtomValue.mockReturnValue(true) rerender() expect(result.current).toEqual(mockPartialAmount) diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGetAmountToSignApprove.tsx b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGetAmountToSignApprove.tsx index 03763aa343..3c34e69984 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGetAmountToSignApprove.tsx +++ b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGetAmountToSignApprove.tsx @@ -1,3 +1,4 @@ +import { useAtomValue } from 'jotai' import { useMemo } from 'react' import { useFeatureFlags } from '@cowprotocol/common-hooks' @@ -7,9 +8,9 @@ import { useNeedsApproval } from 'common/hooks/useNeedsApproval' import { useGetPartialAmountToSignApprove } from './useGetPartialAmountToSignApprove' -import { useSwapPartialApprovalToggleState } from '../../swap/hooks/useSwapSettings' import { MAX_APPROVE_AMOUNT } from '../constants' import { useIsPartialApproveSelectedByUser } from '../state' +import { isPartialApproveEnabledAtom } from '../state/isPartialApproveEnabledAtom' /** * Returns the amount to sign for the approval transaction/permit @@ -23,7 +24,7 @@ export function useGetAmountToSignApprove(): CurrencyAmount | null { const isApprovalNeeded = useNeedsApproval(partialAmountToSign) const isPartialApprovalSelectedByUser = useIsPartialApproveSelectedByUser() const { isPartialApproveEnabled } = useFeatureFlags() - const [isPartialApprovalEnabledInSettings] = useSwapPartialApprovalToggleState(isPartialApproveEnabled) + const isPartialApprovalEnabledInSettings = useAtomValue(isPartialApproveEnabledAtom) return useMemo(() => { if (!partialAmountToSign) return null diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/state/isPartialApproveEnabledAtom.ts b/apps/cowswap-frontend/src/modules/erc20Approve/state/isPartialApproveEnabledAtom.ts new file mode 100644 index 0000000000..fd4cb1174e --- /dev/null +++ b/apps/cowswap-frontend/src/modules/erc20Approve/state/isPartialApproveEnabledAtom.ts @@ -0,0 +1,3 @@ +import { atom } from 'jotai' + +export const isPartialApproveEnabledAtom = atom(false) diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx index c9cb023ad3..70bc408141 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -1,7 +1,7 @@ import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { useFeatureFlags } from '@cowprotocol/common-hooks' -import { getIsNativeToken, isInjectedWidget, isSellOrder } from '@cowprotocol/common-utils' +import { isInjectedWidget, isSellOrder } from '@cowprotocol/common-utils' import { useIsEagerConnectInProgress, useIsSmartContractWallet, useWalletInfo } from '@cowprotocol/wallet' import { Field } from 'legacy/state/types' @@ -52,7 +52,6 @@ export interface SwapWidgetProps { } // TODO: Break down this large function into smaller functions -// TODO: Add proper return type annotation // eslint-disable-next-line max-lines-per-function,complexity export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps): ReactNode { const { showRecipient } = useSwapSettings() @@ -87,7 +86,7 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps): Reac orderKind, isUnlocked, } = useSwapDerivedState() - const doTrade = useHandleSwap(useSafeMemoObject({ deadline: deadlineState[0] }), widgetActions) + const doTrade = useHandleSwap({ deadline: deadlineState[0] }, widgetActions) const hasEnoughWrappedBalanceForSwap = useHasEnoughWrappedBalanceForSwap() const isSmartContractWallet = useIsSmartContractWallet() const { account } = useWalletInfo() @@ -161,8 +160,6 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps): Reac const { isPartialApproveEnabled } = useFeatureFlags() const enablePartialApprovalState = useSwapPartialApprovalToggleState(isPartialApproveEnabled) - const enablePartialApproval = enablePartialApprovalState[0] && inputCurrency && !getIsNativeToken(inputCurrency) - const isConnected = Boolean(account) const isNetworkUnsupported = useIsProviderNetworkUnsupported() @@ -190,7 +187,7 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps): Reac return ( <> {bottomContent} - {enablePartialApproval ? : null} + {tradeWarnings} @@ -215,7 +212,6 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps): Reac hasEnoughWrappedBalanceForSwap, toBeImported, intermediateBuyToken, - enablePartialApproval, ], ), } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts index 6815e299c8..90f3b94676 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts @@ -1,12 +1,9 @@ -import { useTradeFlowContext } from 'modules/tradeFlow' - -import { useSafeMemoObject } from 'common/hooks/useSafeMemo' +import { TradeFlowContext, useTradeFlowContext } from 'modules/tradeFlow' import { useSwapDeadlineState } from './useSwapSettings' -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function useSwapFlowContext() { +export function useSwapFlowContext(): TradeFlowContext | null { const [deadline] = useSwapDeadlineState() - return useTradeFlowContext(useSafeMemoObject({ deadline })) + + return useTradeFlowContext({ deadline }) } diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx b/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx index 1ac1575393..e07b7aee14 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx @@ -3,6 +3,7 @@ import { ReactNode } from 'react' import { isSellOrder, percentToBps } from '@cowprotocol/common-utils' import { AppDataUpdater } from 'modules/appData' +import { Erc20ApproveWidget } from 'modules/erc20Approve' import { EthFlowDeadlineUpdater } from 'modules/ethFlow' import { useIsHooksTradeType } from 'modules/trade' import { useSetTradeQuoteParams } from 'modules/tradeQuote' @@ -13,13 +14,14 @@ import { SetupSwapAmountsFromUrlUpdater } from './SetupSwapAmountsFromUrlUpdater import { UnfillableSwapOrdersUpdater } from './UnfillableSwapOrdersUpdater' import { useFillSwapDerivedState, useSwapDerivedState } from '../hooks/useSwapDerivedState' -import { useSwapDeadlineState } from '../hooks/useSwapSettings' +import { useSwapDeadlineState, useSwapSettings } from '../hooks/useSwapSettings' export function SwapUpdaters(): ReactNode { const { orderKind, inputCurrencyAmount, outputCurrencyAmount, slippage } = useSwapDerivedState() const isSmartSlippageApplied = useIsSmartSlippageApplied() const swapDeadlineState = useSwapDeadlineState() const partiallyFillable = useIsHooksTradeType() + const { enablePartialApprovalBySettings } = useSwapSettings() useFillSwapDerivedState() useSetTradeQuoteParams({ @@ -34,6 +36,7 @@ export function SwapUpdaters(): ReactNode { + {slippage && ( diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useAmountsToSignFromQuote.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useAmountsToSignFromQuote.ts index 6f6c146fb7..acdf2148b5 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useAmountsToSignFromQuote.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useAmountsToSignFromQuote.ts @@ -1,10 +1,11 @@ import { useMemo } from 'react' -import { FractionUtils } from '@cowprotocol/common-utils' +import { currencyAmountToTokenAmount, FractionUtils } from '@cowprotocol/common-utils' import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import { useDerivedTradeState } from './useDerivedTradeState' import { useGetReceiveAmountInfo } from './useGetReceiveAmountInfo' +import { useIsSafeEthFlow } from './useIsSafeEthFlow' const BUY_ORDER_APPROVE_AMOUNT_THRESHOLD = new Percent(1, 100) // 1% @@ -20,19 +21,25 @@ export interface AmountsToSign { export function useAmountsToSignFromQuote(): AmountsToSign | null { const { isQuoteBasedOrder, inputCurrencyAmount, outputCurrencyAmount } = useDerivedTradeState() || {} const { isSell, afterSlippage } = useGetReceiveAmountInfo() || {} + const isSafeBundleEth = useIsSafeEthFlow() return useMemo(() => { - const maximumSendSellAmount = isQuoteBasedOrder ? afterSlippage?.sellAmount : inputCurrencyAmount + const sellAmountAfterSlippage = isQuoteBasedOrder ? afterSlippage?.sellAmount : inputCurrencyAmount const minimumReceiveBuyAmount = isQuoteBasedOrder ? afterSlippage?.buyAmount : outputCurrencyAmount - if (!maximumSendSellAmount || !minimumReceiveBuyAmount) return null + if (!sellAmountAfterSlippage || !minimumReceiveBuyAmount) return null + + // Add 1% threshold for buy orders to level out price/gas fluctuations + const maximumSendSellAmount = isSell + ? sellAmountAfterSlippage + : FractionUtils.addPercent(sellAmountAfterSlippage, BUY_ORDER_APPROVE_AMOUNT_THRESHOLD) return { - // Add 1% threshold for buy orders to level out price/gas fluctuations - maximumSendSellAmount: isSell - ? maximumSendSellAmount - : FractionUtils.addPercent(maximumSendSellAmount, BUY_ORDER_APPROVE_AMOUNT_THRESHOLD), + // Safe ETH bundling uses ETH wrapping, so we should consider WETH as approving token + maximumSendSellAmount: isSafeBundleEth + ? currencyAmountToTokenAmount(maximumSendSellAmount) + : maximumSendSellAmount, minimumReceiveBuyAmount, } - }, [isSell, isQuoteBasedOrder, inputCurrencyAmount, outputCurrencyAmount, afterSlippage]) + }, [isSell, isSafeBundleEth, isQuoteBasedOrder, inputCurrencyAmount, outputCurrencyAmount, afterSlippage]) } diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts index 74340fc290..608349555b 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts @@ -17,10 +17,10 @@ import { safeBundleApprovalFlow, safeBundleEthFlow } from '../services/safeBundl import { swapFlow } from '../services/swapFlow' import { FlowType } from '../types/TradeFlowContext' -// TODO: Break down this large function into smaller functions -// TODO: Add proper return type annotation -// eslint-disable-next-line max-lines-per-function, @typescript-eslint/explicit-function-return-type -export function useHandleSwap(params: TradeFlowParams, actions: TradeWidgetActions) { +export function useHandleSwap( + params: TradeFlowParams, + actions: TradeWidgetActions, +): { callback(): Promise; contextIsReady: boolean } { const tradeFlowType = useTradeFlowType() const tradeFlowContext = useTradeFlowContext(params) const safeBundleFlowContext = useSafeBundleFlowContext() diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts index 04c3adad4c..3056bab4f8 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts @@ -1,12 +1,10 @@ import { useMemo } from 'react' import { useTradeSpenderAddress } from '@cowprotocol/balances-and-allowances' -import { useFeatureFlags } from '@cowprotocol/common-hooks' import { getCurrencyAddress } from '@cowprotocol/common-utils' import { useSendBatchTransactions } from '@cowprotocol/wallet' -import useSWR from 'swr' - +import { useGetAmountToSignApprove } from 'modules/erc20Approve' import { useAmountsToSignFromQuote } from 'modules/trade' import { useTokenContract, useWethContract } from 'common/hooks/useContract' @@ -17,34 +15,31 @@ import { SafeBundleFlowContext } from '../types/TradeFlowContext' export function useSafeBundleFlowContext(): SafeBundleFlowContext | null { const spender = useTradeSpenderAddress() + const amountToApprove = useGetAmountToSignApprove() const sendBatchTransactions = useSendBatchTransactions() const { contract: wrappedNativeContract } = useWethContract() // todo check for safe wallet const { maximumSendSellAmount } = useAmountsToSignFromQuote() || {} - const { isPartialApproveEnabled } = useFeatureFlags() const needsApproval = useNeedsApproval(maximumSendSellAmount) const inputCurrencyAddress = useMemo(() => { return maximumSendSellAmount ? getCurrencyAddress(maximumSendSellAmount.currency) : undefined }, [maximumSendSellAmount]) const { contract: erc20Contract } = useTokenContract(inputCurrencyAddress) - return ( - useSWR( - spender && wrappedNativeContract && erc20Contract - ? [spender, sendBatchTransactions, wrappedNativeContract, needsApproval, erc20Contract] - : null, - ([spender, sendBatchTransactions, wrappedNativeContract, needsApproval, erc20Contract]) => { - return { - spender, - sendBatchTransactions, - wrappedNativeContract, - needsApproval, - erc20Contract, - isPartialApproveEnabled, - } - }, - ).data || null - ) + return useMemo(() => { + if (!spender || !wrappedNativeContract || !erc20Contract || !amountToApprove) { + return null + } + + return { + spender, + sendBatchTransactions, + wrappedNativeContract, + needsApproval, + erc20Contract, + amountToApprove, + } + }, [spender, sendBatchTransactions, wrappedNativeContract, needsApproval, erc20Contract, amountToApprove]) } diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts index 2d5f726009..f9acffafd8 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts @@ -1,6 +1,5 @@ import { SigningScheme } from '@cowprotocol/cow-sdk' import { UiOrderType } from '@cowprotocol/types' -import { MaxUint256 } from '@ethersproject/constants' import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types' import { Percent } from '@uniswap/sdk-core' @@ -43,7 +42,7 @@ export async function safeBundleApprovalFlow( return false } - const { spender, sendBatchTransactions, erc20Contract, isPartialApproveEnabled } = safeBundleContext + const { spender, sendBatchTransactions, erc20Contract, amountToApprove } = safeBundleContext const { chainId } = context const { account, isSafeWallet, recipientAddressOrName, inputAmount, outputAmount, kind } = orderParams @@ -52,8 +51,6 @@ export async function safeBundleApprovalFlow( analytics.approveAndPresign(swapFlowAnalyticsContext) tradeConfirmActions.onSign(tradeAmounts) - const amountToApprove = isPartialApproveEnabled ? BigInt(inputAmount.quotient.toString()) : MaxUint256.toBigInt() - try { // For now, bundling ALWAYS includes 2 steps: approve and presign. // In the feature users will be able to sort/add steps as they see fit @@ -61,7 +58,7 @@ export async function safeBundleApprovalFlow( const approveTx = await buildApproveTx({ erc20Contract, spender, - amountToApprove, + amountToApprove: BigInt(amountToApprove.quotient.toString()), }) orderParams.appData = await removePermitHookFromAppData(orderParams.appData, typedHooks) diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts index 8ec95957a7..c18f13f01c 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts @@ -2,7 +2,6 @@ import { Erc20 } from '@cowprotocol/abis' import { WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/common-const' import { SigningScheme, SupportedChainId } from '@cowprotocol/cow-sdk' import { UiOrderType } from '@cowprotocol/types' -import { MaxUint256 } from '@ethersproject/constants' import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types' import { Percent } from '@uniswap/sdk-core' @@ -48,8 +47,7 @@ export async function safeBundleEthFlow( return false } - const { spender, sendBatchTransactions, needsApproval, wrappedNativeContract, isPartialApproveEnabled } = - safeBundleContext + const { spender, sendBatchTransactions, needsApproval, wrappedNativeContract, amountToApprove } = safeBundleContext const { chainId, inputAmount, outputAmount } = context @@ -80,13 +78,11 @@ export async function safeBundleEthFlow( logTradeFlow(LOG_PREFIX, 'STEP 3: [optional] build approval tx') - const amountToApprove = isPartialApproveEnabled ? BigInt(inputAmount.quotient.toString()) : MaxUint256.toBigInt() - if (needsApproval) { const approveTx = await buildApproveTx({ erc20Contract: wrappedNativeContract as unknown as Erc20, spender, - amountToApprove, + amountToApprove: BigInt(amountToApprove.quotient.toString()), }) txs.push({ diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts index 95a5c46c45..4337b97ab0 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts @@ -58,5 +58,5 @@ export interface SafeBundleFlowContext { wrappedNativeContract: Weth needsApproval: boolean erc20Contract: Erc20 - isPartialApproveEnabled?: boolean + amountToApprove: CurrencyAmount } diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx index 36366a9cf6..df555ada22 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx @@ -1,3 +1,5 @@ +import { ReactNode } from 'react' + import { InlineBanner, StatusColorVariant } from '@cowprotocol/ui' import { useIsTxBundlingSupported, useIsSmartContractWallet } from '@cowprotocol/wallet' @@ -5,9 +7,7 @@ import { useIsHooksTradeType, useIsNativeIn, useWrappedToken } from 'modules/tra import useNativeCurrency from 'lib/hooks/useNativeCurrency' -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function BundleTxWrapBanner() { +export function BundleTxWrapBanner(): ReactNode { const nativeCurrencySymbol = useNativeCurrency().symbol || 'ETH' const wrappedCurrencySymbol = useWrappedToken().symbol || 'WETH'