| Crates.io | semioscan |
| lib.rs | semioscan |
| version | 0.9.0 |
| created_at | 2025-12-02 18:35:41.015507+00 |
| updated_at | 2026-01-16 23:27:28.686218+00 |
| description | Production-grade Rust library for blockchain analytics: gas calculation, price extraction, and block window calculations for EVM chains |
| homepage | https://github.com/semiotic-ai/semioscan |
| repository | https://github.com/semiotic-ai/semioscan |
| max_upload_size | |
| id | 1962386 |
| size | 1,022,301 |
Semioscan is a Rust library for blockchain analytics, providing production-grade tools for calculating gas costs, extracting price data from DEX swaps, and working with block ranges across multiple EVM-compatible chains.
Key differentiator: Semioscan is a library-only crate with no CLI, API server, or database dependencies. You bring your own infrastructure and integrate semioscan into your existing systems.
Built on Alloy, the modern Ethereum library for Rust, semioscan provides type-safe blockchain interactions with zero-copy parsing and excellent performance.
Semioscan is ideal for:
Add semioscan to your Cargo.toml:
[dependencies]
# Core library (gas, block windows, events)
semioscan = "0.4"
# With Odos DEX reference implementation (optional)
semioscan = { version = "0.4", features = ["odos-example"] }
odos-example: Includes OdosPriceSource as a reference implementation of the PriceSource trait for Odos DEX aggregator (optional, not included by default)Calculate total gas costs for transactions between two addresses:
use semioscan::GasCalculator;
use alloy_provider::ProviderBuilder;
use alloy_chains::NamedChain;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Create provider
let provider = ProviderBuilder::new()
.connect_http("https://arb1.arbitrum.io/rpc".parse()?);
// Create gas calculator
let calculator = GasCalculator::new(provider.clone());
// Calculate gas costs for a block range
let from_address = "0x123...".parse()?;
let to_address = "0x456...".parse()?;
let chain_id = NamedChain::Arbitrum as u64;
let result = calculator
.get_gas_cost(chain_id, from_address, to_address, 200_000_000, 200_001_000)
.await?;
println!("Total gas cost: {} wei", result.total_gas_cost);
println!("Transaction count: {}", result.transaction_count);
Ok(())
}
L2 chains (Arbitrum, Base, Optimism) automatically include L1 data fees in the calculation.
Map a UTC date to the corresponding blockchain block range:
use semioscan::BlockWindowCalculator;
use alloy_provider::ProviderBuilder;
use alloy_chains::NamedChain;
use chrono::NaiveDate;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Create provider
let provider = ProviderBuilder::new()
.connect_http("https://arb1.arbitrum.io/rpc".parse()?);
// Create calculator with disk cache
let calculator = BlockWindowCalculator::with_disk_cache(
provider.clone(),
"block_windows.json"
)?;
// Get block window for a specific day
let date = NaiveDate::from_ymd_opt(2025, 10, 15).unwrap();
let window = calculator
.get_daily_window(NamedChain::Arbitrum, date)
.await?;
println!("Date: {}", date);
println!("Block range: [{}, {}]", window.start_block, window.end_block);
println!("Block count: {}", window.block_count());
Ok(())
}
Caching: Block windows are automatically cached to disk for faster subsequent queries.
Use the PriceSource trait to extract price data from on-chain swap events:
use semioscan::price::odos::OdosPriceSource; // requires "odos-example" feature
use semioscan::PriceCalculator;
use alloy_provider::ProviderBuilder;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Create provider
let provider = ProviderBuilder::new()
.connect_http("https://arb1.arbitrum.io/rpc".parse()?);
// Create Odos price source for V2 router
let router_address = "0xa669e7A0d4b3e4Fa48af2dE86BD4CD7126Be4e13".parse()?;
let price_source = OdosPriceSource::new(router_address);
// Create price calculator with the price source
let calculator = PriceCalculator::with_price_source(
provider.clone(),
Box::new(price_source)
);
// Calculate average price for a token over a block range
let token_address = "0x789...".parse()?;
let result = calculator
.get_price(token_address, 200_000_000, 200_001_000)
.await?;
println!("Average price: {}", result.average_price);
println!("Total volume: {}", result.total_volume_in);
Ok(())
}
The examples/ directory contains complete, production-ready examples demonstrating semioscan's capabilities. See examples/README.md for comprehensive documentation, setup instructions, and troubleshooting.
| Example | Use Case | Difficulty |
|---|---|---|
daily_block_window.rs |
Map UTC dates to block ranges | Beginner |
router_token_discovery.rs |
Discover tokens sent to router contracts | Intermediate |
eip4844_blob_gas.rs |
Calculate EIP-4844 blob gas for L2 rollups | Advanced |
custom_dex_integration.rs |
Implement PriceSource for any DEX |
Advanced |
# Basic usage
RPC_URL=https://arb1.arbitrum.io/rpc cargo run --example daily_block_window
# With chain-specific environment variables
ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc cargo run --example router_token_discovery -- arbitrum
# With logging for debugging
RUST_LOG=debug cargo run --example eip4844_blob_gas
For detailed setup, configuration, performance tips, and troubleshooting, see examples/README.md.
A block window maps a calendar date (in UTC) to the range of blocks produced during that day. Different chains have different block production rates:
Block windows enable date-based queries for analytics, reporting, and historical analysis.
L2 chains like Arbitrum, Base, and Optimism post transaction data to Ethereum for security. This creates two separate gas costs:
Semioscan automatically detects L2 chains and calculates both components for accurate total costs. This is critical for profitability calculations in liquidation bots and trading systems.
Semioscan provides flexible caching for block window calculations using a trait-based backend system. You can choose the caching strategy that best fits your needs.
DiskCache (recommended for production)
MemoryCache
NoOpCache
use semioscan::{BlockWindowCalculator, DiskCache, MemoryCache};
use std::time::Duration;
// Disk cache (simplest, recommended)
let calculator = BlockWindowCalculator::with_disk_cache(provider, "cache.json")?;
// Memory cache
let calculator = BlockWindowCalculator::with_memory_cache(provider);
// No cache
let calculator = BlockWindowCalculator::without_cache(provider);
use semioscan::{BlockWindowCalculator, DiskCache};
use std::time::Duration;
// Disk cache with TTL and size limit
let cache = DiskCache::new("cache.json")
.with_ttl(Duration::from_secs(86400 * 7)) // 7 days
.with_max_entries(1000) // Max 1000 entries
.validate()?; // Validate path
let calculator = BlockWindowCalculator::new(provider, Box::new(cache));
// Memory cache with size limit
let cache = MemoryCache::new()
.with_max_entries(500)
.with_ttl(Duration::from_secs(3600));
let calculator = BlockWindowCalculator::new(provider, Box::new(cache));
All cache backends track performance metrics:
let stats = calculator.cache_stats().await;
println!("Hit rate: {:.1}%", stats.hit_rate());
println!("Hits: {}, Misses: {}", stats.hits, stats.misses);
println!("Evictions: {}, Entries: {}", stats.evictions, stats.entries);
DiskCache with TTL for persistent cachingMemoryCache for faster iteration without disk I/ONoOpCache or MemoryCache to avoid file system dependencies.validate() on DiskCache to catch path issues earlyDiskCache uses advisory file locking to prevent corruption when multiple processes share the same cache file. However, for high-concurrency scenarios, consider:
BlockWindowCache trait implementationImplement the BlockWindowCache trait to create custom cache backends (Redis, S3, etc.):
use semioscan::cache::{BlockWindowCache, CacheKey, CacheStats};
use semioscan::DailyBlockWindow;
use async_trait::async_trait;
struct RedisCacheBackend {
client: redis::Client,
}
#[async_trait]
impl BlockWindowCache for RedisCacheBackend {
async fn get(&self, key: &CacheKey) -> Option<DailyBlockWindow> {
// Implement Redis get logic
todo!()
}
async fn insert(&self, key: CacheKey, window: DailyBlockWindow)
-> Result<(), BlockWindowError>
{
// Implement Redis insert logic
todo!()
}
async fn clear(&self) -> Result<(), BlockWindowError> {
todo!()
}
async fn stats(&self) -> CacheStats {
todo!()
}
fn name(&self) -> &'static str {
"RedisCacheBackend"
}
}
Semioscan uses a trait-based architecture that allows you to implement price extraction for any DEX protocol. The PriceSource trait is object-safe and designed for easy extensibility.
use semioscan::price::{PriceSource, SwapData, PriceSourceError};
use alloy_primitives::{Address, B256, U256};
use alloy_rpc_types::Log;
use alloy_sol_types::sol;
// Define Uniswap V3 Swap event
sol! {
event SwapV3(
address indexed sender,
address indexed recipient,
int256 amount0,
int256 amount1,
uint160 sqrtPriceX96,
uint128 liquidity,
int24 tick
);
}
pub struct UniswapV3PriceSource {
pool_address: Address,
token0: Address,
token1: Address,
}
impl PriceSource for UniswapV3PriceSource {
fn router_address(&self) -> Address {
self.pool_address
}
fn event_topics(&self) -> Vec<B256> {
vec![SwapV3::SIGNATURE_HASH]
}
fn extract_swap_from_log(&self, log: &Log) -> Result<Option<SwapData>, PriceSourceError> {
let event = SwapV3::decode_log(&log.into())
.map_err(|e| PriceSourceError::DecodeError(e.to_string()))?;
// Determine swap direction based on amount signs
let (token_in, token_in_amount, token_out, token_out_amount) = if event.amount0.is_negative() {
(self.token0, event.amount0.unsigned_abs(), self.token1, U256::from(event.amount1))
} else {
(self.token1, event.amount1.unsigned_abs(), self.token0, U256::from(event.amount0))
};
Ok(Some(SwapData {
token_in,
token_in_amount,
token_out,
token_out_amount,
sender: Some(event.sender),
tx_hash: log.transaction_hash,
block_number: log.block_number,
}))
}
}
See the PriceSource trait documentation for more details and best practices.
Semioscan is a library-only crate with no binaries, CLI tools, or API servers. You bring your own:
PriceSource trait for your DEX protocolThis design makes semioscan highly composable and easy to integrate into existing systems.
Semioscan works with any EVM-compatible chain. Chains with L2-specific features (like L1 data fees) are automatically detected and handled correctly.
Tested chains include:
Chain support is based on alloy-chains NamedChain enum.
Use SemioscanConfig to customize RPC behavior per chain:
use semioscan::SemioscanConfigBuilder;
use alloy_chains::NamedChain;
let config = SemioscanConfigBuilder::default()
.with_chain_override(
NamedChain::Base,
2000, // max_block_range
500 // rate_limit_per_second
)
.build()?;
// Pass config to calculators
let calculator = GasCalculator::with_config(provider.clone(), Some(config.clone()));
Large block ranges are automatically chunked to prevent RPC timeouts:
Automatic rate limiting protects against RPC provider limits:
Typical performance characteristics (depends on RPC provider):
See examples/README.md#performance-tips for optimization strategies.
Semioscan has comprehensive unit tests for all business logic:
# Run all tests
cargo test --package semioscan --all-features
# Run only unit tests (no integration tests)
cargo test --package semioscan --lib
# Run specific test file
cargo test --package semioscan --test gas_calculator_tests
# Run with logging
RUST_LOG=debug cargo test --package semioscan
Examples demonstrate real-world usage with live blockchain connections:
# Run example with environment variables
RPC_URL=https://arb1.arbitrum.io/rpc cargo run --package semioscan --example daily_block_window
# Run with logging
RUST_LOG=info RPC_URL=https://arb1.arbitrum.io/rpc cargo run --package semioscan --example router_token_discovery
# Run with chain-specific configuration
ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc \
API_KEY=your_api_key \
cargo run --package semioscan --example router_token_discovery -- arbitrum
For detailed example documentation, see examples/README.md.
Rate Limiting (429 Too Many Requests)
Block Range Too Large
max_block_range in config (default: 5,000)Missing Data / No Logs Found
Chain ID Issues
CHAIN_ID environment variable for chains without eth_chainId supportFor comprehensive troubleshooting, see examples/README.md#troubleshooting.
Semioscan may not be the best choice for:
Semioscan excels at batch analytics, historical queries, real-time event streaming, and multi-chain operations where accurate gas cost calculation and flexible price extraction are required.
Semioscan is battle-tested in production for:
Contributions are welcome! Areas of interest:
Licensed under the Apache License, Version 2.0. See LICENSE for details.
Built by Semiotic AI as part of the Likwid liquidation infrastructure. Extracted and open-sourced to benefit the Rust + Ethereum ecosystem.