feat: decode contract event logs using verified ABIs#80
Conversation
📝 WalkthroughWalkthroughAdds an end-to-end event log decoding pipeline: a DB migration extends ChangesEvent Log Decoding Pipeline
Sequence Diagram(s)sequenceDiagram
rect rgba(100, 149, 237, 0.5)
Note over Indexer,event_log_decode_jobs: Indexing path
end
participant Indexer
participant write_batch_internal
participant event_log_decode_jobs
participant EventLogDecodeWorker
participant build_decoder_for_address
participant load_combined_abi
participant event_logs DB
Indexer->>write_batch_internal: pool, copy_client, batch
write_batch_internal->>write_batch_internal: commit DB transaction
write_batch_internal->>event_log_decode_jobs: enqueue_decode_jobs(addresses, full_rescan=false)
loop background worker
EventLogDecodeWorker->>event_log_decode_jobs: CLAIM_JOBS_SQL FOR UPDATE SKIP LOCKED
event_log_decode_jobs-->>EventLogDecodeWorker: claimed job (address, full_rescan)
EventLogDecodeWorker->>build_decoder_for_address: address
build_decoder_for_address->>load_combined_abi: address (proxy-aware ABI)
load_combined_abi-->>build_decoder_for_address: ResolvedContractAbi
build_decoder_for_address-->>EventLogDecodeWorker: EventLogDecoder
EventLogDecodeWorker->>event_logs DB: fetch batch (incremental or full rescan)
event_logs DB-->>EventLogDecodeWorker: Vec<EventLog>
EventLogDecodeWorker->>EventLogDecodeWorker: decode_log + apply_decode_attempt per log
EventLogDecodeWorker->>event_logs DB: UPDATE decode fields in transaction
EventLogDecodeWorker->>event_log_decode_jobs: DELETE job on success
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsStopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- input_data now serializes as 0x-prefixed hex string instead of raw bytes; frontend null guard updated to match string type - normalize_bytecode_for_comparison must run before strip_metadata so immutable offsets apply against full unstripped bytecode
Keep EventLogApiResponse mapping from HEAD; fix query.pagination.page → query.page bug introduced during merge.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
backend/crates/atlas-server/src/contract_abi.rs (1)
20-230: 🏗️ Heavy liftAdd same-file unit coverage for the new ABI/proxy helpers.
This module adds slot parsing, cache refresh, ABI merging, and ABI loading behavior but has no
#[cfg(test)] mod tests. At minimum, covermerge_abisand zero/error/non-zero slot responses.As per coding guidelines, "
backend/crates/**/*.rs: Add unit tests for new logic in a#[cfg(test)] mod testsblock in the same file; run withcargo test --workspace."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/crates/atlas-server/src/contract_abi.rs` around lines 20 - 230, Add a `#[cfg(test)] mod tests` block at the end of the contract_abi.rs file to cover the new ABI and proxy detection logic. Create unit tests for the `merge_abis` function that verify behavior when both ABIs are present, when only one is present, and when neither is present. Additionally, create tests for the `read_address_slot` function that mock RPC responses for non-zero addresses (success case), zero addresses (should return None), and error responses from the RPC endpoint to ensure the function correctly parses storage slots and handles edge cases. These tests should not require actual database or RPC calls; use mock data or mocking libraries as appropriate.Source: Coding guidelines
backend/crates/atlas-server/src/api/handlers/proxy.rs (1)
100-104: ⚡ Quick winReuse the direct ABI in the non-proxy branch.
Line 102 calls
load_combined_abiafter this handler already resolvedproxy == Noneand loadedproxy_abi; that helper performs those lookups again, including RPC proxy-slot checks for non-proxies. Reuse the loaded ABI to avoid duplicate external work.♻️ Proposed refactor
} else { // Not a proxy, just return the contract's ABI - let combined = load_combined_abi(&state.pool, &state.rpc_url, &address) - .await? - .map(|resolved| resolved.abi); + let combined = proxy_abi.as_ref().map(|abi| abi.abi.clone()); Ok(Json(CombinedAbiResponse { is_proxy: false, proxy_address: address, implementation_address: None, proxy_type: None,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/crates/atlas-server/src/api/handlers/proxy.rs` around lines 100 - 104, In the non-proxy branch of the handler (the else block starting around line 100), the code redundantly calls load_combined_abi which re-performs proxy slot checks and lookups that were already completed in determining proxy == None. Instead of calling load_combined_abi again in the else branch, directly reuse the ABI that was already resolved or loaded during the prior proxy detection logic to avoid duplicate external work and RPC calls.backend/crates/atlas-server/src/api/handlers/logs.rs (1)
190-230: ⚡ Quick winAdd same-file coverage for signature fallback enrichment.
This new logic is currently outside the unit tests below. Please add a small
#[cfg(test)]case for the pure behavior here—preserving existing decoded names and filling only unresolvedtopic0matches. As per coding guidelines, “Add unit tests for new logic in a#[cfg(test)] mod testsblock in the same file.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/crates/atlas-server/src/api/handlers/logs.rs` around lines 190 - 230, Add unit test coverage for the enrich_decoded_logs_with_known_signatures function by creating a #[cfg(test)] mod tests block in the same file. The test should verify two key behaviors: that existing event_name values are preserved when already populated, and that unresolved topic0 values get filled with the correct name and event_signature from the signature map. Mock the AppState and EventLog inputs to isolate the function logic from database dependencies.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/crates/atlas-common/src/types.rs`:
- Around line 50-55: The serialize_bytes_as_hex function lacks unit test
coverage. Add a #[cfg(test)] mod tests block at the end of the same file
containing unit tests for the serialize_bytes_as_hex function. Create at least
two test cases: one testing serialization of empty bytes (which should produce
the output "0x") and another testing serialization of a known byte vector (which
should produce "0x" followed by the hex-encoded representation of those bytes).
Use serde_json or an appropriate serializer in the test to verify the function
produces the expected hexadecimal string output.
In `@backend/crates/atlas-server/src/contract_abi.rs`:
- Around line 26-30: The eth_getStorageAt RPC call in the contract_abi function
currently uses a hardcoded "latest" block tag when resolving proxy
implementations, which causes issues with decoding older logs after proxy
upgrades. Replace the hardcoded "latest" string with the actual log block number
or implement proxy resolution that uses the specific block where the log
occurred. This ensures the correct implementation ABI is selected for each log
based on its block timestamp rather than always using the current
implementation.
- Around line 38-49: The code currently uses unwrap_or("0x") on the response's
"result" field, which silently defaults to "0x" when the result is missing.
However, a JSON-RPC error response contains an "error" field instead of a
"result" field, so this approach masks actual errors and misclassifies failed
requests. Instead of using unwrap_or("0x") directly on the result, first check
if the resp contains an "error" field using resp.get("error"), and if it does,
extract and propagate that error as an AtlasError. Only return the default "0x"
value when both the "result" and "error" fields are absent from the response.
In `@backend/crates/atlas-server/src/event_log_decode.rs`:
- Around line 319-323: The ON CONFLICT DO UPDATE SET clauses in the upsert
statements (appearing around lines 319-323 and 358-362) are clearing the
error_message field but not resetting the retry_count to 0 when a decode job is
re-enqueued. This leaves stale retry counts that no longer reflect the fresh
attempt. Add `retry_count = 0` to both SET clauses in addition to the existing
`error_message = NULL` to ensure both fields are properly reset on re-enqueue,
maintaining semantic correctness and accurate observability metrics.
In `@backend/crates/atlas-server/src/indexer/indexer.rs`:
- Around line 966-969: Refactor the `enqueue_decode_jobs` function to accept a
`&mut Transaction` parameter (similar to the
`enqueue_jobs_for_verified_contract_tx` pattern in contracts.rs). Then move the
call to `enqueue_decode_jobs` to execute before `pg_tx.commit()` at line 966,
passing the mutable transaction reference to it. This ensures the decode job
enqueuing happens atomically within the transaction, so if it fails, the
watermark update in the transaction rolls back. Apply the same fix to the
`enqueue_decode_jobs` call in `write_batch_and_clear_failed_block` (around line
750) so it also enqueues jobs before the transaction commits rather than after.
---
Nitpick comments:
In `@backend/crates/atlas-server/src/api/handlers/logs.rs`:
- Around line 190-230: Add unit test coverage for the
enrich_decoded_logs_with_known_signatures function by creating a #[cfg(test)]
mod tests block in the same file. The test should verify two key behaviors: that
existing event_name values are preserved when already populated, and that
unresolved topic0 values get filled with the correct name and event_signature
from the signature map. Mock the AppState and EventLog inputs to isolate the
function logic from database dependencies.
In `@backend/crates/atlas-server/src/api/handlers/proxy.rs`:
- Around line 100-104: In the non-proxy branch of the handler (the else block
starting around line 100), the code redundantly calls load_combined_abi which
re-performs proxy slot checks and lookups that were already completed in
determining proxy == None. Instead of calling load_combined_abi again in the
else branch, directly reuse the ABI that was already resolved or loaded during
the prior proxy detection logic to avoid duplicate external work and RPC calls.
In `@backend/crates/atlas-server/src/contract_abi.rs`:
- Around line 20-230: Add a `#[cfg(test)] mod tests` block at the end of the
contract_abi.rs file to cover the new ABI and proxy detection logic. Create unit
tests for the `merge_abis` function that verify behavior when both ABIs are
present, when only one is present, and when neither is present. Additionally,
create tests for the `read_address_slot` function that mock RPC responses for
non-zero addresses (success case), zero addresses (should return None), and
error responses from the RPC endpoint to ensure the function correctly parses
storage slots and handles edge cases. These tests should not require actual
database or RPC calls; use mock data or mocking libraries as appropriate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e0cb6df0-07d7-4e0e-a86a-2bdaca190238
📒 Files selected for processing (20)
backend/crates/atlas-common/src/types.rsbackend/crates/atlas-server/src/api/handlers/contracts.rsbackend/crates/atlas-server/src/api/handlers/logs.rsbackend/crates/atlas-server/src/api/handlers/proxy.rsbackend/crates/atlas-server/src/contract_abi.rsbackend/crates/atlas-server/src/event_log_decode.rsbackend/crates/atlas-server/src/indexer/event_log_decode_worker.rsbackend/crates/atlas-server/src/indexer/gap_fill_worker.rsbackend/crates/atlas-server/src/indexer/indexer.rsbackend/crates/atlas-server/src/indexer/mod.rsbackend/crates/atlas-server/src/lib.rsbackend/crates/atlas-server/src/main.rsbackend/crates/atlas-server/tests/integration/addresses.rsbackend/crates/atlas-server/tests/integration/logs.rsbackend/crates/atlas-server/tests/integration/main.rsbackend/crates/atlas-server/tests/integration/schema.rsbackend/crates/atlas-server/tests/integration/transactions.rsbackend/migrations/20260528000001_event_log_decode_state.sqlfrontend/src/types/index.tsfrontend/src/utils/abiDecode.ts
| fn serialize_bytes_as_hex<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> | ||
| where | ||
| S: Serializer, | ||
| { | ||
| serializer.serialize_str(&format!("0x{}", hex::encode(bytes))) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add in-file unit tests for serialize_bytes_as_hex.
This introduces new serialization logic but the file has no #[cfg(test)] mod tests coverage. Please add unit tests here for at least empty bytes ("0x") and a known byte vector ("0x...") to lock the contract.
Proposed test block
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde::Serialize;
+
+ #[derive(Serialize)]
+ struct Wrap<'a> {
+ #[serde(serialize_with = "serialize_bytes_as_hex")]
+ bytes: &'a [u8],
+ }
+
+ #[test]
+ fn serialize_bytes_as_hex_empty() {
+ let v = Wrap { bytes: &[] };
+ let s = serde_json::to_string(&v).unwrap();
+ assert_eq!(s, r#"{"bytes":"0x"}"#);
+ }
+
+ #[test]
+ fn serialize_bytes_as_hex_non_empty() {
+ let v = Wrap { bytes: &[0xde, 0xad, 0xbe, 0xef] };
+ let s = serde_json::to_string(&v).unwrap();
+ assert_eq!(s, r#"{"bytes":"0xdeadbeef"}"#);
+ }
+}As per coding guidelines, "backend/**/*.rs: Add unit tests for new logic in a #[cfg(test)] mod tests block in the same file" and "backend/crates/**/*.rs: Add unit tests for new logic in a #[cfg(test)] mod tests block in the same file; run with cargo test --workspace".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn serialize_bytes_as_hex<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> | |
| where | |
| S: Serializer, | |
| { | |
| serializer.serialize_str(&format!("0x{}", hex::encode(bytes))) | |
| } | |
| fn serialize_bytes_as_hex<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> | |
| where | |
| S: Serializer, | |
| { | |
| serializer.serialize_str(&format!("0x{}", hex::encode(bytes))) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use serde::Serialize; | |
| #[derive(Serialize)] | |
| struct Wrap<'a> { | |
| #[serde(serialize_with = "serialize_bytes_as_hex")] | |
| bytes: &'a [u8], | |
| } | |
| #[test] | |
| fn serialize_bytes_as_hex_empty() { | |
| let v = Wrap { bytes: &[] }; | |
| let s = serde_json::to_string(&v).unwrap(); | |
| assert_eq!(s, r#"{"bytes":"0x"}"#); | |
| } | |
| #[test] | |
| fn serialize_bytes_as_hex_non_empty() { | |
| let v = Wrap { bytes: &[0xde, 0xad, 0xbe, 0xef] }; | |
| let s = serde_json::to_string(&v).unwrap(); | |
| assert_eq!(s, r#"{"bytes":"0xdeadbeef"}"#); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/crates/atlas-common/src/types.rs` around lines 50 - 55, The
serialize_bytes_as_hex function lacks unit test coverage. Add a #[cfg(test)] mod
tests block at the end of the same file containing unit tests for the
serialize_bytes_as_hex function. Create at least two test cases: one testing
serialization of empty bytes (which should produce the output "0x") and another
testing serialization of a known byte vector (which should produce "0x" followed
by the hex-encoded representation of those bytes). Use serde_json or an
appropriate serializer in the test to verify the function produces the expected
hexadecimal string output.
Source: Coding guidelines
| let body = serde_json::json!({ | ||
| "jsonrpc": "2.0", | ||
| "method": "eth_getStorageAt", | ||
| "params": [address, slot, "latest"], | ||
| "id": 1 |
There was a problem hiding this comment.
Resolve proxy implementation at the log block, not latest.
Using "latest" means full rescans and older pending logs can be decoded with the current implementation ABI after a proxy upgrade. Pass a block tag through proxy resolution or persist implementation history and select the ABI for each log’s block.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/crates/atlas-server/src/contract_abi.rs` around lines 26 - 30, The
eth_getStorageAt RPC call in the contract_abi function currently uses a
hardcoded "latest" block tag when resolving proxy implementations, which causes
issues with decoding older logs after proxy upgrades. Replace the hardcoded
"latest" string with the actual log block number or implement proxy resolution
that uses the specific block where the log occurred. This ensures the correct
implementation ABI is selected for each log based on its block timestamp rather
than always using the current implementation.
| let resp: serde_json::Value = client | ||
| .post(rpc_url) | ||
| .json(&body) | ||
| .send() | ||
| .await | ||
| .map_err(|e| AtlasError::Rpc(format!("eth_getStorageAt failed: {e}")))? | ||
| .json() | ||
| .await | ||
| .map_err(|e| AtlasError::Rpc(format!("failed to parse eth_getStorageAt response: {e}")))?; | ||
|
|
||
| let raw = resp.get("result").and_then(|r| r.as_str()).unwrap_or("0x"); | ||
|
|
There was a problem hiding this comment.
Propagate JSON-RPC errors instead of treating them as empty slots.
A JSON-RPC error response has no result, so unwrap_or("0x") returns Ok(None) and can misclassify proxies as direct contracts.
🐛 Proposed fix
- let resp: serde_json::Value = client
+ let response = client
.post(rpc_url)
.json(&body)
.send()
.await
- .map_err(|e| AtlasError::Rpc(format!("eth_getStorageAt failed: {e}")))?
+ .map_err(|e| AtlasError::Rpc(format!("eth_getStorageAt failed: {e}")))?
+ .error_for_status()
+ .map_err(|e| AtlasError::Rpc(format!("eth_getStorageAt HTTP error: {e}")))?;
+
+ let resp: serde_json::Value = response
.json()
.await
.map_err(|e| AtlasError::Rpc(format!("failed to parse eth_getStorageAt response: {e}")))?;
- let raw = resp.get("result").and_then(|r| r.as_str()).unwrap_or("0x");
+ if let Some(error) = resp.get("error") {
+ return Err(AtlasError::Rpc(format!(
+ "eth_getStorageAt returned error: {error}"
+ )));
+ }
+
+ let raw = resp
+ .get("result")
+ .and_then(|r| r.as_str())
+ .ok_or_else(|| {
+ AtlasError::Rpc("eth_getStorageAt response missing string result".to_string())
+ })?;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/crates/atlas-server/src/contract_abi.rs` around lines 38 - 49, The
code currently uses unwrap_or("0x") on the response's "result" field, which
silently defaults to "0x" when the result is missing. However, a JSON-RPC error
response contains an "error" field instead of a "result" field, so this approach
masks actual errors and misclassifies failed requests. Instead of using
unwrap_or("0x") directly on the result, first check if the resp contains an
"error" field using resp.get("error"), and if it does, extract and propagate
that error as an AtlasError. Only return the default "0x" value when both the
"result" and "error" fields are absent from the response.
| ON CONFLICT (address) DO UPDATE SET | ||
| full_rescan = event_log_decode_jobs.full_rescan OR EXCLUDED.full_rescan, | ||
| requested_at = EXCLUDED.requested_at, | ||
| updated_at = EXCLUDED.updated_at, | ||
| error_message = NULL", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect decode-job schema and worker claim logic.
# Expectation: If retry/lock metadata gates job selection, the upserts in event_log_decode.rs should reset it on conflict.
rg -n -C4 'event_log_decode_jobs|retry_count|max_retries|locked_at|claimed|SKIP LOCKED|error_message' --iglob '*.rs' --iglob '*.sql'Repository: evstack/atlas
Length of output: 50369
🏁 Script executed:
# Check if event_log_decode_worker has any max retry limit or filtering
rg -n 'max_retries|retry_count|RETRY' backend/crates/atlas-server/src/indexer/event_log_decode_worker.rsRepository: evstack/atlas
Length of output: 139
Consider resetting retry_count to 0 when re-enqueueing a decode job.
On re-enqueue (lines 319–323, 358–362), these upserts clear error_message = NULL but leave retry_count unchanged. When an ABI upload or full-rescan request is issued for a previously failed address, the stale retry_count should be reset to 0 to reflect a fresh attempt. While the worker uses pessimistic locking (FOR UPDATE SKIP LOCKED) rather than retry-count filtering, resetting it maintains semantic correctness and prevents confusing observability metrics.
Add retry_count = 0 to both upsert SET clauses alongside error_message = NULL.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/crates/atlas-server/src/event_log_decode.rs` around lines 319 - 323,
The ON CONFLICT DO UPDATE SET clauses in the upsert statements (appearing around
lines 319-323 and 358-362) are clearing the error_message field but not
resetting the retry_count to 0 when a decode job is re-enqueued. This leaves
stale retry counts that no longer reflect the fresh attempt. Add `retry_count =
0` to both SET clauses in addition to the existing `error_message = NULL` to
ensure both fields are properly reset on re-enqueue, maintaining semantic
correctness and accurate observability metrics.
| pg_tx.commit().await?; | ||
|
|
||
| enqueue_decode_jobs(pool, &event_log_addresses, false).await?; | ||
|
|
There was a problem hiding this comment.
Critical: enqueue_decode_jobs must be called inside the transaction before commit.
The transaction commits at line 966, updating the last_indexed_block watermark (lines 941–950). Then enqueue_decode_jobs is called at line 968 after the commit. If enqueue_decode_jobs fails:
- The error propagates via
?and crashes the indexer. run_with_retryrestarts the indexer, which callsget_start_block()and reads the already-updated watermark.- The indexer resumes from the next block, permanently skipping the blocks whose decode jobs were not enqueued.
- No automatic rescan mechanism exists to recover the lost jobs.
This breaks the decode pipeline's correctness guarantee. The blocks are indexed but never decoded.
Correct pattern (as seen in contracts.rs lines 260–290):
let mut tx = pool.begin().await?;
// ... inserts ...
enqueue_jobs_tx(&mut tx, ...).await?; // inside transaction
tx.commit().await?;If the enqueue fails, the transaction rolls back atomically, leaving the watermark unchanged so the indexer retries the same blocks.
Recommended fix: Refactor enqueue_decode_jobs to accept &mut Transaction (similar to enqueue_jobs_for_verified_contract_tx) and call it before pg_tx.commit() at line 966. Alternatively, move the watermark update after the enqueue call so failures trigger a full batch retry.
The same issue affects write_batch_and_clear_failed_block (line 750), which deletes the failed_blocks row inside the transaction (lines 953–964), then calls enqueue outside. If enqueue fails, the row is deleted but no job is created, and the gap-fill worker cannot retry.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/crates/atlas-server/src/indexer/indexer.rs` around lines 966 - 969,
Refactor the `enqueue_decode_jobs` function to accept a `&mut Transaction`
parameter (similar to the `enqueue_jobs_for_verified_contract_tx` pattern in
contracts.rs). Then move the call to `enqueue_decode_jobs` to execute before
`pg_tx.commit()` at line 966, passing the mutable transaction reference to it.
This ensures the decode job enqueuing happens atomically within the transaction,
so if it fails, the watermark update in the transaction rolls back. Apply the
same fix to the `enqueue_decode_jobs` call in
`write_batch_and_clear_failed_block` (around line 750) so it also enqueues jobs
before the transaction commits rather than after.
Summary
Adds a background worker that decodes EVM contract event logs using verified contract ABIs (including proxy contracts). Decoded results are persisted and surfaced through the API.
Changes
contract_abi.rs: Resolves ABIs for contracts — direct or via EIP-1967/EIP-1822 proxy storage slots (callseth_getStorageAtto find implementation address)event_log_decode.rs: Core decoding logic — matches topic0 to ABI events, decodes indexed/non-indexed params, serializes results; defines decode statuses (pending,decoded,no_abi,no_matching_event,decode_failed)indexer/event_log_decode_worker.rs: Background worker — claims jobs fromevent_log_decode_jobsqueue (SKIP LOCKED), processes logs in batches of 500 per address, supports full rescans20260528000001: Addsdecode_status,decoded_at,decode_attempted_at,decode_sourcecolumns toevent_logs; createsevent_log_decode_jobsqueue table; adds partial index on pending logsapi/handlers/logs.rs: Simplified — strips proxy forwarding logic, returns decoded fields in log responsesapi/handlers/proxy.rs: Cleaned up — removes log-related proxy passthrough now handled nativelyapi/handlers/contracts.rs: Queues a decode job when a contract ABI is uploadedindexer/indexer.rs+main.rs: SpawnsEventLogDecodeWorkeras a tokio task alongside existing workersfrontend/src/types/index.ts: AddsEventLogtype fields for decode status and decoded paramstests/integration/logs.rs: Integration tests for log API responses with decoded dataSummary by CodeRabbit
Release Notes
New Features
Improvements