diff --git a/apps/cowswap-frontend/src/common/hooks/useCancelOrder/index.ts b/apps/cowswap-frontend/src/common/hooks/useCancelOrder/index.ts index 4f86b980c20..6505b639e23 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCancelOrder/index.ts +++ b/apps/cowswap-frontend/src/common/hooks/useCancelOrder/index.ts @@ -56,7 +56,10 @@ export function useCancelOrder(): (order: Order) => UseCancelOrderReturn { (order: Order) => { // Check the 'cancellability' - // The wallet must support off-chain signing + // The wallet must support off-chain signing. + // Pre-signed orders (e.g. an approve+presign bundle) are signed on-chain, so they are excluded + // here via isOrderOffChainCancellable (which requires the EIP-712 scheme) and fall back to the + // on-chain cancellation path — the orderbook rejects an off-chain cancellation for them. const isOffChainCancellable = allowsOffchainSigning && isOrderOffChainCancellable(order) // When the order is not cancellable, there won't be a callback diff --git a/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts index 45b05a0cb45..43c2cffb576 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts @@ -2,6 +2,9 @@ import { useSetAtom } from 'jotai' import { useCallback, useEffect, useMemo, useRef } from 'react' +import { type Config, useConfig } from 'wagmi' +import { getCallsStatus } from 'wagmi/actions' + import { getExplorerOrderLink, timeSinceInSeconds } from '@cowprotocol/common-utils' import { areAddressesEqual, EnrichedOrder, EthflowData, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { UiOrderType } from '@cowprotocol/types' @@ -93,6 +96,7 @@ interface UpdateOrdersParams { getSafeTxInfo: GetSafeTxInfo safeNonce: number | undefined allTransactions: ReturnType + config: Config markPollComplete?: (chainId: ChainId) => void } @@ -143,6 +147,7 @@ export function PendingOrdersUpdater(): null { const updatePresignGnosisSafeTx = useUpdatePresignGnosisSafeTx() const allTransactions = useAllTransactions() const getSafeTxInfo = useGetSafeTxInfo() + const config = useConfig() const getSerializedBridgeOrder = useGetSerializedBridgeOrder() const getSerializedBridgeOrderRef = useRef(getSerializedBridgeOrder) @@ -212,6 +217,7 @@ export function PendingOrdersUpdater(): null { getSafeTxInfo, safeNonce, allTransactions, + config, markPollComplete: shouldMarkCompletion ? markPollComplete : undefined, }).finally(() => { isUpdating.current = false @@ -231,6 +237,7 @@ export function PendingOrdersUpdater(): null { getSafeTxInfo, safeNonce, allTransactions, + config, markPollComplete, ], ) @@ -337,6 +344,50 @@ async function _updateCreatingOrders( await Promise.all(promises) } +/** + * Track EIP-5792 approval+presign bundles submitted by EIP-7702 wallets. + * + * These orders store an EIP-5792 bundle id in `presignGnosisSafeTxHash` (flagged by + * `presignIsEip5792Bundle`) and cannot be resolved through the Safe transaction service. We poll the + * wallet with `getCallsStatus` instead: while the bundle is pending the order stays in + * PRESIGNATURE_PENDING, and once mined the backend reports the order as presigned so the regular poll + * promotes it to PENDING. If the bundle fails, we invalidate the order so it doesn't hang forever. + */ +async function _updateEip5792BundleStatus( + chainId: ChainId, + allPendingOrders: Order[], + config: Config, + invalidateOrdersBatch: InvalidateOrdersBatchCallback, + isSafeWallet: boolean, +): Promise { + const promises = allPendingOrders + .filter( + (order) => + order.presignIsEip5792Bundle && + order.presignGnosisSafeTxHash && + order.status === OrderStatus.PRESIGNATURE_PENDING, + ) + .map((order): Promise => { + const bundleId = order.presignGnosisSafeTxHash as string + + return getCallsStatus(config, { id: bundleId }) + .then((result) => { + // 'pending' -> keep waiting; 'success' -> backend will report the order presigned and the + // regular poll moves it to PENDING, so nothing to do here. + if (result.status === 'failure') { + console.warn('[PendingOrdersUpdater] EIP-5792 bundle failed, invalidating order:', order.id, bundleId) + invalidateOrdersBatch({ ids: [order.id], chainId, isSafeWallet }) + } + }) + .catch((error) => { + // The wallet may not have indexed the bundle yet; keep polling on the next tick. + console.debug('[PendingOrdersUpdater] Failed to fetch EIP-5792 bundle status:', bundleId, error) + }) + }) + + await Promise.all(promises) +} + // TODO: Break down this large function into smaller functions // eslint-disable-next-line max-lines-per-function async function _updateOrders({ @@ -357,6 +408,7 @@ async function _updateOrders({ getSafeTxInfo, safeNonce, allTransactions, + config, markPollComplete, }: UpdateOrdersParams): Promise { // Only check pending orders of current connected account @@ -474,6 +526,8 @@ async function _updateOrders({ cancelOrdersBatch, safeNonce, ) + // Track EIP-5792 approval+presign bundles (EIP-7702 wallets) via getCallsStatus + await _updateEip5792BundleStatus(chainId, orders, config, invalidateOrdersBatch, isSafeWallet) // Update the creating EthFlow orders (if any) await _updateCreatingOrders(chainId, orders, isSafeWallet, addOrUpdateOrders) @@ -495,8 +549,13 @@ async function _updatePresignGnosisSafeTx( safeNonce: number | undefined, ) { const getSafeTxPromises = allPendingOrders - // Update orders that are pending for presingature - .filter((order) => order.presignGnosisSafeTxHash && order.status === OrderStatus.PRESIGNATURE_PENDING) + // Update orders that are pending for presingature (EIP-5792 bundles are tracked separately) + .filter( + (order) => + order.presignGnosisSafeTxHash && + !order.presignIsEip5792Bundle && + order.status === OrderStatus.PRESIGNATURE_PENDING, + ) .map((order): Promise => { // Get safe info and receipt const presignGnosisSafeTxHash = order.presignGnosisSafeTxHash as string diff --git a/apps/cowswap-frontend/src/legacy/state/orders/actions.ts b/apps/cowswap-frontend/src/legacy/state/orders/actions.ts index 0ac093c4918..849d57226dc 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/actions.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/actions.ts @@ -70,8 +70,11 @@ export interface BaseOrder extends OrderCreation { fullAppData?: EnrichedOrder['fullAppData'] // Wallet specific - presignGnosisSafeTxHash?: string // Gnosis Safe tx + presignGnosisSafeTxHash?: string // Gnosis Safe tx, or an EIP-5792 bundle id when presignIsEip5792Bundle is set presignGnosisSafeTx?: SafeMultisigTransactionResponse // Gnosis Safe transaction info + // When true, presignGnosisSafeTxHash holds an EIP-5792 bundle id (EIP-7702 atomic batch) that must + // be tracked via getCallsStatus rather than the Safe transaction service. + presignIsEip5792Bundle?: boolean // Sell amount before the fee applied - necessary for later calculations (unfilled orders) sellAmountBeforeFee: string diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersConfirmModal/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersConfirmModal/index.tsx index eeba00b3416..0dcc788456a 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersConfirmModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersConfirmModal/index.tsx @@ -80,9 +80,7 @@ export function LimitOrdersConfirmModal(props: LimitOrdersConfirmModalProps): Re const inputSymbol = inputAmount?.currency?.symbol || t`token` const canUsePermit = tradeContext.allowsOffchainSigning && isSupportedPermitInfo(tradeContext.permitInfo) - // Temporary: keep limit-order bundles Safe-only until EIP-5792 order lifecycle tracking lands. - const isSafeApprovalBundle = - useIsSafeApprovalBundle(inputAmount) && tradeContext.postOrderParams.isSafeWallet && !canUsePermit + const isSafeApprovalBundle = useIsSafeApprovalBundle(inputAmount) && !canUsePermit const buttonText = isInsufficientBalance ? ( t`Insufficient ${inputSymbol} balance` ) : isSafeApprovalBundle ? ( diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts index 572ebf2a0dc..fd997249ced 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts @@ -62,8 +62,7 @@ export function useHandleOrderPlacement( const safeBundleFlowContext = useSafeBundleFlowContext(tradeContext) const isSafeBundle = useIsSafeApprovalBundle(tradeContext?.postOrderParams.inputAmount) const canUsePermit = tradeContext.allowsOffchainSigning && isSupportedPermitInfo(tradeContext.permitInfo) - // Temporary: keep limit-order bundles Safe-only until EIP-5792 order lifecycle tracking lands. - const shouldUseSafeBundle = isSafeBundle && tradeContext.postOrderParams.isSafeWallet && !canUsePermit + const shouldUseSafeBundle = isSafeBundle && !canUsePermit const alternativeModalAnalytics = useAlternativeModalAnalytics() const analytics = useTradeFlowAnalytics() const { t } = useLingui() @@ -185,8 +184,11 @@ export function useHandleOrderPlacement( // Navigate to open orders after successful placement once the new order is in the store, otherwise you'll be redirected back to OPEN as there would // still be no signing orders. + // Orders that go through the bundle flow are pre-signed on-chain (approveAndPresign), so they + // land in the Signing tab until mined — this covers EIP-7702 accounts, which are EOAs and so + // aren't caught by isSmartContractWallet. Send the user there just like smart-contract wallets. setTimeout(() => { - navigateToOrdersTableTab(isSmartContractWallet ? OrderTabId.SIGNING : OrderTabId.OPEN) + navigateToOrdersTableTab(isSmartContractWallet || shouldUseSafeBundle ? OrderTabId.SIGNING : OrderTabId.OPEN) }) // Analytics event to track alternative modal usage, only if was using alternative modal @@ -218,6 +220,7 @@ export function useHandleOrderPlacement( hideAlternativeOrderModal, alternativeModalAnalytics, isSmartContractWallet, + shouldUseSafeBundle, tradeContext.chainId, ]) } diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/safeBundleFlow/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/safeBundleFlow/index.ts index 85d24e15aa5..adfaa628aa9 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/safeBundleFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/safeBundleFlow/index.ts @@ -197,6 +197,9 @@ export async function safeBundleFlow({ order: { id: order.id, presignGnosisSafeTxHash: safeTxHash, + // Non-Safe atomic wallets (EIP-7702) return an EIP-5792 bundle id here, which is tracked via + // getCallsStatus instead of the Safe transaction service. + presignIsEip5792Bundle: !isSafeWallet, isHidden: false, }, isSafeWallet, diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts b/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts index b419934fb89..8d8b3d1fed1 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts @@ -10,7 +10,6 @@ import { useIsTradeUnsupported, useIsXstockToken, useTryFindToken } from '@cowpr import { useGnosisSafeInfo, useIsRestoringConnection, - useIsSafeWallet, useIsTxBundlingSupported, useWalletDetails, useWalletInfo, @@ -71,7 +70,6 @@ export function useTradeFormValidationContext(): TradeFormValidationCommonContex const isOutputCurrencyXstock = useIsXstockToken(getNonNativeCurrency(outputCurrency)) const isBundlingSupported = useIsTxBundlingSupported() - const isSafeWallet = useIsSafeWallet() const isWrapUnwrap = useIsWrapOrUnwrap() const { allowsOffchainSigning, isSupportedWallet } = useWalletDetails() const gnosisSafeInfo = useGnosisSafeInfo() @@ -84,11 +82,8 @@ export function useTradeFormValidationContext(): TradeFormValidationCommonContex const isSafeReadonlyUser = gnosisSafeInfo?.isReadOnly === true - // Temporary: keep limit-order bundles Safe-only until EIP-5792 order lifecycle tracking lands. - const isBundlingSupportedForContext = - tradeType === TradeType.LIMIT_ORDER ? isSafeWallet && isBundlingSupported : isBundlingSupported const isApproveRequired = useIsApprovalOrPermitRequired({ - isBundlingSupportedOrEnabledForContext: isBundlingSupportedForContext, + isBundlingSupportedOrEnabledForContext: isBundlingSupported, allowsOffchainSigning, }).reason