fix(poll): stop pinning toBlock, scan long ranges in windows - #13
Merged
Conversation
getBlockNumber is served from viem's per-client cache, so the value pinned into getLogs could name a block past the head of a lagging node behind a load balancer, which rejects the range with "block range extends beyond current head block". Omit toBlock on the ERC20 branch so the node resolves its own head. The native branch still needs concrete block numbers, so read the head uncached and advance a per-row cursor instead of re-walking the full range every tick. Absorb transient RPC failures and retry on the next poll: the signal is already submitted, so a single bad response should not fail the transfer.
Providers cap eth_getLogs at ~10k blocks, so a transfer resumed long after its deploy sent a span the provider rejects outright. Walk the range in windows instead, advancing a per-row cursor so scanned windows are not refetched each tick. Only a bounded window pins toBlock; the window reaching the head stays open so the node resolves its own tip. Cap how many blocks the native branch walks per tick, since each costs a getBlock and a far-behind resume would otherwise block the tick on one long sweep. Stop retrying errors the provider will reject identically every time: a range rejection now surfaces at once instead of being absorbed until the poll deadline turns it into a bare timeout.
g4titanx
approved these changes
Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
getBlockNumber()is served from viem's per-client cache, so the value pinned intogetLogs'stoBlockcould name a block past the head of a lagging node behind a load balancer, which rejects the range withblock range extends beyond current head block.eth_getLogsmay span (~10k). A transfer resumed long after its deploy sent a span wider than the cap and was rejected outright.toBlock; the window reaching the head stays open so the node resolves its own tip.Details
fromBlockwas never the problem: it comes from a real deployment receipt. The stale value wastoBlock.Two distinct failures are fixed here. The first is the reported error, caused by a cached head. The second only shows up on resume:
isResumablein the frontend is a field-presence check with no age bound, so a record from any point in the past is resumable and itsfromBlockcan be arbitrarily far behind. That path sent one unboundedgetLogsand hit the provider's range cap.The native path had the stale-head bug in a worse form:
getBlock({ blockNumber })against a lagging node throws "block not found" rather than a range error. It needs concrete block numbers so it cannot drop the bound, hence the uncached head plus cursor.Retry classification matters for the second failure specifically. A range rejection repeats identically, so absorbing it meant retrying every 2s for the full 180s timeout and reporting a generic timeout rather than the real cause. A test confirms this: against the prior code that case takes 5005ms, and now fails in 1ms with the provider's message intact.
maxBlockRangeis exposed onTransferParams(default 10k) for providers with a lower cap.Verification: full suite 119/119,
tsc --noEmitclean,tsupbuild clean.Note: the frontend needs an SDK version bump to consume this. Not included here since it depends on the publish flow.