Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion backend/crates/atlas-common/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bigdecimal::BigDecimal;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Serialize, Serializer};
use sqlx::FromRow;

/// Block data as stored in the database
Expand Down Expand Up @@ -40,12 +40,20 @@ pub struct Transaction {
pub value: BigDecimal,
pub gas_price: BigDecimal,
pub gas_used: i64,
#[serde(serialize_with = "serialize_bytes_as_hex")]
pub input_data: Vec<u8>,
pub status: bool,
pub contract_created: Option<String>,
pub timestamp: i64,
}

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)))
}

/// Address data as stored in the database
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Address {
Expand Down Expand Up @@ -170,6 +178,10 @@ pub struct EventLog {
pub data: Vec<u8>,
pub block_number: i64,
pub decoded: Option<serde_json::Value>,
pub decode_status: String,
pub decoded_at: Option<DateTime<Utc>>,
pub decode_attempted_at: Option<DateTime<Utc>>,
pub decode_source: Option<String>,
}

/// Known event signature for decoding
Expand Down
36 changes: 29 additions & 7 deletions backend/crates/atlas-server/src/api/handlers/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use tokio::fs;

use crate::api::error::ApiResult;
use crate::api::AppState;
use crate::event_log_decode::enqueue_jobs_for_verified_contract;
use atlas_common::{AtlasError, FullContractAbi};

// ── Request / Response types ──────────────────────────────────────────────────
Expand Down Expand Up @@ -220,18 +221,19 @@ pub async fn verify_contract(
// Compile the submitted source
let compiled_contract = compile_source(&solc_path, &req).await?;

// Strip CBOR metadata from both sides before comparing
// Solc reports immutable offsets against the full deployed bytecode, so
// zero those ranges before stripping the trailing CBOR metadata blob.
let deployed_bytes = decode_hex_bytecode(&deployed_hex)?;
let deployed_stripped = strip_metadata(&deployed_bytes);
let compiled_stripped = strip_metadata(&compiled_contract.bytecode);
let deployed_cmp = normalize_bytecode_for_comparison(
deployed_stripped,
let deployed_normalized = normalize_bytecode_for_comparison(
&deployed_bytes,
&compiled_contract.immutable_references,
)?;
let compiled_cmp = normalize_bytecode_for_comparison(
compiled_stripped,
let compiled_normalized = normalize_bytecode_for_comparison(
&compiled_contract.bytecode,
&compiled_contract.immutable_references,
)?;
let deployed_cmp = strip_metadata(&deployed_normalized).to_vec();
let compiled_cmp = strip_metadata(&compiled_normalized).to_vec();

// eth_getCode returns deployed runtime bytecode, so constructor args are not
// part of the bytecode comparison. We still parse and persist them as metadata.
Expand Down Expand Up @@ -282,6 +284,8 @@ pub async fn verify_contract(
return Err(AtlasError::Verification(format!("{address} is already verified")).into());
}

enqueue_jobs_for_verified_contract(&state.pool, &address).await?;

Ok((
StatusCode::OK,
Json(VerifyResponse {
Expand Down Expand Up @@ -1093,6 +1097,24 @@ mod tests {
assert!(matches!(err, AtlasError::Compilation(_)));
}

#[test]
fn normalize_then_strip_metadata_preserves_immutable_offsets() {
let bytecode = vec![
0xaa, 0xbb, 0x11, 0x22, 0xcc, 0xdd, 0x01, 0x02, 0x03, 0x00, 0x03,
];
let normalized = normalize_bytecode_for_comparison(
&bytecode,
&[ImmutableReference {
start: 2,
length: 2,
}],
)
.unwrap();
let stripped = strip_metadata(&normalized);

assert_eq!(stripped, &[0xaa, 0xbb, 0x00, 0x00, 0xcc, 0xdd]);
}

#[test]
fn extract_immutable_references_parses_multiple_entries() {
let refs = extract_immutable_references(&serde_json::json!({
Expand Down
87 changes: 13 additions & 74 deletions backend/crates/atlas-server/src/api/handlers/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::sync::Arc;

use crate::api::error::ApiResult;
use crate::api::AppState;
use crate::event_log_decode::EventLogApiResponse;
use atlas_common::{EventLog, PaginatedResponse, Pagination};

/// Pagination for transaction log endpoints.
Expand Down Expand Up @@ -61,7 +62,7 @@ pub async fn get_transaction_logs(
State(state): State<Arc<AppState>>,
Path(hash): Path<String>,
Query(query): Query<TransactionLogsQuery>,
) -> ApiResult<Json<PaginatedResponse<EventLog>>> {
) -> ApiResult<Json<PaginatedResponse<EventLogApiResponse>>> {
let hash = normalize_hash(&hash);

let total: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM event_logs WHERE tx_hash = $1")
Expand All @@ -70,7 +71,8 @@ pub async fn get_transaction_logs(
.await?;

let logs: Vec<EventLog> = sqlx::query_as(
"SELECT id, tx_hash, log_index, address, topic0, topic1, topic2, topic3, data, block_number, decoded
"SELECT id, tx_hash, log_index, address, topic0, topic1, topic2, topic3, data, block_number,
decoded, decode_status, decoded_at, decode_attempted_at, decode_source
FROM event_logs
WHERE tx_hash = $1
ORDER BY log_index ASC
Expand All @@ -83,7 +85,7 @@ pub async fn get_transaction_logs(
.await?;

Ok(Json(PaginatedResponse::new(
logs,
logs.iter().map(EventLogApiResponse::from).collect(),
query.page,
query.clamped_limit(),
total.0,
Expand All @@ -95,7 +97,7 @@ pub async fn get_address_logs(
State(state): State<Arc<AppState>>,
Path(address): Path<String>,
Query(query): Query<LogsQuery>,
) -> ApiResult<Json<PaginatedResponse<EventLog>>> {
) -> ApiResult<Json<PaginatedResponse<EventLogApiResponse>>> {
let address = normalize_address(&address);

let (total, logs) = if let Some(topic0) = &query.topic0 {
Expand All @@ -109,7 +111,8 @@ pub async fn get_address_logs(
.await?;

let logs: Vec<EventLog> = sqlx::query_as(
"SELECT id, tx_hash, log_index, address, topic0, topic1, topic2, topic3, data, block_number, decoded
"SELECT id, tx_hash, log_index, address, topic0, topic1, topic2, topic3, data, block_number,
decoded, decode_status, decoded_at, decode_attempted_at, decode_source
FROM event_logs
WHERE address = $1 AND topic0 = $2
ORDER BY block_number DESC, log_index DESC
Expand All @@ -130,7 +133,8 @@ pub async fn get_address_logs(
.await?;

let logs: Vec<EventLog> = sqlx::query_as(
"SELECT id, tx_hash, log_index, address, topic0, topic1, topic2, topic3, data, block_number, decoded
"SELECT id, tx_hash, log_index, address, topic0, topic1, topic2, topic3, data, block_number,
decoded, decode_status, decoded_at, decode_attempted_at, decode_source
FROM event_logs
WHERE address = $1
ORDER BY block_number DESC, log_index DESC
Expand All @@ -146,85 +150,20 @@ pub async fn get_address_logs(
};

Ok(Json(PaginatedResponse::new(
logs,
logs.iter().map(EventLogApiResponse::from).collect(),
query.pagination.page,
query.clamped_limit(),
total,
)))
}

/// Enriched log with event name
#[derive(Debug, Clone, serde::Serialize)]
pub struct EnrichedEventLog {
#[serde(flatten)]
pub log: EventLog,
pub event_name: Option<String>,
pub event_signature: Option<String>,
}

/// GET /api/transactions/:hash/logs/decoded - Get decoded logs for a transaction
pub async fn get_transaction_logs_decoded(
State(state): State<Arc<AppState>>,
Path(hash): Path<String>,
Query(query): Query<TransactionLogsQuery>,
) -> ApiResult<Json<PaginatedResponse<EnrichedEventLog>>> {
let hash = normalize_hash(&hash);

let total: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM event_logs WHERE tx_hash = $1")
.bind(&hash)
.fetch_one(&state.pool)
.await?;

let logs: Vec<EventLog> = sqlx::query_as(
"SELECT id, tx_hash, log_index, address, topic0, topic1, topic2, topic3, data, block_number, decoded
FROM event_logs
WHERE tx_hash = $1
ORDER BY log_index ASC
LIMIT $2 OFFSET $3",
)
.bind(&hash)
.bind(query.limit())
.bind(query.offset())
.fetch_all(&state.pool)
.await?;

// Collect unique topic0 values for signature lookup
let topic0s: Vec<String> = logs.iter().map(|l| l.topic0.clone()).collect();

// Fetch known event signatures
let signatures: Vec<(String, String, String)> = sqlx::query_as(
"SELECT signature, name, full_signature FROM event_signatures WHERE signature = ANY($1)",
)
.bind(&topic0s)
.fetch_all(&state.pool)
.await?;

let sig_map: std::collections::HashMap<String, (String, String)> = signatures
.into_iter()
.map(|(sig, name, full)| (sig.to_lowercase(), (name, full)))
.collect();

let enriched: Vec<EnrichedEventLog> = logs
.into_iter()
.map(|log| {
let (event_name, event_signature) = sig_map
.get(&log.topic0.to_lowercase())
.map(|(n, s)| (Some(n.clone()), Some(s.clone())))
.unwrap_or((None, None));
EnrichedEventLog {
log,
event_name,
event_signature,
}
})
.collect();

Ok(Json(PaginatedResponse::new(
enriched,
query.page,
query.clamped_limit(),
total.0,
)))
) -> ApiResult<Json<PaginatedResponse<EventLogApiResponse>>> {
get_transaction_logs(State(state), Path(hash), Query(query)).await
}

fn default_page() -> u32 {
Expand Down
Loading