Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -14,13 +17,33 @@ 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

const amountRaw = BigInt(amountToApprove.quotient.toString())

if (tradeSpenderAddress) {
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
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const preSignCallback = (): void =>
updateApproveProgressModalState({
currency: amountToApprove.currency,
Expand All @@ -33,7 +56,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,
})
Expand All @@ -54,6 +77,7 @@ export function useGeneratePermitInAdvanceToTrade(amountToApprove: CurrencyAmoun
resetApproveProgressModalState,
token.address,
token.name,
tradeSpenderAddress,
updateApproveProgressModalState,
])
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this logic duplicated in three different places.
It looks like it can be implemented only once in useGeneratePermitHook

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()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Do not report widget-hook declines as swap errors.

At Line 102, WidgetHookDeclineError is an expected abort path, but it currently goes through generic error capture/analytics in the catch block, which will create telemetry noise.

Suggested fix
   } catch (err: unknown) {
     const error = normalizeError(err)
+    if (error instanceof WidgetHookDeclineError) {
+      throw error
+    }
 
     logTradeFlow('LIMIT ORDER FLOW', 'STEP 9: ERROR: ', error)
     const swapErrorMessage = getSwapErrorMessage(error)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts`
around lines 102 - 103, The WidgetHookDeclineError thrown at the throw statement
is an expected abort path that should not be treated as a generic swap error in
error analytics. Locate the catch block that handles exceptions from the code
path containing the WidgetHookDeclineError throw statement, and add a specific
check to detect if the caught error is an instance of WidgetHookDeclineError.
When this error type is caught, handle it separately by either skipping the
generic error capture/analytics logic or routing it through a non-error
telemetry path, ensuring that expected widget-hook declines do not create
analytics noise in the swap error reporting.


await beforePermit()
}

postOrderParams.appData = await handlePermit({
permitInfo,
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ export interface SafeBundleFlowContext extends TradeFlowContext {
}

export class PriceImpactDeclineError extends Error {}

export class WidgetHookDeclineError extends Error {}
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
Loading