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
3 changes: 2 additions & 1 deletion backend/crates/atlas-common/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub struct Block {
pub timestamp: i64,
pub gas_used: i64,
pub gas_limit: i64,
pub base_fee_per_gas: Option<String>,
pub transaction_count: i32,
pub indexed_at: DateTime<Utc>,
}
Expand Down Expand Up @@ -240,7 +241,7 @@ pub struct ContractAbi {

/// SQL column list for the `blocks` table, matching the field order in [`Block`].
pub const BLOCK_COLUMNS: &str =
"number, hash, parent_hash, timestamp, gas_used, gas_limit, transaction_count, indexed_at";
"number, hash, parent_hash, timestamp, gas_used, gas_limit, base_fee_per_gas::text AS base_fee_per_gas, transaction_count, indexed_at";

/// Pagination parameters
#[derive(Debug, Clone, Deserialize)]
Expand Down
24 changes: 11 additions & 13 deletions backend/crates/atlas-server/src/api/handlers/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use std::sync::Arc;

use crate::api::error::ApiResult;
use crate::api::AppState;
use atlas_common::{AtlasError, Block, BlockDaStatus, PaginatedResponse, Pagination, Transaction};
use atlas_common::{
AtlasError, Block, BlockDaStatus, PaginatedResponse, Pagination, Transaction, BLOCK_COLUMNS,
};

/// Block response with optional DA status.
/// DA fields are always present in the JSON (null when no data),
Expand Down Expand Up @@ -36,13 +38,10 @@ pub async fn list_blocks(
let limit = pagination.limit();
let cursor = (total_count - 1) - (pagination.page.saturating_sub(1) as i64) * limit;

let blocks: Vec<Block> = sqlx::query_as(
"SELECT number, hash, parent_hash, timestamp, gas_used, gas_limit, transaction_count, indexed_at
FROM blocks
WHERE number <= $2
ORDER BY number DESC
LIMIT $1",
)
let blocks: Vec<Block> = sqlx::query_as(&format!(
"SELECT {} FROM blocks WHERE number <= $2 ORDER BY number DESC LIMIT $1",
BLOCK_COLUMNS
))
.bind(limit)
.bind(cursor)
.fetch_all(&state.pool)
Expand Down Expand Up @@ -82,11 +81,10 @@ pub async fn get_block(
State(state): State<Arc<AppState>>,
Path(number): Path<i64>,
) -> ApiResult<Json<BlockResponse>> {
let block: Block = sqlx::query_as(
"SELECT number, hash, parent_hash, timestamp, gas_used, gas_limit, transaction_count, indexed_at
FROM blocks
WHERE number = $1",
)
let block: Block = sqlx::query_as(&format!(
"SELECT {} FROM blocks WHERE number = $1",
BLOCK_COLUMNS
))
.bind(number)
.fetch_optional(&state.pool)
.await?
Expand Down
20 changes: 9 additions & 11 deletions backend/crates/atlas-server/src/api/handlers/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::sync::Arc;

use crate::api::error::ApiResult;
use crate::api::AppState;
use atlas_common::{Address, Block, Erc20Contract, NftContract, Transaction};
use atlas_common::{Address, Block, Erc20Contract, NftContract, Transaction, BLOCK_COLUMNS};

#[derive(Deserialize)]
pub struct SearchQuery {
Expand Down Expand Up @@ -151,11 +151,10 @@ async fn search_block_by_hash(
hash: &str,
) -> Result<Option<Block>, atlas_common::AtlasError> {
// Hash is already lowercased by caller
sqlx::query_as(
"SELECT number, hash, parent_hash, timestamp, gas_used, gas_limit, transaction_count, indexed_at
FROM blocks
WHERE hash = $1"
)
sqlx::query_as(&format!(
"SELECT {} FROM blocks WHERE hash = $1",
BLOCK_COLUMNS
))
.bind(hash)
.fetch_optional(&state.pool)
.await
Expand All @@ -166,11 +165,10 @@ async fn search_block_by_number(
state: &AppState,
number: i64,
) -> Result<Option<Block>, atlas_common::AtlasError> {
sqlx::query_as(
"SELECT number, hash, parent_hash, timestamp, gas_used, gas_limit, transaction_count, indexed_at
FROM blocks
WHERE number = $1"
)
sqlx::query_as(&format!(
"SELECT {} FROM blocks WHERE number = $1",
BLOCK_COLUMNS
))
.bind(number)
.fetch_optional(&state.pool)
.await
Expand Down
1 change: 1 addition & 0 deletions backend/crates/atlas-server/src/api/handlers/sse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ mod tests {
timestamp: 1_700_000_000 + number,
gas_used: 21_000,
gas_limit: 30_000_000,
base_fee_per_gas: Some("1000000000".to_string()),
transaction_count: 1,
indexed_at: Utc::now(),
}
Expand Down
61 changes: 49 additions & 12 deletions backend/crates/atlas-server/src/api/handlers/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,14 @@ pub struct DailyTxPoint {
#[derive(Serialize)]
pub struct GasPricePoint {
pub bucket: String,
pub avg_gas_price: f64,
pub avg_gas_price: Option<f64>,
}

fn resolve_avg_gas_price(
tx_avg_gas_price: Option<f64>,
block_avg_base_fee_per_gas: Option<f64>,
) -> Option<f64> {
tx_avg_gas_price.or(block_avg_base_fee_per_gas)
}

/// GET /api/stats/blocks-chart?window=1h|6h|24h|7d
Expand Down Expand Up @@ -175,15 +182,15 @@ pub async fn get_daily_txs(
/// GET /api/stats/gas-price?window=1h|6h|24h|7d
///
/// Returns average gas price (in wei) per bucket over the given window.
/// Anchored to the latest indexed transaction timestamp (not NOW()).
/// Anchored to the latest indexed block timestamp (not NOW()).
pub async fn get_gas_price_chart(
State(state): State<Arc<AppState>>,
Query(params): Query<WindowQuery>,
) -> ApiResult<Json<Vec<GasPricePoint>>> {
let window = params.window;
let bucket_secs = window.bucket_secs();

let rows: Vec<(chrono::DateTime<Utc>, Option<f64>)> = sqlx::query_as(
let rows: Vec<(chrono::DateTime<Utc>, Option<f64>, Option<f64>)> = sqlx::query_as(
r#"
WITH latest AS (SELECT MAX(timestamp) AS max_ts FROM blocks),
bounds AS (
Expand All @@ -192,7 +199,7 @@ pub async fn get_gas_price_chart(
max_ts AS end_ts
FROM latest
),
agg AS (
tx_agg AS (
SELECT
(b.start_ts + (((transactions.timestamp - b.start_ts) / $1) * $1))::bigint AS bucket_ts,
AVG(gas_price::float8) AS avg_gas_price
Expand All @@ -202,13 +209,26 @@ pub async fn get_gas_price_chart(
AND transactions.timestamp <= b.end_ts
AND gas_price > 0
GROUP BY 1
),
block_agg AS (
SELECT
(b.start_ts + (((blocks.timestamp - b.start_ts) / $1) * $1))::bigint AS bucket_ts,
AVG(base_fee_per_gas::float8) AS avg_base_fee_per_gas
FROM blocks
CROSS JOIN bounds b
WHERE blocks.timestamp >= b.start_ts
AND blocks.timestamp <= b.end_ts
AND base_fee_per_gas IS NOT NULL
GROUP BY 1
)
SELECT
to_timestamp(gs::float8) AS bucket,
a.avg_gas_price
FROM bounds b
CROSS JOIN generate_series(b.start_ts, b.end_ts - $1, $1::bigint) AS gs
LEFT JOIN agg a ON a.bucket_ts = gs
t.avg_gas_price,
ba.avg_base_fee_per_gas
FROM bounds bo
CROSS JOIN generate_series(bo.start_ts, bo.end_ts - $1, $1::bigint) AS gs
LEFT JOIN tx_agg t ON t.bucket_ts = gs
LEFT JOIN block_agg ba ON ba.bucket_ts = gs
ORDER BY gs ASC
"#,
)
Expand All @@ -219,10 +239,12 @@ pub async fn get_gas_price_chart(

let points = rows
.into_iter()
.map(|(bucket, avg_gas_price)| GasPricePoint {
bucket: bucket.to_rfc3339(),
avg_gas_price: avg_gas_price.unwrap_or(0.0),
})
.map(
|(bucket, tx_avg_gas_price, block_avg_base_fee_per_gas)| GasPricePoint {
bucket: bucket.to_rfc3339(),
avg_gas_price: resolve_avg_gas_price(tx_avg_gas_price, block_avg_base_fee_per_gas),
},
)
.collect();

Ok(Json(points))
Expand Down Expand Up @@ -263,4 +285,19 @@ mod tests {
assert_eq!(Window::SevenDays.duration_secs(), 7 * 24 * 3_600);
assert_eq!(Window::SevenDays.bucket_secs(), 43_200);
}

#[test]
fn resolve_avg_gas_price_prefers_transaction_average() {
assert_eq!(resolve_avg_gas_price(Some(42.0), Some(7.0)), Some(42.0));
}

#[test]
fn resolve_avg_gas_price_falls_back_to_base_fee() {
assert_eq!(resolve_avg_gas_price(None, Some(7.0)), Some(7.0));
}

#[test]
fn resolve_avg_gas_price_returns_none_when_bucket_is_empty() {
assert_eq!(resolve_avg_gas_price(None, None), None);
}
}
1 change: 1 addition & 0 deletions backend/crates/atlas-server/src/api/handlers/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ mod tests {
timestamp: 1_700_000_000 + number,
gas_used: 21_000,
gas_limit: 30_000_000,
base_fee_per_gas: Some("1000000000".to_string()),
transaction_count: 1,
indexed_at: Utc::now(),
}
Expand Down
1 change: 1 addition & 0 deletions backend/crates/atlas-server/src/head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ mod tests {
timestamp: 1_700_000_000 + number,
gas_used: 21_000,
gas_limit: 30_000_000,
base_fee_per_gas: Some("1000000000".to_string()),
transaction_count: 1,
indexed_at: Utc.timestamp_opt(1_700_000_000 + number, 0).unwrap(),
}
Expand Down
7 changes: 7 additions & 0 deletions backend/crates/atlas-server/src/indexer/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub(crate) struct BlockBatch {
pub(crate) b_timestamps: Vec<i64>,
pub(crate) b_gas_used: Vec<i64>,
pub(crate) b_gas_limits: Vec<i64>,
pub(crate) b_base_fee_per_gas: Vec<Option<String>>,
pub(crate) b_tx_counts: Vec<i32>,

// transactions (receipt data merged in at collection time)
Expand Down Expand Up @@ -177,6 +178,7 @@ impl BlockBatch {
debug_assert_eq!(self.b_numbers.len(), self.b_timestamps.len());
debug_assert_eq!(self.b_numbers.len(), self.b_gas_used.len());
debug_assert_eq!(self.b_numbers.len(), self.b_gas_limits.len());
debug_assert_eq!(self.b_numbers.len(), self.b_base_fee_per_gas.len());
debug_assert_eq!(self.b_numbers.len(), self.b_tx_counts.len());

(0..self.b_numbers.len())
Expand All @@ -187,6 +189,7 @@ impl BlockBatch {
timestamp: self.b_timestamps[i],
gas_used: self.b_gas_used[i],
gas_limit: self.b_gas_limits[i],
base_fee_per_gas: self.b_base_fee_per_gas[i].clone(),
transaction_count: self.b_tx_counts[i],
indexed_at,
})
Expand Down Expand Up @@ -294,6 +297,9 @@ mod tests {
batch.b_timestamps.push(1_700_000_042);
batch.b_gas_used.push(21_000);
batch.b_gas_limits.push(30_000_000);
batch
.b_base_fee_per_gas
.push(Some("1000000000".to_string()));
batch.b_tx_counts.push(3);

let indexed_at = Utc.timestamp_opt(1_700_000_100, 0).unwrap();
Expand All @@ -306,6 +312,7 @@ mod tests {
assert_eq!(blocks[0].timestamp, 1_700_000_042);
assert_eq!(blocks[0].gas_used, 21_000);
assert_eq!(blocks[0].gas_limit, 30_000_000);
assert_eq!(blocks[0].base_fee_per_gas.as_deref(), Some("1000000000"));
assert_eq!(blocks[0].transaction_count, 3);
assert_eq!(blocks[0].indexed_at, indexed_at);
}
Expand Down
12 changes: 8 additions & 4 deletions backend/crates/atlas-server/src/indexer/copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub async fn copy_blocks(
timestamp BIGINT,
gas_used BIGINT,
gas_limit BIGINT,
base_fee_per_gas TEXT,
transaction_count INT,
indexed_at TIMESTAMPTZ
) ON COMMIT DELETE ROWS;
Expand All @@ -35,7 +36,7 @@ pub async fn copy_blocks(

let sink = tx
.copy_in(
"COPY tmp_blocks (number, hash, parent_hash, timestamp, gas_used, gas_limit, transaction_count, indexed_at) FROM STDIN BINARY",
"COPY tmp_blocks (number, hash, parent_hash, timestamp, gas_used, gas_limit, base_fee_per_gas, transaction_count, indexed_at) FROM STDIN BINARY",
)
.await?;
let writer = BinaryCopyInWriter::new(
Expand All @@ -47,20 +48,22 @@ pub async fn copy_blocks(
Type::INT8,
Type::INT8,
Type::INT8,
Type::TEXT,
Type::INT4,
Type::TIMESTAMPTZ,
],
);
pin!(writer);

for i in 0..batch.b_numbers.len() {
let row: [&(dyn ToSql + Sync); 8] = [
let row: [&(dyn ToSql + Sync); 9] = [
&batch.b_numbers[i],
&batch.b_hashes[i],
&batch.b_parent_hashes[i],
&batch.b_timestamps[i],
&batch.b_gas_used[i],
&batch.b_gas_limits[i],
&batch.b_base_fee_per_gas[i],
&batch.b_tx_counts[i],
&indexed_at,
];
Expand All @@ -70,15 +73,16 @@ pub async fn copy_blocks(
writer.finish().await?;

tx.execute(
"INSERT INTO blocks (number, hash, parent_hash, timestamp, gas_used, gas_limit, transaction_count, indexed_at)
SELECT number, hash, parent_hash, timestamp, gas_used, gas_limit, transaction_count, indexed_at
"INSERT INTO blocks (number, hash, parent_hash, timestamp, gas_used, gas_limit, base_fee_per_gas, transaction_count, indexed_at)
SELECT number, hash, parent_hash, timestamp, gas_used, gas_limit, base_fee_per_gas::numeric, transaction_count, indexed_at
FROM tmp_blocks
ON CONFLICT (number) DO UPDATE SET
hash = EXCLUDED.hash,
parent_hash = EXCLUDED.parent_hash,
timestamp = EXCLUDED.timestamp,
gas_used = EXCLUDED.gas_used,
gas_limit = EXCLUDED.gas_limit,
base_fee_per_gas = EXCLUDED.base_fee_per_gas,
transaction_count = EXCLUDED.transaction_count,
indexed_at = EXCLUDED.indexed_at",
&[],
Expand Down
8 changes: 7 additions & 1 deletion backend/crates/atlas-server/src/indexer/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ impl Indexer {
known_nft: &HashSet<String>,
fetched: FetchedBlock,
) {
use alloy::consensus::Transaction as TxTrait;
use alloy::consensus::{BlockHeader, Transaction as TxTrait};

let block = fetched.block;
let block_num = fetched.number;
Expand All @@ -525,6 +525,12 @@ impl Indexer {
batch.b_timestamps.push(block.header.timestamp as i64);
batch.b_gas_used.push(block.header.gas_used as i64);
batch.b_gas_limits.push(block.header.gas_limit as i64);
batch.b_base_fee_per_gas.push(
block
.header
.base_fee_per_gas()
.map(|base_fee| base_fee.to_string()),
);
batch.b_tx_counts.push(tx_count);

// --- Transactions ---
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE blocks
ADD COLUMN IF NOT EXISTS base_fee_per_gas NUMERIC(78, 0);
3 changes: 0 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ services:
DA_RPC_REQUESTS_PER_SECOND: ${DA_RPC_REQUESTS_PER_SECOND:-50}
DA_WORKER_CONCURRENCY: ${DA_WORKER_CONCURRENCY:-50}
FAUCET_ENABLED: ${FAUCET_ENABLED:-false}
FAUCET_PRIVATE_KEY: ${FAUCET_PRIVATE_KEY:-}
FAUCET_AMOUNT: ${FAUCET_AMOUNT:-}
FAUCET_COOLDOWN_MINUTES: ${FAUCET_COOLDOWN_MINUTES:-}
CHAIN_NAME: ${CHAIN_NAME:-Unknown}
CHAIN_LOGO_URL: ${CHAIN_LOGO_URL:-}
ACCENT_COLOR: ${ACCENT_COLOR:-}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/chartData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface DailyTxPoint {

export interface GasPricePoint {
bucket: string; // ISO timestamp
avg_gas_price: number; // wei
avg_gas_price: number | null; // wei
}

export function getBlocksChart(window: ChartWindow): Promise<BlockChartPoint[]> {
Expand Down
Loading
Loading