Skip to content

fix(bridge): refresh and requote stale routes#159

Merged
simcheolhwan merged 24 commits intomainfrom
fix/bridge-route-quote-freshness
Feb 25, 2026
Merged

fix(bridge): refresh and requote stale routes#159
simcheolhwan merged 24 commits intomainfrom
fix/bridge-route-quote-freshness

Conversation

@tansawit
Copy link
Contributor

@tansawit tansawit commented Feb 17, 2026

Summary

This change fixes inconsistent bridge quote behavior after returning to the swap form and attempting a second swap. Users could sometimes see conversion values derived from stale route data, and in some paths proceed with outdated preview information.

User impact

When a user performs one swap, comes back, and enters a new amount, route and conversion data now refresh deterministically instead of depending on incidental react-query cache state. At confirm time, stale preview data is actively refreshed and requires explicit reconfirmation if the route changed.

Root cause

The bridge route query did not have a short, route-specific freshness policy and could reuse cached route data after navigation. The preview flow also lacked a strict freshness check at confirm time, so a previously previewed route could remain in state longer than intended.

What changed

  • Added route refresh windows tuned by route type:
    • same-chain L1: 5s
    • same-chain Initia L2: 2s
    • all other routes: 10s
  • Updated route query behavior to refresh proactively:
    • staleTime tied to refresh window
    • polling via refetchInterval
    • refetch on mount/focus/reconnect
  • On Preview route, force a fresh route refetch before navigating to preview and store quoteVerifiedAt timestamp.
  • On confirm, enforce strict max quote age of 10s:
    • if quote is older than 10s, requote before execution
    • if requote changes route details, block execution and require explicit reconfirm
    • if requote fails, show error and prevent submission
  • After successful bridge tx, clear route-related query caches to avoid stale reuse on subsequent swaps.

Why this fix

This approach keeps preview data responsive during input while still enforcing execution-time freshness. It reduces stale quote exposure without adding unnecessary requotes for every click.


Note

Medium Risk
Touches routing/quoting and transaction confirmation flow, so regressions could block swaps or require extra reconfirmation, but changes are localized and add explicit UI/error handling.

Overview
Fixes stale bridge quotes by making route simulation actively refresh and by enforcing quote freshness at preview/confirm time.

On the form, useRouteQuery now supports configurable refresh windows (2s/5s/10s), uses them as staleTime, and polls via refetchInterval with refetch-on-mount/focus/reconnect; clicking Preview route forces a refetch(), records quoteVerifiedAt, and shows a dedicated refresh loading/error state.

On the preview screen, a new useRouteRefresh hook requotes if the saved quote is older than 10s and, if the route materially changed, updates location state to require an explicit reconfirm (with updated confirm button label + status messaging). After a successful bridge tx, route-related react-query caches are cleared to prevent reuse across subsequent swaps; deposit transfer flow now also preserves quoteVerifiedAt and recipient state via a new buildTransferLocationState helper.

Written by Cursor Bugbot for commit e65e5cb. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Adaptive route auto-refresh with reconfirmation flow when routes change.
    • Quote verification timestamps recorded and passed through preview/navigation and submit flows.
    • Preview footer shows refresh progress, contextual labels, and prompts to confirm updated routes.
    • Route queries refetch more responsively (focus/reconnect/mount) with configurable intervals.
  • Bug Fixes

    • Clearer user-facing error messages and visible refresh failure feedback.
    • More stable parameter handling to prevent stale requests and unnecessary re-renders.

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds dynamic route refresh intervals (Layer1/L2/default), wires refreshMs into route queries, refetches latest route before submit, surfaces refresh errors and route warnings, tracks quoteVerifiedAt and requiresReconfirm state, refreshes stale routes via route API, and clears route caches after successful submission.

Changes

Cohort / File(s) Summary
Bridge fields & submit flow
packages/interwovenkit-react/src/pages/bridge/BridgeFields.tsx
Compute routeRefreshMs (Layer1/L2/default) via useLayer1, pass refreshMs into route queries, asynchronously refetch latest route on submit, handle refresh errors (showing previewRefreshError), store quoteVerifiedAt, open reconfirm modal when route warnings exist, and navigate using latest route and quoteVerifiedAt.
Preview footer & confirm flow
packages/interwovenkit-react/src/pages/bridge/BridgePreviewFooter.tsx
Add isRefreshing/refreshError/lastVerifiedAt/requiresReconfirm, introduce ROUTE_MAX_AGE_MS and deterministic route signature detection, implement refreshRouteIfNeeded calling route API and comparing signatures, surface status messages, gate confirm/mutate flows with refresh/reconfirm logic, and update Props to include optional fee and callback fields.
Route query behavior
packages/interwovenkit-react/src/pages/bridge/data/simulate.ts
Replace fixed staleTime with dynamic refreshMs (from opWithdrawal.refreshMs or default), set staleTime/refetchInterval to refreshMs when enabled, and enable refetchOnWindowFocus/refetchOnReconnect/refetchOnMount = "always".
Preview state & cache clearing
packages/interwovenkit-react/src/pages/bridge/data/tx.ts
Extend BridgePreviewState with quoteVerifiedAt?: number and requiresReconfirm?: boolean; clear route-related query caches (route and routeErrorInfo) on successful submission.
Deposit transfer state
packages/interwovenkit-react/src/pages/deposit/TransferFields.tsx
Introduce quoteVerifiedAt derived from route dataUpdatedAt, include quoteVerifiedAt in navigation state, and update effect deps to include quoteVerifiedAt.
Footer params memoization
packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx
Memoize request keys/params to stabilize effect deps, use memoized params for POST payload, and update useEffect deps to reduce unnecessary refetches and ensure correct validation checks.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant BridgePreviewFooter
    participant RouteAPI as Route API (v2/fungible/route)
    participant BridgeTx as Bridge Tx Mutation
    participant Navigation

    User->>BridgePreviewFooter: Click Confirm
    activate BridgePreviewFooter

    alt route age > ROUTE_MAX_AGE_MS
        BridgePreviewFooter->>BridgePreviewFooter: set isRefreshing = true
        BridgePreviewFooter->>RouteAPI: POST current inputs (refresh)
        activate RouteAPI
        RouteAPI-->>BridgePreviewFooter: updated route
        deactivate RouteAPI

        alt route signature changed
            BridgePreviewFooter-->>User: show "Confirm updated route" (requiresReconfirm)
            deactivate BridgePreviewFooter
            User->>BridgePreviewFooter: Click Confirm again
            activate BridgePreviewFooter
        end

        BridgePreviewFooter->>BridgePreviewFooter: set isRefreshing = false
    end

    BridgePreviewFooter->>BridgeTx: Call mutate()
    activate BridgeTx
    BridgeTx-->>BridgePreviewFooter: Transaction submitted
    deactivate BridgeTx

    BridgePreviewFooter->>Navigation: Navigate to result
    deactivate BridgePreviewFooter
Loading
sequenceDiagram
    participant User
    participant BridgeFields
    participant RouteQuery as useRouteQuery
    participant API as Route API (skip.post)
    participant Modal as Confirmation Modal
    participant Navigation

    User->>BridgeFields: Submit form
    activate BridgeFields

    BridgeFields->>BridgeFields: compute routeRefreshMs (Layer1/L2/default)
    BridgeFields->>RouteQuery: refetch latest route with refreshMs
    activate RouteQuery
    RouteQuery->>API: request latest route
    activate API
    API-->>RouteQuery: route data (with warnings?)
    deactivate API
    deactivate RouteQuery

    alt route contains warning
        BridgeFields->>Modal: open with latest route + quoteVerifiedAt
        activate Modal
        Modal->>User: show warning
        User->>Modal: Confirm to proceed
        deactivate Modal
    end

    BridgeFields->>BridgeFields: set quoteVerifiedAt
    BridgeFields->>Navigation: navigate to preview (latestRoute, quoteVerifiedAt)
    deactivate BridgeFields
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I nibble bytes and time their ticks,
Fresh routes hum as systems mix,
A verified quote, a cautious hop,
Warnings watched before we hop—
Bridges bloom, and off we zip! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (14 files):

⚔️ examples/vite/tsconfig.json (content)
⚔️ examples/vite/vite.config.ts (content)
⚔️ packages/interwovenkit-react/CHANGELOG.md (content)
⚔️ packages/interwovenkit-react/env.d.ts (content)
⚔️ packages/interwovenkit-react/package.json (content)
⚔️ packages/interwovenkit-react/src/pages/bridge/BridgeFields.tsx (content)
⚔️ packages/interwovenkit-react/src/pages/bridge/BridgePreviewFooter.tsx (content)
⚔️ packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx (content)
⚔️ packages/interwovenkit-react/src/pages/bridge/data/simulate.ts (content)
⚔️ packages/interwovenkit-react/src/pages/bridge/data/skip.ts (content)
⚔️ packages/interwovenkit-react/src/pages/bridge/data/tx.ts (content)
⚔️ packages/interwovenkit-react/src/pages/deposit/TransferFields.tsx (content)
⚔️ packages/interwovenkit-react/src/pages/wallet/components/Version.tsx (content)
⚔️ packages/interwovenkit-react/vite.config.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(bridge): refresh and requote stale routes' clearly and concisely summarizes the main objective of this changeset: introducing route refresh and requotation logic to handle stale routes in the bridge flow.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/bridge-route-quote-freshness
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch fix/bridge-route-quote-freshness
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

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

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 17, 2026

Deploying interwovenkit-staging with  Cloudflare Pages  Cloudflare Pages

Latest commit: e65e5cb
Status: ✅  Deploy successful!
Preview URL: https://c6c48bbb.interwovenkit-staging.pages.dev
Branch Preview URL: https://fix-bridge-route-quote-fresh.interwovenkit-staging.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 17, 2026

Deploying interwovenkit-testnet with  Cloudflare Pages  Cloudflare Pages

Latest commit: e65e5cb
Status: ✅  Deploy successful!
Preview URL: https://c2969b57.interwovenkit-testnet.pages.dev
Branch Preview URL: https://fix-bridge-route-quote-fresh.interwovenkit-testnet.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 17, 2026

Deploying interwovenkit with  Cloudflare Pages  Cloudflare Pages

Latest commit: e65e5cb
Status: ✅  Deploy successful!
Preview URL: https://cc374dc4.interwovenkit.pages.dev
Branch Preview URL: https://fix-bridge-route-quote-fresh.interwovenkit.pages.dev

View logs

@tansawit tansawit marked this pull request as ready for review February 17, 2026 03:27
Copy link

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/interwovenkit-react/src/pages/bridge/BridgePreviewFooter.tsx`:
- Around line 58-101: The refreshRouteIfNeeded function currently falls back to
{ decimals: 0 } when
queryClient.getQueryData<RouterAsset>(skipQueryKeys.asset(...)) returns
undefined, which can under-scale toBaseUnit(values.quantity, { decimals:
srcDecimals }); update refreshRouteIfNeeded to detect missing asset metadata
(i.e., when queryClient.getQueryData returns undefined or decimals is
null/undefined) and fail fast or fetch the asset before computing amount_in:
either setRefreshError and return true (or block refresh) when decimals are
unavailable, or call the existing asset-fetch path to populate decimals, then
proceed to call toBaseUnit and post the route; ensure you reference
skipQueryKeys.asset, queryClient.getQueryData, toBaseUnit, and navigate in your
change so the code path handles missing decimals consistently.

Copy link

@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.

🧹 Nitpick comments (1)
packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx (1)

27-29: Consider memoizing the JSON keys individually.

JSON.stringify runs on every render for addressList, route.operations, and signedOpHook. If route.operations is a large array, this adds serialization overhead on each render even when values haven't changed.

♻️ Optional: memoize stringification
-  const addressListKey = JSON.stringify(addressList)
-  const operationsKey = JSON.stringify(route.operations)
-  const signedOpHookKey = JSON.stringify(signedOpHook ?? null)
+  const addressListKey = useMemo(() => JSON.stringify(addressList), [addressList])
+  const operationsKey = useMemo(() => JSON.stringify(route.operations), [route.operations])
+  const signedOpHookKey = useMemo(() => JSON.stringify(signedOpHook ?? null), [signedOpHook])

This still re-stringifies when references change, but avoids redundant work when references are stable across renders.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx` around
lines 27 - 29, The three JSON.stringify calls (addressListKey, operationsKey,
signedOpHookKey) run on every render and should be memoized: wrap each
JSON.stringify in a React.useMemo so you only re-stringify when its specific
dependency changes (use addressList for addressListKey, route.operations for
operationsKey, and signedOpHook for signedOpHookKey), and ensure React.useMemo
is imported; this avoids repeated serialization work when references are stable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx`:
- Around line 27-29: The three JSON.stringify calls (addressListKey,
operationsKey, signedOpHookKey) run on every render and should be memoized: wrap
each JSON.stringify in a React.useMemo so you only re-stringify when its
specific dependency changes (use addressList for addressListKey,
route.operations for operationsKey, and signedOpHook for signedOpHookKey), and
ensure React.useMemo is imported; this avoids repeated serialization work when
references are stable.

Copy link

@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.

🧹 Nitpick comments (3)
packages/interwovenkit-react/src/pages/bridge/BridgePreviewFooter.tsx (2)

62-111: Consider aborting refresh on component unmount.

The refreshRouteIfNeeded async function can complete after the component unmounts (e.g., user navigates away), potentially causing state updates on an unmounted component. While React 18+ generally handles this gracefully, it's good practice to abort in-flight requests.

💡 Add abort handling
const abortControllerRef = useRef<AbortController | null>(null)

useEffect(() => {
  return () => {
    abortControllerRef.current?.abort()
  }
}, [])

const refreshRouteIfNeeded = async () => {
  // ... existing checks ...
  
  abortControllerRef.current = new AbortController()
  try {
    const refreshedRoute = await skip
      .post("v2/fungible/route", {
        json: { /* ... */ },
        signal: abortControllerRef.current.signal,
      })
      .json<RouterRouteResponseJson>()
    // ...
  } catch (error) {
    if (error instanceof DOMException && error.name === "AbortError") return true
    setRefreshError((await normalizeError(error)).message)
    return true
  }
  // ...
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/interwovenkit-react/src/pages/bridge/BridgePreviewFooter.tsx` around
lines 62 - 111, refreshRouteIfNeeded can complete after the component unmounts
and set state; add abort handling by creating an abortControllerRef
(useRef<AbortController | null>) and a useEffect cleanup that calls
abortControllerRef.current?.abort(), then in refreshRouteIfNeeded assign
abortControllerRef.current = new AbortController() and pass
abortControllerRef.current.signal into the skip.post call; in the catch, detect
an AbortError (DOMException name === "AbortError") and return early without
calling setRefreshError or other state setters, and in finally clear
abortControllerRef.current = null to avoid stale references while still calling
setIsRefreshing(false) only when not aborted. Ensure you reference
refreshRouteIfNeeded, abortControllerRef, skip.post, setRefreshError and
setIsRefreshing when making these changes.

32-46: Potential false positives from JSON.stringify key ordering.

JSON.stringify doesn't guarantee consistent property ordering for objects. If the API returns properties in different order between calls (even with identical values), getRouteSignature may incorrectly detect a route change, triggering unnecessary reconfirmation prompts.

💡 Consider a deterministic comparison
 function getRouteSignature(route: RouterRouteResponseJson) {
-  return JSON.stringify({
+  // Sort keys for deterministic comparison
+  return JSON.stringify({
     amount_in: route.amount_in,
     amount_out: route.amount_out,
     usd_amount_in: route.usd_amount_in,
     usd_amount_out: route.usd_amount_out,
-    operations: route.operations,
+    operations: route.operations, // Array order is preserved
     estimated_fees: route.estimated_fees,
     estimated_route_duration_seconds: route.estimated_route_duration_seconds,
     warning: route.warning,
     extra_infos: route.extra_infos,
     extra_warnings: route.extra_warnings,
     required_op_hook: route.required_op_hook,
-  })
+  }, Object.keys(route).sort())
 }

Alternatively, compare individual fields or use a library like fast-deep-equal for reliable deep comparison.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/interwovenkit-react/src/pages/bridge/BridgePreviewFooter.tsx` around
lines 32 - 46, getRouteSignature currently uses JSON.stringify which can produce
false positives due to non-deterministic object key ordering; update the
comparison logic so route changes are detected deterministically by either: 1)
replacing getRouteSignature with a deterministic serializer that sorts keys (or
builds a canonical string by reading the known fields in a fixed order) for
RouterRouteResponseJson, or 2) replace usage of getRouteSignature with a proper
deep equality check (e.g., fast-deep-equal) or explicit field-by-field
comparison of the listed properties (amount_in, amount_out, usd_amount_in,
usd_amount_out, operations, estimated_fees, estimated_route_duration_seconds,
warning, extra_infos, extra_warnings, required_op_hook) to avoid ordering
issues.
packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx (1)

27-56: Unnecessary JSON roundtrip in memoization.

The pattern of JSON.stringifyJSON.parse is wasteful. The stringified keys are useful for stable dependency comparison, but there's no need to parse them back when the original values are available.

♻️ Suggested simplification
   const addressListKey = useMemo(() => JSON.stringify(addressList), [addressList])
   const operationsKey = useMemo(() => JSON.stringify(route.operations), [route.operations])
   const signedOpHookKey = useMemo(() => JSON.stringify(signedOpHook ?? null), [signedOpHook])
 
   const params = useMemo(
     () => ({
-      address_list: JSON.parse(addressListKey) as string[],
+      address_list: addressList,
       amount_in: route.amount_in,
       amount_out: route.amount_out,
       source_asset_chain_id: route.source_asset_chain_id,
       source_asset_denom: route.source_asset_denom,
       dest_asset_chain_id: route.dest_asset_chain_id,
       dest_asset_denom: route.dest_asset_denom,
       slippage_tolerance_percent: values.slippagePercent,
-      operations: JSON.parse(operationsKey) as RouterRouteResponseJson["operations"],
-      signed_op_hook: (JSON.parse(signedOpHookKey) as SignedOpHook | null) ?? undefined,
+      operations: route.operations,
+      signed_op_hook: signedOpHook,
     }),
     [
       addressListKey,
       operationsKey,
+      signedOpHookKey,
       route.amount_in,
       route.amount_out,
       route.source_asset_chain_id,
       route.source_asset_denom,
       route.dest_asset_chain_id,
       route.dest_asset_denom,
       values.slippagePercent,
-      signedOpHookKey,
     ],
   )

The stringified keys already serve as stable dependency markers—use the original values in the object construction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx` around
lines 27 - 56, The JSON.stringify → JSON.parse roundtrip inside the params
useMemo is unnecessary; update the params factory (the useMemo that produces
params) to use the original values (addressList, route.operations, and
signedOpHook) instead of parsing addressListKey/operationsKey/signedOpHookKey,
while keeping the stringified keys (addressListKey, operationsKey,
signedOpHookKey) in the dependency array for stable comparisons; adjust the
signed_op_hook expression to use signedOpHook (or signedOpHook ?? undefined) and
keep other route/value fields as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/interwovenkit-react/src/pages/bridge/BridgePreviewFooter.tsx`:
- Around line 62-111: refreshRouteIfNeeded can complete after the component
unmounts and set state; add abort handling by creating an abortControllerRef
(useRef<AbortController | null>) and a useEffect cleanup that calls
abortControllerRef.current?.abort(), then in refreshRouteIfNeeded assign
abortControllerRef.current = new AbortController() and pass
abortControllerRef.current.signal into the skip.post call; in the catch, detect
an AbortError (DOMException name === "AbortError") and return early without
calling setRefreshError or other state setters, and in finally clear
abortControllerRef.current = null to avoid stale references while still calling
setIsRefreshing(false) only when not aborted. Ensure you reference
refreshRouteIfNeeded, abortControllerRef, skip.post, setRefreshError and
setIsRefreshing when making these changes.
- Around line 32-46: getRouteSignature currently uses JSON.stringify which can
produce false positives due to non-deterministic object key ordering; update the
comparison logic so route changes are detected deterministically by either: 1)
replacing getRouteSignature with a deterministic serializer that sorts keys (or
builds a canonical string by reading the known fields in a fixed order) for
RouterRouteResponseJson, or 2) replace usage of getRouteSignature with a proper
deep equality check (e.g., fast-deep-equal) or explicit field-by-field
comparison of the listed properties (amount_in, amount_out, usd_amount_in,
usd_amount_out, operations, estimated_fees, estimated_route_duration_seconds,
warning, extra_infos, extra_warnings, required_op_hook) to avoid ordering
issues.

In `@packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx`:
- Around line 27-56: The JSON.stringify → JSON.parse roundtrip inside the params
useMemo is unnecessary; update the params factory (the useMemo that produces
params) to use the original values (addressList, route.operations, and
signedOpHook) instead of parsing addressListKey/operationsKey/signedOpHookKey,
while keeping the stringified keys (addressListKey, operationsKey,
signedOpHookKey) in the dependency array for stable comparisons; adjust the
signed_op_hook expression to use signedOpHook (or signedOpHook ?? undefined) and
keep other route/value fields as-is.

Copy link

@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.

🧹 Nitpick comments (2)
packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx (2)

44-58: Redundant dependencies: both original values and their stringified keys are included.

The dependency array lists both addressList and addressListKey, route.operations and operationsKey, etc. Since each *Key memo already depends on its source value, including both is redundant—when addressList changes, addressListKey will recompute, triggering this memo anyway. Consider keeping only the stringified keys if you want stable reference detection, or only the original values if not:

♻️ Simplified dependency array
   }, [
     addressList,
-    addressListKey,
     route.operations,
-    operationsKey,
-    signedOpHookKey,
     signedOpHook,
     route.amount_in,
     ...
   ])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx` around
lines 44 - 58, The dependency array for the memo includes both raw values and
their derived string keys (e.g., addressList + addressListKey, route.operations
+ operationsKey, signedOpHook + signedOpHookKey), which is redundant; update the
dependency list inside the useMemo in FooterWithMsgs.tsx to remove the original
raw values and keep only the stable stringified keys (addressListKey,
operationsKey, signedOpHookKey) plus the other primitive route fields and
values.slippagePercent so the memo relies on the canonical stable dependencies.

26-42: hasStableKeys is always truthy, making the conditional ineffective.

The *Key variables are always non-empty strings since JSON.stringify returns "[]", "{}", or "null" for empty/null values—all truthy. Thus hasStableKeys will always be true, and the ternary at line 42 will always evaluate to signedOpHook ?? undefined.

If this is intentional (i.e., you're only using the keys for referential stability in the dependency array), consider simplifying:

♻️ Suggested simplification
   const params = useMemo(() => {
-    const hasStableKeys = !!addressListKey && !!operationsKey && !!signedOpHookKey
     return {
       address_list: addressList,
       amount_in: route.amount_in,
       ...
-      signed_op_hook: hasStableKeys ? (signedOpHook ?? undefined) : undefined,
+      signed_op_hook: signedOpHook ?? undefined,
     }
   }, [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx` around
lines 26 - 42, hasStableKeys is always truthy because
addressListKey/operationsKey/signedOpHookKey are JSON.stringify results, so
replace that ineffective check by using explicit null/emptiness checks: remove
addressListKey/operationsKey/signedOpHookKey usage for truthiness, change
hasStableKeys to something like (signedOpHook !== null && signedOpHook !==
undefined) or include explicit checks for addressList.length and
route.operations.length, and then set signed_op_hook: signedOpHook ?? undefined
(or only include it when the explicit check passes) inside the params object so
the conditional actually reflects presence/absence of the underlying values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/interwovenkit-react/src/pages/bridge/FooterWithMsgs.tsx`:
- Around line 44-58: The dependency array for the memo includes both raw values
and their derived string keys (e.g., addressList + addressListKey,
route.operations + operationsKey, signedOpHook + signedOpHookKey), which is
redundant; update the dependency list inside the useMemo in FooterWithMsgs.tsx
to remove the original raw values and keep only the stable stringified keys
(addressListKey, operationsKey, signedOpHookKey) plus the other primitive route
fields and values.slippagePercent so the memo relies on the canonical stable
dependencies.
- Around line 26-42: hasStableKeys is always truthy because
addressListKey/operationsKey/signedOpHookKey are JSON.stringify results, so
replace that ineffective check by using explicit null/emptiness checks: remove
addressListKey/operationsKey/signedOpHookKey usage for truthiness, change
hasStableKeys to something like (signedOpHook !== null && signedOpHook !==
undefined) or include explicit checks for addressList.length and
route.operations.length, and then set signed_op_hook: signedOpHook ?? undefined
(or only include it when the explicit check passes) inside the params object so
the conditional actually reflects presence/absence of the underlying values.

@linear
Copy link

linear bot commented Feb 17, 2026

@simcheolhwan
Copy link
Contributor

simcheolhwan commented Feb 17, 2026

49d92c8 — refactor(bridge): extract route refresh into shared hook

This commit extracts the inline route-refresh logic from BridgePreviewFooter into a dedicated useRouteRefresh hook, and pulls the shared API call into a standalone fetchRoute() function.

Changes by file:

  1. useRouteRefresh.ts (new) — Reusable hook containing stale-quote detection, re-fetching, and deterministic route comparison. Exposes refreshRouteIfNeeded(), isRefreshing, refreshError, and clearRefreshError.

  2. simulate.ts — Extracts fetchRoute() from useRouteQuery so both useRouteQuery and useRouteRefresh share the same route-request code path.

  3. BridgePreviewFooter.tsx — Shrinks from ~134 to ~15 lines of local logic by delegating to useRouteRefresh.

  4. BridgeFields.tsx — Adds previewRefreshing state so the Preview button shows "Refreshing route..." during refetch, prevents the "Simulating..." label from appearing concurrently, and disables the button while refreshing.

  5. FooterWithMsgs.tsx — Replaces the single memoized params object with individually stabilized references (stableAddressList, stableOperations, stableSignedOpHook), making useEffect dependencies explicit.

  6. TransferFields.tsx — Removes quoteVerifiedAt from useEffect deps. dataUpdatedAt changes on every refetch even when data is identical, causing unnecessary navigate(0, ...) calls. The latest value is captured via useEffectEvent instead.


Follow-up fix: 0ba9ee3 — The extracted fetchRoute() silently fell back to decimals: 0 when source asset metadata was missing in the query cache, which would send a wrong amount_in to the API. Restored the explicit error that the original inline code had.

@tansawit tansawit self-assigned this Feb 18, 2026
@tansawit tansawit added the type:enhancement New feature or request label Feb 18, 2026
@tansawit tansawit force-pushed the fix/bridge-route-quote-freshness branch from b66ea97 to efa190e Compare February 18, 2026 08:37
tansawit and others added 13 commits February 18, 2026 15:38
- Extract fetchRoute() from useRouteQuery for reuse
- Move refresh logic from BridgePreviewFooter to useRouteRefresh hook
- Add previewRefreshing state to show "Refreshing route..." on button
- Stabilize reference-type deps individually in FooterWithMsgs
- Drop quoteVerifiedAt from TransferFields effect deps
The shared fetchRoute silently fell back to decimals: 0 when source
asset metadata was unavailable, which would send a wrong amount to
the API. Restore the explicit error that the old inline code had.
Co-authored-by: Sawit Trisirisatayawong <tansawit@users.noreply.github.com>
Co-authored-by: Sawit Trisirisatayawong <tansawit@users.noreply.github.com>
- fetchRoute: explain why missing asset metadata throws
- BridgePreviewFooter: clarify Date.now() fallback rationale
- TransferFields: document quoteVerifiedAt exclusion from
  useEffect deps to avoid 10s refetch re-navigation
Co-authored-by: Sawit Trisirisatayawong <tansawit@users.noreply.github.com>
Co-authored-by: Sawit Trisirisatayawong <tansawit@users.noreply.github.com>
Co-authored-by: Sawit Trisirisatayawong <tansawit@users.noreply.github.com>
Co-authored-by: Sawit Trisirisatayawong <tansawit@users.noreply.github.com>
@tansawit tansawit force-pushed the fix/bridge-route-quote-freshness branch from efa190e to cd54727 Compare February 18, 2026 08:38
@evilpeach
Copy link

I think it'd be better to merge #157 first.

…e-freshness

# Conflicts:
#	packages/interwovenkit-react/src/pages/deposit/TransferFields.tsx
…e-freshness

# Conflicts:
#	packages/interwovenkit-react/src/pages/bridge/data/simulate.ts
@simcheolhwan
Copy link
Contributor

Code review

Found 2 issues:

  1. track("Bridge Simulation Success") fires from two call sites: the queryFn in simulate.ts (on every background poll, every 2–10 s) and the submit handler in BridgeFields.tsx (on "Preview route" click). Commit 340df0b explicitly moved the call out of queryFn to prevent background refetches from emitting analytics events. This PR re-adds it to queryFn while also keeping the new call in the submit handler, causing duplicate and inflated analytics.

track("Bridge Simulation Success", debouncedValues)

track("Bridge Simulation Success", {
quantity: values.quantity,
srcChainId: values.srcChainId,
srcDenom: values.srcDenom,
dstChainId: values.dstChainId,
dstDenom: values.dstDenom,
})

  1. useEffect in FooterWithMsgs.tsx calls fetchMessages() without a cleanup function to cancel the previous in-flight request. With the new polling-driven deps (refetchInterval on the route query), the deps now change every 2–10 s. If a dep change triggers a new fetch while a previous one is still in flight, the earlier response can resolve later and overwrite state with stale TX data via setValue(tx). Adding a boolean guard (let cancelled = false) or AbortController in the cleanup return would prevent the race.

useEffect(() => {
const fetchMessages = async () => {
try {
if (route.required_op_hook && !stableSignedOpHook) {
throw new Error("Op hook is required")
}
setLoading(true)
setError(null)
const params = {
address_list: stableAddressList,
amount_in: route.amount_in,
amount_out: route.amount_out,
source_asset_chain_id: route.source_asset_chain_id,
source_asset_denom: route.source_asset_denom,
dest_asset_chain_id: route.dest_asset_chain_id,
dest_asset_denom: route.dest_asset_denom,
slippage_tolerance_percent: values.slippagePercent,
operations: stableOperations,
signed_op_hook: stableSignedOpHook,
}
const { txs } = await skip
.post("v2/fungible/msgs", { json: params })
.json<MsgsResponseJson>()
if (!txs || txs.length === 0) throw new Error("No transaction data found")
const [tx] = txs
setValue(tx)
} catch (error) {
setError(error as Error)
} finally {
setLoading(false)
}
}
fetchMessages()
}, [
stableAddressList,
stableOperations,
stableSignedOpHook,
route.amount_in,
route.amount_out,
route.source_asset_chain_id,
route.source_asset_denom,
route.dest_asset_chain_id,
route.dest_asset_denom,
route.required_op_hook,
values.slippagePercent,
skip,
])

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

- remove track() from queryFn to prevent firing on every poll
- add cancelled guard in FooterWithMsgs useEffect cleanup
@simcheolhwan
Copy link
Contributor

Code review

Found 2 issues — both fixed in e65e5cb.

  1. track("Bridge Simulation Success") fires from two call sites: the queryFn in simulate.ts (on every background poll, every 2–10 s) and the submit handler in BridgeFields.tsx (on "Preview route" click). Commit 340df0b explicitly moved the call out of queryFn to prevent background refetches from emitting analytics events. This PR re-added it to queryFn while also keeping the new call in the submit handler, causing duplicate and inflated analytics.

  2. useEffect in FooterWithMsgs.tsx calls fetchMessages() without a cleanup function to cancel the previous in-flight request. With the new polling-driven deps (refetchInterval on the route query), the deps now change every 2–10 s. If a dep change triggers a new fetch while a previous one is still in flight, the earlier response can resolve later and overwrite state with stale TX data via setValue(tx).

🤖 Generated with Claude Code

@simcheolhwan simcheolhwan merged commit 8f933dd into main Feb 25, 2026
8 of 9 checks passed
@simcheolhwan simcheolhwan deleted the fix/bridge-route-quote-freshness branch February 25, 2026 07:17
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before Autofix could start.

extra_infos: route.extra_infos,
extra_warnings: route.extra_warnings,
required_op_hook: route.required_op_hook,
})
Copy link

Choose a reason for hiding this comment

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

USD amounts in route signature cause spurious reconfirmation

Medium Severity

getRouteSignature includes usd_amount_in and usd_amount_out in the comparison used to detect whether a route has "changed." These USD valuations are derived from live market prices and can fluctuate between API calls even when the actual swap terms (amount_in, amount_out, operations, estimated_fees) are identical. This means a confirm-time requote can trigger a "Route updated — please review and confirm again" reconfirmation prompt even though nothing material about the swap has changed, leading to unnecessary friction and potentially repeated reconfirm cycles.

Fix in Cursor Fix in Web

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

Labels

priority:high type:enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants