[DRAFT] feat: use balances watcher service to sync balances#7226
[DRAFT] feat: use balances watcher service to sync balances#7226limitofzero wants to merge 1 commit intodevelopfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟡 MinorRemove unused
invalidateCacheTriggerprop from interface.The
invalidateCacheTriggerprop 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.
createBwSessionandupdateBwSessionare 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 theeslint-disablecomment.Per coding guidelines,
eslint-disablecomments 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
📒 Files selected for processing (18)
apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/CommonPriorityBalancesAndAllowancesUpdater.tsxlibs/balances-and-allowances/src/constants/bff-balances-swr-config.tslibs/balances-and-allowances/src/hooks/usePersistBalancesFromBW.tslibs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.test.tsxlibs/balances-and-allowances/src/hooks/usePersistBalancesFromBff.tslibs/balances-and-allowances/src/index.tslibs/balances-and-allowances/src/services/balanceWatcherApi.tslibs/balances-and-allowances/src/state/isBalanceWatcherFailedAtom.tslibs/balances-and-allowances/src/state/isBffFailedAtom.tslibs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsxlibs/balances-and-allowances/src/updaters/BalancesBffUpdater.tsxlibs/balances-and-allowances/src/updaters/BalancesBwUpdater.tsxlibs/balances-and-allowances/src/utils/isBffSupportedNetwork.tslibs/balances-and-allowances/src/utils/isBwSupportedNetwork.tslibs/common-const/src/balanceWatcher.tslibs/common-const/src/index.tslibs/tokens/src/hooks/lists/useAllListsSources.tslibs/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
| 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]) |
There was a problem hiding this comment.
🧩 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
doneRepository: 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 150Repository: 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 200Repository: 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 250Repository: 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.tsxRepository: 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(), |
There was a problem hiding this comment.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://chainwatch.tech/docs
- 2: https://github.com/balanced/balanced-docs
- 3: https://www.smmbalancemanager.com/documentation.php
- 4: https://docs.balancer.fi/data-and-analytics/data-and-analytics/balancer-api/balancer-api.html
- 5: https://docs.bitquery.io/docs/blockchain/Ethereum/balances/balance-api/
- 6: Binance watchBalance doesn't handle balanceUpdate event properly ccxt/ccxt#24484
- 7: Binance watchOrders / watchBalance not returning a value immediately ccxt/ccxt#6995
- 8: https://stackoverflow.com/questions/50870716/why-are-my-server-sent-events-arriving-as-a-batch
- 9: https://medium.com/@moali314/server-sent-events-a-comprehensive-guide-e4b15d147576
- 10: https://news.ycombinator.com/item?id=42511318
- 11: https://stackoverflow.com/questions/74861447/correct-method-to-use-server-sent-events
- 12: https://shopify.engineering/server-sent-events-data-streaming
- 13: https://singhajit.com/server-sent-events-explained
- 14: https://www.reddit.com/r/ExperiencedDevs/comments/1m9k2c5/strategies_for_handling_transient_serversent/
- 15: https://www.balanceviewer.com/
- 16: https://awardwallet.com/blog/balance-watch/
- 17: https://bubble.dynalogix.eu/wear-stand-up-alert-v3/
- 18: https://github.com/NethermindEth/validators-watcher
- 19: http://turnwatcher.com/
- 20: https://github.com/Alexcs24/BalanceChecker-Pro
- 21: https://docs.bitquery.io/docs/usecases/real-time-balance-tracker/overview
- 22: https://github.com/JubasNTC/blocks-watcher
- 23: https://docs.extractor.live/compliance-monitoring/detectors/balance-monitor
- 24: https://bavix.github.io/laravel-wallet/guide/events/balance-updated-event.html
- 25: Binance futures watchBalance/handleBalance not working ccxt/ccxt#9836
- 26: https://docs.watchdata.io/powered-api/token-api/watch_gettokenbalances
- 27: watchOrders keep-alive causes errors with Binance ccxt/ccxt#20034
- 28: https://stackoverflow.com/questions/32662533/how-to-see-errors-about-server-sent-events
- 29: https://docs.bellwether.sh/guides/remote-servers
- 30: https://watchtowers.app/docs.html
- 31: https://docs.blnkfinance.com/reference/create-balance-monitor
🏁 Script executed:
# Find and examine the file mentioned in the review
find . -type f -name "usePersistBalancesFromBW.ts" 2>/dev/nullRepository: 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 -20Repository: 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 -100Repository: 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.tsRepository: 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.tsRepository: 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 -30Repository: 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_modulesRepository: cowprotocol/cowswap
Length of output: 169
🏁 Script executed:
# Check if there are tests for this hook
find . -type f -name "*usePersistBalancesFromBW*" 2>/dev/nullRepository: cowprotocol/cowswap
Length of output: 131
🏁 Script executed:
# Read the Balance Watcher API implementation
cat -n ./libs/balances-and-allowances/src/services/balanceWatcherApi.tsRepository: cowprotocol/cowswap
Length of output: 1653
🏁 Script executed:
# Read the constants file
cat -n ./libs/common-const/src/balanceWatcher.tsRepository: 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 10Repository: 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 -20Repository: 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/nullRepository: 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 fireRepository: 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:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/EventSource/error_event
- 2: https://developer.mozilla.org/en-US/docs/Web/API/EventSource
- 3: https://html.spec.whatwg.org/multipage/server-sent-events.html
- 4: https://html.spec.whatwg.org/C/server-sent-events.html
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.
| 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.
| 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]) |
There was a problem hiding this comment.
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:
- Triggering a full reconnect on update failure
- Retrying the update with backoff
- 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.
| 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.
| 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}`) | ||
| } |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -30Repository: cowprotocol/cowswap
Length of output: 1326
🏁 Script executed:
#!/bin/bash
# Read the fetchWithTimeout implementation
cat -n libs/common-utils/src/fetch/index.tsRepository: 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 2Repository: 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 -20Repository: cowprotocol/cowswap
Length of output: 165
🏁 Script executed:
#!/bin/bash
# Check Balance Watcher configuration
cat -n libs/common-const/src/balanceWatcher.tsRepository: 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.tsRepository: 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.
| export const isBalanceWatcherFailedAtom = atom(false) | ||
|
|
||
| export function useIsBalanceWatcherFailed(): boolean { | ||
| return useAtomValue(isBalanceWatcherFailedAtom) | ||
| } | ||
|
|
||
| export function useSetIsBalanceWatcherFailed(): (value: boolean) => void { | ||
| return useSetAtom(isBalanceWatcherFailedAtom) |
There was a problem hiding this comment.
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' | |||
There was a problem hiding this comment.
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.
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
aboutBackground
Optional: Give background information for changes you've made, that might be difficult to explain via comments
Summary by CodeRabbit
Release Notes