Skip to content
Merged
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
4 changes: 4 additions & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,9 @@ bigdecimal = { version = "0.4", features = ["serde"] }
hex = "0.4"
chrono = { version = "0.4", features = ["serde"] }

# Testing
testcontainers = "0.27"
testcontainers-modules = { version = "0.15", features = ["postgres"] }

# Internal crates
atlas-common = { path = "crates/atlas-common" }
8 changes: 8 additions & 0 deletions backend/crates/atlas-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,11 @@ tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"] }
tokio-postgres-rustls = "0.12"
rustls = "0.23"
webpki-roots = "0.26"

[dev-dependencies]
testcontainers = { workspace = true }
testcontainers-modules = { workspace = true }
tokio = { workspace = true }
tower = { workspace = true, features = ["util"] }
serde_json = { workspace = true }
sqlx = { workspace = true }
19 changes: 8 additions & 11 deletions backend/crates/atlas-server/src/head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::collections::VecDeque;
use tokio::sync::RwLock;
use tracing::{info, warn};

pub(crate) struct HeadTracker {
pub struct HeadTracker {
replay_capacity: usize,
state: RwLock<HeadState>,
}
Expand All @@ -15,17 +15,14 @@ struct HeadState {
replay: VecDeque<Block>,
}

pub(crate) struct ReplaySnapshot {
pub struct ReplaySnapshot {
pub buffer_start: Option<i64>,
pub buffer_end: Option<i64>,
pub blocks_after_cursor: Vec<Block>,
}

impl HeadTracker {
pub(crate) async fn bootstrap(
pool: &PgPool,
replay_capacity: usize,
) -> Result<Self, sqlx::Error> {
pub async fn bootstrap(pool: &PgPool, replay_capacity: usize) -> Result<Self, sqlx::Error> {
let mut blocks = sqlx::query_as::<_, Block>(&format!(
"SELECT {} FROM blocks ORDER BY number DESC LIMIT $1",
BLOCK_COLUMNS
Expand All @@ -49,19 +46,19 @@ impl HeadTracker {
})
}

pub(crate) fn empty(replay_capacity: usize) -> Self {
pub fn empty(replay_capacity: usize) -> Self {
Self {
replay_capacity,
state: RwLock::new(HeadState::default()),
}
}

pub(crate) async fn clear(&self) {
pub async fn clear(&self) {
let mut state = self.state.write().await;
*state = HeadState::default();
}

pub(crate) async fn publish_committed_batch(&self, blocks: Vec<Block>) {
pub async fn publish_committed_batch(&self, blocks: Vec<Block>) {
if blocks.is_empty() {
return;
}
Expand Down Expand Up @@ -89,11 +86,11 @@ impl HeadTracker {
}
}

pub(crate) async fn latest(&self) -> Option<Block> {
pub async fn latest(&self) -> Option<Block> {
self.state.read().await.latest.clone()
}

pub(crate) async fn replay_after(&self, after_block: Option<i64>) -> ReplaySnapshot {
pub async fn replay_after(&self, after_block: Option<i64>) -> ReplaySnapshot {
let state = self.state.read().await;

let blocks_after_cursor = match after_block {
Expand Down
3 changes: 3 additions & 0 deletions backend/crates/atlas-server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod api;
pub mod faucet;
pub mod head;
116 changes: 116 additions & 0 deletions backend/crates/atlas-server/tests/integration/addresses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use axum::{
body::Body,
http::{Request, StatusCode},
};
use tower::ServiceExt;

use crate::common;

// Block range: 5000-5999

const ADDR: &str = "0x5000000000000000000000000000000000000001";
const ADDR_TO: &str = "0x5000000000000000000000000000000000000002";
const TX_HASH_A: &str = "0x5000000000000000000000000000000000000000000000000000000000000001";
const TX_HASH_B: &str = "0x5000000000000000000000000000000000000000000000000000000000000002";

async fn seed_address_data(pool: &sqlx::PgPool) {
sqlx::query(
"INSERT INTO blocks (number, hash, parent_hash, timestamp, gas_used, gas_limit, transaction_count, indexed_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
ON CONFLICT (number) DO NOTHING",
)
.bind(5000i64)
.bind(format!("0x{:064x}", 5000))
.bind(format!("0x{:064x}", 4999))
.bind(1_700_005_000i64)
.bind(42_000i64)
.bind(30_000_000i64)
.bind(2i32)
.execute(pool)
.await
.expect("seed block");

sqlx::query(
"INSERT INTO addresses (address, is_contract, first_seen_block, tx_count)
VALUES ($1, $2, $3, $4)
ON CONFLICT (address) DO NOTHING",
)
.bind(ADDR)
.bind(true)
.bind(5000i64)
.bind(2i32)
.execute(pool)
.await
.expect("seed address");

for (idx, hash) in [TX_HASH_A, TX_HASH_B].iter().enumerate() {
sqlx::query(
"INSERT INTO transactions (hash, block_number, block_index, from_address, to_address, value, gas_price, gas_used, input_data, status, timestamp)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (hash, block_number) DO NOTHING",
)
.bind(hash)
.bind(5000i64)
.bind(idx as i32)
.bind(ADDR)
.bind(ADDR_TO)
.bind(0i64)
.bind(20_000_000_000i64)
.bind(21_000i64)
.bind(Vec::<u8>::new())
.bind(true)
.bind(1_700_005_000i64)
.execute(pool)
.await
.expect("seed transaction");
}
}

#[test]
fn get_address_detail() {
common::run(async {
let pool = common::pool();
seed_address_data(pool).await;

let app = common::test_router();
let response = app
.oneshot(
Request::builder()
.uri(format!("/api/addresses/{}", ADDR))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::OK);
let body = common::json_body(response).await;
assert_eq!(body["address"].as_str().unwrap(), ADDR);
assert_eq!(body["address_type"].as_str().unwrap(), "contract");
assert_eq!(body["tx_count"].as_i64().unwrap(), 2);
});
}

#[test]
fn get_address_transactions() {
common::run(async {
let pool = common::pool();
seed_address_data(pool).await;

let app = common::test_router();
let response = app
.oneshot(
Request::builder()
.uri(format!("/api/addresses/{}/transactions", ADDR))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::OK);
let body = common::json_body(response).await;
let data = body["data"].as_array().unwrap();
assert_eq!(data.len(), 2);
});
}
101 changes: 101 additions & 0 deletions backend/crates/atlas-server/tests/integration/blocks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use axum::{
body::Body,
http::{Request, StatusCode},
};
use tower::ServiceExt;

use crate::common;

// Block range: 1000-1999

async fn seed_blocks(pool: &sqlx::PgPool) {
for i in 1000..1005 {
sqlx::query(
"INSERT INTO blocks (number, hash, parent_hash, timestamp, gas_used, gas_limit, transaction_count, indexed_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
ON CONFLICT (number) DO NOTHING",
)
.bind(i as i64)
.bind(format!("0x{:064x}", i))
.bind(format!("0x{:064x}", i - 1))
.bind(1_700_000_000i64 + i as i64)
.bind(21_000i64)
.bind(30_000_000i64)
.bind(0i32)
.execute(pool)
.await
.expect("seed block");
}
}

#[test]
fn list_blocks_paginated() {
common::run(async {
let pool = common::pool();
seed_blocks(pool).await;

let app = common::test_router();
let response = app
.oneshot(
Request::builder()
.uri("/api/blocks?page=1&limit=2")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::OK);
let body = common::json_body(response).await;
assert_eq!(body["data"].as_array().unwrap().len(), 2);
assert!(body["total"].as_i64().unwrap() >= 5);

// Blocks should be in DESC order
let first = body["data"][0]["number"].as_i64().unwrap();
let second = body["data"][1]["number"].as_i64().unwrap();
assert!(first > second);
});
}

#[test]
fn get_block_by_number() {
common::run(async {
let pool = common::pool();
seed_blocks(pool).await;

let app = common::test_router();
let response = app
.oneshot(
Request::builder()
.uri("/api/blocks/1002")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::OK);
let body = common::json_body(response).await;
assert_eq!(body["number"].as_i64().unwrap(), 1002);
assert_eq!(body["hash"].as_str().unwrap(), &format!("0x{:064x}", 1002));
assert_eq!(body["gas_used"].as_i64().unwrap(), 21_000);
});
}

#[test]
fn get_block_not_found() {
common::run(async {
let app = common::test_router();
let response = app
.oneshot(
Request::builder()
.uri("/api/blocks/999999")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::NOT_FOUND);
});
}
Loading
Loading