Running log of improvements to gulltoppr (the heimdall-rs ABI decompilation
backend behind abi.ninja). Part of the larger effort to
make abi.ninja usable by AI agents — see IDEATION.md in the abi-agent
workspace for the overall plan and how gulltoppr fits in.
-
HEIMDALL_VERSIONin/health. The Dockerfile sets it as a runtime env from the pinned build arg;/healthnow reportsheimdall_version(e.g.0.9.3). This is the source of truth because heimdall's own--versionstring lags its release tags (the 0.9.3 release binary self-reports0.9.2). Defaults to"unknown"when the env isn't set (local dev). -
GET /v1/decode/{tx_hash}?rpc_url=host— decode a transaction's calldata into{ name, signature, inputs, decoded_inputs }("what did this tx do"), viaheimdall decode. Wrapped with provenance (source: "heimdall-decoded",cached,elapsed_ms). Cached by tx hash + rpc (calldata is immutable), and it rides the same coalescing path. -
Refactored
heimdall.rs:decompile_abianddecode_txnow share a privaterun_to_json(subcommand, target, …, output_file);read_output/find_nestedare filename-generic. -
Extracted
cached_or_run()inmain.rs— the coalescing/cache logic is now shared byresolve_abiandresolve_decode. Cache keys are namespaced (abi:/decode:) so the two operations never collide. -
Added
normalize_tx_hashvalidation (0x + 64 hex) and anInvalidTxHasherror (400 invalid_tx_hash). -
Tests: +7 (tx-hash validation, namespace collision, decode success/shape, decode caching, invalid hash). 32 total. Verified live against a real mainnet tx (decoded a
transfer(address,uint256)).
- Request coalescing. Switched the cache from naive
get-then-insertto moka's entry API (or_try_insert_with). N concurrent requests for the same uncached(address, rpc)now trigger a single decompile; the rest await that result. Failures are not cached. TheX-CacheHIT/MISS flag stays accurate viaEntry::is_fresh(). - HTTP integration tests. Added a
http_testsmodule (9 tests) that drives the real handlers + cache + subprocess wiring throughactix_web::test, using a stubheimdallshell script (no network / no real heimdall). Covers: greet, health, invalid address (400), missing rpc_url (400), decompile success + cache HIT,/v1shape, decompilation failure (502), no-bytecode (422), missing binary (503), and a concurrency test proving 5 simultaneous misses → 1 decompile. - CI. Added
.github/workflows/ci.yml:cargo fmt --check,cargo clippy -D warnings,cargo test, and a Docker image build (validates the Dockerfile- pinned heimdall download). Tests need no heimdall binary.
- Fixed dead deploy trigger.
fly-deploy.ymlwatchedmain, but the repo's default branch ismaster, so auto-deploy never fired. Pointed it atmaster. - Fly health check. Added an
[[http_service.checks]]probe on/health. - Extracted
configure()(shared route wiring) andbuild_state()somainand the tests build the exact same app.
Verified: 25 tests pass, clippy clean with -D warnings.
- Correctness: async decompile via
tokio::process(no longer blocks Actix worker threads); configurable hard timeout; per-request temp dir output (auto-cleaned, no moreoutput/disk leak); address + rpc_url validation; configurableHEIMDALL_BIN(runs locally, not just at the hardcoded path). - Caching: in-memory moka cache keyed by
(address, rpc_url), 24h TTL. - API: structured JSON errors with stable codes + correct statuses
(400/422/502/503/504); provenance headers (
X-Source/X-Cache/X-Elapsed-Ms); newGET /v1/{address}structured endpoint; realGET /health. - Freshness: bumped actix-web/cors/governor/env_logger; added serde_json,
tokio, moka, tempfile. Rewrote the Dockerfile (multi-stage, pinned precompiled
heimdall
0.9.3,trixie-slimruntime for glibc ≥ 2.39, no toolchain at runtime → ~191MB). Removed a 13MB binary mistakenly committed to the repo. - Structure: split
main.rsintoconfig/validation/heimdall/errormodules; added unit tests. - The
GET /{address}success contract stays backwards-compatible (raw ABI array).
- SSRF consideration.
rpc_urllets a caller make the server fetch arbitrary hosts. Not hardened deliberately: blocking private/localhost addresses would break abi.ninja's localhost (chain 31337) decompilation. Revisit if the public instance needs it (e.g. an allowlist gated by env). - Self-destructed contracts. heimdall supports
--etherscan-api-keyto fetch creation bytecode for self-destructed contracts; could thread an env-configured key through. - Negative caching. Optionally cache "no bytecode" briefly to avoid re-running heimdall on repeated EOA lookups.
- Metrics/observability. Expose decompile durations, cache hit rate, in-flight
count (e.g.
/metrics). - Agent-facing surface. Provenance/confidence is already in
/v1; the broader resolution ladder (Etherscan → Sourcify → proxy → heimdall → 4byte) and the MCP/SDK wrappers live at the abi-agent project level, not here. - Raw calldata decode.
heimdall decodealso accepts a raw calldata hex string (no RPC needed). Could expose this (e.g.POST /v1/decodewith a body) alongside the tx-hash variant — done in Round 3 for tx hashes only. - Transaction trace inspection.
heimdall inspectdecodes full traces + logs (richer thandecode). A heavier "explain this tx" endpoint could build on it.