Skip to content

Add EVM contract data + search support — AgentKeys v2 stage-1 contracts on Heima mainnet #4

@hanwencheng

Description

@hanwencheng

Background

AgentKeys (litentry/agentKeys) v2 stage 1 has deployed its four anchor contracts to Heima mainnet (chain_id 212013). These are written in Solidity 0.8.20, target EVM london (matching Heima's Frontier EVM level), and are the on-chain source of truth for the AgentKeys protocol's identity + scope + audit state.

The existing subscan-essentials explorer is Substrate-side only — it indexes pallet extrinsics + events but does NOT decode EVM contract calls, EVM events, or EVM contract storage. Today, the only way to inspect these contracts is via direct RPC (eth_getCode, cast call, cast logs). For operators + auditors, that's friction; for the project's "everything auditable on-chain" guarantee (see arch.md §6 + §15.3 in litentry/agentKeys), we need a first-class explorer view of the AgentKeys contracts.

This issue tracks adding EVM contract data + search support to subscan-essentials, scoped to a basic-but-extensible MVP. Future stages add ABI decoding for arbitrary contracts (not just AgentKeys'); this stage hard-codes our four.

Live mainnet addresses to support

All on Heima mainnet (chain_id 212013, RPC https://rpc.heima-parachain.heima.network):

Contract Address Bytecode size
AgentKeysScope 0x14C23B5D1cE20c094af643a20e6b0972dAD12aa8 3146 bytes
SidecarRegistry 0x76D574a107727bE87fc1422661A030FEFda70786 3301 bytes
K3EpochCounter 0x8396dEc50ff755d6DE7728DABB00Be2eFBCdf4dF 687 bytes
CredentialAudit 0x1801ded1a4FBD8c9224Ab18B9EcbB293B8674c06 1421 bytes

ABIs + source live in crates/agentkeys-chain/ in the agentKeys repo. Canonical address registry: docs/spec/deployed-contracts.md.

MVP scope (what this issue ships)

1. EVM contract index ingestion

The backend indexer learns to ingest contract data from the Frontier RPC namespace:

  • Bytecode — per address, via eth_getCode(addr, 'latest'). Indexed at deploy block (one-shot; updates only if selfdestruct triggers, which AgentKeys contracts don't use).
  • Transaction list — every to == contract_addr transaction. Indexed continuously via eth_getBlockByNumber walking. Each entry stores: block, txHash, from, value, calldata, status, gasUsed.
  • Event log — every Log whose address == contract_addr. Index by topic[0] (event signature hash) for cheap filtering. Each entry stores: block, txHash, logIndex, topics[], data.
  • Storage layout snapshot (optional, v1.1) — eth_getStorageAt(addr, slot) for declared slots from the ABI's storage layout output.

2. Per-contract view (UI)

For each of the four AgentKeys contracts, the subscan-essentials-ui-react frontend adds a route:

/contract/<addr>          → overview: name, bytecode hash, deploy date, balance, tx count
/contract/<addr>/txs      → paginated tx list (decoded function name + args via ABI)
/contract/<addr>/events   → paginated event list (decoded event name + indexed topics + data)
/contract/<addr>/call     → read-only view function caller (cast call equivalent)

ABI decoding is hard-coded for the four AgentKeys contracts in MVP (ship the ABIs in the indexer's config). v1.1+ adds operator-supplied ABI uploads.

3. Search

Search bar accepts:

  • Contract address (0x...40 hex) — direct route to /contract/<addr>
  • Tx hash (0x...64 hex) — existing substrate tx route + new EVM tx route (auto-detect by hash length)
  • Block number — existing route
  • EVM event filter — new: `Sid:keccak256(EventSig)` syntax routes to events page filtered by topic[0]. E.g., `SidecarRegistry.DeviceRegistered` → `/contract/0x76D5.../events?topic0=0x`. Hard-coded keyword → topic mapping for AgentKeys events in MVP.
  • Per-operator filter — `actor_omni:<64hex>` filters events across ALL four contracts where any indexed topic matches that actor_omni. Backend joins across the contract event indexes.

4. Per-actor cross-contract view (THIS is what makes the explorer useful for AgentKeys)

A new top-level route /agentkeys/actor/<operator_omni_hex> shows ONE operator's activity across ALL four contracts:

  • Devices bound to them (from SidecarRegistry.DeviceRegistered filtered on indexed operatorOmni)
  • Their scope grants (from AgentKeysScope.ScopeUpdated)
  • Their audit log entries (from CredentialAudit.AuditAppended)
  • Current K3 epoch (from K3EpochCounter.K3Rotated, latest)

This is the auditor's view: "show me everything this operator has done on-chain."

Acceptance criteria / test standard

The MVP is complete when ALL of these pass:

A. Backend ingestion (Go service)

A.1. make build + make test pass with the new indexer modules added.
A.2. For each of the four AgentKeys contracts, running the indexer against Heima mainnet produces:

  • A non-empty contract row in the DB
  • A bytecode_size matching the on-chain bytecode size (3146, 3301, 687, 1421 bytes respectively)
  • All historical transactions to that address recorded with status + gas + decoded function names
  • All historical events recorded with topic0 → event name decoded

A.3. Re-running the indexer is idempotent (no duplicate rows).
A.4. The indexer keeps up with chain head — when a new tx hits one of the contracts, it appears in the DB within 1 block of inclusion.

B. Frontend (React)

B.1. `/contract/0x14C23B5D1cE20c094af643a20e6b0972dAD12aa8` (AgentKeysScope) loads and shows:

  • Name: `AgentKeysScope`
  • Bytecode size: 3146 bytes
  • Deploy tx: linkable to the original deploy tx page
  • Function call counter (number of writes since deploy)
  • Tab navigation: Overview / Transactions / Events / Read Functions

B.2. `/contract/0x76D574a107727bE87fc1422661A030FEFda70786/events` (SidecarRegistry) lists at least the one DeviceRegistered event from block 9620483, with topic[0] decoded as DeviceRegistered(bytes32,bytes32,bytes32,uint8,uint8,bytes32) and indexed args (deviceKeyHash, operatorOmni, actorOmni) shown.

B.3. `/contract/0x76D574a107727bE87fc1422661A030FEFda70786/call` exposes a form for the read-only functions (isActive(bytes32), getDevice(bytes32), operatorMasterWallet(bytes32), etc.) — filling the args + clicking "Call" returns the decoded result without a tx.

B.4. `/agentkeys/actor/0x941cb1c3260518bbf40eac7d02663517fc7cff304d9b03e80d2cc54126c6bef2` (the live registered operator from block 9620483) shows:

  • 1 device registered
  • 0 scope grants (we haven't issued any yet)
  • 0 audit entries
  • Current K3 epoch = 1

C. Search

C.1. Pasting 0x14C23B5D1cE20c094af643a20e6b0972dAD12aa8 into the search bar routes to the contract overview page.
C.2. Pasting 0x8f1d7cca5710c2859b4f8b942c36df41d3c6b8b02a862d1f506285a6176c988b (the device-register tx) routes to the EVM tx detail page, with the decoded function call SidecarRegistry.registerMasterDevice(...) and all its args shown.
C.3. Typing the keyword DeviceRegistered → autocomplete suggests the SidecarRegistry events route filtered to topic[0] of that event sig.

D. Performance (acceptance smoke)

D.1. Indexer cold-start (from genesis) on Heima mainnet completes the four contracts' history in under 30 minutes on a 4-core / 8GB host.
D.2. Frontend pages (overview, txs, events) load under 500ms p50 on the indexed data.

E. Backward-compatibility

E.1. All existing subscan-essentials Substrate-side routes (`/extrinsic/`, `/account/`, etc.) continue to work unchanged.
E.2. No schema migration breaks for existing operators upgrading from the previous indexer version. New EVM tables are additive.

Future revision plan (out of scope for this issue)

These are tracked as follow-up issues, NOT blocking MVP:

  1. Operator-supplied ABI uploads — let any operator point at any contract address + upload its ABI; the explorer decodes it. Today's MVP hard-codes the four AgentKeys contracts.
  2. Bytecode verification — match deployed bytecode against compiled source (forge inspect) and flag verified contracts with a checkmark.
  3. Per-actor activity dashboards — analytics across operators (top-N by audit volume, scope-change rate, etc.).
  4. Cross-contract trace — given a tx hash that calls A which calls B, render the full call tree (would need debug_traceTransaction RPC support on Heima collators).
  5. WebSocket subscriptions — real-time event push so dashboards update without refresh.

Reference

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions