refactor(frontend): prepare the ERC transaction path for a backend cache - #13729
Draft
sbpublic wants to merge 12 commits into
Draft
refactor(frontend): prepare the ERC transaction path for a backend cache#13729sbpublic wants to merge 12 commits into
sbpublic wants to merge 12 commits into
Conversation
The initial load reads the newest page of stored transactions and is handed a cursor to the page below it, then drops it. Keep it, so paging back can continue through the backend instead of asking Etherscan for history the backend already holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ETH and EVM token views showed the newest page of stored transactions and nothing before it: the service that pages further back existed but was never wired to anything, so older history was unreachable. Wrap the date groups in an infinite scroll, as the Solana and ICP views already do. Native entries only - older ERC transfers come from a different Etherscan endpoint, which this path does not query. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nded ids `check:tests` type-checks the spec files that `check` skips: the store key is a branded `TokenId`, and the chain id belongs to the network env rather than the token's `Network` type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Grounded against the current code: the cache reaches only the native and Solana paths, the backend already carries an `Erc20` token-id variant, and the ERC Etherscan actions hardcode their block range. Records the constraint that shapes the change: caching ERC20 history without an ERC20 paging path would reproduce the ten-row cap #13728 removes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ERC721/ERC1155 stay out, `WALLET_PAGINATION` stays at 10, and the cache stores raw ERC4626 rows. Storing raw makes the vault mint/burn normalisation cross-cutting: with a cache and a paging path, vault rows enter the store at three places and only one applies it today, so the mapping has to be hoisted into a shared helper. The transform is idempotent, so applying it more than once is harmless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read out of the backend rather than assumed: - storage is capped per (principal, token) at 10 000, trimming the oldest - a save above MAX_SAVE_USER_TRANSACTIONS_BATCH (500) is rejected outright, so an ERC20 token's first save has to be chunked - duplicates are silently skipped; DuplicateTransaction is reserved and never constructed, so rows may be re-offered freely - the Erc20 token key is a bare String with no normalisation, so the contract address has to be lowercased consistently or one token gets two keys Etherscan's own docs state no response ceiling for tokentx, so the spec now passes offset/page explicitly instead of relying on an undocumented default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`tokentx`, `tokennfttx` and `token1155tx` each hardcoded the whole history newest-first. Paging back through a token's transfers needs to ask for a window that ends where the loaded list does. Defaults reproduce the current query exactly, so no caller moves. The inventory action keeps its parameters untouched: it lists owned token ids, not a block range. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The zero-address-to-vault rewrite lived inside the load service, at the one place vault transfers entered the store. Once stored history and a paging path also feed that store, the same rewrite has to apply there, so it moves into `erc4626.utils` as a helper. Behaviour is unchanged; the helper is idempotent, so applying it to rows that have already been through it is a no-op. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Enabling refactors first, then the cache itself, split where behaviour starts changing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from
fix/frontend/eth-transactions-pagination
to
main
August 14, 2026 10:05
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.
Motivation
ERC20 token views re-fetch a token's entire transfer history from Etherscan on every load:
loadErcTransactionsnever touches the backend cache, which reaches only the native EVM and Solana paths. That costs atokentxrequest per token per load against a 5-call/second budget, plus the spam filter's per-hash RPC calls, and it caps reachable history at whatever onetokentxresponse returns — beyond that, older transfers are unreachable forever, because nothing stores them.This PR carries the spec plus the enabling refactors. No behaviour changes — the cache itself lands in a follow-up on top of this one, per the two-PR split recorded in the spec.
Stacked on #13728, which fixes the mirror-image problem on the native path.
Changes
Adds
docs/ai/spec-driven-development/specs/2026-08-14-impr-cache-erc20-transaction-history.md, grounded against the current code and the backend's actual contract rather than assumptions.The constraint the spec is built around: the backend cache is precisely what creates the ten-row window #13728 removes, so ERC20 caching and an ERC20 paging path have to ship together, or every ERC20 view regresses.
Confirmed by reading
src/backend/src/transactions/model.rsandsrc/shared/src/types/:backend.didchange is needed —TokenIdalready carriesErc20(ErcTokenId, ChainId), and theUserTransactionmappers already round-trip everything they'd need (includingnft_token_id). Entirely frontend work.MAX_SAVE_USER_TRANSACTIONS_BATCHis 500 and an oversized batch is rejected whole, so an ERC20 token's first save — its complete history — must be chunked. The native path never hit this because it only ever saves an incremental slice.ErcTokenIdis a bareStringwith no normalisation, compared byte-for-byte in the stable key. Our env tokens are checksummed and Etherscan returns lowercase, so the contract address has to be lowercased at both save and load or one token silently gets two histories.(principal, token)at 10 000, trimming the oldest at a whole-block boundary. Tokens cannot starve each other.DuplicateTransactionis reserved and constructed nowhere, so rows can be re-offered freely.Decisions recorded in the spec: ERC721/ERC1155 stay out,
WALLET_PAGINATIONstays at 10, and the cache stores raw ERC4626 rows — which makes the vault mint/burn normalisation cross-cutting, since with a cache and paging those rows enter the store at three places and only one normalises them today.One open question remains: Etherscan's v2 docs state no response ceiling for
tokentx(page/offsetappear only as examples), so the spec passes them explicitly rather than relying on an undocumented default.Enabling refactors
Both are provably behaviour-neutral, which is why they are split out from the cache.
tokentx,tokennfttxandtoken1155txeach hardcodedstartblock: 0withsort: 'desc'; they now accept thestartBlock/endBlock/sortparameters the nativegetHistoryalready had, defaulting to exactly today's query. Paging back needs a window that ends where the loaded list does.erc721TokenInventoryis deliberately left alone — it lists owned token ids, not a block range, and has no production callers.erc4626.utils. The zero-address-to-vault rewrite sat inside the load service, at the one place vault transfers entered the store. Once stored history and a paging path feed the same store, the rewrite has to apply there too, so it becomes a reusable helper. Idempotent, so rows that have already been through it are unaffected.Tests
etherscan.providers.spec.ts— the default query is unchanged; a window is passed through asstartblock/endblock/sort;endblockis omitted when not given.erc4626.utils.spec.ts— mints read as coming from the vault, burns as going to it, the zero address matches in any case, a plain transfer is untouched, and the helper is idempotent.npm run format,npm run lint -- --max-warnings 0,npm run check(0 errors),npm run check:tests(0 errors); 676 tests pass across the ETH services, providers and utils specs.🤖 Generated with Claude Code — Claude Opus 5 (claude-opus-5)