alloy_flashblocks/
provider_ext.rs

1//! Extension trait for streaming flashblocks and querying flashblock state.
2
3use alloy::{
4    eips::BlockNumberOrTag,
5    network::Network,
6    primitives::{Address, Bytes, TxHash, U256},
7    providers::Provider,
8    rpc::types::{Filter, Log, simulate::SimulatePayload},
9    transports::TransportResult,
10};
11use std::future::Future;
12
13use crate::FlashblockPoller;
14
15/// Preconfirmation polling interval (50ms for responsive feedback).
16pub const PRECONF_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
17
18/// Extension trait for streaming Base flashblocks and querying flashblock state.
19///
20/// Flashblocks provide ~200ms preconfirmations on Base L2. This trait adds
21/// methods to stream flashblocks, query pending state, and wait for preconfirmations.
22///
23/// # State Query Methods
24///
25/// All `flashblock_*` methods query against the pending/flashblock state:
26/// - [`flashblock`](Self::flashblock) - Get current flashblock with full txs
27/// - [`flashblock_balance`](Self::flashblock_balance) - Balance including preconfirmed txs
28/// - [`flashblock_nonce`](Self::flashblock_nonce) - Nonce for sending next tx
29/// - [`flashblock_call`](Self::flashblock_call) - Simulate against flashblock state
30/// - [`flashblock_estimate_gas`](Self::flashblock_estimate_gas) - Gas estimate against flashblock
31/// - [`flashblock_logs`](Self::flashblock_logs) - Logs up to flashblock
32/// - [`flashblock_simulate`](Self::flashblock_simulate) - Multi-call simulation
33///
34/// # Preconfirmation Helpers
35///
36/// - [`wait_for_preconfirmation`](Self::wait_for_preconfirmation) - Poll until tx is preconfirmed
37/// - [`is_preconfirmed`](Self::is_preconfirmed) - Check if tx has preconfirmed receipt
38pub trait FlashblocksProviderExt<N: Network>: Provider<N> + Clone + Send + Sync + 'static {
39    // === Streaming ===
40
41    /// Watch for flashblock updates via RPC polling.
42    ///
43    /// Polls `eth_getBlockByNumber("pending")` every 120ms and yields new blocks
44    /// when the block hash changes.
45    ///
46    /// # Example
47    ///
48    /// ```ignore
49    /// use alloy::providers::ProviderBuilder;
50    /// use alloy_flashblocks::FlashblocksProviderExt;
51    /// use futures_util::StreamExt;
52    ///
53    /// let provider = ProviderBuilder::new()
54    ///     .connect_http("https://mainnet-preconf.base.org".parse()?);
55    ///
56    /// let mut stream = provider.watch_flashblocks().into_stream();
57    ///
58    /// while let Some(block) = stream.next().await {
59    ///     println!("Block {}: {} txs", block.header.number, block.transactions.len());
60    /// }
61    /// ```
62    fn watch_flashblocks(&self) -> FlashblockPoller<N, Self>
63    where
64        Self: Sized,
65    {
66        FlashblockPoller::new(self.clone())
67    }
68
69    // === Flashblock State Queries ===
70
71    /// Get the current flashblock.
72    ///
73    /// Returns the pending block with transaction hashes. Use the individual
74    /// transaction hashes to fetch full transaction details if needed.
75    ///
76    /// This is equivalent to `eth_getBlockByNumber("pending", false)`.
77    ///
78    /// # Example
79    ///
80    /// ```ignore
81    /// if let Some(block) = provider.flashblock().await? {
82    ///     println!("Flashblock {} | {} txs", block.header.number, block.transactions.len());
83    /// }
84    /// ```
85    fn flashblock(&self) -> impl Future<Output = TransportResult<Option<N::BlockResponse>>> + Send {
86        async move {
87            self.get_block_by_number(BlockNumberOrTag::Pending).await
88        }
89    }
90
91    /// Get balance including preconfirmed transactions.
92    ///
93    /// This is equivalent to `eth_getBalance(addr, "pending")`.
94    ///
95    /// # Example
96    ///
97    /// ```ignore
98    /// let balance = provider.flashblock_balance(addr).await?;
99    /// println!("Balance (with preconfirmed txs): {} wei", balance);
100    /// ```
101    fn flashblock_balance(
102        &self,
103        addr: Address,
104    ) -> impl Future<Output = TransportResult<U256>> + Send {
105        async move {
106            self.get_balance(addr)
107                .block_id(BlockNumberOrTag::Pending.into())
108                .await
109        }
110    }
111
112    /// Get nonce including preconfirmed transactions.
113    ///
114    /// Use this to determine the nonce for sending your next transaction,
115    /// accounting for any pending transactions already in the flashblock.
116    ///
117    /// This is equivalent to `eth_getTransactionCount(addr, "pending")`.
118    ///
119    /// # Example
120    ///
121    /// ```ignore
122    /// let nonce = provider.flashblock_nonce(addr).await?;
123    /// let tx = TransactionRequest::default().nonce(nonce);
124    /// ```
125    fn flashblock_nonce(&self, addr: Address) -> impl Future<Output = TransportResult<u64>> + Send {
126        async move {
127            self.get_transaction_count(addr)
128                .block_id(BlockNumberOrTag::Pending.into())
129                .await
130        }
131    }
132
133    /// Execute a call against flashblock state.
134    ///
135    /// Simulates the transaction against the current pending state, including
136    /// any preconfirmed transactions.
137    ///
138    /// This is equivalent to `eth_call(tx, "pending")`.
139    ///
140    /// # Example
141    ///
142    /// ```ignore
143    /// let result = provider.flashblock_call(&tx).await?;
144    /// ```
145    fn flashblock_call(
146        &self,
147        tx: &N::TransactionRequest,
148    ) -> impl Future<Output = TransportResult<Bytes>> + Send {
149        let tx = tx.clone();
150        async move {
151            self.call(tx)
152                .block(BlockNumberOrTag::Pending.into())
153                .await
154        }
155    }
156
157    /// Estimate gas against flashblock state.
158    ///
159    /// Estimates gas for the transaction against the current pending state.
160    ///
161    /// This is equivalent to `eth_estimateGas(tx, "pending")`.
162    ///
163    /// # Example
164    ///
165    /// ```ignore
166    /// let gas = provider.flashblock_estimate_gas(&tx).await?;
167    /// let tx = tx.gas_limit(gas);
168    /// ```
169    fn flashblock_estimate_gas(
170        &self,
171        tx: &N::TransactionRequest,
172    ) -> impl Future<Output = TransportResult<u64>> + Send {
173        let tx = tx.clone();
174        async move {
175            self.estimate_gas(tx)
176                .block(BlockNumberOrTag::Pending.into())
177                .await
178        }
179    }
180
181    /// Get logs up to the current flashblock.
182    ///
183    /// Returns logs matching the filter, including logs from preconfirmed
184    /// transactions in the current flashblock.
185    ///
186    /// # Example
187    ///
188    /// ```ignore
189    /// let filter = Filter::new().address(contract_addr);
190    /// let logs = provider.flashblock_logs(filter).await?;
191    /// ```
192    fn flashblock_logs(
193        &self,
194        filter: Filter,
195    ) -> impl Future<Output = TransportResult<Vec<Log>>> + Send {
196        async move {
197            let filter = filter.to_block(BlockNumberOrTag::Pending);
198            self.get_logs(&filter).await
199        }
200    }
201
202    /// Simulate multiple transactions against flashblock state.
203    ///
204    /// Executes a multi-call simulation against the current pending state.
205    ///
206    /// This is equivalent to `eth_simulateV1` with pending block.
207    ///
208    /// # Example
209    ///
210    /// ```ignore
211    /// use alloy::rpc::types::simulate::{SimulatePayload, SimBlock};
212    ///
213    /// let payload = SimulatePayload::default().extend(SimBlock::default());
214    /// let results = provider.flashblock_simulate(&payload).await?;
215    /// ```
216    fn flashblock_simulate(
217        &self,
218        payload: &SimulatePayload,
219    ) -> impl Future<Output = TransportResult<Vec<alloy::rpc::types::simulate::SimulatedBlock<N::BlockResponse>>>>
220           + Send
221    {
222        let payload = payload.clone();
223        async move {
224            self.simulate(&payload)
225                .block_id(BlockNumberOrTag::Pending.into())
226                .await
227        }
228    }
229
230    // === Preconfirmation Helpers ===
231
232    /// Wait for a transaction to be preconfirmed.
233    ///
234    /// Polls for the transaction receipt every 50ms until it appears.
235    /// This is the Rust equivalent of ethers.js `tx.wait(0)` for instant
236    /// preconfirmation feedback on flashblocks-enabled chains.
237    ///
238    /// # Example
239    ///
240    /// ```ignore
241    /// let pending = provider.send_transaction(tx).await?;
242    /// let receipt = provider.wait_for_preconfirmation(*pending.tx_hash()).await?;
243    /// println!("Preconfirmed! Status: {:?}", receipt.status());
244    /// ```
245    fn wait_for_preconfirmation(
246        &self,
247        tx_hash: TxHash,
248    ) -> impl Future<Output = TransportResult<N::ReceiptResponse>> + Send {
249        async move {
250            loop {
251                if let Some(receipt) = self.get_transaction_receipt(tx_hash).await? {
252                    return Ok(receipt);
253                }
254                tokio::time::sleep(PRECONF_POLL_INTERVAL).await;
255            }
256        }
257    }
258
259    /// Check if a transaction has been preconfirmed.
260    ///
261    /// Returns `true` if the transaction has a receipt (preconfirmed or finalized).
262    ///
263    /// # Example
264    ///
265    /// ```ignore
266    /// if provider.is_preconfirmed(tx_hash).await? {
267    ///     println!("Transaction is preconfirmed!");
268    /// }
269    /// ```
270    fn is_preconfirmed(
271        &self,
272        tx_hash: TxHash,
273    ) -> impl Future<Output = TransportResult<bool>> + Send {
274        async move { Ok(self.get_transaction_receipt(tx_hash).await?.is_some()) }
275    }
276}
277
278impl<N: Network, P: Provider<N> + Clone + Send + Sync + 'static> FlashblocksProviderExt<N> for P {}