feat: address-aware ABI decoding for clauses, events, and reverts#594
Merged
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>
Block explorer – Preview
Preview environment destroyed after PR was closed |
Agilulfo1820
temporarily deployed
to
preview-pr-594
May 30, 2026 06:31 — with
GitHub Actions
Inactive
Agilulfo1820
temporarily deployed
to
preview-pr-594
May 30, 2026 06:35 — with
GitHub Actions
Inactive
PR Docker Image PublishedRun locallydocker run --rm -p 3000:3000 ghcr.io/vechain/block-explorer:pr.594.31ceed1 |
Agilulfo1820
marked this pull request as draft
May 30, 2026 06:43
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>
Agilulfo1820
temporarily deployed
to
preview-pr-594
May 30, 2026 06:54 — with
GitHub Actions
Inactive
Agilulfo1820
temporarily deployed
to
preview-pr-594
May 30, 2026 06:57 — with
GitHub Actions
Inactive
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>
Agilulfo1820
temporarily deployed
to
preview-pr-594
May 30, 2026 07:05 — with
GitHub Actions
Inactive
Agilulfo1820
temporarily deployed
to
preview-pr-594
May 30, 2026 07:08 — with
GitHub Actions
Inactive
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>
Agilulfo1820
temporarily deployed
to
preview-pr-594
May 30, 2026 07:25 — with
GitHub Actions
Inactive
Agilulfo1820
temporarily deployed
to
preview-pr-594
May 30, 2026 07:28 — with
GitHub Actions
Inactive
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>
Agilulfo1820
temporarily deployed
to
preview-pr-594
May 30, 2026 07:32 — with
GitHub Actions
Inactive
Agilulfo1820
temporarily deployed
to
preview-pr-594
May 30, 2026 07:36 — with
GitHub Actions
Inactive
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>
Agilulfo1820
temporarily deployed
to
preview-pr-594
June 5, 2026 15:55 — with
GitHub Actions
Inactive
Agilulfo1820
temporarily deployed
to
preview-pr-594
June 5, 2026 15:58 — with
GitHub Actions
Inactive
Agilulfo1820
marked this pull request as ready for review
June 8, 2026 13:08
Agilulfo1820
temporarily deployed
to
preview-pr-594
June 8, 2026 13:12 — with
GitHub Actions
Inactive
Agilulfo1820
temporarily deployed
to
preview-pr-594
June 8, 2026 13:15 — with
GitHub Actions
Inactive
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>
Agilulfo1820
temporarily deployed
to
preview-pr-594
June 8, 2026 13:58 — with
GitHub Actions
Inactive
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>
Agilulfo1820
temporarily deployed
to
preview-pr-594
June 8, 2026 14:05 — with
GitHub Actions
Inactive
Agilulfo1820
temporarily deployed
to
preview-pr-594
June 8, 2026 14:07 — with
GitHub Actions
Inactive
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>
Agilulfo1820
temporarily deployed
to
preview-pr-594
June 8, 2026 14:24 — with
GitHub Actions
Inactive
Agilulfo1820
temporarily deployed
to
preview-pr-594
June 8, 2026 14:27 — with
GitHub Actions
Inactive
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>
Agilulfo1820
temporarily deployed
to
preview-pr-594
June 8, 2026 14:31 — with
GitHub Actions
Inactive
Agilulfo1820
temporarily deployed
to
preview-pr-594
June 8, 2026 14:33 — with
GitHub Actions
Inactive
5 tasks
* perf: consolidate signature decoding into a single cached service
Replaces the three per-source proxy routes (/api/sourcify, /api/b32,
/api/openchain) with a service-layer abstraction that caches at the
question level rather than the datasource level.
The previous shape leaked implementation through the API: callers had
to know about b32 vs OpenChain and orchestrate "try one, fall back to
the other" in two separate hooks. Caching was per-route, so concurrent
in-flight requests for the same selector still hit upstream N times
and a 429 / 5xx burst could starve our App Runner egress IP.
Two services, one cache pattern each:
- /api/sourcify keeps its name (single source, no abstraction needed)
but now sits behind fetchSourcify in lib/sourcify-cache.ts.
- /api/decode/selector is new and replaces /api/b32 + /api/openchain.
decodeSelector in lib/selector-decoder/ fans out to b32 and OpenChain
in parallel, returning a discriminated result so the client knows
whether it got a baked ABI fragment (b32) or a bare signature it has
to interpret (OpenChain — needed because only the caller knows how
many topics are indexed for events).
Each service owns two storage tiers:
- async-cache-dedupe holds successes (1d TTL, 7d SWR). Provides single-
flight: N concurrent callers for the same key share one upstream
promise, so cold-start stampedes collapse to one upstream call.
Rejections are not stored, so 5xx and 429 never poison the cache —
but in-flight dedup still applies during the rejection window.
- lru-cache holds 404 markers (5m TTL). Lets freshly-verified
contracts and newly-curated selectors surface within minutes,
independent of the long success TTL.
OpenChain batching is preserved via a module-scoped DataLoader per
kind. Concurrent in-flight selector requests across the whole pod
coalesce into one upstream call per ~5ms tick, so the upstream cost
stays bounded even though the client now hits the route one selector
at a time.
Client-side simplifications:
- services/b32.ts and services/openchain.ts collapse into one
services/selector-decoder.ts with a single useDecodedSelector hook.
- useDecodeInputData and useDecodeEvent lose the explicit b32 →
OpenChain fallback ladder (the service handles it server-side) and
drop one "wait for b32 to settle before trying OpenChain" gate.
- The revert decoder in services/thor/transaction.ts uses the same
unified service instead of going directly to OpenChain.
Known limitation unchanged from the previous caching attempt: the
in-memory caches are per-pod and lost on restart. Persistent cache
via async-cache-dedupe's Redis storage backend is straightforward to
add when rate-of-change justifies it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: migrate Sourcify proxy from v1 to v2
Sourcify is running scheduled brownouts of API v1 — multi-hour windows
where every /files/any/{chainId}/{address} request returns 503 with a
"please migrate to v2" body. They do this to force consumers off the
deprecated API ahead of permanent removal. Today's window
(10:00–18:00 UTC) was hitting every /api/sourcify call on local dev
with 502s upstream.
Two changes in lib/sourcify-cache.ts:
- URL: /files/any/{chainId}/{address}
→ /v2/contract/{chainId}/{address}?fields=abi,compilation
- Response shape: v1 returned a `files[]` array we had to walk looking
for `metadata.json`, then parse its content as JSON to extract
`output.abi` and `settings.compilationTarget`. v2 returns `abi` as a
top-level field and `compilation.name` for the contract name, so the
extractAbi helper goes away entirely.
404 handling is unchanged — v2 still uses HTTP 404 for unverified
contracts. The cache, dedup, and TTL machinery is untouched; only the
upstream call shape changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: normalize selector hash to lowercase before upstream lookup
decodeSelector lowercased the cache key but forwarded the original-case
hash to fetchB32 / fetchOpenchain. b32's URL path is case-sensitive and
OpenChain echoes lowercase keys in its response bucket, so an uppercase
request would miss upstream and poison the lowercased cache key with a
spurious not-found.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
daithihearn
temporarily deployed
to
preview-pr-594
June 9, 2026 15:02 — with
GitHub Actions
Inactive
daithihearn
approved these changes
Jun 9, 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
Ports the decoding pipeline from inspector-app#204 into the explorer. Today's
useDecodeInputData/useDecodeEventonly ask b32 for a 4-byte/topic0 match — when b32 doesn't have the signature (most non-trivial VeBetterDAO clauses, every Uniswap/Aave fork, every custom error, every Panic) the user sees raw hex. This PR adds three more sources behind the existing ones and threads the clause / emitter address through the decoder so we can use the most specific ABI available.Decoding pipeline (high → low confidence)
Authority,VTHO,Executor,Extension,Params,Prototype,Staker) plus a curated address → name map for ~30 VeBetterDAO / Stargate / Smart Account / oracle / legacy-nodes contracts on mainnet + testnet. Synchronous, no network calls./api/sourcifyproxiessourcify.dev/server. The client-side resolver reads the EIP-1967 implementation slot viaThorClient.accounts.getStorageAtand prefers the impl ABI when present, falling back to the address itself. This is what handles all OZ TransparentUpgradeable / UUPS proxies (i.e. essentially every VeBetterDAO deployment)./api/b32hook; now skipped when an upstream source already decoded./api/openchainproxiesapi.openchain.xyz. Synthesises ABI items from canonical signatures; for events it tries indexed-first then indexed-last layouts so OZ-convention and reversed-indexed contracts both decode.Caches are session-scoped via TanStack Query with
staleTime: Infinity— no IndexedDB layer.Other UX changes
clause.toinTransactionClausesand next to the emitter address inEventList. UsesuseContractName(address)which prefers built-in/curated name, then Sourcify'smetadata.contractName.InputDataandEventList. The "No ABI found" placeholder in the decoded pane stands in when nothing resolves, so the UI feels consistently verified.lib/contract-sniff.ts).Revert decoding
getTransactionFailureInsightno longer relies solely on the SDK'sdecodeRevertReason(which only handlesError(string)). New path:0x08c379a0→ string decode (as before).0x4e487b71→Panic(uint256)with human-readable code descriptions (e.g.Panic(0x11): arithmetic overflow or underflow).The result is typed with a
revertKinddiscriminator so the UI can format payloads richly in follow-up work.