Skip to content

Conversation

@crutch12
Copy link
Contributor

@crutch12 crutch12 commented Dec 25, 2025

Summary

  1. Prevent "Swap and bridge quote is loaded twice" problem (Swap and bridge quote is loaded twice #6675)
  2. Prevent re-fetch quote loop problem: quote -> suggestedSlippageBps -> quote -> suggestedSlippageBps ... (Quote is loaded twice or more times #6762)

Since quote and suggestedSlippageBps update each other, we have to break the re-fetch loop that happens if suggestedSlippageBps changes on the next quote response.

The easiest workaround: throttle (1.5s) quote calls in case when only suggestedSlippageBps was changed.

TODO

  • yarn test fails
  • add e2e tests for smart/manual slippage with re-fetch loop emulation
  • slippage mode change (manual -> auto) doesn't trigger quote request
  • initial quote request always fetches with slippageBips: 50 (e.g. on pade load or on token change). UPD: won't do, I think it's okay

To Test

  1. (for developers) change getSlippageTolerance return value as comment says. it will allow to 100% catch re-fetch loop problem
  2. select input/output token
  3. update input sell amount value
  4. should see only TWO /quote requests (fast and optimal)

Background

I don't really like this throttle solution, maybe it's possible to keep needed state in tradeQuotesAtom to distinguish intended and unintended quoteParams updates 🤷

Summary by CodeRabbit

  • Bug Fixes

    • Prevented unnecessary quote refreshes and re-fetch loops when slippage updates rapidly.
    • Fixed quote expiration/timing to use precise millisecond calculations.
  • Improvements

    • Smart slippage is applied consistently (no longer blocked by loading) and preserved across quotes.
    • Throttling and “only-slippage-changed” detection reduce redundant slippage-driven fetches.
  • New Features

    • Added a synced-ref hook for stable, up-to-date value access.
  • Tests

    • Updated timing tests to millisecond precision.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Dec 25, 2025

@crutch12 is attempting to deploy a commit to the cow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 25, 2025

Walkthrough

Adds a cached suggested slippage to tradeQuote state, reads smart slippage from that cache, throttles/refuses rapid slippage-only quote re-fetches (~1.5s), converts quote-deadline math/tests to millisecond precision, introduces a useSyncedRef hook, and adds a utility to detect slippage-only param changes.

Changes

Cohort / File(s) Summary
TradeQuote state
apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts
Add `suggestedSlippageBps: number
Smart slippage consumer
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts
Read suggested slippage from tradeQuote?.suggestedSlippageBps (cached) instead of deep quote path to avoid re-fetch loops; keep bounding/cap logic.
Quote param handling
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts
Remove isLoading guard/import; always include smart slippage when tradeSlippage.type === 'smart'.
Quote param utility
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteParamsChanges.ts
Add checkOnlySlippageBpsChanged(...) to detect changes that affect only swapSlippageBps/bridgeSlippageBps (ignores signer).
Polling / throttling
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
Add QUOTE_SLIPPAGE_CHANGE_THROTTLE_INTERVAL (~1500ms); track prevQuoteParamsRef; use checkOnlySlippageBpsChanged and useSyncedRef to throttle/skip fetches when only slippage changed and last quote is recent.
TradeQuote manager
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts
When an OPTIMAL quote arrives, also set suggestedSlippageBps on successful quote update and mark isLoading false.
Deadline timing & tests
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.ts, apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.test.ts
Move deadline/time math and tests to millisecond precision (use Date.getTime() / ONE_SEC); adjust expectations and offsets.
BFF client (non-functional)
apps/cowswap-frontend/src/common/services/bff/cowBffClient.ts
Minor formatting/comment change in returned object literal; no behavior change.
Common hooks library
libs/common-hooks/src/useSyncedRef.ts, libs/common-hooks/src/index.ts
Add useSyncedRef hook (returns readonly wrapper with live current) and re-export it from the library index.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant UI as UI / Hooks
    participant Poll as useTradeQuotePolling
    participant BFF as BFF Client
    participant State as tradeQuoteAtom
    participant Smart as useSmartSlippageFromQuote

    rect rgb(240,248,255)
    Note over UI,Poll: Initial quote request (normal flow)
    UI->>Poll: requestQuote(quoteParams)
    Poll->>BFF: fetchQuote(quoteParams)
    BFF-->>Poll: quote + suggestedSlippageBps
    Poll->>State: updateTradeQuote(quote, suggestedSlippageBps)
    State-->>State: cache suggestedSlippageBps, set localQuoteTimestamp
    end

    rect rgb(255,250,240)
    Note over UI,Poll: Rapid user slippage adjustments (throttled)
    UI->>Poll: requestQuote(newQuoteParams)
    Poll->>Poll: compare prevQuoteParams (checkOnlySlippageBpsChanged)
    alt only slippage changed AND lastQuoteAge < 1.5s
        Poll-->>Poll: skip fetch (throttle)
    else
        Poll->>BFF: fetchQuote(newQuoteParams)
        BFF-->>State: quote + suggestedSlippageBps
        State->>State: update cached suggestedSlippageBps (if applicable)
    end
    end

    rect rgb(245,255,250)
    Note over Smart,State: Smart slippage usage (consumer)
    Smart->>State: read suggestedSlippageBps (cached)
    Smart-->>UI: return bounded slippage value
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • #6706 — Directly related change to slippage-inclusion guard in useQuoteParams.ts (opposing/adjacent logic).
  • #5979 — Related updates to quote polling behavior and fetch-trigger control flow.
  • #5830 — Related changes to sourcing smart slippage and useSmartSlippageFromQuote flow.

Suggested reviewers

  • elena-zh
  • shoom3301
  • alfetopito

Poem

🐰 I cached a slippage hop so fleet,
I paused the fetch when inputs repeat.
Millisecond hops, tidy and spry,
Quotes breathe a moment — then softly reply. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The PR description provides context but deviates significantly from the template structure with incomplete sections and informal TODO items. Reorganize the description to follow the template: add numbered testing steps with checkboxes, clarify expected outcomes, and move informal TODOs to comments or mark them properly.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: fixing quote double loading and preventing a re-fetch loop, directly addressing the core problems described in the PR.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.test.ts (2)

48-59: Test inconsistency: localQuoteTimestamp units don't match implementation.

The test uses localQuoteTimestamp = 1713167232 which appears to be in seconds (a Unix epoch timestamp), while the implementation now expects localQuoteTimestamp in milliseconds (from Date.now()). Additionally, validFor from the API is in seconds, not milliseconds.

With the current implementation (Math.ceil(localQuoteTimestamp / 1000) + validFor), when localQuoteTimestamp = 1713167232:

  • expectedValidTo = Math.ceil(1713167232 / 1000) + 60000 = 1713168 + 60000 = 1773168
  • But quoteValidTo = 1713167232 + 60000 = 1713227232

These don't match, so the test would fail. The localQuoteTimestamp in this test should be in milliseconds (e.g., 1713167232000) and validFor should remain in seconds (e.g., 60).

🔎 Proposed fix
  it('When expected validTo and quote validTo are the same, then should return 0', () => {
-     const validFor = 60 * ONE_SEC // 1 minute
-     const localQuoteTimestamp = 1713167232
+     const validFor = 60 // 1 minute in seconds (as returned by API)
+     const localQuoteTimestamp = 1713167232000 // ms

      expect(
        getQuoteTimeOffset(
          getQuoteState({
            validFor,
            localQuoteTimestamp,
-           quoteValidTo: localQuoteTimestamp + validFor,
+           quoteValidTo: Math.ceil(localQuoteTimestamp / 1000) + validFor,
          }),
        ),
      ).toEqual(0)
    })

161-170: Same unit inconsistency pattern in isQuoteExpired tests.

These tests have the same issue: deadline is multiplied by ONE_SEC making it milliseconds, but validFor in the API response is seconds. The quoteValidTo calculation also incorrectly adds millisecond values.

This pattern repeats across all isQuoteExpired tests (lines 161, 178, 193, 214, 227, 230). Given the PR mentions "TODO: yarn test currently fails", these unit mismatches are likely the cause.

🧹 Nitpick comments (1)
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts (1)

47-53: Consider simplifying the redundant conditional.

Both 'user' and 'smart' branches return tradeSlippage.value, making the nested ternary redundant.

🔎 Proposed simplification
-  // Slippage value for quote params:
-  // - User slippage: always include (re-quotes when user changes it)
-  // - Smart slippage: always include (re-quotes when user changes amount, etc.)
-  const slippageBps =
-    tradeSlippage.type === 'user'
-      ? tradeSlippage.value
-      : tradeSlippage.type === 'smart'
-        ? tradeSlippage.value
-        : undefined
+  // Slippage value for quote params:
+  // - User slippage: always include (re-quotes when user changes it)
+  // - Smart slippage: always include (re-quotes when user changes amount, etc.)
+  const slippageBps =
+    tradeSlippage.type === 'user' || tradeSlippage.type === 'smart'
+      ? tradeSlippage.value
+      : undefined
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14686d1 and 685b982.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (8)
  • apps/cowswap-frontend/src/common/services/bff/cowBffClient.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.test.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteParamsChanges.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6299
File: apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.test.tsx:62-67
Timestamp: 2025-09-25T08:49:32.256Z
Learning: In the cowswap-frontend codebase, when testing hooks that use multiple bridge providers, both providers are always properly mocked as complete jest.Mocked<BridgeProvider<BridgeQuoteResult>> objects with all required methods stubbed, ensuring no undefined returns that could break the hook logic.
Learnt from: alfetopito
Repo: cowprotocol/cowswap PR: 5830
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts:5-9
Timestamp: 2025-06-16T15:58:57.402Z
Learning: In the cowswap system, 0 bps will never be suggested as a slippage value, so falsy checks on suggested slippage values are safe and won't drop valid 0 values.
📚 Learning: 2025-06-16T15:58:57.402Z
Learnt from: alfetopito
Repo: cowprotocol/cowswap PR: 5830
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts:5-9
Timestamp: 2025-06-16T15:58:57.402Z
Learning: In the cowswap system, 0 bps will never be suggested as a slippage value, so falsy checks on suggested slippage values are safe and won't drop valid 0 values.

Applied to files:

  • apps/cowswap-frontend/src/common/services/bff/cowBffClient.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteParamsChanges.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts
📚 Learning: 2025-06-23T07:03:50.760Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 5859
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts:76-82
Timestamp: 2025-06-23T07:03:50.760Z
Learning: In the useTradeQuotePolling hook, there are two useLayoutEffect hooks that work together: one resets the counter to 0 when the confirmation modal closes, and another automatically triggers pollQuote(false, true) whenever the counter reaches 0. This creates an intentional chain reaction for immediate quote updates.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
📚 Learning: 2025-09-25T08:46:43.815Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6299
File: apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.test.ts:376-385
Timestamp: 2025-09-25T08:46:43.815Z
Learning: In fetchAndProcessQuote.ts, when bridgingSdk.getBestQuote() returns null, no loading state reset is needed because loading state management is handled through onQuoteResult() callback for results and processQuoteError() for errors. The null case is intentionally designed not to trigger any manager methods.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
📚 Learning: 2025-10-10T20:28:16.565Z
Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 6347
File: apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx:49-49
Timestamp: 2025-10-10T20:28:16.565Z
Learning: In apps/cowswap-frontend/src/modules/trade, TradeConfirmation follows a two-layer architecture: TradeConfirmationView (pure/stateless) in pure/TradeConfirmation/index.tsx renders the UI, while TradeConfirmation (container) in containers/TradeConfirmation/index.tsx wraps it to freeze props during pending trades (via useStableTradeConfirmationProps), wire in signing state (useSigningStep), and inject trade confirmation state (useTradeConfirmState). Consuming modules should import the container TradeConfirmation from 'modules/trade' to preserve this stateful behavior.
<!-- [add_learning]
When reviewing component refactoring in apps/cowswap-frontend/src/modules/trade, recognize the pattern where a pure view component (e.g., TradeConfirmationView) is separated from a stateful container (e.g., TradeConfirmation) that wraps it. The container adds runtime state management (prop stabilization, signing state, etc.) while the view remains testable and composable. Do not flag usages that import th...

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts
📚 Learning: 2025-10-13T19:41:31.440Z
Learnt from: limitofzero
Repo: cowprotocol/cowswap PR: 6351
File: apps/cowswap-frontend/src/modules/erc20Approve/containers/TradeApproveModal/useTradeApproveCallback.ts:87-121
Timestamp: 2025-10-13T19:41:31.440Z
Learning: In apps/cowswap-frontend/src/modules/erc20Approve, useApproveCallback returns Promise<TransactionResponse | undefined> and is distinct from useApproveCurrency, which can return Promise<TransactionReceipt | SafeMultisigTransactionResponse>. When reviewing approval flows, verify which hook is actually being used before flagging Safe wallet concerns.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts
📚 Learning: 2025-08-08T13:56:18.009Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6125
File: libs/tokens/src/updaters/TokensListsUpdater/index.tsx:29-31
Timestamp: 2025-08-08T13:56:18.009Z
Learning: In libs/tokens/src/updaters/TokensListsUpdater/index.tsx, the project’s current Jotai version requires using `unstable_getOnInit` (not `getOnInit`) in atomWithStorage options; keep `{ unstable_getOnInit: true }` until Jotai is upgraded.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
📚 Learning: 2025-03-25T10:03:35.157Z
Learnt from: alfetopito
Repo: cowprotocol/cowswap PR: 5532
File: libs/common-hooks/src/useOnScroll.tsx:22-38
Timestamp: 2025-03-25T10:03:35.157Z
Learning: In React useEffect hooks, refs (RefObjects) should generally be included in dependency arrays even though changes to ref.current don't trigger effects. This is because the ref object itself could change between renders (especially in custom hooks where refs are passed as parameters), and it follows React's best practice of including all external values used in the effect.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
🧬 Code graph analysis (3)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteParamsChanges.ts (1)
apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts (1)
  • TradeQuoteState (12-23)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.test.ts (1)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.ts (1)
  • getOrderValidTo (62-72)
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts (1)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteParamsChanges.ts (1)
  • checkOnlySlippageBpsChanged (7-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Setup
  • GitHub Check: Cypress
🔇 Additional comments (9)
apps/cowswap-frontend/src/common/services/bff/cowBffClient.ts (1)

56-59: LGTM - Debug test line preserved as documentation.

The commented-out random slippage line serves as useful documentation for reproducing the re-fetch loop issue during future debugging. The implementation correctly returns the slippageBps from the API response.

apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.ts (2)

29-29: LGTM - Timestamp unit alignment.

The change correctly uses localQuoteTimestamp directly since it's now stored in milliseconds (via Date.now()), eliminating the redundant * 1000 multiplication.


54-54: LGTM - Proper unit conversion for time offset calculation.

The Math.ceil(localQuoteTimestamp / 1000) correctly converts the ms timestamp to seconds before adding validFor (which is in seconds), maintaining proper unit consistency with quoteValidTo (also in seconds).

apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts (1)

14-15: LGTM - Core fix for the re-fetch loop.

Reading from the cached slippageBps field instead of quote.quoteResults.suggestedSlippageBps breaks the update cycle. The falsy check with || null is safe since, based on learnings, 0 bps is never suggested as a slippage value.

apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteParamsChanges.ts (1)

7-21: LGTM - Clean utility for detecting slippage-only changes.

The logic correctly identifies when only slippageBps has changed between quote params by:

  1. Ensuring not in loading state
  2. Confirming slippageBps values differ
  3. Verifying all other fields (excluding signer reference) are equal via deep comparison
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts (2)

56-59: LGTM - Correct ref update timing for comparison.

Using useEffect to update prevQuoteParamsRef ensures the ref holds the previous value when useLayoutEffect runs (since useLayoutEffect executes before useEffect). This timing is intentional and correct for the comparison logic.


86-101: LGTM - Effective throttle for slippage-only changes.

The throttling logic correctly:

  1. Detects when only slippageBps changed using the new utility
  2. Skips re-fetch if the last quote was received within 1.5 seconds
  3. Falls through to normal polling for other parameter changes

This breaks the re-fetch loop described in the PR objectives by preventing cascading updates when slippage adjusts from the quote response.

apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts (2)

21-22: LGTM - New cached slippage field.

The slippageBps field correctly caches the suggested slippage from the quote response, enabling the throttling mechanism in the polling hook.

Also applies to: 34-34


59-68: LGTM - Proper slippage caching with FAST quote handling.

The logic correctly:

  1. Preserves the previous quote when nextState.quote is undefined (partial updates)
  2. Updates localQuoteTimestamp to Date.now() only when a new quote arrives
  3. Caches slippageBps from optimal quotes while ignoring FAST quotes (which return a default value)

The comment on line 66 helpfully documents why FAST quote slippage is ignored.

@crutch12 crutch12 force-pushed the fix/swap-bridge-quote-loaded-twice branch from 685b982 to 434cb44 Compare December 25, 2025 23:19
@crutch12 crutch12 force-pushed the fix/swap-bridge-quote-loaded-twice branch from 434cb44 to eac18d7 Compare December 25, 2025 23:20
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.test.ts (1)

47-92: Critical: Multiple tests have unit mismatches.

Several other tests in this file have similar unit issues:

Lines 47-92 (getQuoteTimeOffset tests):

  • validFor is multiplied by ONE_SEC (milliseconds)
  • localQuoteTimestamp is a plain number like 1713167232 (appears to be seconds)
  • quoteValidTo is calculated as localQuoteTimestamp + validFor, mixing seconds + milliseconds

Lines 157-240 (isQuoteExpired tests):

  • deadline is multiplied by ONE_SEC (milliseconds)
  • localQuoteTimestamp is set to NOW_TIME (milliseconds)
  • quoteValidTo is calculated as localQuoteTimestamp + deadline (both milliseconds), but should be in seconds

The implementation expects:

  • validFor: seconds (see line 54 in quoteDeadline.ts)
  • quoteValidTo: Unix timestamp in seconds
  • localQuoteTimestamp: milliseconds (see line 29 in quoteDeadline.ts)
🔎 Recommended approach to fix all tests
  1. For getQuoteTimeOffset tests (lines 47-92):

    • Keep validFor in seconds (remove * ONE_SEC)
    • Keep localQuoteTimestamp in seconds
    • Calculate quoteValidTo in seconds
  2. For isQuoteExpired tests (lines 157-240):

    • Keep deadline (which is validFor) in seconds (remove * ONE_SEC)
    • Keep localQuoteTimestamp as NOW_TIME (milliseconds)
    • Calculate quoteValidTo as Math.ceil(localQuoteTimestamp / 1000) + deadline (seconds)
  3. Update all expectations to match the seconds-based return values.

Example for lines 47-60:

  it('When expected validTo and quote validTo are the same, then should return 0', () => {
-     const validFor = 60 * ONE_SEC // 1 minute
-     const localQuoteTimestamp = 1713167232
+     const validFor = 60 // 1 minute in seconds
+     const localQuoteTimestamp = 1713167232000 // milliseconds

      expect(
        getQuoteTimeOffset(
          getQuoteState({
            validFor,
            localQuoteTimestamp,
-           quoteValidTo: localQuoteTimestamp + validFor,
+           quoteValidTo: Math.ceil(localQuoteTimestamp / 1000) + validFor,
          }),
        ),
      ).toEqual(0)
    })

Also applies to: 157-240

♻️ Duplicate comments (1)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.test.ts (1)

112-123: Critical: Test uses incorrect units despite past review.

The test multiplies deadline and offset by ONE_SEC (converting to milliseconds), but the implementation expects these values in seconds:

  • Line 54 in quoteDeadline.ts: expectedValidTo = Math.ceil(localQuoteTimestamp / 1000) + validForvalidFor must be in seconds
  • Line 67-68 in getOrderValidTo: const now = Date.now() / 1000 — returns seconds
  • Line 118: quoteValidTo: localQuoteTimestamp + deadline + offset mixes milliseconds (localQuoteTimestamp) with millisecond-converted deadline/offset, but quoteValidTo should be a Unix timestamp in seconds
  • Line 122: Expects NOW_TIME + deadline + offset (milliseconds), but getOrderValidTo returns seconds

This was flagged in a previous review as "✅ Addressed" but the fix was not applied.

🔎 Proposed fix to use correct units
  it('ValidTo should be now + deadline + timeOffset', () => {
-     const deadline = 5400 * ONE_SEC // 1.5 hours
-     const offset = 3600 * ONE_SEC // 1 hour
+     const deadline = 5400 // 1.5 hours in seconds
+     const offset = 3600 // 1 hour in seconds
      const localQuoteTimestamp = NOW_TIME
      const quoteDeadlineParams = {
        validFor: deadline,
-       quoteValidTo: localQuoteTimestamp + deadline + offset,
+       quoteValidTo: Math.ceil(localQuoteTimestamp / 1000) + deadline + offset,
        localQuoteTimestamp: localQuoteTimestamp,
      }

-     expect(getOrderValidTo(deadline, getQuoteState(quoteDeadlineParams))).toEqual(NOW_TIME + deadline + offset)
+     const expectedSeconds = Math.floor(deadline + NOW_TIME / 1000 - offset)
+     expect(getOrderValidTo(deadline, getQuoteState(quoteDeadlineParams))).toEqual(expectedSeconds)
    })
🧹 Nitpick comments (1)
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts (1)

26-26: Optional: Consider removing the eslint-disable directive.

The max-lines-per-function rule is disabled for the entire function. If possible, consider refactoring to reduce function size rather than disabling the rule. However, this can be addressed in a future PR if needed.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 685b982 and eac18d7.

📒 Files selected for processing (8)
  • apps/cowswap-frontend/src/common/services/bff/cowBffClient.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.test.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteParamsChanges.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteParamsChanges.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6299
File: apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.test.tsx:62-67
Timestamp: 2025-09-25T08:49:32.256Z
Learning: In the cowswap-frontend codebase, when testing hooks that use multiple bridge providers, both providers are always properly mocked as complete jest.Mocked<BridgeProvider<BridgeQuoteResult>> objects with all required methods stubbed, ensuring no undefined returns that could break the hook logic.
📚 Learning: 2025-06-23T07:03:50.760Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 5859
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts:76-82
Timestamp: 2025-06-23T07:03:50.760Z
Learning: In the useTradeQuotePolling hook, there are two useLayoutEffect hooks that work together: one resets the counter to 0 when the confirmation modal closes, and another automatically triggers pollQuote(false, true) whenever the counter reaches 0. This creates an intentional chain reaction for immediate quote updates.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts
📚 Learning: 2025-08-08T13:56:18.009Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6125
File: libs/tokens/src/updaters/TokensListsUpdater/index.tsx:29-31
Timestamp: 2025-08-08T13:56:18.009Z
Learning: In libs/tokens/src/updaters/TokensListsUpdater/index.tsx, the project’s current Jotai version requires using `unstable_getOnInit` (not `getOnInit`) in atomWithStorage options; keep `{ unstable_getOnInit: true }` until Jotai is upgraded.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
📚 Learning: 2025-03-25T10:03:35.157Z
Learnt from: alfetopito
Repo: cowprotocol/cowswap PR: 5532
File: libs/common-hooks/src/useOnScroll.tsx:22-38
Timestamp: 2025-03-25T10:03:35.157Z
Learning: In React useEffect hooks, refs (RefObjects) should generally be included in dependency arrays even though changes to ref.current don't trigger effects. This is because the ref object itself could change between renders (especially in custom hooks where refs are passed as parameters), and it follows React's best practice of including all external values used in the effect.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
📚 Learning: 2025-09-25T08:46:43.815Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6299
File: apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.test.ts:376-385
Timestamp: 2025-09-25T08:46:43.815Z
Learning: In fetchAndProcessQuote.ts, when bridgingSdk.getBestQuote() returns null, no loading state reset is needed because loading state management is handled through onQuoteResult() callback for results and processQuoteError() for errors. The null case is intentionally designed not to trigger any manager methods.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts
📚 Learning: 2025-06-16T15:58:57.402Z
Learnt from: alfetopito
Repo: cowprotocol/cowswap PR: 5830
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts:5-9
Timestamp: 2025-06-16T15:58:57.402Z
Learning: In the cowswap system, 0 bps will never be suggested as a slippage value, so falsy checks on suggested slippage values are safe and won't drop valid 0 values.

Applied to files:

  • apps/cowswap-frontend/src/common/services/bff/cowBffClient.ts
📚 Learning: 2025-10-10T20:28:16.565Z
Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 6347
File: apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx:49-49
Timestamp: 2025-10-10T20:28:16.565Z
Learning: In apps/cowswap-frontend/src/modules/trade, TradeConfirmation follows a two-layer architecture: TradeConfirmationView (pure/stateless) in pure/TradeConfirmation/index.tsx renders the UI, while TradeConfirmation (container) in containers/TradeConfirmation/index.tsx wraps it to freeze props during pending trades (via useStableTradeConfirmationProps), wire in signing state (useSigningStep), and inject trade confirmation state (useTradeConfirmState). Consuming modules should import the container TradeConfirmation from 'modules/trade' to preserve this stateful behavior.
<!-- [add_learning]
When reviewing component refactoring in apps/cowswap-frontend/src/modules/trade, recognize the pattern where a pure view component (e.g., TradeConfirmationView) is separated from a stateful container (e.g., TradeConfirmation) that wraps it. The container adds runtime state management (prop stabilization, signing state, etc.) while the view remains testable and composable. Do not flag usages that import th...

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts
📚 Learning: 2025-10-13T19:41:31.440Z
Learnt from: limitofzero
Repo: cowprotocol/cowswap PR: 6351
File: apps/cowswap-frontend/src/modules/erc20Approve/containers/TradeApproveModal/useTradeApproveCallback.ts:87-121
Timestamp: 2025-10-13T19:41:31.440Z
Learning: In apps/cowswap-frontend/src/modules/erc20Approve, useApproveCallback returns Promise<TransactionResponse | undefined> and is distinct from useApproveCurrency, which can return Promise<TransactionReceipt | SafeMultisigTransactionResponse>. When reviewing approval flows, verify which hook is actually being used before flagging Safe wallet concerns.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts
🧬 Code graph analysis (2)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.test.ts (1)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.ts (1)
  • getOrderValidTo (62-72)
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts (1)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteParamsChanges.ts (1)
  • checkOnlySlippageBpsChanged (7-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Setup
  • GitHub Check: Cypress
🔇 Additional comments (4)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteDeadline.ts (1)

29-29: LGTM! Timestamp calculations now correctly use millisecond precision.

The changes properly align localQuoteTimestamp handling to use milliseconds throughout:

  • Line 29 removes the * 1000 multiplication since localQuoteTimestamp is already in milliseconds
  • Line 54 converts milliseconds to seconds before adding validFor

This fixes the timing drift mentioned in the PR objectives.

Also applies to: 54-54

apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts (2)

56-59: LGTM! Tracking previous quote params enables throttling logic.

The new effect correctly captures the previous quoteParams for comparison in the throttling logic below.


86-101: LGTM! Throttling breaks the slippage re-fetch loop.

The throttling logic effectively prevents the quote → slippage → quote loop by:

  1. Detecting when only slippageBps changed (line 86-90)
  2. Checking if the last quote was fetched within 1.5 seconds (line 93-98)
  3. Skipping the fetch if both conditions are met

The timestamp arithmetic is correct (both Date.now() and localQuoteTimestamp are in milliseconds), and the 1.5s interval aligns with the PR objective.

apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts (1)

47-53: LGTM! Smart slippage now always included, aligned with throttling strategy.

Removing the loading check for smart slippage simplifies the logic and works correctly with the throttling mechanism in useTradeQuotePolling.ts. The 1.5s throttle prevents rapid re-fetches when slippage updates, making it safe to always include the smart slippage value.

Comment on lines +56 to +59
return {
slippageBps: data.slippageBps,
// slippageBps: data.slippageBps + Math.floor(Math.random() * 25), // uncomment to test smart slippage re-fetch quote loop problem (fixed) (#6675)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove debug comment before merging.

The commented-out line 58 is for testing the re-fetch loop scenario. This debug code should be removed before merging to prevent accidental activation in production.

🔎 Proposed cleanup
      log(`Retrieved slippage tolerance from API: ${data.slippageBps} BPS`)
-     return {
-       slippageBps: data.slippageBps,
-       // slippageBps: data.slippageBps + Math.floor(Math.random() * 25), // uncomment to test smart slippage re-fetch quote loop problem (fixed) (#6675)
-     }
+     return { slippageBps: data.slippageBps }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return {
slippageBps: data.slippageBps,
// slippageBps: data.slippageBps + Math.floor(Math.random() * 25), // uncomment to test smart slippage re-fetch quote loop problem (fixed) (#6675)
}
return { slippageBps: data.slippageBps }
🤖 Prompt for AI Agents
In apps/cowswap-frontend/src/common/services/bff/cowBffClient.ts around lines 56
to 59, remove the commented-out debug line that mutates slippageBps ("//
slippageBps: data.slippageBps + Math.floor(Math.random() * 25)...") so the
returned object only uses the real value (slippageBps: data.slippageBps); delete
the comment entirely to avoid accidental activation in production and keep the
code clean.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts (1)

28-28: Consider refactoring to reduce function complexity.

The eslint-disable indicates the function has grown large. As noted in the PR description, the current throttling approach is a workaround. Consider the suggested alternative of tracking intent in tradeQuotesAtom to better separate concerns and reduce this function's complexity.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eac18d7 and 0558792.

📒 Files selected for processing (3)
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
  • libs/common-hooks/src/index.ts
  • libs/common-hooks/src/useSyncedRef.ts
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 5859
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts:76-82
Timestamp: 2025-06-23T07:03:50.760Z
Learning: In the useTradeQuotePolling hook, there are two useLayoutEffect hooks that work together: one resets the counter to 0 when the confirmation modal closes, and another automatically triggers pollQuote(false, true) whenever the counter reaches 0. This creates an intentional chain reaction for immediate quote updates.
📚 Learning: 2025-03-25T10:03:35.157Z
Learnt from: alfetopito
Repo: cowprotocol/cowswap PR: 5532
File: libs/common-hooks/src/useOnScroll.tsx:22-38
Timestamp: 2025-03-25T10:03:35.157Z
Learning: In React useEffect hooks, refs (RefObjects) should generally be included in dependency arrays even though changes to ref.current don't trigger effects. This is because the ref object itself could change between renders (especially in custom hooks where refs are passed as parameters), and it follows React's best practice of including all external values used in the effect.

Applied to files:

  • libs/common-hooks/src/useSyncedRef.ts
📚 Learning: 2025-06-23T07:03:50.760Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 5859
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts:76-82
Timestamp: 2025-06-23T07:03:50.760Z
Learning: In the useTradeQuotePolling hook, there are two useLayoutEffect hooks that work together: one resets the counter to 0 when the confirmation modal closes, and another automatically triggers pollQuote(false, true) whenever the counter reaches 0. This creates an intentional chain reaction for immediate quote updates.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
📚 Learning: 2025-08-08T13:56:18.009Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6125
File: libs/tokens/src/updaters/TokensListsUpdater/index.tsx:29-31
Timestamp: 2025-08-08T13:56:18.009Z
Learning: In libs/tokens/src/updaters/TokensListsUpdater/index.tsx, the project’s current Jotai version requires using `unstable_getOnInit` (not `getOnInit`) in atomWithStorage options; keep `{ unstable_getOnInit: true }` until Jotai is upgraded.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
📚 Learning: 2025-10-10T20:28:16.565Z
Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 6347
File: apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx:49-49
Timestamp: 2025-10-10T20:28:16.565Z
Learning: In apps/cowswap-frontend/src/modules/trade, TradeConfirmation follows a two-layer architecture: TradeConfirmationView (pure/stateless) in pure/TradeConfirmation/index.tsx renders the UI, while TradeConfirmation (container) in containers/TradeConfirmation/index.tsx wraps it to freeze props during pending trades (via useStableTradeConfirmationProps), wire in signing state (useSigningStep), and inject trade confirmation state (useTradeConfirmState). Consuming modules should import the container TradeConfirmation from 'modules/trade' to preserve this stateful behavior.
<!-- [add_learning]
When reviewing component refactoring in apps/cowswap-frontend/src/modules/trade, recognize the pattern where a pure view component (e.g., TradeConfirmationView) is separated from a stateful container (e.g., TradeConfirmation) that wraps it. The container adds runtime state management (prop stabilization, signing state, etc.) while the view remains testable and composable. Do not flag usages that import th...

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
🧬 Code graph analysis (1)
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts (3)
libs/common-hooks/src/useSyncedRef.ts (1)
  • useSyncedRef (9-23)
apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useIsSmartSlippageApplied.ts (1)
  • useIsSmartSlippageApplied (3-5)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteParamsChanges.ts (1)
  • checkOnlySlippageBpsChanged (7-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Setup
  • GitHub Check: Cypress
🔇 Additional comments (6)
libs/common-hooks/src/useSyncedRef.ts (1)

9-23: LGTM! Clean implementation of synchronized ref.

The implementation correctly provides an immutable ref wrapper that always reflects the latest value. The direct assignment to ref.current during render (line 12) is safe since refs don't trigger re-renders, and the frozen wrapper with useMemo([], ...) ensures a stable object identity across renders.

libs/common-hooks/src/index.ts (1)

26-26: LGTM!

Export is correctly added and consistent with the existing pattern.

apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts (4)

2-2: LGTM! Necessary imports for throttling feature.

All new imports are appropriately used in the throttling implementation to prevent the re-fetch loop in smart slippage mode.

Also applies to: 4-4, 9-9, 22-22, 26-26


58-61: LGTM! Correct pattern for tracking previous params.

The ref initialization and useEffect update correctly preserve the previous quoteParams value for comparison in the useLayoutEffect. The execution order ensures the useLayoutEffect sees the old value before the useEffect updates it.


63-63: LGTM! Appropriate use of useSyncedRef.

Using useSyncedRef here is the right choice—it lets the effect access the latest smart slippage state when quoteParams changes without adding isSmartSlippageApplied to the dependencies (which would trigger unnecessary effect executions).


90-107: Throttling logic is correct and timestamp precision is consistent.

Verification confirms localQuoteTimestamp is consistently set in milliseconds via Date.now() (tradeQuoteAtom.ts line 65) and used throughout with millisecond-based arithmetic (ONE_SEC = 1000 in tests). The comparison at line 103 correctly subtracts millisecond timestamps and compares against a millisecond-based throttle interval.

The implementation properly:

  • Isolates slippage-only changes via checkOnlySlippageBpsChanged
  • Safely handles missing timestamps (skips throttling)
  • Uses refs to access latest state without dependency churn
  • Returns early to prevent redundant fetches
  • Uses consistent millisecond precision for all timestamp operations

@vercel
Copy link

vercel bot commented Dec 26, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
swap-dev Error Error Dec 26, 2025 10:56am

: undefined
// in "smart" slippage mode slippageBps updates on every fetch /quote response
// so we should throttle duplicated additional requests caused by following slippageBps updates to prevent re-fetch loop (#6675)
if (typeof quoteTimestampDiff === 'number' && quoteTimestampDiff < QUOTE_SLIPPAGE_CHANGE_THROTTLE_INTERVAL) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another suggestion: instead of throttling by time, we can compare new/prev values and if the difference is less than N% - skip following quote fetch

(ofc only for "smart" slippage mode)

But... sometimes suggestedSlippageBps may jump from 0.5% to 1.25% to 4.75% just one after another 🫠

@crutch12 crutch12 force-pushed the fix/swap-bridge-quote-loaded-twice branch from 0558792 to 9024f60 Compare December 26, 2025 09:52
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts (2)

112-112: Optional: isSmartSlippageApplied dependency is not strictly necessary.

Since useSyncedRef returns a stable frozen object, including isSmartSlippageApplied in the dependency array doesn't cause re-runs and has no functional impact. The effect correctly accesses .current to read the latest value.

You may optionally remove it from the dependency array, though keeping it could aid readability for future maintainers.


63-63: Throttling logic correctly breaks the re-fetch loop.

The implementation effectively prevents the quotesuggestedSlippageBps cycle by:

  • Detecting slippage-only changes via checkOnlySlippageBpsChanged
  • Throttling rapid re-fetches within 1.5s of the last quote
  • Only applying throttling in smart slippage mode
  • Allowing non-slippage changes to bypass throttling

The useSyncedRef pattern provides stable access to the current smart slippage state without causing effect re-runs.

The two-hook chain (lines 68–78 resets counter; lines 117–121 triggers pollQuote) creates the intended effect sequence for immediate quote updates. Consider adding an explicit test case for the throttling behavior to document the expected 1.5s window for slippage-only changes, as current tests cover only the basic quote-fetching flow.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0558792 and 9024f60.

📒 Files selected for processing (3)
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
  • libs/common-hooks/src/index.ts
  • libs/common-hooks/src/useSyncedRef.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • libs/common-hooks/src/index.ts
  • libs/common-hooks/src/useSyncedRef.ts
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6299
File: apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.test.tsx:62-67
Timestamp: 2025-09-25T08:49:32.256Z
Learning: In the cowswap-frontend codebase, when testing hooks that use multiple bridge providers, both providers are always properly mocked as complete jest.Mocked<BridgeProvider<BridgeQuoteResult>> objects with all required methods stubbed, ensuring no undefined returns that could break the hook logic.
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 5859
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts:76-82
Timestamp: 2025-06-23T07:03:50.760Z
Learning: In the useTradeQuotePolling hook, there are two useLayoutEffect hooks that work together: one resets the counter to 0 when the confirmation modal closes, and another automatically triggers pollQuote(false, true) whenever the counter reaches 0. This creates an intentional chain reaction for immediate quote updates.
📚 Learning: 2025-06-23T07:03:50.760Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 5859
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts:76-82
Timestamp: 2025-06-23T07:03:50.760Z
Learning: In the useTradeQuotePolling hook, there are two useLayoutEffect hooks that work together: one resets the counter to 0 when the confirmation modal closes, and another automatically triggers pollQuote(false, true) whenever the counter reaches 0. This creates an intentional chain reaction for immediate quote updates.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
📚 Learning: 2025-06-16T15:58:57.402Z
Learnt from: alfetopito
Repo: cowprotocol/cowswap PR: 5830
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSmartSlippageFromQuote.ts:5-9
Timestamp: 2025-06-16T15:58:57.402Z
Learning: In the cowswap system, 0 bps will never be suggested as a slippage value, so falsy checks on suggested slippage values are safe and won't drop valid 0 values.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
📚 Learning: 2025-08-08T13:56:18.009Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6125
File: libs/tokens/src/updaters/TokensListsUpdater/index.tsx:29-31
Timestamp: 2025-08-08T13:56:18.009Z
Learning: In libs/tokens/src/updaters/TokensListsUpdater/index.tsx, the project’s current Jotai version requires using `unstable_getOnInit` (not `getOnInit`) in atomWithStorage options; keep `{ unstable_getOnInit: true }` until Jotai is upgraded.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
📚 Learning: 2025-10-10T20:28:16.565Z
Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 6347
File: apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx:49-49
Timestamp: 2025-10-10T20:28:16.565Z
Learning: In apps/cowswap-frontend/src/modules/trade, TradeConfirmation follows a two-layer architecture: TradeConfirmationView (pure/stateless) in pure/TradeConfirmation/index.tsx renders the UI, while TradeConfirmation (container) in containers/TradeConfirmation/index.tsx wraps it to freeze props during pending trades (via useStableTradeConfirmationProps), wire in signing state (useSigningStep), and inject trade confirmation state (useTradeConfirmState). Consuming modules should import the container TradeConfirmation from 'modules/trade' to preserve this stateful behavior.
<!-- [add_learning]
When reviewing component refactoring in apps/cowswap-frontend/src/modules/trade, recognize the pattern where a pure view component (e.g., TradeConfirmationView) is separated from a stateful container (e.g., TradeConfirmation) that wraps it. The container adds runtime state management (prop stabilization, signing state, etc.) while the view remains testable and composable. Do not flag usages that import th...

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts
🧬 Code graph analysis (1)
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts (3)
libs/common-hooks/src/useSyncedRef.ts (1)
  • useSyncedRef (9-23)
apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useIsSmartSlippageApplied.ts (1)
  • useIsSmartSlippageApplied (3-5)
apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteParamsChanges.ts (1)
  • checkOnlySlippageBpsChanged (7-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cypress
  • GitHub Check: Setup
🔇 Additional comments (1)
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts (1)

58-61: Correct pattern for tracking previous values.

The prevQuoteParamsRef correctly captures the previous quoteParams value for comparison in the throttling logic.

@elena-zh
Copy link
Contributor

Hey @crutch12 , the build is crashing.
image
Could you please review what causes this error?

@crutch12
Copy link
Contributor Author

@elena-zh sorry, didn't mention

fixed build errors

This PR is still in progress, there is a problem with slippage manual/auto changing (updated description - TODO)

@crutch12 crutch12 marked this pull request as draft December 26, 2025 11:42
*/
if (isConfirmOpen) return

if (isSmartSlippageApplied.current) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe it should be checked inside quoteUsingSameParameters

@crutch12
Copy link
Contributor Author

crutch12 commented Dec 26, 2025

I've fixed the main found functionality problems.

Other:
Initial quote request always fetches with slippageBips: 50, but it's being fixed on the second request (e.g. when user changes amount)
It's much better than in main branch, where slippageBips is always sent with 50 value in the duplicated request, so in fact quote ignores slippageBips in "smart" slippage mode

UPD
I think it's okay. If I "fix" it, users will always get double request on token change

@crutch12 crutch12 marked this pull request as ready for review December 26, 2025 12:39
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts (1)

42-42: Consider refactoring to reduce complexity.

The addition of eslint-disable-next-line complexity indicates the function has grown complex. Given the PR description mentions this is a "workaround" approach and suggests an alternative of tracking intent in tradeQuotesAtom, consider refactoring this function into smaller, focused helpers (e.g., separate functions for race condition checks, timestamp updates, and cache preservation).

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f3417b and bcbb688.

📒 Files selected for processing (2)
  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 5859
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts:76-82
Timestamp: 2025-06-23T07:03:50.760Z
Learning: In the useTradeQuotePolling hook, there are two useLayoutEffect hooks that work together: one resets the counter to 0 when the confirmation modal closes, and another automatically triggers pollQuote(false, true) whenever the counter reaches 0. This creates an intentional chain reaction for immediate quote updates.
📚 Learning: 2025-09-25T08:46:43.815Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6299
File: apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.test.ts:376-385
Timestamp: 2025-09-25T08:46:43.815Z
Learning: In fetchAndProcessQuote.ts, when bridgingSdk.getBestQuote() returns null, no loading state reset is needed because loading state management is handled through onQuoteResult() callback for results and processQuoteError() for errors. The null case is intentionally designed not to trigger any manager methods.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts
📚 Learning: 2025-06-23T07:03:50.760Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 5859
File: apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts:76-82
Timestamp: 2025-06-23T07:03:50.760Z
Learning: In the useTradeQuotePolling hook, there are two useLayoutEffect hooks that work together: one resets the counter to 0 when the confirmation modal closes, and another automatically triggers pollQuote(false, true) whenever the counter reaches 0. This creates an intentional chain reaction for immediate quote updates.

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts
📚 Learning: 2025-10-10T20:28:16.565Z
Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 6347
File: apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx:49-49
Timestamp: 2025-10-10T20:28:16.565Z
Learning: In apps/cowswap-frontend/src/modules/trade, TradeConfirmation follows a two-layer architecture: TradeConfirmationView (pure/stateless) in pure/TradeConfirmation/index.tsx renders the UI, while TradeConfirmation (container) in containers/TradeConfirmation/index.tsx wraps it to freeze props during pending trades (via useStableTradeConfirmationProps), wire in signing state (useSigningStep), and inject trade confirmation state (useTradeConfirmState). Consuming modules should import the container TradeConfirmation from 'modules/trade' to preserve this stateful behavior.
<!-- [add_learning]
When reviewing component refactoring in apps/cowswap-frontend/src/modules/trade, recognize the pattern where a pure view component (e.g., TradeConfirmationView) is separated from a stateful container (e.g., TradeConfirmation) that wraps it. The container adds runtime state management (prop stabilization, signing state, etc.) while the view remains testable and composable. Do not flag usages that import th...

Applied to files:

  • apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts
  • apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts
🔇 Additional comments (4)
apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts (1)

76-82: LGTM: Correct conditional caching of suggestedSlippageBps for optimal quotes.

The logic correctly sets suggestedSlippageBps only when priceQuality is OPTIMAL, which aligns with the comment on line 79 explaining that the SDK returns a default value for FAST quotes. This prevents stale or default slippage values from overwriting the cached suggested slippage.

apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts (3)

21-22: LGTM: Clear field addition for caching suggested slippage.

The new suggestedSlippageBps field is well-named, appropriately typed (number | null), and properly initialized in the default state. The comment clearly explains its purpose.

Also applies to: 34-34


48-57: LGTM: Correct race condition handling for FAST vs OPTIMAL quotes.

The logic correctly prevents FAST quotes from overwriting OPTIMAL quotes when they belong to the same request (matched by fetchStartTimestamp). The use of optional chaining safely handles cases where prevQuote.fetchParams might be null, and the early return prevents unnecessary state updates.


64-67: LGTM: Correct caching pattern for suggestedSlippageBps.

The conditional logic correctly preserves the cached suggestedSlippageBps when nextState.suggestedSlippageBps is undefined, and updates it (including to null) when explicitly provided. This pattern enables the intended behavior: OPTIMAL quotes set the value, and FAST quotes preserve it.

...nextState,
quote: typeof nextState.quote === 'undefined' ? prevQuote.quote : nextState.quote,
localQuoteTimestamp: nextState.quote ? Math.ceil(Date.now() / 1000) : null,
localQuoteTimestamp: nextState.quote ? Date.now() : null,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix timestamp preservation when quote is preserved.

When nextState.quote is undefined, line 62 correctly preserves prevQuote.quote, but line 63 sets localQuoteTimestamp to null because nextState.quote is falsy. This breaks the invariant that a quote and its timestamp should stay synchronized.

Example scenario:

  • Caller passes { isLoading: true } (no quote field)
  • Expected: preserve existing quote and its timestamp
  • Actual: preserve quote but reset timestamp to null
🔎 Proposed fix
-        localQuoteTimestamp: nextState.quote ? Date.now() : null,
+        localQuoteTimestamp: typeof nextState.quote === 'undefined' 
+          ? prevQuote.localQuoteTimestamp 
+          : (nextState.quote ? Date.now() : null),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
localQuoteTimestamp: nextState.quote ? Date.now() : null,
localQuoteTimestamp: typeof nextState.quote === 'undefined'
? prevQuote.localQuoteTimestamp
: (nextState.quote ? Date.now() : null),
🤖 Prompt for AI Agents
In apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts around
line 63, localQuoteTimestamp is being set based on the truthiness of
nextState.quote which causes the timestamp to be nulled when nextState.quote is
undefined even if prevQuote.quote is preserved; change the logic to set
localQuoteTimestamp to Date.now() only when nextState.quote is explicitly
provided (e.g., nextState.quote !== undefined), otherwise preserve
prevQuote.localQuoteTimestamp so the quote and its timestamp remain
synchronized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants