diff --git a/benchmarks/aurora-rpc.yml b/benchmarks/aurora-rpc.yml index e40060b5..ffb33df7 100644 --- a/benchmarks/aurora-rpc.yml +++ b/benchmarks/aurora-rpc.yml @@ -37,7 +37,7 @@ methodology: - "Chain scope: every query on this page is pinned to `chain=\"aurora\"`. Provider coverage: 1 no-key endpoint (Aurora Official). Ankr and dRPC require an API key for Aurora and have been removed." findings: - - "{{best_name}} currently leads free Aurora RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h) — 1 measured provider." + - "{{best_name}} currently leads free Aurora RPC at {{best_p50}} (`eth_getBlockByNumber` p50, 24h), 1 measured provider." faq: - q: "What is the fastest free Aurora RPC right now?" diff --git a/benchmarks/kaia-rpc.yml b/benchmarks/kaia-rpc.yml index 338f5432..38c54c32 100644 --- a/benchmarks/kaia-rpc.yml +++ b/benchmarks/kaia-rpc.yml @@ -38,7 +38,7 @@ methodology: - "Chain scope: every query on this page is pinned to `chain=\"kaia\"`. Provider coverage: 1 no-key endpoint (Kaia Foundation). dRPC removed (500 paid plan only)." findings: - - "{{best_name}} currently leads free Kaia RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h) — 1 measured provider." + - "{{best_name}} currently leads free Kaia RPC at {{best_p50}} (`eth_getBlockByNumber(\"latest\", false)` p50, 24h), 1 measured provider." faq: - q: "What is the fastest free Kaia RPC right now?" diff --git a/benchmarks/layerzero-message-latency.yml b/benchmarks/layerzero-message-latency.yml index 343b036c..20f2a9c1 100644 --- a/benchmarks/layerzero-message-latency.yml +++ b/benchmarks/layerzero-message-latency.yml @@ -84,11 +84,11 @@ providers: tag: Ethereum → any chain, ~12s block time floor before DVN verification begins formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Ethereum." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="ethereum"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="ethereum"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="ethereum"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="ethereum"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="ethereum"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="ethereum"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="ethereum"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="ethereum"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="ethereum"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="ethereum"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="ethereum"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="ethereum"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="ethereum"}[1h]))) - slug: solana @@ -96,11 +96,11 @@ providers: tag: Solana → any chain, sub-second slot inclusion + DVN verification formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Solana." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="solana"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="solana"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="solana"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="solana"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="solana"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="solana"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="solana"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="solana"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="solana"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="solana"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="solana"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="solana"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="solana"}[1h]))) - slug: bnb @@ -108,11 +108,11 @@ providers: tag: BNB Chain → any chain, ~3s block time + DVN verification formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from BNB Chain." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="bnb"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="bnb"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="bnb"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="bnb"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="bnb"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="bnb"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="bnb"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="bnb"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="bnb"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="bnb"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="bnb"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="bnb"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="bnb"}[1h]))) - slug: arbitrum @@ -120,11 +120,11 @@ providers: tag: Arbitrum One → any chain, sub-second L2 block + DVN verification formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Arbitrum." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="arbitrum"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="arbitrum"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="arbitrum"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="arbitrum"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="arbitrum"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="arbitrum"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="arbitrum"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="arbitrum"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="arbitrum"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="arbitrum"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="arbitrum"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="arbitrum"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="arbitrum"}[1h]))) - slug: base @@ -132,11 +132,11 @@ providers: tag: Base → any chain, 2s block time + DVN verification formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Base." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="base"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="base"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="base"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="base"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="base"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="base"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="base"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="base"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="base"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="base"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="base"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="base"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="base"}[1h]))) - slug: optimism @@ -144,11 +144,11 @@ providers: tag: Optimism → any chain, 2s block time + DVN verification formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Optimism." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="optimism"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="optimism"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="optimism"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="optimism"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="optimism"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="optimism"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="optimism"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="optimism"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="optimism"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="optimism"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="optimism"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="optimism"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="optimism"}[1h]))) - slug: polygon @@ -156,11 +156,11 @@ providers: tag: Polygon PoS → any chain, Bor block time + DVN verification formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Polygon." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="polygon"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="polygon"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="polygon"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="polygon"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="polygon"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="polygon"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="polygon"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="polygon"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="polygon"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="polygon"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="polygon"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="polygon"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="polygon"}[1h]))) - slug: avalanche @@ -168,11 +168,11 @@ providers: tag: Avalanche C-Chain → any chain, Snowman++ sub-second finality + DVN verification formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Avalanche." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="avalanche"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="avalanche"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="avalanche"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="avalanche"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="avalanche"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="avalanche"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="avalanche"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="avalanche"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="avalanche"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="avalanche"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="avalanche"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="avalanche"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="avalanche"}[1h]))) - slug: robinhood @@ -180,11 +180,11 @@ providers: tag: Robinhood Chain → any chain, Nitro block time + DVN verification formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Robinhood Chain." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="robinhood"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="robinhood"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="robinhood"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="robinhood"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="robinhood"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="robinhood"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="robinhood"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="robinhood"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="robinhood"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="robinhood"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="robinhood"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="robinhood"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="robinhood"}[1h]))) - slug: monad @@ -192,130 +192,130 @@ providers: tag: Monad → any chain, sub-second block + DVN verification formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Monad." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="monad"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="monad"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="monad"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="monad"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="monad"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="monad"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="monad"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="monad"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="monad"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="monad"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="monad"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="monad"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="monad"}[1h]))) - slug: berachain name: Berachain tag: Berachain → any chain formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Berachain." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="berachain"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="berachain"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="berachain"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="berachain"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="berachain"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="berachain"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="berachain"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="berachain"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="berachain"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="berachain"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="berachain"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="berachain"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="berachain"}[1h]))) - slug: celo name: Celo tag: Celo → any chain formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Celo." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="celo"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="celo"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="celo"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="celo"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="celo"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="celo"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="celo"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="celo"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="celo"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="celo"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="celo"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="celo"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="celo"}[1h]))) - slug: injective name: Injective tag: Injective (Cosmos) → any chain formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Injective." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="injective"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="injective"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="injective"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="injective"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="injective"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="injective"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="injective"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="injective"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="injective"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="injective"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="injective"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="injective"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="injective"}[1h]))) - slug: ink name: Ink tag: Ink L2 → any chain formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Ink." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="ink"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="ink"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="ink"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="ink"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="ink"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="ink"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="ink"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="ink"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="ink"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="ink"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="ink"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="ink"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="ink"}[1h]))) - slug: linea name: Linea tag: Linea → any chain formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Linea." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="linea"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="linea"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="linea"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="linea"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="linea"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="linea"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="linea"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="linea"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="linea"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="linea"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="linea"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="linea"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="linea"}[1h]))) - slug: mantle name: Mantle tag: Mantle → any chain formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Mantle." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="mantle"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="mantle"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="mantle"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="mantle"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="mantle"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="mantle"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="mantle"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="mantle"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="mantle"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="mantle"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="mantle"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="mantle"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="mantle"}[1h]))) - slug: moonbeam name: Moonbeam tag: Moonbeam parachain → any chain formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Moonbeam." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="moonbeam"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="moonbeam"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="moonbeam"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="moonbeam"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="moonbeam"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="moonbeam"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="moonbeam"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="moonbeam"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="moonbeam"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="moonbeam"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="moonbeam"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="moonbeam"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="moonbeam"}[1h]))) - slug: scroll name: Scroll tag: Scroll → any chain formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Scroll." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="scroll"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="scroll"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="scroll"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="scroll"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="scroll"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="scroll"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="scroll"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="scroll"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="scroll"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="scroll"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="scroll"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="scroll"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="scroll"}[1h]))) - slug: sui name: Sui tag: Sui → any chain formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Sui." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="sui"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="sui"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="sui"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="sui"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="sui"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="sui"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="sui"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="sui"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="sui"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="sui"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="sui"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="sui"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="sui"}[1h]))) - slug: unichain name: Unichain tag: Unichain → any chain formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from Unichain." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="unichain"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="unichain"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="unichain"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="unichain"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="unichain"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="unichain"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="unichain"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="unichain"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="unichain"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="unichain"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="unichain"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="unichain"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="unichain"}[1h]))) - slug: world-chain name: World Chain tag: World Chain → any chain formula: "50th percentile over 24h of LayerZero end-to-end delivery latency for messages sourced from World Chain." queries: - p50: histogram_quantile(0.50, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="world-chain"}) - p90: histogram_quantile(0.90, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="world-chain"}) - p99: histogram_quantile(0.99, ocb:lz_message_latency_ms_bucket:rate24h{source_chain="world-chain"}) - mean: ocb:lz_message_latency_ms_sum:rate24h{source_chain="world-chain"} / ocb:lz_message_latency_ms_count:rate24h{source_chain="world-chain"} - sample_size: ocb:lz_message_seen:increase24h{source_chain="world-chain"} + p50: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="world-chain"}[24h]))) + p90: histogram_quantile(0.90, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="world-chain"}[24h]))) + p99: histogram_quantile(0.99, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="world-chain"}[24h]))) + mean: sum(rate(lz_message_latency_milliseconds_sum{source_chain="world-chain"}[24h])) / sum(rate(lz_message_latency_milliseconds_count{source_chain="world-chain"}[24h])) + sample_size: sum(increase(lz_message_seen_total{source_chain="world-chain"}[24h])) series: histogram_quantile(0.50, sum by(le) (rate(lz_message_latency_milliseconds_bucket{source_chain="world-chain"}[1h]))) diff --git a/harnesses/aggregator-head-lag/cmd/script/defined_auth.go b/harnesses/aggregator-head-lag/cmd/script/defined_auth.go index 717e75ad..17cb1928 100644 --- a/harnesses/aggregator-head-lag/cmd/script/defined_auth.go +++ b/harnesses/aggregator-head-lag/cmd/script/defined_auth.go @@ -58,12 +58,45 @@ func decodeJWTExpiration(token string) (time.Time, error) { return time.Unix(claims.Exp, 0), nil } +// tryTokenService calls the Paris-box sidecar (DEFINED_TOKEN_SERVICE_URL) for a fresh JWE. +func tryTokenService(baseURL string) (string, error) { + client := &http.Client{Timeout: 5 * time.Second} + for _, path := range []string{"/token", "/jwt", "/"} { + resp, err := client.Get(baseURL + path) + if err != nil { + continue + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + if resp.StatusCode != 200 { + continue + } + var parsed struct{ Token string `json:"token"` } + if err := json.Unmarshal(body, &parsed); err == nil && parsed.Token != "" { + return parsed.Token, nil + } + s := strings.TrimSpace(string(body)) + if len(s) > 50 { + return s, nil + } + } + return "", fmt.Errorf("no working path on sidecar") +} + // GetDefinedJWTToken returns a cached JWT token or generates a new one if expired. -// If CODEX_JWT env var is set, it is returned directly (bypasses session-cookie flow). +// Priority: CODEX_JWT env var > DEFINED_TOKEN_SERVICE_URL sidecar > inline mint. func GetDefinedJWTToken(sessionCookie string) (string, error) { if jwt := os.Getenv("CODEX_JWT"); jwt != "" { return jwt, nil } + // Sidecar token service (Paris box chromedp scraper, auto-refreshes every 25 min) + if svcURL := os.Getenv("DEFINED_TOKEN_SERVICE_URL"); svcURL != "" { + if tok, err := tryTokenService(svcURL); err == nil && tok != "" { + fmt.Printf("[DEFINED-AUTH] Got token from sidecar (len=%d)\n", len(tok)) + return tok, nil + } + } + globalTokenCache.mu.RLock() // Check if we have a valid cached token diff --git a/harnesses/solana-exec/Dockerfile b/harnesses/solana-exec/Dockerfile new file mode 100644 index 00000000..a3752b65 --- /dev/null +++ b/harnesses/solana-exec/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.24-alpine AS builder +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o /app/collector ./cmd/collector +RUN CGO_ENABLED=0 go build -o /app/materializer ./cmd/materializer +RUN CGO_ENABLED=0 go build -o /app/api ./cmd/api + +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates tzdata +COPY --from=builder /app/collector /app/collector +COPY --from=builder /app/materializer /app/materializer +COPY --from=builder /app/api /app/api +COPY migrations/ /app/migrations/ diff --git a/harnesses/solana-exec/cmd/api/main.go b/harnesses/solana-exec/cmd/api/main.go new file mode 100644 index 00000000..9cecff45 --- /dev/null +++ b/harnesses/solana-exec/cmd/api/main.go @@ -0,0 +1,189 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "net/http" + "os" + "sort" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func main() { + ctx := context.Background() + pool, err := pgxpool.New(ctx, mustEnv("DATABASE_URL")) + if err != nil { + log.Fatalf("exec-api: connect: %v", err) + } + defer pool.Close() + + mux := http.NewServeMux() + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"ok"}`)) + }) + mux.HandleFunc("/api/exec-leaderboard", corsJSON(handleExecLeaderboard(pool))) + + addr := ":2116" + log.Printf("exec-api listening on %s", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatalf("exec-api: serve: %v", err) + } +} + +// WindowStats holds aggregated execution metrics for one time window. +type WindowStats struct { + TxCount int64 `json:"txCount"` + AvgPriorityFeeLamports float64 `json:"avgPriorityFeeLamports"` + P50CUPriceMicro float64 `json:"p50CUPriceMicro"` + P95CUPriceMicro float64 `json:"p95CUPriceMicro"` + AvgPlatformFeeLamports float64 `json:"avgPlatformFeeLamports"` + JitoRate float64 `json:"jitoRate"` + AvgCUConsumed float64 `json:"avgCUConsumed"` +} + +type PlatformRow struct { + Platform string `json:"platform"` + LatestBkt string `json:"latestBucket"` + Windows map[string]WindowStats `json:"windows"` +} + +type ExecLeaderboardResponse struct { + UpdatedAt string `json:"updatedAt"` + Platforms []PlatformRow `json:"platforms"` +} + +func handleExecLeaderboard(pool *pgxpool.Pool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + rows, err := pool.Query(ctx, ` + SELECT + platform, + AVG(avg_priority_fee_lamports) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_prio, + AVG(p50_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_p50, + AVG(p95_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_p95, + AVG(avg_platform_fee_lamports) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_pfee, + AVG(jito_rate) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_jito, + AVG(avg_cu_consumed) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_cu, + SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '24 hours') AS h24_count, + + AVG(avg_priority_fee_lamports) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_prio, + AVG(p50_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_p50, + AVG(p95_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_p95, + AVG(avg_platform_fee_lamports) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_pfee, + AVG(jito_rate) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_jito, + AVG(avg_cu_consumed) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_cu, + SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '7 days') AS d7_count, + + AVG(avg_priority_fee_lamports) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_prio, + AVG(p50_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_p50, + AVG(p95_cu_price_micro) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_p95, + AVG(avg_platform_fee_lamports) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_pfee, + AVG(jito_rate) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_jito, + AVG(avg_cu_consumed) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_cu, + SUM(tx_count) FILTER (WHERE bucket_start >= now() - INTERVAL '30 days') AS d30_count, + + MAX(bucket_start)::text AS latest_bucket + FROM solana_exec_facts + GROUP BY platform`, + ) + if err != nil { + log.Printf("exec-api: query: %v", err) + http.Error(w, "internal", http.StatusInternalServerError) + return + } + defer rows.Close() + + var platforms []PlatformRow + for rows.Next() { + var plt, latestBkt string + var h24Prio, h24P50, h24P95, h24Pfee, h24Jito, h24Cu *float64 + var h24Count *int64 + var d7Prio, d7P50, d7P95, d7Pfee, d7Jito, d7Cu *float64 + var d7Count *int64 + var d30Prio, d30P50, d30P95, d30Pfee, d30Jito, d30Cu *float64 + var d30Count *int64 + + if err := rows.Scan( + &plt, + &h24Prio, &h24P50, &h24P95, &h24Pfee, &h24Jito, &h24Cu, &h24Count, + &d7Prio, &d7P50, &d7P95, &d7Pfee, &d7Jito, &d7Cu, &d7Count, + &d30Prio, &d30P50, &d30P95, &d30Pfee, &d30Jito, &d30Cu, &d30Count, + &latestBkt, + ); err != nil { + log.Printf("exec-api: scan: %v", err) + continue + } + + p := PlatformRow{ + Platform: plt, + LatestBkt: latestBkt, + Windows: map[string]WindowStats{ + "24h": buildWindow(h24Prio, h24P50, h24P95, h24Pfee, h24Jito, h24Cu, h24Count), + "7d": buildWindow(d7Prio, d7P50, d7P95, d7Pfee, d7Jito, d7Cu, d7Count), + "30d": buildWindow(d30Prio, d30P50, d30P95, d30Pfee, d30Jito, d30Cu, d30Count), + }, + } + platforms = append(platforms, p) + } + + // Sort by 30d tx count descending. + sort.Slice(platforms, func(i, j int) bool { + return platforms[i].Windows["30d"].TxCount > platforms[j].Windows["30d"].TxCount + }) + + resp := ExecLeaderboardResponse{ + UpdatedAt: time.Now().UTC().Format(time.RFC3339), + Platforms: platforms, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + } +} + +func buildWindow(prio, p50, p95, pfee, jito, cu *float64, count *int64) WindowStats { + ws := WindowStats{} + if count != nil { + ws.TxCount = *count + } + if prio != nil { + ws.AvgPriorityFeeLamports = *prio + } + if p50 != nil { + ws.P50CUPriceMicro = *p50 + } + if p95 != nil { + ws.P95CUPriceMicro = *p95 + } + if pfee != nil { + ws.AvgPlatformFeeLamports = *pfee + } + if jito != nil { + ws.JitoRate = *jito + } + if cu != nil { + ws.AvgCUConsumed = *cu + } + return ws +} + +func corsJSON(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Cache-Control", "public, max-age=60, stale-while-revalidate=300") + h(w, r) + } +} + +func mustEnv(key string) string { + v := os.Getenv(key) + if v == "" { + log.Fatalf("missing env: %s", key) + } + return v +} diff --git a/harnesses/solana-exec/cmd/collector/main.go b/harnesses/solana-exec/cmd/collector/main.go new file mode 100644 index 00000000..5dcd5b05 --- /dev/null +++ b/harnesses/solana-exec/cmd/collector/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/solana-exec/internal/helius" + "github.com/ChainBench/OpenChainBench/harnesses/solana-exec/internal/platform" + "github.com/ChainBench/OpenChainBench/harnesses/solana-exec/internal/store" +) + +// baseFeePerSig is the fixed Solana base fee per signature in lamports. +const baseFeePerSig = 5000 + +func main() { + ctx := context.Background() + + db, err := store.New(ctx, mustEnv("DATABASE_URL")) + if err != nil { + log.Fatalf("collector: %v", err) + } + defer db.Close() + + h := helius.New(mustEnv("HELIUS_API_KEY")) + + log.Printf("collector: monitoring %d platforms", len(platform.FeeAccounts)) + + for { + for plt, feeAccount := range platform.FeeAccounts { + if err := collect(ctx, db, h, plt, feeAccount); err != nil { + log.Printf("collector: %s: %v", plt, err) + } + } + time.Sleep(10 * time.Minute) + } +} + +func collect(ctx context.Context, db *store.DB, h *helius.Client, plt, feeAccount string) error { + cursor, err := db.GetCursor(ctx, plt) + if err != nil { + return fmt.Errorf("get cursor: %w", err) + } + + // Fetch signatures newer than last seen. Results are newest-first. + sigs, err := h.GetSignaturesForAddress(ctx, feeAccount, 100, cursor.LastSig) + if err != nil { + return fmt.Errorf("get sigs: %w", err) + } + if len(sigs) == 0 { + return nil + } + + // Reverse to process oldest-first so cursor is always the true watermark. + reversed := make([]helius.SigEntry, len(sigs)) + for i, s := range sigs { + reversed[len(sigs)-1-i] = s + } + + // Pre-filter failed txs to avoid wasting enhanced-API credits. + // Failed txs don't generate platform fees; priority-fee stats should reflect + // the cost of successful trades only. + var sigStrs []string + for _, s := range reversed { + if s.Err == nil { + sigStrs = append(sigStrs, s.Signature) + } + } + + if len(sigStrs) == 0 { + // All sigs in this batch were failed txs; advance cursor and skip. + newest := sigs[0] + return db.SaveCursor(ctx, plt, newest.Signature, newest.Slot) + } + + txs, err := h.GetEnhancedTransactions(ctx, sigStrs) + if err != nil { + return fmt.Errorf("get enhanced txs: %w", err) + } + + events := make([]store.ExecEvent, 0, len(txs)) + for _, tx := range txs { + if tx.TransactionError != nil || tx.Timestamp == 0 { + continue + } + + priorityFee := tx.Fee - baseFeePerSig + if priorityFee < 0 { + priorityFee = 0 + } + + var platformFeeLamports, jitoTipLamports int64 + isJito := false + for _, xfer := range tx.NativeTransfers { + if xfer.ToUserAccount == feeAccount { + platformFeeLamports += xfer.Amount + } + if platform.JitoTipAccounts[xfer.ToUserAccount] { + jitoTipLamports += xfer.Amount + isJito = true + } + } + + var cuPriceMicro int64 + if tx.ComputeUnitsConsumed > 0 && priorityFee > 0 { + cuPriceMicro = priorityFee * 1_000_000 / tx.ComputeUnitsConsumed + } + + events = append(events, store.ExecEvent{ + Sig: tx.Signature, + Platform: plt, + Slot: tx.Slot, + BlockTime: time.Unix(tx.Timestamp, 0).UTC(), + TotalFeeLamports: tx.Fee, + PriorityFeeLamports: priorityFee, + PlatformFeeLamports: platformFeeLamports, + IsJitoBundle: isJito, + JitoTipLamports: jitoTipLamports, + CUConsumed: tx.ComputeUnitsConsumed, + CUPriceMicro: cuPriceMicro, + }) + } + + if err := db.UpsertEvents(ctx, events); err != nil { + return fmt.Errorf("upsert: %w", err) + } + + // Advance cursor to the newest sig (first in original order = last in reversed). + newest := sigs[0] + if err := db.SaveCursor(ctx, plt, newest.Signature, newest.Slot); err != nil { + return fmt.Errorf("save cursor: %w", err) + } + + log.Printf("collector: %s: ingested %d txs (newest slot %d)", plt, len(events), newest.Slot) + return nil +} + +func mustEnv(key string) string { + v := os.Getenv(key) + if v == "" { + log.Fatalf("missing env: %s", key) + } + return v +} diff --git a/harnesses/solana-exec/cmd/materializer/main.go b/harnesses/solana-exec/cmd/materializer/main.go new file mode 100644 index 00000000..98fc753a --- /dev/null +++ b/harnesses/solana-exec/cmd/materializer/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "log" + "os" + "time" + + "github.com/ChainBench/OpenChainBench/harnesses/solana-exec/internal/platform" + "github.com/ChainBench/OpenChainBench/harnesses/solana-exec/internal/store" +) + +func main() { + ctx := context.Background() + + db, err := store.New(ctx, mustEnv("DATABASE_URL")) + if err != nil { + log.Fatalf("materializer: %v", err) + } + defer db.Close() + + for { + for plt := range platform.FeeAccounts { + if err := db.Materialize(ctx, plt); err != nil { + log.Printf("materializer: %s: %v", plt, err) + } else { + log.Printf("materializer: %s: done", plt) + } + } + time.Sleep(5 * time.Minute) + } +} + +func mustEnv(key string) string { + v := os.Getenv(key) + if v == "" { + log.Fatalf("missing env: %s", key) + } + return v +} diff --git a/harnesses/solana-exec/docker-compose.solana-exec.yml b/harnesses/solana-exec/docker-compose.solana-exec.yml new file mode 100644 index 00000000..63d252d0 --- /dev/null +++ b/harnesses/solana-exec/docker-compose.solana-exec.yml @@ -0,0 +1,46 @@ +# Fragment to merge into /opt/ocb/docker-compose.yml on VPS. +# Requires: ocb-postgres (from docker-compose.apps.yml), HELIUS_API_KEY in env. + +services: + solana-exec-collector: + build: /opt/ocb/harnesses/solana-exec + container_name: ocb-solana-exec-collector + restart: unless-stopped + env_file: /run/ocb/.env.solana-exec + command: ["/app/collector"] + networks: [web] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "20m", max-file: "5" } + + solana-exec-materializer: + build: /opt/ocb/harnesses/solana-exec + container_name: ocb-solana-exec-materializer + restart: unless-stopped + env_file: /run/ocb/.env.solana-exec + command: ["/app/materializer"] + networks: [web] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } + + solana-exec-api: + build: /opt/ocb/harnesses/solana-exec + container_name: ocb-solana-exec-api + restart: unless-stopped + env_file: /run/ocb/.env.solana-exec + command: ["/app/api"] + networks: [web] + expose: ["2116"] + depends_on: + postgres: + condition: service_healthy + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } diff --git a/harnesses/solana-exec/env.solana-exec.example b/harnesses/solana-exec/env.solana-exec.example new file mode 100644 index 00000000..d341a264 --- /dev/null +++ b/harnesses/solana-exec/env.solana-exec.example @@ -0,0 +1,10 @@ +# /run/ocb/.env.solana-exec — encrypt with SOPS before deploy +# Shares the same Postgres instance as apps harness. + +POSTGRES_USER=ocb +POSTGRES_PASSWORD=CHANGEME +POSTGRES_DB=apps +DATABASE_URL=postgresql://ocb:CHANGEME@ocb-postgres:5432/apps?sslmode=disable + +# Helius API key — https://helius.dev (free tier works for start) +HELIUS_API_KEY=CHANGEME diff --git a/harnesses/solana-exec/go.mod b/harnesses/solana-exec/go.mod new file mode 100644 index 00000000..868e901f --- /dev/null +++ b/harnesses/solana-exec/go.mod @@ -0,0 +1,14 @@ +module github.com/ChainBench/OpenChainBench/harnesses/solana-exec + +go 1.24.0 + +require github.com/jackc/pgx/v5 v5.7.2 + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/harnesses/solana-exec/go.sum b/harnesses/solana-exec/go.sum new file mode 100644 index 00000000..731b5dfd --- /dev/null +++ b/harnesses/solana-exec/go.sum @@ -0,0 +1,28 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/harnesses/solana-exec/internal/helius/client.go b/harnesses/solana-exec/internal/helius/client.go new file mode 100644 index 00000000..b35fd148 --- /dev/null +++ b/harnesses/solana-exec/internal/helius/client.go @@ -0,0 +1,143 @@ +package helius + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +// Client wraps the Helius RPC + enhanced-transactions APIs. +type Client struct { + httpClient *http.Client + apiKey string + rpcURL string // https://mainnet.helius-rpc.com/?api-key=KEY + enhURL string // https://api.helius.xyz/v0/transactions?api-key=KEY +} + +func New(apiKey string) *Client { + return &Client{ + httpClient: &http.Client{Timeout: 30 * time.Second}, + apiKey: apiKey, + rpcURL: fmt.Sprintf("https://mainnet.helius-rpc.com/?api-key=%s", apiKey), + enhURL: fmt.Sprintf("https://api.helius.xyz/v0/transactions?api-key=%s", apiKey), + } +} + +// SigEntry is one result from getSignaturesForAddress. +type SigEntry struct { + Signature string `json:"signature"` + Slot uint64 `json:"slot"` + BlockTime int64 `json:"blockTime"` // unix seconds, may be 0 + Err any `json:"err"` // nil = success +} + +// GetSignaturesForAddress fetches up to `limit` finalized signatures +// for `address`. Pass `until=""` to get the most recent; pass a sig +// to get only transactions newer than that sig (exclusive). +// Results are returned newest-first. +func (c *Client) GetSignaturesForAddress(ctx context.Context, address string, limit int, until string) ([]SigEntry, error) { + params := map[string]any{ + "limit": limit, + "commitment": "finalized", + } + if until != "" { + params["until"] = until + } + + body, _ := json.Marshal(map[string]any{ + "jsonrpc": "2.0", + "id": 1, + "method": "getSignaturesForAddress", + "params": []any{address, params}, + }) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.rpcURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("helius RPC HTTP %d", resp.StatusCode) + } + + var out struct { + Result []SigEntry `json:"result"` + Error *struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error"` + } + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return nil, fmt.Errorf("helius RPC decode: %w", err) + } + if out.Error != nil { + return nil, fmt.Errorf("helius RPC error %d: %s", out.Error.Code, out.Error.Message) + } + return out.Result, nil +} + +// NativeTransfer is a SOL transfer extracted by Helius. +type NativeTransfer struct { + FromUserAccount string `json:"fromUserAccount"` + ToUserAccount string `json:"toUserAccount"` + Amount int64 `json:"amount"` // lamports +} + +// EnhancedTx is the Helius-parsed transaction. +type EnhancedTx struct { + Signature string `json:"signature"` + Slot uint64 `json:"slot"` + Timestamp int64 `json:"timestamp"` // unix seconds + Fee int64 `json:"fee"` // total fee lamports + FeePayer string `json:"feePayer"` + NativeTransfers []NativeTransfer `json:"nativeTransfers"` + TransactionError any `json:"transactionError"` + ComputeUnitsConsumed int64 `json:"computeUnitsConsumed"` +} + +// GetEnhancedTransactions fetches enhanced parsed transactions for up to 100 +// signatures in a single request. Helius caps the batch at 100. +func (c *Client) GetEnhancedTransactions(ctx context.Context, sigs []string) ([]EnhancedTx, error) { + if len(sigs) == 0 { + return nil, nil + } + if len(sigs) > 100 { + return nil, fmt.Errorf("helius: enhanced batch max 100 sigs, got %d", len(sigs)) + } + + body, _ := json.Marshal(map[string]any{ + "transactions": sigs, + }) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.enhURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("helius enhanced HTTP %d", resp.StatusCode) + } + + var txs []EnhancedTx + if err := json.NewDecoder(resp.Body).Decode(&txs); err != nil { + return nil, fmt.Errorf("helius enhanced decode: %w", err) + } + return txs, nil +} diff --git a/harnesses/solana-exec/internal/platform/platforms.go b/harnesses/solana-exec/internal/platform/platforms.go new file mode 100644 index 00000000..16864d1d --- /dev/null +++ b/harnesses/solana-exec/internal/platform/platforms.go @@ -0,0 +1,24 @@ +package platform + +// FeeAccounts maps platform name → its on-chain fee-receiver account. +// These are the accounts that receive platform fees from user trades. +// Adding a new platform = one line here. +var FeeAccounts = map[string]string{ + "pump.fun": "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM", + // TODO: add FOMO, Axiom, GMGN once their fee accounts are confirmed + // Discover by inspecting any known tx from each platform and finding + // the SOL recipient that is not the user, not Jito, not rent. +} + +// JitoTipAccounts is the authoritative set of 8 Jito tip wallets. +// Verified against solana-tx-landing harness wallets.go (2026-08). +var JitoTipAccounts = map[string]bool{ + "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5": true, + "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe": true, + "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY": true, + "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49": true, + "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh": true, + "ADuUkR4vqLUMWXxW9gh6D6L8pivKeVBBjNo7XZQshxw3": true, + "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL": true, + "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT": true, +} diff --git a/harnesses/solana-exec/internal/store/store.go b/harnesses/solana-exec/internal/store/store.go new file mode 100644 index 00000000..1ed3f826 --- /dev/null +++ b/harnesses/solana-exec/internal/store/store.go @@ -0,0 +1,133 @@ +package store + +import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +type DB struct { + pool *pgxpool.Pool +} + +func New(ctx context.Context, connStr string) (*DB, error) { + pool, err := pgxpool.New(ctx, connStr) + if err != nil { + return nil, fmt.Errorf("store: connect: %w", err) + } + return &DB{pool: pool}, nil +} + +func (db *DB) Close() { db.pool.Close() } + +func (db *DB) Pool() *pgxpool.Pool { return db.pool } + +// ExecEvent is a parsed Solana transaction from a monitored platform. +type ExecEvent struct { + Sig string + Platform string + Slot uint64 + BlockTime time.Time + TotalFeeLamports int64 + PriorityFeeLamports int64 + PlatformFeeLamports int64 + IsJitoBundle bool + JitoTipLamports int64 + CUConsumed int64 + CUPriceMicro int64 +} + +// UpsertEvents inserts exec events idempotently; existing sigs are skipped. +func (db *DB) UpsertEvents(ctx context.Context, events []ExecEvent) error { + if len(events) == 0 { + return nil + } + tx, err := db.pool.Begin(ctx) + if err != nil { + return fmt.Errorf("store: begin: %w", err) + } + defer tx.Rollback(ctx) + + for _, e := range events { + _, err := tx.Exec(ctx, ` + INSERT INTO solana_exec_events + (sig, platform, slot, block_time, + total_fee_lamports, priority_fee_lamports, platform_fee_lamports, + is_jito_bundle, jito_tip_lamports, cu_consumed, cu_price_micro) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) + ON CONFLICT (sig) DO NOTHING`, + e.Sig, e.Platform, e.Slot, e.BlockTime, + e.TotalFeeLamports, e.PriorityFeeLamports, e.PlatformFeeLamports, + e.IsJitoBundle, e.JitoTipLamports, e.CUConsumed, e.CUPriceMicro, + ) + if err != nil { + return fmt.Errorf("store: upsert %s: %w", e.Sig, err) + } + } + return tx.Commit(ctx) +} + +// Cursor holds the last processed signature for a platform. +type Cursor struct { + LastSig string + Slot uint64 +} + +func (db *DB) GetCursor(ctx context.Context, platform string) (Cursor, error) { + var c Cursor + err := db.pool.QueryRow(ctx, + `SELECT last_sig, slot FROM solana_exec_cursors WHERE platform = $1`, platform, + ).Scan(&c.LastSig, &c.Slot) + if err != nil { + return Cursor{}, nil // no cursor yet = start from recent + } + return c, nil +} + +func (db *DB) SaveCursor(ctx context.Context, platform, lastSig string, slot uint64) error { + _, err := db.pool.Exec(ctx, ` + INSERT INTO solana_exec_cursors (platform, last_sig, slot, updated_at) + VALUES ($1,$2,$3,now()) + ON CONFLICT (platform) DO UPDATE SET + last_sig=EXCLUDED.last_sig, slot=EXCLUDED.slot, updated_at=now()`, + platform, lastSig, slot, + ) + return err +} + +// Materialize recomputes hourly facts for the given platform and time range. +func (db *DB) Materialize(ctx context.Context, platform string) error { + _, err := db.pool.Exec(ctx, ` + INSERT INTO solana_exec_facts + (platform, bucket_start, tx_count, + avg_priority_fee_lamports, p50_cu_price_micro, p95_cu_price_micro, + avg_platform_fee_lamports, jito_rate, avg_cu_consumed, computed_at) + SELECT + platform, + date_trunc('hour', block_time) AS bucket_start, + COUNT(*) AS tx_count, + AVG(priority_fee_lamports) AS avg_priority_fee_lamports, + COALESCE(percentile_cont(0.5) WITHIN GROUP (ORDER BY cu_price_micro) FILTER (WHERE cu_price_micro > 0), 0) AS p50_cu_price_micro, + COALESCE(percentile_cont(0.95) WITHIN GROUP (ORDER BY cu_price_micro) FILTER (WHERE cu_price_micro > 0), 0) AS p95_cu_price_micro, + AVG(platform_fee_lamports) AS avg_platform_fee_lamports, + AVG(CASE WHEN is_jito_bundle THEN 1.0 ELSE 0.0 END) AS jito_rate, + AVG(cu_consumed) AS avg_cu_consumed, + now() + FROM solana_exec_events + WHERE platform = $1 + GROUP BY platform, date_trunc('hour', block_time) + ON CONFLICT (platform, bucket_start) DO UPDATE SET + tx_count = EXCLUDED.tx_count, + avg_priority_fee_lamports = EXCLUDED.avg_priority_fee_lamports, + p50_cu_price_micro = EXCLUDED.p50_cu_price_micro, + p95_cu_price_micro = EXCLUDED.p95_cu_price_micro, + avg_platform_fee_lamports = EXCLUDED.avg_platform_fee_lamports, + jito_rate = EXCLUDED.jito_rate, + avg_cu_consumed = EXCLUDED.avg_cu_consumed, + computed_at = now()`, + platform, + ) + return err +} diff --git a/harnesses/solana-exec/migrations/001_init.sql b/harnesses/solana-exec/migrations/001_init.sql new file mode 100644 index 00000000..840ee08b --- /dev/null +++ b/harnesses/solana-exec/migrations/001_init.sql @@ -0,0 +1,49 @@ +BEGIN; + +-- Raw events. One row per transaction that touched a monitored platform fee account. +-- Immutable: only upsert, never update. sig is globally unique on Solana. +CREATE TABLE IF NOT EXISTS solana_exec_events ( + sig TEXT PRIMARY KEY, + platform TEXT NOT NULL, + slot BIGINT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + -- fee = base + priority. Base ≈ 5000 lamports × num_signers. + total_fee_lamports BIGINT NOT NULL DEFAULT 0, + priority_fee_lamports BIGINT NOT NULL DEFAULT 0, + -- SOL transferred to the platform's fee account in this tx. + platform_fee_lamports BIGINT NOT NULL DEFAULT 0, + is_jito_bundle BOOLEAN NOT NULL DEFAULT false, + jito_tip_lamports BIGINT NOT NULL DEFAULT 0, + cu_consumed BIGINT NOT NULL DEFAULT 0, + -- microlamports per CU (from ComputeBudget instruction if present) + cu_price_micro BIGINT NOT NULL DEFAULT 0, + ingested_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS solana_exec_events_platform_bt + ON solana_exec_events (platform, block_time DESC); + +-- Derived hourly facts. Recomputable from solana_exec_events at any time. +CREATE TABLE IF NOT EXISTS solana_exec_facts ( + platform TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, -- truncated to 1h + tx_count INT NOT NULL DEFAULT 0, + avg_priority_fee_lamports DOUBLE PRECISION NOT NULL DEFAULT 0, + p50_cu_price_micro DOUBLE PRECISION NOT NULL DEFAULT 0, + p95_cu_price_micro DOUBLE PRECISION NOT NULL DEFAULT 0, + avg_platform_fee_lamports DOUBLE PRECISION NOT NULL DEFAULT 0, + jito_rate DOUBLE PRECISION NOT NULL DEFAULT 0, + avg_cu_consumed DOUBLE PRECISION NOT NULL DEFAULT 0, + computed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (platform, bucket_start) +); + +-- Cursor per platform: tracks last-seen signature to enable incremental polling. +CREATE TABLE IF NOT EXISTS solana_exec_cursors ( + platform TEXT PRIMARY KEY, + last_sig TEXT NOT NULL, + slot BIGINT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +COMMIT; diff --git a/src/app/apps/exec/page.tsx b/src/app/apps/exec/page.tsx new file mode 100644 index 00000000..378bb644 --- /dev/null +++ b/src/app/apps/exec/page.tsx @@ -0,0 +1,77 @@ +import type { Metadata } from "next"; +import { pageMetadata } from "@/lib/page-metadata"; +import { safeJsonLd, buildBreadcrumbJsonLd } from "@/lib/jsonld"; +import { SITE } from "@/data/site"; +import { fetchExecLeaderboard } from "@/lib/solana-exec"; +import { SolanaExecTable } from "@/components/solana-exec-table"; + +const DESCRIPTION = + "Solana trading platform execution quality: priority fees, Jito bundle rates, and platform fees — measured passively from on-chain data. Updated every hour."; + +export const metadata: Metadata = pageMetadata({ + path: "/apps/exec", + title: "Solana Execution Quality — pump.fun, FOMO, Axiom, GMGN | OpenChainBench", + description: DESCRIPTION, +}); + +export const revalidate = 60; + +export default async function SolanaExecPage() { + const data = await fetchExecLeaderboard(); + + const breadcrumb = { + "@context": "https://schema.org", + ...buildBreadcrumbJsonLd([ + { name: "Home", item: SITE.url }, + { name: "Apps", item: `${SITE.url}/apps` }, + { name: "Solana Execution", item: `${SITE.url}/apps/exec` }, + ]), + }; + + return ( +
+