Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Expand Up @@ -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'
Expand Down Expand Up @@ -93,6 +96,7 @@ interface UpdateOrdersParams {
getSafeTxInfo: GetSafeTxInfo
safeNonce: number | undefined
allTransactions: ReturnType<typeof useAllTransactions>
config: Config
markPollComplete?: (chainId: ChainId) => void
}

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -212,6 +217,7 @@ export function PendingOrdersUpdater(): null {
getSafeTxInfo,
safeNonce,
allTransactions,
config,
markPollComplete: shouldMarkCompletion ? markPollComplete : undefined,
}).finally(() => {
isUpdating.current = false
Expand All @@ -231,6 +237,7 @@ export function PendingOrdersUpdater(): null {
getSafeTxInfo,
safeNonce,
allTransactions,
config,
markPollComplete,
],
)
Expand Down Expand Up @@ -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<void> {
const promises = allPendingOrders
.filter(
(order) =>
order.presignIsEip5792Bundle &&
order.presignGnosisSafeTxHash &&
order.status === OrderStatus.PRESIGNATURE_PENDING,
)
.map((order): Promise<void> => {
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)
}
Comment on lines +347 to +389

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant file and nearby update flow without running repo code.
if [ -f apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts ]; then
  echo "=== file exists ==="
  wc -l apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts
  echo "=== outline ==="
  ast-grep outline apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts || true
  echo "=== relevant lines 300-410 ==="
  sed -n '300,410p' apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts | nl -ba - v300
  echo "=== getCallsStatus references ==="
  rg -n "getCallsStatus|PRESIGNATURE_PENDING|_updateEip5792BundleStatus|presignIsEip5792Bundle|presignGnosisSafeTxHash" apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts -C 3
else
  echo "missing file"
fi

echo "=== broader getCallsStatus definitions/usages ==="
rg -n "getCallsStatus|type .*Call|call|eip-5792|EIP-5792|rpc" apps/cowswap-frontend/src -S || true

Repository: cowprotocol/cowswap

Length of output: 1430


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== relevant lines 340-405 ==="
sed -n '340,405p' apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts

echo "=== call status polling / retry constants around updater ==="
sed -n '1,180p' apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts
sed -n '180,340p' apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts

echo "=== broader getCallsStatus references ==="
rg -n "getCallsStatus|PRESIGNATURE_PENDING|_updateEip5792BundleStatus|presignIsEip5792Bundle|presignGnosisSafeTxHash" apps/cowswap-frontend/src -S || true

Repository: cowprotocol/cowswap

Length of output: 23700


🌐 Web query:

EIP-5792 getCallsStatus unknown batchId error pending response

💡 Result:

The "unknown batchId" error in the context of EIP-5792 is formally represented by RPC error code 5730, which explicitly indicates that the provided bundle ID is unknown or has not been submitted via wallet_sendCalls [1][2]. If you are encountering this error, it typically means one of the following: 1. Invalid or Expired ID: The batchId (or id) provided to wallet_getCallsStatus does not match any active or recent batch known to the wallet [1][2]. Per the EIP-5792 specification, wallets are recommended to maintain status information for at least 24 hours after the corresponding wallet_sendCalls call [1][2]. If you are attempting to poll for a status after this window, or if the wallet has cleared its cache, the ID will be unknown [1]. 2. Incorrect ID format: Ensure that the id being passed to wallet_getCallsStatus is the exact string returned by the initial wallet_sendCalls call [1][3]. 3. Wallet Implementation Issues: Some wallets may not persist batch IDs reliably across sessions or may have bugs in their implementation of the wallet_getCallsStatus method [4]. Regarding "pending" responses: The wallet_getCallsStatus method is designed to be polled [5]. When a batch has been submitted but not yet fully processed/included on-chain, the wallet should return a status code of 100 (Pending) [6][7]. If you are implementing a client, you should continue to poll the getCallsStatus endpoint until the status transitions to a terminal state, such as 200 (Success) or various failure codes (e.g., 300-700 range) [6][8]. If you are using libraries like Viem or Wagmi, they often abstract these RPC calls [9][4]. If you see inconsistent results (e.g., "failure" status while transactions actually succeeded), it may be due to the wallet's specific implementation of status reporting or a divergence in how the client interprets those codes [4][10]. In such cases, verifying the transaction hashes directly via eth_getTransactionReceipt is a standard fallback strategy to determine the true state of the transactions [10].

Citations:


🌐 Web query:

EIP-5792 rpcErrorCode 5730 unknownCallsBatchId wallet sendCalls getCallsStatus

💡 Result:

In the context of EIP-5792, rpcErrorCode 5730 represents an Unknown Bundle ID error [1][2]. This error occurs when a wallet receives a request containing a call bundle identifier that it does not recognize or that has not been previously submitted via wallet_sendCalls [1][2]. This error code is specifically associated with the following EIP-5792 RPC methods [1][2]: 1. wallet_getCallsStatus: Used by applications to query the status of a previously submitted call batch. If the provided ID is unknown to the wallet, it must return error 5730 [3][4]. 2. wallet_showCallsStatus: Used to request that a wallet display information about a specific call bundle. Similarly, if the ID is not recognized, the wallet returns error 5730 [5][6]. In technical implementations, such as those using the Viem library, this is categorized as an UnknownBundleIdError [7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== package references for viem/wagmi versions ==="
for f in package.json apps/cowswap-frontend/package.json yarn.lock pnpm-lock.yaml package-lock.json; do
  if [ -f "$f" ]; then
    echo "---- $f ----"
    rg -n '"viem"|viem@|wagmi@|wagmi"' "$f" | head -50 || true
  fi
done

echo "=== deterministic EIP-5792 behavior from source if docs are vendored ==="
rg -n "unknownCallsBatchId|5730|UnknownBundleIdError|Unknown Calls Batch|getCallsStatus|Pending.*status" -S \
  node_modules \
  "$PWD" 2>/dev/null | head -80 || true

Repository: cowprotocol/cowswap

Length of output: 37965


Do not retry permanent bundle-status errors forever.

The catch block currently treats every getCallsStatus error as temporary. EIP-5792 uses RPC error 5730 for unknown bundle IDs, which should be treated as terminal; a permanently unavailable/unknown bundle ID can otherwise keep the order in PRESIGNATURE_PENDING until the next regular flow update. Add bounded retry/expiry handling and invalidate the order on terminal unknown-bundle errors while still allowing retries for transient wallet indexing failures.

🤖 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/common/updaters/orders/PendingOrdersUpdater.ts`
around lines 347 - 389, The catch block in _updateEip5792BundleStatus should
distinguish terminal EIP-5792 unknown-bundle RPC errors (code 5730) from
transient wallet-indexing failures. Add bounded retry/expiry tracking for status
checks, invalidate the associated order when error 5730 occurs or the retry
limit expires, and retain polling retries for transient errors within the bound.


// TODO: Break down this large function into smaller functions
// eslint-disable-next-line max-lines-per-function
async function _updateOrders({
Expand All @@ -357,6 +408,7 @@ async function _updateOrders({
getSafeTxInfo,
safeNonce,
allTransactions,
config,
markPollComplete,
}: UpdateOrdersParams): Promise<void> {
// Only check pending orders of current connected account
Expand Down Expand Up @@ -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)

Expand All @@ -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<void> => {
// Get safe info and receipt
const presignGnosisSafeTxHash = order.presignGnosisSafeTxHash as string
Expand Down
5 changes: 4 additions & 1 deletion apps/cowswap-frontend/src/legacy/state/orders/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { useIsTradeUnsupported, useIsXstockToken, useTryFindToken } from '@cowpr
import {
useGnosisSafeInfo,
useIsRestoringConnection,
useIsSafeWallet,
useIsTxBundlingSupported,
useWalletDetails,
useWalletInfo,
Expand Down Expand Up @@ -64,7 +63,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()
Expand All @@ -77,11 +75,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

Expand Down
Loading