perf: cache the Sourcify / OpenChain / b32 proxy responses#598
Closed
Agilulfo1820 wants to merge 26 commits into
Closed
perf: cache the Sourcify / OpenChain / b32 proxy responses#598Agilulfo1820 wants to merge 26 commits into
Agilulfo1820 wants to merge 26 commits into
Conversation
Adds a four-source decoding pipeline (known contracts → Sourcify with EIP-1967 proxy follow → b32 → OpenChain) so input data, event logs, and custom errors decode against the most specific ABI available rather than giving up after b32 misses. Inline contract names render next to clause targets and event emitters, and contract pages get an ERC-20 / ERC-721 badge based on bytecode sniffing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues from the PR review: - The revert reason alert overflowed horizontally when the decoder fell through to raw hex. Add `wordBreak: 'break-all'` and `minW: 0` on the content so long un-spaced hex wraps inside the alert box. - Most VeBetterDAO / Stargate impls aren't verified on Sourcify (only the proxies are), so the Sourcify-with-EIP-1967-follow path returned the proxy's ABI and missed every error / function defined on the impl. Bundle the full set of b32-catalogued VeBetterDAO + Stargate ABIs so curated addresses resolve to the impl ABI synchronously, no Sourcify round-trip. Mirrors the inspector-app strategy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With React Query 5's skipToken, a skipped query stays in status='pending' forever (fetchStatus='idle'). The hook was aggregating isPending from all three sources (resolved-ABI, b32, OpenChain) — so as soon as the first source resolved successfully and we skipped the downstream ones via skipToken, the hook reported isPending: true permanently and the InputData / EventList components rendered the loading Skeleton instead of the decoded payload. Switch the aggregate to isFetching, which only reflects in-flight requests. Skipped branches no longer keep the hook pending, so successful upstream decodes actually render. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups from PR review: - Custom-error reverts often surface from a contract called internally, not the clause's target. Inspector handles this by sweeping every imported ABI; do the same here by iterating all bundled ABIs after the target's resolved ABI misses. Adds getAllBundledAbis() exporter. - The parameter zod schema required internalType, which is fine for b32 ABIs (always present) but fails for OpenChain-synthesised items that only carry name+type. zodParse swallowed the error and returned raw data anyway, but the console noise + schema mismatch was hiding real issues. Mark internalType optional. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cross-referenced against the VeBetterDAO frontend config. Gaps: - Node Management (mainnet + testnet) — bundled new ABI from b32 - B3TR MultiSig (mainnet only) — bundled multi-sig-wallet ABI from b32 - VeBetter Passport, DBA Pool, Relayers Rewards Pool, X2Earn Creator NFT, Grants Manager — addresses on testnet were missing, so curated name lookup never hit. ABIs were already bundled, just needed wiring. veDelegate, veDelegateAutoDeposit, VetDomains, and the per-library governor/passport addresses are not on b32 and not bundled. The library contracts get inlined into the governor/passport ABIs at compile time anyway, so they don't need separate entries for decoding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recasts the transaction detail view around what the tx actually did and
gates the low-level plumbing behind an Expert toggle. Based on the
ui_kits/block-explorer design.
- Lean default 3-up Overview (Value transferred / Fee Paid / Status with
confirmations badge) replaces the wall of technical details as the
primary readout. Fee is the single accented figure.
- New Expert mode switch in the page header. When off:
- Technical-details grid (Type / Confirmations / Size / Expiration /
Nonce) and the Fees, Gas and VTHO breakdown are hidden.
- Each clause's Raw/Decoded input-data toggle is hidden; the decoded
pane (with its "No ABI found" placeholder) is the only view.
- Per-event Raw/Decoded toggle is hidden; events render decoded only.
- Activity card header now reads "Activity" with pill tabs
"Clauses (N)" / "Events (M)" so users can see the counts up front.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three things were off vs the kit's clause spec: - The Call / Transfer type badge in the clause header was always shown; in non-expert it's just noise. Gate it on expert. - The function signature line (e.g. castVoteOnBehalfOf(address,uint256)) rendered above the param table in every view; same rationale — hide it in non-expert. - The clause-header address was only truncated on mobile. The design uses the short 0xABCD…WXYZ form on desktop too. Always truncate. Also tightened the decoded param-table column widths so they match the design's proportions (narrow #, fixed-width name/type, data takes the rest) instead of evenly distributing four 1fr columns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously gated on expert mode after the earlier hide-by-default pass. The method name is the single most useful identifier of what a clause does — keep it visible regardless of expert state. The Call/Transfer type badge stays expert-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applies to both clause Input data and event params. The type is the single most technical column and adds noise for the lean default view; it pops back in when expert is on (alongside the indexed badge for events). Grid template collapses to # / Name / Data so the data column gets the reclaimed width. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CopyableAddressLink sits inside the Accordion.ItemTrigger, so opening the contract page or copying the address was also toggling the panel. Wrap just the link/copy controls with stopPropagation so the rest of the header still toggles the accordion as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wrapping border + alternating-row borders on the decoded param table made the inner block feel boxed-in inside the already-bordered accordion item. Strip the outer border, drop the header background, and use a hairline rgba(255,255,255,.08) divider between rows — the same whisper the inspector design uses. Also bump the typography one step (bodyS -> bodyM for index/name, bodyXs -> bodyS for type and value), widen the # / Name / Type columns slightly so labels breathe, and switch the Data column from text-secondary to text-primary so the actual value reads as the important content. Pad row gaps from py=3 to py=4 for more breathing room. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Loading event rows used to collapse to a flat 120px gray block, which made the activity card jump around as decodes streamed in. Replace it with an EventCardSkeleton that keeps the real card chrome (border, bg, internal gaps), two stacked lines for the name + emitter, and two faux param rows on the same 52px / 180px / data grid as the live table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useContractName now consults vechain/token-registry between the bundled-known step and Sourcify, prefering the registry's symbol (B3TR, USDT, ...) over the full name. This picks up the long tail of ERC-20s and ERC-721s that aren't in our curated VeBetterDAO/Stargate set, so clause targets and event emitters render with a recognisable short name when the address matches a registered token. Priority is now: bundled known → token registry → Sourcify metadata.contractName. The Sourcify call only fires when the first two miss, which means most tx pages skip the network round-trip entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Crawled the top-500 toplists on vechainstats.com/contracts/ across the clauses-received, vtho-burned, co2e and new tabs (so 2000 entries with overlap), kept the rows that carry a curated display name, deduped against our existing built-in + curated registry, and stripped trailing "(...)" suffixes to keep the names compact. Result: 436 new mainnet addresses with human names — DEXes (Vexchange, VeRocket), X2Earn apps (Mugshot, Greencart, Cleanify, Toolchain *), oracles, certificate-issuance contracts, etc. Wired into getKnownContract as a third lookup tier (after BUILTIN and CURATED_ADDRESS_TO_NAME): mainnet-only, name without ABI. Sourcify still owns ABI resolution downstream, so the new entries cost nothing beyond the JSON they load from. Testnet is unaffected (vechainstats doesn't ship a testnet equivalent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rawl The vechainstats crawl pulled in 130 entries under the *.vedelegate.vet sub-domain (mostly per-delegator auto-generated wallets like "12553503427381...vedelegate.vet" plus the three top-level veDelegate / veDelegate Trigger / veDelegate Auto Deposit entries). Those clutter clause headers with names that don't help interpretation, so strip the lot from the bundle. Drops the registry from 436 to 306. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the source-of-data reference from the filename. The file is just an address → display-name map; provenance lives in commit history. Variable + import renamed to match (ADDRESS_NAMES / addressNamesRaw). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Method name now inlines as ContractName.method right after the contract label in the clause header, so the call's intent is read in one go without scanning past the address. - Drop the success/pending TxStatusBadge from the page header — it added noise on success and the Overview's confirmations badge is the meaningful indicator. Reverts still surface via the revert alert. TxStatusBadge is now unused so its definition is removed (TxStatusIcon stays, the recent-tx tables use it). - Switch the decoded param table to mono fontSize='xs' across index / name / type / value, matching the inspector spec. - Lose the outer border on the 'No ABI found' placeholder so it doesn't double-box inside the already-bordered clause/event card. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
env.api.ts threw on import when B32_URL wasn't set in process.env, and since every API route pulls something out of this module, the throw took down /api/b32, /api/sourcify and /api/openchain with a 500. That's why decoding looked dead on the preview env and on production for any clause that needed b32 or OpenChain (e.g. swapTokensForExactETH on non-known router contracts) — the routes never even ran their handlers. The Dockerfile already has ARG B32_URL=https://b32.vecha.in and ENV B32_URL=${B32_URL}, but App Runner's runtime env substitution doesn't preserve image ENV. Apply the same default at the env module so the client survives a missing runtime variable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CopyableAddressLink renders a <button> for its copy icon, and the whole
clause header sat inside Accordion.ItemTrigger which is also a <button>.
React refused to hydrate that ("button cannot be a descendant of
button") and the row would re-render incorrectly.
Restructure the header as a Flex row with two Accordion.ItemTrigger
zones — one wrapping the index + type badge, the other wrapping the
VET balance + chevron — and the ClauseTarget (with its address +
copy button) sitting in the middle as a plain inline element. Clicking
the address opens the contract page or copies; clicking either trigger
zone toggles the panel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chakra's Accordion.ItemTrigger applies width: 100% via its recipe, which made the second trigger expand to fill the row and push everything past the first trigger off-screen — including the address link and VET value. Switch both triggers to render via Accordion.ItemTrigger asChild around a chakra.button element with explicit width="auto" so they shrink to their content. Also inline the resolved ContractName.method label inside the first trigger (text-only, no button-in-button risk) and move the CopyableAddressLink between the two triggers as a sibling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues raised on a reverted testnet B3TR Emissions.distribute() tx: 1. **No revert reason shown.** The fallback was a post-block simulation at the tx's own blockID, but reverts in time-gated / randomness / internal-out-of-gas paths can't be reproduced cleanly that way — the simulation succeeds even though the on-chain tx reverted. Inspector PR #204 sidestepped this by replaying the actual on-chain execution via /debug/tracers and walking the resulting call tree. Mirror that here: - Call `thorClient.debug.traceTransactionClause` on each clause with the 'call' tracer. - Walk to the deepest frame that carries an `error` string. - If the frame has an `output`, decode it as revert data (Error / Panic / custom error / OpenChain). - Otherwise surface the frame's `error` (`out of gas`, `invalid opcode`, …) as the vm-error revert reason. - Simulation stays as the secondary path for nodes without /debug/tracers enabled or when the tracer can't reach the clause. - As a final guard, return a generic "execution reverted" so the alert never silently disappears for a reverted tx. 2. **"Input data" header with empty body** on zero-arg functions like `distribute()` / `claim()`. The decoder succeeded but ParamRows returned null, leaving just the label. Emit an explicit "No parameters." placeholder so the section reads cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every /api/sourcify, /api/openchain, /api/b32 hit was passing straight
through to the upstream — TanStack Query's staleTime: Infinity only
helps within one browser session, and the routes set no cache headers
or `next: { revalidate }`, so distinct (chainId, address) /
(kind, hash) / (signature) tuples paid the full upstream cost every
time. Sourcify's free tier is ~14 req/s per IP and our App Runner
egress IP is what gets throttled — one busy preview env can starve
prod.
Two changes per route:
1. Wrap the upstream fetch in `next: { revalidate, tags }` so repeat
calls inside the TTL are served from Next's data cache
(.next/cache/fetch-cache, in-memory under output: 'standalone').
Tagged so a future maintenance route can revalidateTag() without
redeploying.
2. Set Cache-Control on the JSON response so browsers and any future
CDN cache too. TTLs match each source's actual rate of change:
- Sourcify: 7d / 30d SWR on 200, 30m / 1d SWR on 404. ABIs are
tied to one-shot verifications and effectively immutable per
address; the 30m 404 window lets a freshly verified contract
surface inside an hour.
- OpenChain: 7d / 30d SWR when every requested hash resolved,
30m / 1d SWR when any hash came back null. Hash → canonical
signature is a cryptographic fact, but the DB grows so misses
deserve a short window.
- b32: 1d / 7d SWR on 200, 30m / 1d SWR on 404. b32 is a
community-curated GitHub repo where entries are added and
occasionally corrected; a 1-day window keeps corrections from
getting stuck for a week.
5xx / timeout / network error responses carry no-store so an
upstream blip doesn't poison the cache.
/api/b32 also switches from the apiClient.get wrapper (which doesn't
forward `next` options) to a direct fetch, matching the pattern in
the other two routes.
Known limitation: Next's data cache lives in the container fs and is
wiped on every deploy and instance restart. Each pod pays the cold
upstream cost for its first hit per (chainId, address). Persistent
cache via a custom cacheHandler (Redis / S3) is left as future work —
rate-of-change is low enough that the per-pod ephemeral cache should
pay for itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sourcify and OpenChain success cache drops from 7d to 1d so they match — easier to reason about than three different windows. b32 shortens further to 1h since it's the most actively curated upstream and corrections should propagate quickly. 404 windows (30m) are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-entry tags on the upstream fetch let us call `revalidateTag` after seeing a 429, so Next's data cache doesn't sit on the rate-limit response for the success TTL. 404 response Cache-Control drops from 30min to 5min so freshly-verified contracts / newly-indexed signatures surface fast. 200 TTLs unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Member
|
Closing this as it was implemented in a different PR |
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.
Why
The decoding pipeline introduced in #594 has every
/api/sourcify,/api/openchain, and/api/b32call pass straight through to the upstream. TanStack Query'sstaleTime: Infinityon the client only helps within one browser session; the routes set no cache headers and don't wrap their upstreamfetch()innext: { revalidate }. With ~14 req/s headroom on Sourcify's free tier, our single App Runner egress IP is one busy preview env away from being throttled in production.This PR adds a thin caching layer to the three proxy routes. Stacked on top of #594 — merge target is
feat/decoding-pipeline, notmain.What
For each route:
fetchinnext: { revalidate, tags }so repeats inside the TTL are served from Next's data cache (.next/cache/fetch-cacheunderoutput: 'standalone'). No external CDN required. Tagged so a future maintenance route canrevalidateTag()without redeploying.Cache-Controlon the JSON response so browsers and any future CDN cache too.TTLs differ by upstream stability:
next: { revalidate }(200)Cache-ControlCache-Control/api/sourcifys-maxage=604800, stale-while-revalidate=2592000, max-age=86400s-maxage=1800, stale-while-revalidate=86400, max-age=300/api/openchain/api/b32s-maxage=86400, stale-while-revalidate=604800, max-age=3600Rationale for the split:
hash → canonical signatureis a cryptographic fact; the DB only grows. 200s can be cached for a week. The route now switches between "all hit" and "partial" Cache-Control depending on whether every requested hash resolved, so a newly indexed selector / topic surfaces inside 30 min instead of being stuck as a "no match" for a week.5xx / timeout / network errors return
Cache-Control: no-storeso an upstream blip can't poison the cache./api/b32also switches fromapiClient.get(which doesn't forwardnextoptions) to a directfetch, matching the pattern in the other two routes.Known limitation — deploy / restart
Next's data cache lives in the container filesystem under
.next/cache/fetch-cacheand is wiped on every deploy and instance restart / scale-up. With 1–10 App Runner pods (perprod.yaml), the worst-case cold burst ispods × unique-contracts hit in the warming minute. Sourcify's ~14 req/s should absorb this; the durable fix is a persistent customcacheHandler(Redis / Memcached / S3) — flagged as future work.Test plan
pnpm ts-check && pnpm lint && pnpm knip && pnpm test(223 pass)pnpm dev): all three routes return the expectedCache-Controlheader (verified viacurl -sIagainst0x5ef79995…,0x095ea7b3,0x4a25d94a)./api/sourcify?chainId=100009&address=0x5ef79995…twice via curl, confirm the second response is markedly faster and carries the 7d header./api/sourcifyfrom disk cache / 304, no upstream Sourcify fire visible in dev-server log.s-maxage.🤖 Generated with Claude Code