Skip to content

feat(stores-eth): hybrid ERC20 balance query (Alchemy + batch eth_call)#1930

Open
piatoss3612 wants to merge 16 commits intodevelopfrom
rowan/erc20-batch-balance-hybrid
Open

feat(stores-eth): hybrid ERC20 balance query (Alchemy + batch eth_call)#1930
piatoss3612 wants to merge 16 commits intodevelopfrom
rowan/erc20-batch-balance-hybrid

Conversation

@piatoss3612
Copy link
Copy Markdown
Member

@piatoss3612 piatoss3612 commented Apr 20, 2026

Summary

Alchemy alchemy_getTokenBalancesmaxCount: 100으로 호출되며 pageKey 페이지네이션을 하지 않아, 응답 첫 100개에 포함되지 못한 ERC20 토큰은 잔고가 조회되지 않는다. 이전에 하드코딩으로 우회했던 Base axlUSDC 케이스도 아마 같은 원인(응답 tail에 밀려 누락)으로 보인다. 이번 변경은 registry에 등록된 토큰 중 응답에 없는 것을 batch eth_call fallback으로 자동 처리한다.

  • Alchemy 응답에 포함된 토큰 → 기존대로 Alchemy 값 사용 (중복 eth_call 없음)
  • 응답에 없는 registry 토큰 → batch eth_call fallback
  • forceNativeERC20Query prop 및 axlUSDC 하드코딩 제거 — hybrid가 자동 대체

Details

  • ObservableQueryEthereumERC20BalancesBatchParent — refcount Map<contractLower, number> + ObservableJsonRpcBatchQuery[] (chunks of 10). 200ms debounced reaction rebuild. chunk 쿼리에서 에러 집계.
  • ERC20BalanceBatchParentStore(chainId, address) 단위 공유. Ethereum Registry + Thirdparty Registry가 같은 BatchParent를 주입받아 중복 쿼리 방지.
  • Thirdparty Child는 reaction으로 조건부 등록 — Alchemy 응답에 이미 포함된 토큰은 배치에 등록하지 않음. 응답에 없음 또는 Alchemy 에러 시에만 batch에 등록.
  • fallback 상태일 때 fetch/waitFreshResponse/error/isFetching가 BatchParent 상태를 반영하도록 수정 — imperative refresh / 에러 표면화가 Alchemy 경로뿐 아니라 fallback 경로에서도 동작.
  • Starknet staking의 ObservableJsonRpcBatchQuery + chunk + reaction rebuild 패턴 재사용.

Test plan

  • Base 체인에서 Alchemy 응답 상위 100개에 포함된 ERC20 잔고 정상 표시
  • Base axlUSDC 잔고 정상 표시 (hybrid 적용 전 하드코딩으로 처리되던 케이스 — 이번엔 batch fallback이 자동 처리)
  • Alchemy 응답 100개를 초과하는 등록 토큰 보유 지갑에서 101번째 이후로 밀린 registry 토큰도 잔고 표시 (batch fallback)
  • Alchemy 미지원 체인(예: Avalanche) ERC20 잔고 정상, network 탭에서 배치 호출 확인
  • 송금 후 balance refresh 정상 — fallback 전용 토큰도 stale 아님
  • Custom token 추가 시 ~200ms 내 반영
  • 네트워크/RPC 실패 시 에러 표시 또는 재시도, .ready(false) 영구 stuck 없음
  • 기존 스왑/send flow regression 없음

- Introduced `ObservableQueryEthereumERC20BalancesBatchParent` to handle batch requests for ERC20 token balances.
- Updated `ObservableQueryEthereumERC20BalanceImpl` to utilize the new batch querying mechanism.
- Refactored `ObservableQueryThirdpartyERC20BalancesImplParent` to integrate with the batch parent for improved balance fetching.
- Removed hardcoded balance handling for specific tokens in `RootStore`.
- Added `ERC20BalanceBatchParentStore` to manage instances of batch queries efficiently.
- Enhanced overall balance querying performance by reducing redundant calls.
- Remove orphaned forceNativeERC20Query callback in root.tsx
- Surface batch RPC errors via aggregated BatchParent.error
- Wait for pending debounced rebuild before resolving waitFreshResponse
- Forward batch parent lifecycle state when balance is served from fallback
- Refresh batch queries alongside Alchemy on imperative fetch when in fallback
@piatoss3612 piatoss3612 requested a review from a team as a code owner April 20, 2026 04:23
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 20, 2026

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

Project Deployment Actions Updated (UTC)
keplr-wallet-extension Ready Ready Preview, Comment Apr 20, 2026 8:04am

Request Review

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Downstream readiness gates in hooks-evm/tx/{amount,fee}.ts test truthiness
of bal.response to decide whether to render a loading-block. With the batch
refactor the Child used to return undefined unconditionally, stranding EVM
send/fee flows in loading on Alchemy-unsupported chains and on fallback
paths when Alchemy errored. Synthesize a truthy response from the batch
parent's per-contract balance so gates progress once data arrives.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

hooks-evm readiness gates early-return on `response` before touching
`balance`, so observation tracking keyed only on `balance` left native
ERC20 contracts unregistered with the batch parent. Extend the attach
to cover `response` as well, using a local refcount so duplicate
observation doesn't double-register or tear down prematurely.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

isInBatch only flips to true after Alchemy responds and the reaction
runs. The previous sequential branch took the non-batch path on the
first refresh and resolved before the fallback batch eth_call fired,
leaving callers with stale/ready(false) balances for missing tokens.
Always await Alchemy first, then await batch if the reaction flipped
the state in the meantime.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

Two related correctness fixes for hybrid ERC20 balance queries:

- Thirdparty Child's `response` getter was forwarding `parent.response`
  for every token once Alchemy answered, even for tokens not present in
  the Alchemy payload. hooks-evm readiness gates on `bal.response` then
  passed with `.ready(false)` balances, surfacing false insufficient-
  balance errors. Gate the response on batch data when the contract is
  handled via fallback. Also mark `isInBatch` observable so the computed
  re-evaluates when the reaction flips it.
- BatchParent.waitFreshResponse only waited for a debounced rebuild
  when batchQueries was empty. Adding a contract to a non-empty parent
  equally requires a rebuild before resolving. Track the last-built key
  and wait via `when` until it matches the current refcount keys.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

… error

- Per-contract batch error: replaced BatchParent.error with getError(contract)
  that returns only the error of the chunk owning the contract. A failing
  chunk no longer contaminates siblings in successful chunks, which previously
  surfaced spurious `bal.error` warnings across the ERC20 list.
- Alchemy-error fallback: the reaction that chose covered/missing held a
  stale `response` after an Alchemy failure (ObservableQuery retains the
  previous response on error, alchemyContractSet only updates on success),
  stranding tokens in the covered state. Prioritize `parent.error` → missing
  so affected tokens drop into batch fallback.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

…per-request batch errors

- Imperative ERC20 fetch() outside a reactive observer no longer falls
  through: the Child now force-adds/removes the contract around
  waitFreshResponse so post-send refresh paths actually issue an
  eth_call even when nothing is observing balance/response yet.
- ObservableJsonRpcBatchQuery exposes per-request errors via a new
  perRequestErrors map. Individual failures inside a batch response
  were previously dropped silently, leaving bad contract addresses
  stuck at ready(false) with no error signal. BatchParent.getError
  checks this map so per-contract error propagation works for both
  HTTP-level and per-request failures.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

- Extend observation tracking to the `error` getter so consumers that
  only read `bal.error` (e.g. validators that short-circuit on error)
  keep the batch fallback subscription alive. Both Child impls now
  register when any of balance/response/error is observed.
- Once the batch fallback has actual data for a contract, suppress
  the lingering Alchemy error. Previously a transient Alchemy failure
  plus a successful batch refresh still surfaced `parent.error` to
  callers gating on `bal.error`.
Convergent findings from 4 parallel review rounds:

- Thirdparty Child's imperative fetch() / waitFreshResponse / waitResponse
  now evaluate Alchemy coverage directly and force-register to the batch
  parent when the contract is missing, so non-reactive refresh paths
  (post-tx refresh, explicit fetch) actually exercise the fallback
  instead of resolving after Alchemy only.
- BatchParent.getError now reads chunk ownership from a snapshot taken
  at rebuild time instead of recomputing index from the live refcount
  ordering, eliminating misattribution during the debounce window.
- Child.error suppresses parent.error while batch fallback is still
  loading (no data, no batch error yet) so consumers stay in the
  loading state rather than warning on a transient Alchemy failure.
- ObservableJsonRpcBatchQuery synthesizes a per-request error for any
  requested id missing from the batch response, preventing permanent
  loading on partial/dropped server responses.
- Child.waitFreshResponse / waitResponse now return the effective
  response object, matching the existing query interface contract.
@piatoss3612
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50b5b6d491

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/stores-eth/src/queries/erc20-balances.ts
…sing

Mirror the observation-driven reaction: if parent.error is set, the
imperative awaitFallbackIfMissing() path must not trust a cached
response and should drop into batch fallback, so fetch()/wait*()
produce fresh data instead of silently returning the stale Alchemy
balance.
@piatoss3612
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 581a3e36ab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/hooks-internal/src/swap/amount.ts Outdated
The previous commit swept in an in-progress USDT approve(0) debug
patch (`0xdeadbeef` tx data) that is unrelated to this PR. Restore
the file to match develop.
@piatoss3612
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 53b9ef436e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/stores-eth/src/queries/erc20-balances.ts Outdated
…isInBatch

isInBatch is only flipped by the observation-driven reaction, so
imperative callers (fetch()/waitFreshResponse/waitResponse with no
reactive observers) completed the batch fallback but still read
parent.response / parent.error via the old gate, regressing to false
loading or stale Alchemy state for tokens missing from Alchemy.

Replace the isInBatch gate on response/error/isFetching with a shared
alchemyCovers() predicate that inspects current data state, matching
balance's existing behavior and mirroring the reaction's logic.
@piatoss3612
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe82d22dd4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/stores-eth/src/queries/erc20-balance-batch.ts Outdated
Previously the rebuild reaction tracked only the refcount set, so a
runtime endpoint change (Keplr custom RPC configuration) left
batchQueries bound to the stale URL until the contract set happened
to change again. Include the current RPC URL in the reaction key and
in waitFreshResponse's match predicate, and check refcount.size for
the empty-contracts branch instead of relying on the key string.
@piatoss3612
Copy link
Copy Markdown
Member Author

@codex review

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.

1 participant