From 6ee6edb7ee3ce51cabd77d2dd3403d09024b311b Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:44:23 +0200 Subject: [PATCH 1/4] Fix empty gas price chart buckets --- .../atlas-server/src/api/handlers/stats.rs | 4 ++-- frontend/src/api/chartData.ts | 2 +- frontend/src/hooks/useChartData.ts | 18 +++++++++++++++++- frontend/src/pages/StatusPage.tsx | 8 ++++++-- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/backend/crates/atlas-server/src/api/handlers/stats.rs b/backend/crates/atlas-server/src/api/handlers/stats.rs index 5947df5..6218c3c 100644 --- a/backend/crates/atlas-server/src/api/handlers/stats.rs +++ b/backend/crates/atlas-server/src/api/handlers/stats.rs @@ -77,7 +77,7 @@ pub struct DailyTxPoint { #[derive(Serialize)] pub struct GasPricePoint { pub bucket: String, - pub avg_gas_price: f64, + pub avg_gas_price: Option, } /// GET /api/stats/blocks-chart?window=1h|6h|24h|7d @@ -221,7 +221,7 @@ pub async fn get_gas_price_chart( .into_iter() .map(|(bucket, avg_gas_price)| GasPricePoint { bucket: bucket.to_rfc3339(), - avg_gas_price: avg_gas_price.unwrap_or(0.0), + avg_gas_price, }) .collect(); diff --git a/frontend/src/api/chartData.ts b/frontend/src/api/chartData.ts index 724d5b3..4c7ac73 100644 --- a/frontend/src/api/chartData.ts +++ b/frontend/src/api/chartData.ts @@ -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 { diff --git a/frontend/src/hooks/useChartData.ts b/frontend/src/hooks/useChartData.ts index c00f0c1..2dee5a5 100644 --- a/frontend/src/hooks/useChartData.ts +++ b/frontend/src/hooks/useChartData.ts @@ -21,6 +21,22 @@ interface ChartData { gasPriceError: string | null; } +function fillMissingGasPriceBuckets(points: GasPricePoint[]): GasPricePoint[] { + let lastObservedPrice: number | null = null; + + return points.map((point) => { + if (point.avg_gas_price !== null) { + lastObservedPrice = point.avg_gas_price; + return point; + } + + return { + ...point, + avg_gas_price: lastObservedPrice, + }; + }); +} + function getChartErrorMessage(err: unknown, fallback: string): string { if (err && typeof err === 'object' && 'error' in err && typeof (err as { error: unknown }).error === 'string') { return (err as { error: string }).error; @@ -124,7 +140,7 @@ export function useChartData(window: ChartWindow): ChartData { try { setGasPriceError(null); const gasPrice = await getGasPriceChart(window); - if (mounted) setGasPriceChart(gasPrice); + if (mounted) setGasPriceChart(fillMissingGasPriceBuckets(gasPrice)); } catch (err) { if (mounted) { setGasPriceError(getChartErrorMessage(err, 'Failed to load gas price chart')); diff --git a/frontend/src/pages/StatusPage.tsx b/frontend/src/pages/StatusPage.tsx index 7b6009e..8d39b47 100644 --- a/frontend/src/pages/StatusPage.tsx +++ b/frontend/src/pages/StatusPage.tsx @@ -235,7 +235,7 @@ export default function StatusPage() { contentStyle={{ background: CHART_TOOLTIP_BG, border: `1px solid ${CHART_GRID}`, borderRadius: 8 }} labelStyle={{ color: CHART_AXIS_TEXT }} itemStyle={{ color: '#f8fafc' }} - formatter={(v: unknown) => [formatGwei(v as number), 'Avg Gas Price']} + formatter={(v: unknown) => [formatGwei(v as number | null), 'Avg Gas Price']} labelFormatter={(v) => formatBucketTooltip(v, window)} /> = 1_000) return `${(gwei / 1_000).toFixed(1)}K gwei`; if (gwei >= 1) return `${gwei.toFixed(2)} gwei`; From a737dd4cdbc04195e3f8ececcc84b050de288a1f Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:34:31 +0200 Subject: [PATCH 2/4] Use base fee fallback for gas chart --- backend/crates/atlas-common/src/types.rs | 3 ++- .../atlas-server/src/api/handlers/blocks.rs | 24 +++++++++---------- .../atlas-server/src/api/handlers/search.rs | 20 +++++++--------- .../atlas-server/src/api/handlers/sse.rs | 1 + .../atlas-server/src/api/handlers/stats.rs | 22 +++++++++++++---- .../atlas-server/src/api/handlers/status.rs | 1 + backend/crates/atlas-server/src/head.rs | 1 + .../crates/atlas-server/src/indexer/batch.rs | 7 ++++++ .../crates/atlas-server/src/indexer/copy.rs | 12 ++++++---- .../atlas-server/src/indexer/indexer.rs | 8 ++++++- ...0407000002_add_blocks_base_fee_per_gas.sql | 2 ++ frontend/src/hooks/useChartData.ts | 18 +------------- frontend/src/types/index.ts | 2 ++ 13 files changed, 69 insertions(+), 52 deletions(-) create mode 100644 backend/migrations/20260407000002_add_blocks_base_fee_per_gas.sql diff --git a/backend/crates/atlas-common/src/types.rs b/backend/crates/atlas-common/src/types.rs index 6edd8a9..8a3f139 100644 --- a/backend/crates/atlas-common/src/types.rs +++ b/backend/crates/atlas-common/src/types.rs @@ -12,6 +12,7 @@ pub struct Block { pub timestamp: i64, pub gas_used: i64, pub gas_limit: i64, + pub base_fee_per_gas: Option, pub transaction_count: i32, pub indexed_at: DateTime, } @@ -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)] diff --git a/backend/crates/atlas-server/src/api/handlers/blocks.rs b/backend/crates/atlas-server/src/api/handlers/blocks.rs index 67106f0..cb1d9b0 100644 --- a/backend/crates/atlas-server/src/api/handlers/blocks.rs +++ b/backend/crates/atlas-server/src/api/handlers/blocks.rs @@ -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), @@ -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 = 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 = 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) @@ -82,11 +81,10 @@ pub async fn get_block( State(state): State>, Path(number): Path, ) -> ApiResult> { - 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? diff --git a/backend/crates/atlas-server/src/api/handlers/search.rs b/backend/crates/atlas-server/src/api/handlers/search.rs index 523bc59..87a4d6a 100644 --- a/backend/crates/atlas-server/src/api/handlers/search.rs +++ b/backend/crates/atlas-server/src/api/handlers/search.rs @@ -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 { @@ -151,11 +151,10 @@ async fn search_block_by_hash( hash: &str, ) -> Result, 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 @@ -166,11 +165,10 @@ async fn search_block_by_number( state: &AppState, number: i64, ) -> Result, 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 diff --git a/backend/crates/atlas-server/src/api/handlers/sse.rs b/backend/crates/atlas-server/src/api/handlers/sse.rs index 16c60ae..4d1acb7 100644 --- a/backend/crates/atlas-server/src/api/handlers/sse.rs +++ b/backend/crates/atlas-server/src/api/handlers/sse.rs @@ -202,6 +202,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(), } diff --git a/backend/crates/atlas-server/src/api/handlers/stats.rs b/backend/crates/atlas-server/src/api/handlers/stats.rs index 6218c3c..ce6a7be 100644 --- a/backend/crates/atlas-server/src/api/handlers/stats.rs +++ b/backend/crates/atlas-server/src/api/handlers/stats.rs @@ -192,7 +192,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 @@ -202,13 +202,25 @@ 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 + COALESCE(t.avg_gas_price, ba.avg_base_fee_per_gas) AS avg_gas_price + 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 "#, ) diff --git a/backend/crates/atlas-server/src/api/handlers/status.rs b/backend/crates/atlas-server/src/api/handlers/status.rs index e85a195..b5f322c 100644 --- a/backend/crates/atlas-server/src/api/handlers/status.rs +++ b/backend/crates/atlas-server/src/api/handlers/status.rs @@ -93,6 +93,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(), } diff --git a/backend/crates/atlas-server/src/head.rs b/backend/crates/atlas-server/src/head.rs index 13f273e..50fd5ff 100644 --- a/backend/crates/atlas-server/src/head.rs +++ b/backend/crates/atlas-server/src/head.rs @@ -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(), } diff --git a/backend/crates/atlas-server/src/indexer/batch.rs b/backend/crates/atlas-server/src/indexer/batch.rs index 7c49f1c..25a8db0 100644 --- a/backend/crates/atlas-server/src/indexer/batch.rs +++ b/backend/crates/atlas-server/src/indexer/batch.rs @@ -36,6 +36,7 @@ pub(crate) struct BlockBatch { pub(crate) b_timestamps: Vec, pub(crate) b_gas_used: Vec, pub(crate) b_gas_limits: Vec, + pub(crate) b_base_fee_per_gas: Vec>, pub(crate) b_tx_counts: Vec, // transactions (receipt data merged in at collection time) @@ -164,6 +165,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()) @@ -174,6 +176,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, }) @@ -266,6 +269,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(); @@ -278,6 +284,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); } diff --git a/backend/crates/atlas-server/src/indexer/copy.rs b/backend/crates/atlas-server/src/indexer/copy.rs index 1c9dff3..0b83924 100644 --- a/backend/crates/atlas-server/src/indexer/copy.rs +++ b/backend/crates/atlas-server/src/indexer/copy.rs @@ -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; @@ -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( @@ -47,6 +48,7 @@ pub async fn copy_blocks( Type::INT8, Type::INT8, Type::INT8, + Type::TEXT, Type::INT4, Type::TIMESTAMPTZ, ], @@ -54,13 +56,14 @@ pub async fn copy_blocks( 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, ]; @@ -70,8 +73,8 @@ 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, @@ -79,6 +82,7 @@ pub async fn copy_blocks( 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", &[], diff --git a/backend/crates/atlas-server/src/indexer/indexer.rs b/backend/crates/atlas-server/src/indexer/indexer.rs index deccc07..61857df 100644 --- a/backend/crates/atlas-server/src/indexer/indexer.rs +++ b/backend/crates/atlas-server/src/indexer/indexer.rs @@ -424,7 +424,7 @@ impl Indexer { known_nft: &HashSet, fetched: FetchedBlock, ) { - use alloy::consensus::Transaction as TxTrait; + use alloy::consensus::{BlockHeader, Transaction as TxTrait}; let block = fetched.block; let block_num = fetched.number; @@ -448,6 +448,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 --- diff --git a/backend/migrations/20260407000002_add_blocks_base_fee_per_gas.sql b/backend/migrations/20260407000002_add_blocks_base_fee_per_gas.sql new file mode 100644 index 0000000..2a0be69 --- /dev/null +++ b/backend/migrations/20260407000002_add_blocks_base_fee_per_gas.sql @@ -0,0 +1,2 @@ +ALTER TABLE blocks +ADD COLUMN IF NOT EXISTS base_fee_per_gas NUMERIC(78, 0); diff --git a/frontend/src/hooks/useChartData.ts b/frontend/src/hooks/useChartData.ts index 2dee5a5..c00f0c1 100644 --- a/frontend/src/hooks/useChartData.ts +++ b/frontend/src/hooks/useChartData.ts @@ -21,22 +21,6 @@ interface ChartData { gasPriceError: string | null; } -function fillMissingGasPriceBuckets(points: GasPricePoint[]): GasPricePoint[] { - let lastObservedPrice: number | null = null; - - return points.map((point) => { - if (point.avg_gas_price !== null) { - lastObservedPrice = point.avg_gas_price; - return point; - } - - return { - ...point, - avg_gas_price: lastObservedPrice, - }; - }); -} - function getChartErrorMessage(err: unknown, fallback: string): string { if (err && typeof err === 'object' && 'error' in err && typeof (err as { error: unknown }).error === 'string') { return (err as { error: string }).error; @@ -140,7 +124,7 @@ export function useChartData(window: ChartWindow): ChartData { try { setGasPriceError(null); const gasPrice = await getGasPriceChart(window); - if (mounted) setGasPriceChart(fillMissingGasPriceBuckets(gasPrice)); + if (mounted) setGasPriceChart(gasPrice); } catch (err) { if (mounted) { setGasPriceError(getChartErrorMessage(err, 'Failed to load gas price chart')); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 73f560a..91fc875 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -6,6 +6,7 @@ export interface Block { timestamp: number; gas_used: number; gas_limit: number; + base_fee_per_gas?: string | null; transaction_count: number; indexed_at: string; da_status?: BlockDaStatus | null; @@ -122,6 +123,7 @@ export interface BlockSearchResult extends SearchResult { timestamp: number; gas_used: number; gas_limit: number; + base_fee_per_gas?: string | null; transaction_count: number; indexed_at: string; } From 5c2e357cbb074c5f43bd8f2497968f27ff666547 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:13:23 +0200 Subject: [PATCH 3/4] Add gas price fallback tests and fix compose env --- .../atlas-server/src/api/handlers/stats.rs | 36 ++++++++++++++++--- docker-compose.yml | 3 -- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/backend/crates/atlas-server/src/api/handlers/stats.rs b/backend/crates/atlas-server/src/api/handlers/stats.rs index ce6a7be..20ec9b7 100644 --- a/backend/crates/atlas-server/src/api/handlers/stats.rs +++ b/backend/crates/atlas-server/src/api/handlers/stats.rs @@ -80,6 +80,13 @@ pub struct GasPricePoint { pub avg_gas_price: Option, } +fn resolve_avg_gas_price( + tx_avg_gas_price: Option, + block_avg_base_fee_per_gas: Option, +) -> Option { + tx_avg_gas_price.or(block_avg_base_fee_per_gas) +} + /// GET /api/stats/blocks-chart?window=1h|6h|24h|7d /// /// Returns tx count and avg gas utilization bucketed over the given window. @@ -175,7 +182,7 @@ 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>, Query(params): Query, @@ -183,7 +190,7 @@ pub async fn get_gas_price_chart( let window = params.window; let bucket_secs = window.bucket_secs(); - let rows: Vec<(chrono::DateTime, Option)> = sqlx::query_as( + let rows: Vec<(chrono::DateTime, Option, Option)> = sqlx::query_as( r#" WITH latest AS (SELECT MAX(timestamp) AS max_ts FROM blocks), bounds AS ( @@ -216,7 +223,8 @@ pub async fn get_gas_price_chart( ) SELECT to_timestamp(gs::float8) AS bucket, - COALESCE(t.avg_gas_price, ba.avg_base_fee_per_gas) AS avg_gas_price + 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 @@ -231,9 +239,12 @@ pub async fn get_gas_price_chart( let points = rows .into_iter() - .map(|(bucket, avg_gas_price)| GasPricePoint { + .map(|(bucket, tx_avg_gas_price, block_avg_base_fee_per_gas)| GasPricePoint { bucket: bucket.to_rfc3339(), - avg_gas_price, + avg_gas_price: resolve_avg_gas_price( + tx_avg_gas_price, + block_avg_base_fee_per_gas, + ), }) .collect(); @@ -275,4 +286,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); + } } diff --git a/docker-compose.yml b/docker-compose.yml index 4ce3d37..859fb56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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:-} From 6b25defb3721c45456a6fe0041be680d579dceb0 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:48:30 +0200 Subject: [PATCH 4/4] Format gas price fallback handler --- .../crates/atlas-server/src/api/handlers/stats.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/backend/crates/atlas-server/src/api/handlers/stats.rs b/backend/crates/atlas-server/src/api/handlers/stats.rs index 20ec9b7..bf44246 100644 --- a/backend/crates/atlas-server/src/api/handlers/stats.rs +++ b/backend/crates/atlas-server/src/api/handlers/stats.rs @@ -239,13 +239,12 @@ pub async fn get_gas_price_chart( let points = rows .into_iter() - .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, - ), - }) + .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))