diff --git a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.ts b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.ts index 95bc25ddfa5..54a0416874e 100644 --- a/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.ts +++ b/apps/cowswap-frontend/src/modules/erc20Approve/hooks/useGeneratePermitInAdvanceToTrade.ts @@ -1,9 +1,12 @@ import { useCallback } from 'react' -import { getWrappedToken, isRejectRequestProviderError } from '@cowprotocol/common-utils' +import { useTradeSpenderAddress } from '@cowprotocol/balances-and-allowances' +import { currencyAmountToTokenAmount, getWrappedToken, isRejectRequestProviderError } from '@cowprotocol/common-utils' import { Currency, CurrencyAmount } from '@cowprotocol/currency' import { useWalletInfo } from '@cowprotocol/wallet' +import { WidgetHookEvents } from '@cowprotocol/widget-lib' +import { callWidgetHook } from 'modules/injectedWidget' import { useGeneratePermitHook, usePermitInfo } from 'modules/permit' import { TradeType } from 'modules/trade' @@ -14,12 +17,30 @@ 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) return useCallback(async () => { - if (!account || !permitInfo) return false + if (!account || !permitInfo || !tradeSpenderAddress) return false + + const amountRaw = BigInt(amountToApprove.quotient.toString()) + + 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: amountRaw.toString(), + walletAddress: account, + spenderAddress: tradeSpenderAddress, + }) + + if (!isWidgetHookPassed) return false const preSignCallback = (): void => updateApproveProgressModalState({ @@ -33,7 +54,7 @@ export function useGeneratePermitInAdvanceToTrade(amountToApprove: CurrencyAmoun inputToken: { name: token.name || '', address: token.address as `0x${string}` }, account, permitInfo, - amount: BigInt(amountToApprove.quotient.toString()), + amount: amountRaw, preSignCallback, postSignCallback: resetApproveProgressModalState, }) @@ -54,6 +75,7 @@ export function useGeneratePermitInAdvanceToTrade(amountToApprove: CurrencyAmoun resetApproveProgressModalState, token.address, token.name, + tradeSpenderAddress, updateApproveProgressModalState, ]) } diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts index f69f471e5d9..67960ca4825 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts @@ -17,7 +17,7 @@ import { useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimit import { useSafeBundleFlowContext } from 'modules/limitOrders/hooks/useSafeBundleFlowContext' import { safeBundleFlow } from 'modules/limitOrders/services/safeBundleFlow' import { tradeFlow } from 'modules/limitOrders/services/tradeFlow' -import { PriceImpactDeclineError, TradeFlowContext } from 'modules/limitOrders/services/types' +import { PriceImpactDeclineError, TradeFlowContext, WidgetHookDeclineError } from 'modules/limitOrders/services/types' import { LimitOrdersSettingsState } from 'modules/limitOrders/state/limitOrdersSettingsAtom' import { partiallyFillableOverrideAtom } from 'modules/limitOrders/state/partiallyFillableOverride' import { calculateLimitOrdersDeadline } from 'modules/limitOrders/utils/calculateLimitOrdersDeadline' @@ -202,6 +202,10 @@ export function useHandleOrderPlacement( }) .catch((error) => { if (error instanceof PriceImpactDeclineError) return + if (error instanceof WidgetHookDeclineError) { + tradeConfirmActions.onDismiss() + return + } if (error instanceof OperatorError) { tradeConfirmActions.onError(error.message || error.description) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts index 4d09e81e00f..30b909d76ff 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts @@ -1,8 +1,16 @@ -import { captureError, ERROR_TYPES, normalizeError, reportPermitWithDefaultSigner } from '@cowprotocol/common-utils' -import { SigningScheme } from '@cowprotocol/cow-sdk' +import { + captureError, + COW_PROTOCOL_VAULT_RELAYER_ADDRESS, + currencyAmountToTokenAmount, + ERROR_TYPES, + normalizeError, + reportPermitWithDefaultSigner, +} from '@cowprotocol/common-utils' +import { SigningScheme, SupportedChainId } from '@cowprotocol/cow-sdk' import { Percent } from '@cowprotocol/currency' import { isSupportedPermitInfo } from '@cowprotocol/permit-utils' import { Command, UiOrderType } from '@cowprotocol/types' +import { WidgetHookEvents } from '@cowprotocol/widget-lib' import { tradingSdk } from 'tradingSdk/tradingSdk' import { sendTransaction } from 'wagmi/actions' @@ -11,8 +19,9 @@ import { PriceImpact } from 'legacy/hooks/usePriceImpact' import { partialOrderUpdate } from 'legacy/state/orders/utils' import { mapUnsignedOrderToOrder, wrapErrorInOperatorError } from 'legacy/utils/trade' +import { callWidgetHook } from 'modules/injectedWidget' import { LOW_RATE_THRESHOLD_PERCENT } from 'modules/limitOrders/const/trade' -import { PriceImpactDeclineError, TradeFlowContext } from 'modules/limitOrders/services/types' +import { PriceImpactDeclineError, TradeFlowContext, WidgetHookDeclineError } from 'modules/limitOrders/services/types' import { LimitOrdersSettingsState } from 'modules/limitOrders/state/limitOrdersSettingsAtom' import { calculateLimitOrdersDeadline } from 'modules/limitOrders/utils/calculateLimitOrdersDeadline' import { emitPostedOrderEvent } from 'modules/orders' @@ -27,7 +36,7 @@ import { getSwapErrorMessage } from 'common/utils/getSwapErrorMessage' import type { Hex } from 'viem' // TODO: Break down this large function into smaller functions -// eslint-disable-next-line max-lines-per-function +// eslint-disable-next-line max-lines-per-function, complexity export async function tradeFlow( params: TradeFlowContext, priceImpact: PriceImpact, @@ -73,7 +82,28 @@ export async function tradeFlow( try { logTradeFlow('LIMIT ORDER FLOW', 'STEP 2: handle permit') - if (isSupportedPermitInfo(permitInfo)) await beforePermit() + if (isSupportedPermitInfo(permitInfo)) { + const cachedPermit = await params.getCachedPermit(sellToken.address) + + if (!cachedPermit) { + const sellTokenAmount = currencyAmountToTokenAmount(inputAmount) + const isWidgetHookPassed = await callWidgetHook(WidgetHookEvents.ON_BEFORE_APPROVAL, { + chainId: sellTokenAmount.currency.chainId, + sellToken: { + ...sellTokenAmount.currency, + name: sellTokenAmount.currency.name || '', + symbol: sellTokenAmount.currency.symbol || '', + }, + sellAmount: (permitAmountToSign ?? 0n).toString(), + walletAddress: account, + spenderAddress: COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId as SupportedChainId], + }) + + if (!isWidgetHookPassed) throw new WidgetHookDeclineError() + } + + await beforePermit() + } postOrderParams.appData = await handlePermit({ permitInfo, @@ -196,6 +226,11 @@ export async function tradeFlow( } catch (err: unknown) { const error = normalizeError(err) + // Expected abort path: skip generic swap-error analytics so widget-hook declines don't pollute telemetry. + if (error instanceof WidgetHookDeclineError) { + throw error + } + logTradeFlow('LIMIT ORDER FLOW', 'STEP 9: ERROR: ', error) const swapErrorMessage = getSwapErrorMessage(error) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/types.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/types.ts index 766a792f226..f28c7d1d808 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/types.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/types.ts @@ -36,3 +36,5 @@ export interface SafeBundleFlowContext extends TradeFlowContext { } export class PriceImpactDeclineError extends Error {} + +export class WidgetHookDeclineError extends Error {} diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts index 4cbf2d3c015..4184d54f631 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts @@ -1,16 +1,19 @@ import { captureError, + COW_PROTOCOL_VAULT_RELAYER_ADDRESS, + currencyAmountToTokenAmount, delay, ERROR_TYPES, getCurrencyAddress, normalizeError, reportPermitWithDefaultSigner, } from '@cowprotocol/common-utils' -import { SigningScheme, SigningStepManager } from '@cowprotocol/cow-sdk' +import { SigningScheme, SigningStepManager, SupportedChainId } from '@cowprotocol/cow-sdk' import { Percent } from '@cowprotocol/currency' import { isSupportedPermitInfo } from '@cowprotocol/permit-utils' import { CoWShedEip1271SignatureInvalid } from '@cowprotocol/sdk-cow-shed' import { UiOrderType } from '@cowprotocol/types' +import { WidgetHookEvents } from '@cowprotocol/widget-lib' import { SigningSteps } from 'entities/trade' import ms from 'ms.macro' @@ -21,6 +24,7 @@ import { PriceImpact } from 'legacy/hooks/usePriceImpact' import { partialOrderUpdate } from 'legacy/state/orders/utils' import { mapUnsignedOrderToOrder, wrapErrorInOperatorError } from 'legacy/utils/trade' +import { callWidgetHook } from 'modules/injectedWidget' import { emitPostedOrderEvent } from 'modules/orders' import { callDataContainsPermitSigner, handlePermit } from 'modules/permit' import { addPendingOrderStep } from 'modules/trade/utils/addPendingOrderStep' @@ -86,13 +90,28 @@ export async function swapFlow( try { logTradeFlow('SWAP FLOW', 'STEP 2: handle permit') + const { appData, account, isSafeWallet, recipientAddressOrName, kind } = orderParams + if (shouldSignPermit) { + const sellTokenAmount = currencyAmountToTokenAmount(inputAmount) + const isWidgetHookPassed = await callWidgetHook(WidgetHookEvents.ON_BEFORE_APPROVAL, { + chainId: sellTokenAmount.currency.chainId, + sellToken: { + ...sellTokenAmount.currency, + name: sellTokenAmount.currency.name || '', + symbol: sellTokenAmount.currency.symbol || '', + }, + sellAmount: (permitAmountToSign ?? 0n).toString(), + walletAddress: account, + spenderAddress: COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId as SupportedChainId], + }) + + if (!isWidgetHookPassed) return false + setSigningStep(isBridgingOrder ? '1/3' : '1/2', SigningSteps.PermitSigning) tradeConfirmActions.requestPermitSignature(tradeAmounts) } - const { appData, account, isSafeWallet, recipientAddressOrName, inputAmount, outputAmount, kind } = orderParams - orderParams.appData = await handlePermit({ appData, typedHooks,