Skip to content

[DRAFT] feat: use balances watcher service to sync balances#7226

Draft
limitofzero wants to merge 1 commit intodevelopfrom
feat/balance-watcher-integration
Draft

[DRAFT] feat: use balances watcher service to sync balances#7226
limitofzero wants to merge 1 commit intodevelopfrom
feat/balance-watcher-integration

Conversation

@limitofzero
Copy link
Copy Markdown
Contributor

@limitofzero limitofzero commented Mar 26, 2026

Summary

<<if there's an issue>>Fixes #issueNumber

High-level description of what your changes are accomplishing

Add screenshots if applicable. Images are nice :)

To Test

  1. <> Open the page about
  • <<What to expect?>> Verify it contains about information...
  • Checkbox Style list of things a QA person could verify, i.e.
  • Should display Text Input our storybook
  • Input should not accept Numbers
  1. <> ...

Background

Optional: Give background information for changes you've made, that might be difficult to explain via comments

Summary by CodeRabbit

Release Notes

  • New Features
    • Upgraded balance-fetching infrastructure to BalanceWatcher service for improved reliability and real-time updates
    • Extended network support to Mainnet, Arbitrum One, and Sepolia for balance synchronization
    • Enhanced balance monitoring with automatic updates when token balances change

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 26, 2026

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

Project Deployment Actions Updated (UTC)
cowfi Ready Ready Preview Mar 26, 2026 9:45am
explorer-dev Ready Ready Preview Mar 26, 2026 9:45am
sdk-tools Ready Ready Preview Mar 26, 2026 9:45am
swap-dev Ready Ready Preview Mar 26, 2026 9:45am
widget-configurator Ready Ready Preview Mar 26, 2026 9:45am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
cosmos Ignored Ignored Mar 26, 2026 9:45am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Walkthrough

This PR migrates balance fetching from BFF (Backend For Frontend) to BalanceWatcher (BW), replacing SWR-based polling with Server-Sent Events (SSE) streaming. It removes BFF implementations, adds BW service integration, updates state management, and rewires all components to use the new backend with token list and custom token address support.

Changes

Cohort / File(s) Summary
BW Service Layer
libs/balances-and-allowances/src/services/balanceWatcherApi.ts, libs/common-const/src/balanceWatcher.ts
New service module with REST functions for BW session management (createBwSession, updateBwSession) and SSE URL generation. Environment variable-backed BALANCE_WATCHER_URL constant added.
State Management
libs/balances-and-allowances/src/state/isBalanceWatcherFailedAtom.ts, libs/balances-and-allowances/src/state/isBffFailedAtom.ts
Added Jotai atom isBalanceWatcherFailedAtom with reader/setter hooks; removed legacy BFF failure atom and hooks.
Balance Persistence – Core Migration
libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts, libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts, libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsx
New SSE-driven hook with exponential backoff reconnection, session parameter updates, and balance snapshot/diff handling; removed old SWR-based hook and tests.
Updater Components
libs/balances-and-allowances/src/updaters/BalancesBwUpdater.tsx, libs/balances-and-allowances/src/updaters/BalancesBffUpdater.tsx, libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx
New BW updater component; removed BFF updater; core updater refactored to accept tokenListUrls/customTokenAddresses and use isBwSwitchedOn/isBwEnabled.
Network Support Utilities
libs/balances-and-allowances/src/utils/isBwSupportedNetwork.ts, libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts
New utility with Mainnet/Arbitrum/Sepolia support; removed BFF network check (which excluded Sepolia).
SWR Configuration
libs/balances-and-allowances/src/constants/bff-balances-swr-config.ts
Removed SWR configuration with focus tracking and exponential backoff retry logic.
Barrel Exports & Utilities
libs/balances-and-allowances/src/index.ts, libs/common-const/src/index.ts, libs/tokens/src/index.ts, libs/tokens/src/hooks/lists/useAllListsSources.ts
Updated public API: BFF hooks/updaters replaced with BW equivalents; added useAllListsSources hook; exposed balanceWatcher module.
Consumer Implementation
apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/CommonPriorityBalancesAndAllowancesUpdater.tsx
Replaced BFF gating logic with BW equivalents; added tokenListUrls and customTokenAddresses derivation and propagation.

Sequence Diagram(s)

sequenceDiagram
    participant App as CommonPriorityBalancesAndAllowancesUpdater
    participant Updater as BalancesBwUpdater
    participant Hook as usePersistBalancesFromBW
    participant Service as balanceWatcherApi
    participant BW as BalanceWatcher<br/>(Backend)
    participant Atoms as Jotai Atoms

    App->>Updater: Render with account,<br/>chainId, tokenListUrls,<br/>customTokenAddresses
    Updater->>Hook: usePersistBalancesFromBW({...})
    Hook->>Service: createBwSession(chainId,<br/>owner, params)
    Service->>BW: POST /session
    BW-->>Service: ✓ Session created
    Service-->>Hook: Session ready
    Hook->>Service: getBwSseUrl(chainId, owner)
    Service-->>Hook: SSE URL
    Hook->>BW: EventSource(sseUrl)
    activate BW
    BW-->>Hook: balance_update event (snapshot)
    Hook->>Atoms: Update balancesAtom<br/>with snapshot
    Note over Hook: Accumulate in-memory
    BW-->>Hook: balance_update event (diff)
    Hook->>Hook: Merge diff into<br/>accumulated map
    Hook->>Atoms: Update balancesUpdateAtom<br/>with timestamp
    alt Connection Error
        BW-->>Hook: onerror event
        Hook->>Atoms: setIsBalanceWatcherFailed(true)
        Hook->>Hook: Schedule exponential<br/>backoff reconnect
    end
    deactivate BW
    App->>Hook: tokenListUrls or<br/>customTokenAddresses change
    Hook->>Service: updateBwSession(...)
    Service->>BW: PUT /session
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • shoom3301

Poem

🐰 A balance-watcher hops in place,

SSE streams at a speedy pace,

No polling polls, just live events flow,

Through atoms they glow, rebalance below! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is a template with unfilled placeholders; it lacks concrete issue linkage, high-level description of changes, testing steps, and background information required to understand the purpose and validation of this significant refactoring. Fill in all required sections: link an issue number if applicable, provide a detailed description of the BFF-to-BW migration, include testing steps or checklists, and add relevant background context.
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: integrating a balance watcher service to sync balances, which aligns with the substantial refactoring from BFF to BW throughout the codebase.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/balance-watcher-integration

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
Copy Markdown
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: 7

Caution

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

⚠️ Outside diff range comments (1)
libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx (1)

25-34: ⚠️ Potential issue | 🟡 Minor

Remove unused invalidateCacheTrigger prop from interface.

The invalidateCacheTrigger prop is declared in the interface but is not destructured or used anywhere in the component. Per coding guidelines, unused params should be removed immediately.

🧹 Proposed fix
 export interface BalancesAndAllowancesUpdaterProps {
   account: string | undefined
   chainId: SupportedChainId
-  invalidateCacheTrigger: number
   excludedTokens: Set<string>
   isBwSwitchedOn: boolean
   isBwEnabled?: boolean
   tokenListUrls: string[]
   customTokenAddresses: string[]
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx`
around lines 25 - 34, The BalancesAndAllowancesUpdaterProps interface declares
an unused property invalidateCacheTrigger; remove invalidateCacheTrigger from
the BalancesAndAllowancesUpdaterProps definition and any related type references
(so the component signature and consumers no longer expect or pass
invalidateCacheTrigger), then run TypeScript checks and update any call sites
that currently pass invalidateCacheTrigger to stop supplying it or to pass a
supported prop instead.
🧹 Nitpick comments (2)
libs/balances-and-allowances/src/services/balanceWatcherApi.ts (1)

9-43: Consider extracting shared logic to reduce duplication.

createBwSession and updateBwSession are nearly identical, differing only in the HTTP method and error message. A small helper would reduce duplication and make future changes easier.

♻️ Proposed refactor
+async function bwSessionRequest(
+  chainId: SupportedChainId,
+  owner: string,
+  params: BwSessionParams,
+  method: 'POST' | 'PUT',
+): Promise<void> {
+  const url = `${BALANCE_WATCHER_URL}/${chainId}/sessions/${owner}`
+  const res = await fetch(url, {
+    method,
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify(params),
+  })
+  if (!res.ok) {
+    const action = method === 'POST' ? 'create' : 'update'
+    throw new Error(`BW ${action} session failed: ${res.status} ${res.statusText}`)
+  }
+}
+
 export async function createBwSession(
   chainId: SupportedChainId,
   owner: string,
   params: BwSessionParams,
 ): Promise<void> {
-  const url = `${BALANCE_WATCHER_URL}/${chainId}/sessions/${owner}`
-  const res = await fetch(url, {
-    method: 'POST',
-    headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify(params),
-  })
-  if (!res.ok) {
-    throw new Error(`BW create session failed: ${res.status} ${res.statusText}`)
-  }
+  return bwSessionRequest(chainId, owner, params, 'POST')
 }
 
 export async function updateBwSession(
   chainId: SupportedChainId,
   owner: string,
   params: BwSessionParams,
 ): Promise<void> {
-  const url = `${BALANCE_WATCHER_URL}/${chainId}/sessions/${owner}`
-  const res = await fetch(url, {
-    method: 'PUT',
-    headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify(params),
-  })
-  if (!res.ok) {
-    throw new Error(`BW update session failed: ${res.status} ${res.statusText}`)
-  }
+  return bwSessionRequest(chainId, owner, params, 'PUT')
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/balances-and-allowances/src/services/balanceWatcherApi.ts` around lines
9 - 43, Extract the duplicated request logic from createBwSession and
updateBwSession into a small helper (e.g., postOrPutBwSession or
sendBwSessionRequest) that accepts chainId, owner, params, and an HTTP method
string; have it build the url using BALANCE_WATCHER_URL, call fetch with headers
and JSON body, and throw an error containing the method-specific message and
response info when !res.ok. Then simplify createBwSession and updateBwSession to
call this helper with method 'POST' and 'PUT' respectively (referencing the
existing BwSessionParams, createBwSession, updateBwSession, and
BALANCE_WATCHER_URL identifiers).
libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts (1)

21-27: Consider refactoring to remove the eslint-disable comment.

Per coding guidelines, eslint-disable comments should not be shipped. The hook is complex (~200 lines) but could potentially be split by extracting the SSE connection logic into a separate module or custom hook to reduce per-function complexity.

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

In `@libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts` around
lines 21 - 27, The function usePersistBalancesFromBW currently carries an
eslint-disable comment for max-lines-per-function; split the large hook by
extracting the Server-Sent Events logic and related side-effect handling into a
separate module or custom hook (e.g., create useBWEventSource or bwSSEClient)
and move SSE setup/teardown, message parsing, and reconnection logic there,
keeping usePersistBalancesFromBW thin and focused on wiring
account/chainId/tokenListUrls/customTokenAddresses (PersistBalancesFromBWParams)
into the new hook; remove the eslint-disable and ensure tests and imports are
updated to use the extracted functions so the original hook falls under the line
limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/CommonPriorityBalancesAndAllowancesUpdater.tsx`:
- Around line 64-72: The current logic lets usePersistBalancesFromBW call
createBwSession immediately using possibly-empty derived refs (tokenListUrls,
customTokenAddresses) because listsEnabledStateAtom hydrates async, which can
create a session with missing list data and rely on updateBwSession to patch it;
to fix, gate initial session creation until lists are hydrated by checking a
non-empty lists state (use listsStatesListAtom or explicitly ensure
listsEnabledState is initialized) inside the effect that calls createBwSession,
or alternatively move session creation into the effect that depends on [account,
chainId, tokenListUrls, customTokenAddresses] so createBwSession is only invoked
when tokenListUrls/customTokenAddresses are populated; update references to
usePersistBalancesFromBW, createBwSession, updateBwSession, tokenListUrls,
customTokenAddresses, and listsStatesListAtom to implement the gate.

In `@libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts`:
- Line 64: Replace the manual address normalization call
targetAccount.toLowerCase() with the standardized helper getAddressKey from
`@cowprotocol/cow-sdk` in usePersistBalancesFromBW.ts; import getAddressKey and
use getAddressKey(targetAccount) where the object key is created (the line
currently building {[targetAccount.toLowerCase()]: Date.now()}), so all stored
address keys follow the project's normalization utility.
- Around line 213-222: The current useEffect calls updateBwSession and merely
logs errors, allowing the SSE (eventSourceRef) to continue using stale token
list parameters; modify the error handler in the useEffect that calls
updateBwSession so that on failure it forces a reconnect of the BW SSE (close
and recreate the EventSource referenced by eventSourceRef) or triggers the
existing reconnection helper (e.g., a restartEventSource/reconnectEventSource
function if present), and optionally propagate the error to state (e.g.,
setBwSessionError) so the UI can show a failure; specifically update the catch
block for updateBwSession in the effect to close eventSourceRef.current,
clear/reset related refs/state, invoke the reconnection routine, and set an
error flag for the UI.
- Around line 155-174: Remove the redundant EventSource error listener: delete
the es.addEventListener('error', ...) block (including its try/catch JSON parse
and close/set state/scheduleReconnect calls) and rely solely on es.onerror to
handle errors; ensure es.onerror performs es.close(), sets
eventSourceRef.current = null, calls setIsBalanceWatcherFailed(true) and
scheduleReconnect(targetAccount) so reconnect logic and state mutation only
occur once.

In `@libs/balances-and-allowances/src/services/balanceWatcherApi.ts`:
- Around line 16-24: The fetch calls in createBwSession and updateBwSession can
hang indefinitely; replace direct fetch(...) with fetchWithTimeout(...) imported
from `@cowprotocol/common-utils` and pass a timeout (e.g., 10000–30000 ms). Update
both functions to call fetchWithTimeout(url, { method, headers, body }, timeout)
(or the library's expected signature), preserve the existing response check
(!res.ok) and error message (`BW create session failed` / `BW update session
failed`), and ensure the import for fetchWithTimeout is added at the top of the
file so network timeouts trigger the existing backoff/reconnect logic.

In `@libs/balances-and-allowances/src/state/isBalanceWatcherFailedAtom.ts`:
- Around line 3-10: The global isBalanceWatcherFailedAtom leaks failure state
across wallet/chain switches; change it to a keyed map (e.g., a Record keyed by
`${address.toLowerCase()}_${chainId}`) instead of a single boolean, update the
accessors useIsBalanceWatcherFailed and useSetIsBalanceWatcherFailed to accept
the current account and network (or derive them) and read/write the keyed entry,
and ensure you clear or reset the entry on account/network change (or when
session ends) so failures are scoped per session.

In `@libs/common-const/src/balanceWatcher.ts`:
- Line 1: The current export BALANCE_WATCHER_URL uses || which treats an empty
string as a valid signal to fall back to production; change it so empty values
are treated as unset and do not silently route to prod—e.g., treat
process.env['REACT_APP_BALANCE_WATCHER_URL'] === undefined? (or === '' )
specially and only use the 'https://balance-watcher.info' default in production
builds, otherwise leave BALANCE_WATCHER_URL undefined or throw/config-validate
at startup; update the BALANCE_WATCHER_URL initializer and add a config
validation that references BALANCE_WATCHER_URL (and consider moving allowed
endpoints into a shared config/enum) so preview/dev environments fail fast
instead of routing traffic to production.

---

Outside diff comments:
In `@libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx`:
- Around line 25-34: The BalancesAndAllowancesUpdaterProps interface declares an
unused property invalidateCacheTrigger; remove invalidateCacheTrigger from the
BalancesAndAllowancesUpdaterProps definition and any related type references (so
the component signature and consumers no longer expect or pass
invalidateCacheTrigger), then run TypeScript checks and update any call sites
that currently pass invalidateCacheTrigger to stop supplying it or to pass a
supported prop instead.

---

Nitpick comments:
In `@libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts`:
- Around line 21-27: The function usePersistBalancesFromBW currently carries an
eslint-disable comment for max-lines-per-function; split the large hook by
extracting the Server-Sent Events logic and related side-effect handling into a
separate module or custom hook (e.g., create useBWEventSource or bwSSEClient)
and move SSE setup/teardown, message parsing, and reconnection logic there,
keeping usePersistBalancesFromBW thin and focused on wiring
account/chainId/tokenListUrls/customTokenAddresses (PersistBalancesFromBWParams)
into the new hook; remove the eslint-disable and ensure tests and imports are
updated to use the extracted functions so the original hook falls under the line
limit.

In `@libs/balances-and-allowances/src/services/balanceWatcherApi.ts`:
- Around line 9-43: Extract the duplicated request logic from createBwSession
and updateBwSession into a small helper (e.g., postOrPutBwSession or
sendBwSessionRequest) that accepts chainId, owner, params, and an HTTP method
string; have it build the url using BALANCE_WATCHER_URL, call fetch with headers
and JSON body, and throw an error containing the method-specific message and
response info when !res.ok. Then simplify createBwSession and updateBwSession to
call this helper with method 'POST' and 'PUT' respectively (referencing the
existing BwSessionParams, createBwSession, updateBwSession, and
BALANCE_WATCHER_URL identifiers).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2e196ebf-3d31-45a1-a238-e3b50c300600

📥 Commits

Reviewing files that changed from the base of the PR and between 0c8f326 and c44f5ec.

📒 Files selected for processing (18)
  • apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/CommonPriorityBalancesAndAllowancesUpdater.tsx
  • libs/balances-and-allowances/src/constants/bff-balances-swr-config.ts
  • libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts
  • libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsx
  • libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts
  • libs/balances-and-allowances/src/index.ts
  • libs/balances-and-allowances/src/services/balanceWatcherApi.ts
  • libs/balances-and-allowances/src/state/isBalanceWatcherFailedAtom.ts
  • libs/balances-and-allowances/src/state/isBffFailedAtom.ts
  • libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx
  • libs/balances-and-allowances/src/updaters/BalancesBffUpdater.tsx
  • libs/balances-and-allowances/src/updaters/BalancesBwUpdater.tsx
  • libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts
  • libs/balances-and-allowances/src/utils/isBwSupportedNetwork.ts
  • libs/common-const/src/balanceWatcher.ts
  • libs/common-const/src/index.ts
  • libs/tokens/src/hooks/lists/useAllListsSources.ts
  • libs/tokens/src/index.ts
💤 Files with no reviewable changes (6)
  • libs/balances-and-allowances/src/state/isBffFailedAtom.ts
  • libs/balances-and-allowances/src/utils/isBffSupportedNetwork.ts
  • libs/balances-and-allowances/src/updaters/BalancesBffUpdater.tsx
  • libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsx
  • libs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.ts
  • libs/balances-and-allowances/src/constants/bff-balances-swr-config.ts

Comment on lines +64 to +72
const listsSources = useAllListsSources()
const listsEnabledState = useListsEnabledState()
const tokenListUrls = useMemo(
() => listsSources.filter((s) => listsEnabledState[s.source]).map((s) => s.source),
[listsSources, listsEnabledState],
)

const userAddedTokens = useUserAddedTokens()
const customTokenAddresses = useMemo(() => userAddedTokens.map((t) => t.address), [userAddedTokens])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

for pattern in \
  'useListsEnabledState\.(ts|tsx)$' \
  'useUserAddedTokens\.(ts|tsx)$' \
  'BalancesAndAllowancesUpdater\.(ts|tsx)$' \
  'usePersistBalancesFromBW\.(ts|tsx)$'
do
  file="$(fd "$pattern" . | head -n1)"
  if [ -z "$file" ]; then
    echo "Missing file for pattern: $pattern" >&2
    exit 1
  fi

  echo "===== $file ====="
  sed -n '1,260p' "$file"
  echo
  rg -n -C3 'tokenListUrls|customTokenAddresses|listsEnabledState|userAddedTokens|useEffect|createBwSession|updateBwSession' "$file"
  echo
done

Repository: cowprotocol/cowswap

Length of output: 14244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1. Find and inspect listsEnabledStateAtom definition
echo "===== listsEnabledStateAtom ====="
fd 'tokenListsStateAtom\.(ts|tsx)$' . | head -n1 | xargs cat

echo -e "\n===== userAddedTokensListAtom ====="
# 2. Find and inspect userAddedTokensAtom
fd 'userAddedTokensAtom\.(ts|tsx)$' . | head -n1 | xargs cat

echo -e "\n===== CommonPriorityBalancesAndAllowancesUpdater (caller) ====="
# 3. Show the full caller to see if there's any gating
fd 'CommonPriorityBalancesAndAllowancesUpdater\.(ts|tsx)$' . | head -n1 | xargs cat -n | head -n 150

Repository: cowprotocol/cowswap

Length of output: 13602


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== BalancesAndAllowancesUpdater implementation ====="
fd 'BalancesAndAllowancesUpdater\.(ts|tsx)$' --type f | grep -v node_modules | head -n1 | xargs cat -n | head -n 200

Repository: cowprotocol/cowswap

Length of output: 2746


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if it's in libs or look for the package
echo "===== Searching for BalancesAndAllowancesUpdater in monorepo ====="
find . -name "BalancesAndAllowancesUpdater.tsx" -o -name "BalancesAndAllowancesUpdater.ts" 2>/dev/null | head -n5

# Also check where `@cowprotocol/balances-and-allowances` points to
echo -e "\n===== Checking package.json for `@cowprotocol/balances-and-allowances` ====="
cd apps/cowswap-frontend
cat package.json | grep -A2 "balances-and-allowances"

Repository: cowprotocol/cowswap

Length of output: 477


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== BalancesAndAllowancesUpdater.tsx implementation ====="
cat -n ./libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx | head -n 250

Repository: cowprotocol/cowswap

Length of output: 4495


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== BalancesBwUpdater.tsx implementation ====="
cat -n ./libs/balances-and-allowances/src/updaters/BalancesBwUpdater.tsx

Repository: cowprotocol/cowswap

Length of output: 760


Confirm that BW session creation is gated or properly updated when token lists hydrate.

listsEnabledStateAtom is an async atom that starts empty. The derived tokenListUrls and customTokenAddresses will be empty on initial render. usePersistBalancesFromBW calls createBwSession immediately on the [account, chainId] effect with those empty ref values, then relies on a second effect [account, chainId, tokenListUrls, customTokenAddresses] to call updateBwSession when lists hydrate.

This works if updateBwSession reliably patches the session and doesn't fail silently. However, per the shoom3301 learning, the pattern elsewhere in the codebase is to wait for lists to initialize before proceeding, not to create with empty state and patch later. Consider whether initial session creation should be gated until listsEnabledState is non-empty (use listsStatesListAtom), or document why the update-on-change pattern is safe here.

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

In
`@apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/CommonPriorityBalancesAndAllowancesUpdater.tsx`
around lines 64 - 72, The current logic lets usePersistBalancesFromBW call
createBwSession immediately using possibly-empty derived refs (tokenListUrls,
customTokenAddresses) because listsEnabledStateAtom hydrates async, which can
create a session with missing list data and rely on updateBwSession to patch it;
to fix, gate initial session creation until lists are hydrated by checking a
non-empty lists state (use listsStatesListAtom or explicitly ensure
listsEnabledState is initialized) inside the effect that calls createBwSession,
or alternatively move session creation into the effect that depends on [account,
chainId, tokenListUrls, customTokenAddresses] so createBwSession is only invoked
when tokenListUrls/customTokenAddresses are populated; update references to
usePersistBalancesFromBW, createBwSession, updateBwSession, tokenListUrls,
customTokenAddresses, and listsStatesListAtom to implement the gate.

...state,
[currentChainId]: {
...state[currentChainId],
[targetAccount.toLowerCase()]: Date.now(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use getAddressKey instead of toLowerCase() for address normalization.

Per coding guidelines, address normalization should use getAddressKey from @cowprotocol/cow-sdk rather than manual toLowerCase() calls.

🔧 Proposed fix
+import { getAddressKey } from '@cowprotocol/cow-sdk'
 
 // Line 64:
-        [targetAccount.toLowerCase()]: Date.now(),
+        [getAddressKey(targetAccount)]: Date.now(),
 
 // Line 127:
-            acc[address.toLowerCase()] = BigNumber.from(amount)
+            acc[getAddressKey(address)] = BigNumber.from(amount)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts` at line
64, Replace the manual address normalization call targetAccount.toLowerCase()
with the standardized helper getAddressKey from `@cowprotocol/cow-sdk` in
usePersistBalancesFromBW.ts; import getAddressKey and use
getAddressKey(targetAccount) where the object key is created (the line currently
building {[targetAccount.toLowerCase()]: Date.now()}), so all stored address
keys follow the project's normalization utility.

Comment on lines +155 to +174
es.addEventListener('error', (event: MessageEvent) => {
try {
const data = JSON.parse(event.data)
console.warn('[BW] SSE error event:', data.code, data.message)
} catch {
// not a JSON error event, likely a connection error
}

es.close()
eventSourceRef.current = null
setIsBalanceWatcherFailed(true)
scheduleReconnect(targetAccount)
})

es.onerror = () => {
es.close()
eventSourceRef.current = null
setIsBalanceWatcherFailed(true)
scheduleReconnect(targetAccount)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does the Balance Watcher SSE API send custom 'error' events with JSON payloads?

💡 Result:

No, there is no evidence that a "Balance Watcher SSE API" exists or sends custom 'error' events with JSON payloads. Extensive searches across general web, API documentation, GitHub repositories, and specific phrases like "Balance Watcher SSE API" yielded no matching official documentation or technical sources describing such an API. Related results included balance monitoring tools (e.g., AwardWallet Balance Watch, validators-watcher on GitHub) and general SSE guides, but none reference a Balance Watcher SSE API with error events.

Citations:


🏁 Script executed:

# Find and examine the file mentioned in the review
find . -type f -name "usePersistBalancesFromBW.ts" 2>/dev/null

Repository: cowprotocol/cowswap

Length of output: 131


🏁 Script executed:

# Look for related SSE or balance watcher implementation
find . -type f -name "*[Bb]alance*[Ww]atcher*" -o -name "*[Ss][Ss][Ee]*" 2>/dev/null | head -20

Repository: cowprotocol/cowswap

Length of output: 1741


🏁 Script executed:

# Search for documentation or setup of the Balance Watcher API
rg -i "balance.watcher|balance.*sse" --type ts --type tsx -A 5 -B 5 2>/dev/null | head -100

Repository: cowprotocol/cowswap

Length of output: 45


🏁 Script executed:

# Read the file to understand the error handling context
wc -l ./libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts

Repository: cowprotocol/cowswap

Length of output: 135


🏁 Script executed:

# Read the file content around the error handlers
cat -n ./libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts | sed -n '140,190p'

Repository: cowprotocol/cowswap

Length of output: 1857


🏁 Script executed:

# Read the entire file to understand scheduleReconnect and context
cat -n ./libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts

Repository: cowprotocol/cowswap

Length of output: 8874


🏁 Script executed:

# Search for Balance Watcher API implementation/documentation
rg -i "balancewatcher|balance.watcher" --type ts --type tsx --type md 2>/dev/null | grep -v "node_modules" | head -30

Repository: cowprotocol/cowswap

Length of output: 45


🏁 Script executed:

# Look for the balanceWatcherApi service to understand the protocol
find . -type f -name "*balanceWatcher*" 2>/dev/null | grep -v node_modules

Repository: cowprotocol/cowswap

Length of output: 169


🏁 Script executed:

# Check if there are tests for this hook
find . -type f -name "*usePersistBalancesFromBW*" 2>/dev/null

Repository: cowprotocol/cowswap

Length of output: 131


🏁 Script executed:

# Read the Balance Watcher API implementation
cat -n ./libs/balances-and-allowances/src/services/balanceWatcherApi.ts

Repository: cowprotocol/cowswap

Length of output: 1653


🏁 Script executed:

# Read the constants file
cat -n ./libs/common-const/src/balanceWatcher.ts

Repository: cowprotocol/cowswap

Length of output: 183


🏁 Script executed:

# Search for any documentation or comments about the error event protocol
rg "addEventListener.*error|onerror" -B 5 -A 5 --type ts --type tsx 2>/dev/null | grep -i balance -A 10 -B 10

Repository: cowprotocol/cowswap

Length of output: 45


🏁 Script executed:

# Check git history for this file
cd ./libs/balances-and-allowances/src/hooks && git log --oneline usePersistBalancesFromBW.ts 2>/dev/null | head -20

Repository: cowprotocol/cowswap

Length of output: 106


🏁 Script executed:

# Search for any references or comments about custom error events
rg "error.*event|custom.*error" ./libs/balances-and-allowances -B 3 -A 3 --type ts 2>/dev/null

Repository: cowprotocol/cowswap

Length of output: 1216


🏁 Script executed:

# Verify EventSource SSE specification - check if addEventListener('error') and onerror both fire
web_search
MDN EventSource addEventListener error vs onerror when does each fire

Repository: cowprotocol/cowswap

Length of output: 155


🌐 Web query:

EventSource addEventListener('error') vs onerror handler when does each fire SSE

💡 Result:

EventSource.addEventListener('error', handler) and the onerror property handler both fire under the same conditions for Server-Sent Events (SSE). They are two equivalent ways to handle the same 'error' event on the EventSource object. Both fire when: - A connection fails to open (e.g., network error preventing establishment). - The server returns an invalid response (status not 200 or Content-Type not text/event-stream), causing connection failure. - A fatal connection error occurs (sets readyState to CLOSED, no reconnect). - During reconnection attempts (sets readyState to CONNECTING and fires error event). For recoverable network errors, the spec queues an error event, then restarts the connection (reconnects). The error event fires repeatedly on reconnect attempts until success or fatal failure. Examples from MDN: evtSource.addEventListener("error", (e) => { ... }); evtSource.onerror = (e) => { ... }; The event is a generic Event (not ErrorEvent), non-cancelable, non-bubbling. No difference in firing timing or conditions between the two methods; addEventListener supports multiple listeners, while onerror is a single property (last-set wins, per EventTarget behavior).

Citations:


Consolidate redundant error handlers to prevent duplicate reconnect scheduling.

Both addEventListener('error') and onerror fire on the same EventSource error event, causing scheduleReconnect() to be invoked twice with redundant state mutations. Remove the addEventListener('error') handler since onerror is the standard way to handle connection failures on EventSource.

🔧 Proposed fix
-      es.addEventListener('error', (event: MessageEvent) => {
-        try {
-          const data = JSON.parse(event.data)
-          console.warn('[BW] SSE error event:', data.code, data.message)
-        } catch {
-          // not a JSON error event, likely a connection error
-        }
-
-        es.close()
-        eventSourceRef.current = null
-        setIsBalanceWatcherFailed(true)
-        scheduleReconnect(targetAccount)
-      })
-
       es.onerror = () => {
+        console.warn('[BW] SSE connection error')
         es.close()
         eventSourceRef.current = null
         setIsBalanceWatcherFailed(true)
         scheduleReconnect(targetAccount)
       }
📝 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
es.addEventListener('error', (event: MessageEvent) => {
try {
const data = JSON.parse(event.data)
console.warn('[BW] SSE error event:', data.code, data.message)
} catch {
// not a JSON error event, likely a connection error
}
es.close()
eventSourceRef.current = null
setIsBalanceWatcherFailed(true)
scheduleReconnect(targetAccount)
})
es.onerror = () => {
es.close()
eventSourceRef.current = null
setIsBalanceWatcherFailed(true)
scheduleReconnect(targetAccount)
}
es.onerror = () => {
console.warn('[BW] SSE connection error')
es.close()
eventSourceRef.current = null
setIsBalanceWatcherFailed(true)
scheduleReconnect(targetAccount)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts` around
lines 155 - 174, Remove the redundant EventSource error listener: delete the
es.addEventListener('error', ...) block (including its try/catch JSON parse and
close/set state/scheduleReconnect calls) and rely solely on es.onerror to handle
errors; ensure es.onerror performs es.close(), sets eventSourceRef.current =
null, calls setIsBalanceWatcherFailed(true) and scheduleReconnect(targetAccount)
so reconnect logic and state mutation only occur once.

Comment on lines +213 to +222
useEffect(() => {
if (!account || !eventSourceRef.current) return

updateBwSession(chainId, account, {
tokensListsUrls: tokenListUrls,
customTokens: customTokenAddresses,
}).catch((e) => {
console.warn('[BW] Failed to update session:', e)
})
}, [account, chainId, tokenListUrls, customTokenAddresses])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent failure on session update may cause stale token lists.

When updateBwSession fails, the error is logged but the SSE connection continues with potentially outdated token list parameters. Consider either:

  1. Triggering a full reconnect on update failure
  2. Retrying the update with backoff
  3. Surfacing the failure state to the UI
🔧 Proposed fix - reconnect on failure
   useEffect(() => {
     if (!account || !eventSourceRef.current) return
 
     updateBwSession(chainId, account, {
       tokensListsUrls: tokenListUrls,
       customTokens: customTokenAddresses,
     }).catch((e) => {
       console.warn('[BW] Failed to update session:', e)
+      // Reconnect to ensure fresh session with correct params
+      connectSse(account)
     })
-  }, [account, chainId, tokenListUrls, customTokenAddresses])
+  }, [account, chainId, tokenListUrls, customTokenAddresses, connectSse])
 }
📝 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
useEffect(() => {
if (!account || !eventSourceRef.current) return
updateBwSession(chainId, account, {
tokensListsUrls: tokenListUrls,
customTokens: customTokenAddresses,
}).catch((e) => {
console.warn('[BW] Failed to update session:', e)
})
}, [account, chainId, tokenListUrls, customTokenAddresses])
useEffect(() => {
if (!account || !eventSourceRef.current) return
updateBwSession(chainId, account, {
tokensListsUrls: tokenListUrls,
customTokens: customTokenAddresses,
}).catch((e) => {
console.warn('[BW] Failed to update session:', e)
// Reconnect to ensure fresh session with correct params
connectSse(account)
})
}, [account, chainId, tokenListUrls, customTokenAddresses, connectSse])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts` around
lines 213 - 222, The current useEffect calls updateBwSession and merely logs
errors, allowing the SSE (eventSourceRef) to continue using stale token list
parameters; modify the error handler in the useEffect that calls updateBwSession
so that on failure it forces a reconnect of the BW SSE (close and recreate the
EventSource referenced by eventSourceRef) or triggers the existing reconnection
helper (e.g., a restartEventSource/reconnectEventSource function if present),
and optionally propagate the error to state (e.g., setBwSessionError) so the UI
can show a failure; specifically update the catch block for updateBwSession in
the effect to close eventSourceRef.current, clear/reset related refs/state,
invoke the reconnection routine, and set an error flag for the UI.

Comment on lines +16 to +24
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
})

if (!res.ok) {
throw new Error(`BW create session failed: ${res.status} ${res.statusText}`)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify fetchWithTimeout exists in common-utils
ast-grep --pattern 'export function fetchWithTimeout($$$)'

Repository: cowprotocol/cowswap

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Search for timeout-related exports in common-utils
rg -t ts "export.*timeout|export.*Timeout" --files-with-matches | head -20

Repository: cowprotocol/cowswap

Length of output: 488


🏁 Script executed:

#!/bin/bash
# Find and read the full balanceWatcherApi.ts file
fd "balanceWatcherApi.ts" --type f -x cat -n {}

Repository: cowprotocol/cowswap

Length of output: 1653


🏁 Script executed:

#!/bin/bash
# Check what's available in `@cowprotocol/common-utils` related to fetch/timeout
rg "withTimeout|fetchWithTimeout" --type ts | head -30

Repository: cowprotocol/cowswap

Length of output: 1326


🏁 Script executed:

#!/bin/bash
# Read the fetchWithTimeout implementation
cat -n libs/common-utils/src/fetch/index.ts

Repository: cowprotocol/cowswap

Length of output: 1032


🏁 Script executed:

#!/bin/bash
# Find where createBwSession and updateBwSession are called
rg "createBwSession|updateBwSession" --type ts -B 2 -A 2

Repository: cowprotocol/cowswap

Length of output: 2561


🏁 Script executed:

#!/bin/bash
# Check if Balance Watcher is documented or configured anywhere
rg "BALANCE_WATCHER|Balance Watcher" --type ts -l | head -20

Repository: cowprotocol/cowswap

Length of output: 165


🏁 Script executed:

#!/bin/bash
# Check Balance Watcher configuration
cat -n libs/common-const/src/balanceWatcher.ts

Repository: cowprotocol/cowswap

Length of output: 183


🏁 Script executed:

#!/bin/bash
# Read the hook that uses these functions to see error handling context
cat -n libs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.ts

Repository: cowprotocol/cowswap

Length of output: 8874


Add timeout to prevent indefinite hangs on network issues.

The fetch calls in createBwSession and updateBwSession lack timeouts. If the Balance Watcher service becomes unresponsive, these requests can hang indefinitely since the promise never resolves or rejects, preventing the hook's error handling and reconnection logic from activating. Use fetchWithTimeout from @cowprotocol/common-utils with a reasonable timeout (e.g., 10–30 seconds) to ensure failed requests are caught and trigger the existing backoff/reconnect mechanism.

🛠️ Proposed fix using fetchWithTimeout
+import { fetchWithTimeout } from '@cowprotocol/common-utils'
 import { BALANCE_WATCHER_URL } from '@cowprotocol/common-const'
 import { SupportedChainId } from '@cowprotocol/cow-sdk'
 
+const BW_REQUEST_TIMEOUT = 10_000
+
 // ... in createBwSession:
-  const res = await fetch(url, {
+  const res = await fetchWithTimeout(url, {
     method: 'POST',
     headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify(params),
-  })
+  }, { timeout: BW_REQUEST_TIMEOUT })
 
 // ... in updateBwSession:
-  const res = await fetch(url, {
+  const res = await fetchWithTimeout(url, {
     method: 'PUT',
     headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify(params),
-  })
+  }, { timeout: BW_REQUEST_TIMEOUT })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/balances-and-allowances/src/services/balanceWatcherApi.ts` around lines
16 - 24, The fetch calls in createBwSession and updateBwSession can hang
indefinitely; replace direct fetch(...) with fetchWithTimeout(...) imported from
`@cowprotocol/common-utils` and pass a timeout (e.g., 10000–30000 ms). Update both
functions to call fetchWithTimeout(url, { method, headers, body }, timeout) (or
the library's expected signature), preserve the existing response check
(!res.ok) and error message (`BW create session failed` / `BW update session
failed`), and ensure the import for fetchWithTimeout is added at the top of the
file so network timeouts trigger the existing backoff/reconnect logic.

Comment on lines +3 to +10
export const isBalanceWatcherFailedAtom = atom(false)

export function useIsBalanceWatcherFailed(): boolean {
return useAtomValue(isBalanceWatcherFailedAtom)
}

export function useSetIsBalanceWatcherFailed(): (value: boolean) => void {
return useSetAtom(isBalanceWatcherFailedAtom)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope the BW failure state by account/network.

This atom is global, so one BW failure can leak across wallet or chain switches and keep the fallback path active in contexts that never failed. Please key it by session or clear it on account/network changes before using it as a gate.

As per coding guidelines, "Use keyed objects/maps for multi-entity state with lowercase keys (toLowerCase()) and reset on network/account changes".

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

In `@libs/balances-and-allowances/src/state/isBalanceWatcherFailedAtom.ts` around
lines 3 - 10, The global isBalanceWatcherFailedAtom leaks failure state across
wallet/chain switches; change it to a keyed map (e.g., a Record keyed by
`${address.toLowerCase()}_${chainId}`) instead of a single boolean, update the
accessors useIsBalanceWatcherFailed and useSetIsBalanceWatcherFailed to accept
the current account and network (or derive them) and read/write the keyed entry,
and ensure you clear or reset the entry on account/network change (or when
session ends) so failures are scoped per session.

@@ -0,0 +1 @@
export const BALANCE_WATCHER_URL = process.env['REACT_APP_BALANCE_WATCHER_URL'] || 'https://balance-watcher.info'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't let missing config silently route BW traffic to prod.

|| treats both unset and empty values as “use the production endpoint”. That hides misconfiguration in preview/dev environments and routes their traffic to the production service. Prefer an explicit env value, or at least gate the fallback to production builds only.

As per coding guidelines, "Use shared config/enums instead of hardcoding environment-specific lists/toggles".

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

In `@libs/common-const/src/balanceWatcher.ts` at line 1, The current export
BALANCE_WATCHER_URL uses || which treats an empty string as a valid signal to
fall back to production; change it so empty values are treated as unset and do
not silently route to prod—e.g., treat
process.env['REACT_APP_BALANCE_WATCHER_URL'] === undefined? (or === '' )
specially and only use the 'https://balance-watcher.info' default in production
builds, otherwise leave BALANCE_WATCHER_URL undefined or throw/config-validate
at startup; update the BALANCE_WATCHER_URL initializer and add a config
validation that references BALANCE_WATCHER_URL (and consider moving allowed
endpoints into a shared config/enum) so preview/dev environments fail fast
instead of routing traffic to production.

@kernelwhisperer kernelwhisperer marked this pull request as draft April 8, 2026 09:30
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