alloy-flashblocks 0.1.0

Stream Base L2 flashblocks and query preconfirmation state using Alloy.
Documentation
//! Extension trait for streaming flashblocks and querying flashblock state.

use alloy::{
    eips::BlockNumberOrTag,
    network::Network,
    primitives::{Address, Bytes, TxHash, U256},
    providers::Provider,
    rpc::types::{Filter, Log, simulate::SimulatePayload},
    transports::TransportResult,
};
use std::future::Future;

use crate::FlashblockPoller;

/// Preconfirmation polling interval (50ms for responsive feedback).
pub const PRECONF_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);

/// Extension trait for streaming Base flashblocks and querying flashblock state.
///
/// Flashblocks provide ~200ms preconfirmations on Base L2. This trait adds
/// methods to stream flashblocks, query pending state, and wait for preconfirmations.
///
/// # State Query Methods
///
/// All `flashblock_*` methods query against the pending/flashblock state:
/// - [`flashblock`](Self::flashblock) - Get current flashblock with full txs
/// - [`flashblock_balance`](Self::flashblock_balance) - Balance including preconfirmed txs
/// - [`flashblock_nonce`](Self::flashblock_nonce) - Nonce for sending next tx
/// - [`flashblock_call`](Self::flashblock_call) - Simulate against flashblock state
/// - [`flashblock_estimate_gas`](Self::flashblock_estimate_gas) - Gas estimate against flashblock
/// - [`flashblock_logs`](Self::flashblock_logs) - Logs up to flashblock
/// - [`flashblock_simulate`](Self::flashblock_simulate) - Multi-call simulation
///
/// # Preconfirmation Helpers
///
/// - [`wait_for_preconfirmation`](Self::wait_for_preconfirmation) - Poll until tx is preconfirmed
/// - [`is_preconfirmed`](Self::is_preconfirmed) - Check if tx has preconfirmed receipt
pub trait FlashblocksProviderExt<N: Network>: Provider<N> + Clone + Send + Sync + 'static {
    // === Streaming ===

    /// Watch for flashblock updates via RPC polling.
    ///
    /// Polls `eth_getBlockByNumber("pending")` every 120ms and yields new blocks
    /// when the block hash changes.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use alloy::providers::ProviderBuilder;
    /// use alloy_flashblocks::FlashblocksProviderExt;
    /// use futures_util::StreamExt;
    ///
    /// let provider = ProviderBuilder::new()
    ///     .connect_http("https://mainnet-preconf.base.org".parse()?);
    ///
    /// let mut stream = provider.watch_flashblocks().into_stream();
    ///
    /// while let Some(block) = stream.next().await {
    ///     println!("Block {}: {} txs", block.header.number, block.transactions.len());
    /// }
    /// ```
    fn watch_flashblocks(&self) -> FlashblockPoller<N, Self>
    where
        Self: Sized,
    {
        FlashblockPoller::new(self.clone())
    }

    // === Flashblock State Queries ===

    /// Get the current flashblock.
    ///
    /// Returns the pending block with transaction hashes. Use the individual
    /// transaction hashes to fetch full transaction details if needed.
    ///
    /// This is equivalent to `eth_getBlockByNumber("pending", false)`.
    ///
    /// # Example
    ///
    /// ```ignore
    /// if let Some(block) = provider.flashblock().await? {
    ///     println!("Flashblock {} | {} txs", block.header.number, block.transactions.len());
    /// }
    /// ```
    fn flashblock(&self) -> impl Future<Output = TransportResult<Option<N::BlockResponse>>> + Send {
        async move {
            self.get_block_by_number(BlockNumberOrTag::Pending).await
        }
    }

    /// Get balance including preconfirmed transactions.
    ///
    /// This is equivalent to `eth_getBalance(addr, "pending")`.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let balance = provider.flashblock_balance(addr).await?;
    /// println!("Balance (with preconfirmed txs): {} wei", balance);
    /// ```
    fn flashblock_balance(
        &self,
        addr: Address,
    ) -> impl Future<Output = TransportResult<U256>> + Send {
        async move {
            self.get_balance(addr)
                .block_id(BlockNumberOrTag::Pending.into())
                .await
        }
    }

    /// Get nonce including preconfirmed transactions.
    ///
    /// Use this to determine the nonce for sending your next transaction,
    /// accounting for any pending transactions already in the flashblock.
    ///
    /// This is equivalent to `eth_getTransactionCount(addr, "pending")`.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let nonce = provider.flashblock_nonce(addr).await?;
    /// let tx = TransactionRequest::default().nonce(nonce);
    /// ```
    fn flashblock_nonce(&self, addr: Address) -> impl Future<Output = TransportResult<u64>> + Send {
        async move {
            self.get_transaction_count(addr)
                .block_id(BlockNumberOrTag::Pending.into())
                .await
        }
    }

    /// Execute a call against flashblock state.
    ///
    /// Simulates the transaction against the current pending state, including
    /// any preconfirmed transactions.
    ///
    /// This is equivalent to `eth_call(tx, "pending")`.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let result = provider.flashblock_call(&tx).await?;
    /// ```
    fn flashblock_call(
        &self,
        tx: &N::TransactionRequest,
    ) -> impl Future<Output = TransportResult<Bytes>> + Send {
        let tx = tx.clone();
        async move {
            self.call(tx)
                .block(BlockNumberOrTag::Pending.into())
                .await
        }
    }

    /// Estimate gas against flashblock state.
    ///
    /// Estimates gas for the transaction against the current pending state.
    ///
    /// This is equivalent to `eth_estimateGas(tx, "pending")`.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let gas = provider.flashblock_estimate_gas(&tx).await?;
    /// let tx = tx.gas_limit(gas);
    /// ```
    fn flashblock_estimate_gas(
        &self,
        tx: &N::TransactionRequest,
    ) -> impl Future<Output = TransportResult<u64>> + Send {
        let tx = tx.clone();
        async move {
            self.estimate_gas(tx)
                .block(BlockNumberOrTag::Pending.into())
                .await
        }
    }

    /// Get logs up to the current flashblock.
    ///
    /// Returns logs matching the filter, including logs from preconfirmed
    /// transactions in the current flashblock.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let filter = Filter::new().address(contract_addr);
    /// let logs = provider.flashblock_logs(filter).await?;
    /// ```
    fn flashblock_logs(
        &self,
        filter: Filter,
    ) -> impl Future<Output = TransportResult<Vec<Log>>> + Send {
        async move {
            let filter = filter.to_block(BlockNumberOrTag::Pending);
            self.get_logs(&filter).await
        }
    }

    /// Simulate multiple transactions against flashblock state.
    ///
    /// Executes a multi-call simulation against the current pending state.
    ///
    /// This is equivalent to `eth_simulateV1` with pending block.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use alloy::rpc::types::simulate::{SimulatePayload, SimBlock};
    ///
    /// let payload = SimulatePayload::default().extend(SimBlock::default());
    /// let results = provider.flashblock_simulate(&payload).await?;
    /// ```
    fn flashblock_simulate(
        &self,
        payload: &SimulatePayload,
    ) -> impl Future<Output = TransportResult<Vec<alloy::rpc::types::simulate::SimulatedBlock<N::BlockResponse>>>>
           + Send
    {
        let payload = payload.clone();
        async move {
            self.simulate(&payload)
                .block_id(BlockNumberOrTag::Pending.into())
                .await
        }
    }

    // === Preconfirmation Helpers ===

    /// Wait for a transaction to be preconfirmed.
    ///
    /// Polls for the transaction receipt every 50ms until it appears.
    /// This is the Rust equivalent of ethers.js `tx.wait(0)` for instant
    /// preconfirmation feedback on flashblocks-enabled chains.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let pending = provider.send_transaction(tx).await?;
    /// let receipt = provider.wait_for_preconfirmation(*pending.tx_hash()).await?;
    /// println!("Preconfirmed! Status: {:?}", receipt.status());
    /// ```
    fn wait_for_preconfirmation(
        &self,
        tx_hash: TxHash,
    ) -> impl Future<Output = TransportResult<N::ReceiptResponse>> + Send {
        async move {
            loop {
                if let Some(receipt) = self.get_transaction_receipt(tx_hash).await? {
                    return Ok(receipt);
                }
                tokio::time::sleep(PRECONF_POLL_INTERVAL).await;
            }
        }
    }

    /// Check if a transaction has been preconfirmed.
    ///
    /// Returns `true` if the transaction has a receipt (preconfirmed or finalized).
    ///
    /// # Example
    ///
    /// ```ignore
    /// if provider.is_preconfirmed(tx_hash).await? {
    ///     println!("Transaction is preconfirmed!");
    /// }
    /// ```
    fn is_preconfirmed(
        &self,
        tx_hash: TxHash,
    ) -> impl Future<Output = TransportResult<bool>> + Send {
        async move { Ok(self.get_transaction_receipt(tx_hash).await?.is_some()) }
    }
}

impl<N: Network, P: Provider<N> + Clone + Send + Sync + 'static> FlashblocksProviderExt<N> for P {}