Skip to main content

reth_rpc/eth/
pubsub.rs

1//! `eth_` `PubSub` RPC handler implementation
2
3use std::sync::Arc;
4
5use alloy_primitives::TxHash;
6use alloy_rpc_types_eth::{
7    pubsub::{
8        Params, PubSubSyncStatus, SubscriptionKind, SyncStatusMetadata, TransactionReceiptsParams,
9    },
10    Filter, Log,
11};
12use futures::StreamExt;
13use jsonrpsee::{
14    server::SubscriptionMessage, types::ErrorObject, PendingSubscriptionSink, SubscriptionSink,
15};
16use reth_chain_state::CanonStateSubscriptions;
17use reth_network_api::NetworkInfo;
18use reth_rpc_convert::RpcHeader;
19use reth_rpc_eth_api::{
20    helpers::EthSubscriptions, pubsub::EthPubSubApiServer, RpcConvert, RpcNodeCore, RpcTransaction,
21};
22use reth_rpc_server_types::result::{internal_rpc_err, invalid_params_rpc_err};
23use reth_storage_api::BlockNumReader;
24use reth_tasks::Runtime;
25use reth_transaction_pool::{NewTransactionEvent, TransactionPool};
26use serde::Serialize;
27use tokio_stream::{
28    wrappers::{BroadcastStream, ReceiverStream},
29    Stream,
30};
31use tracing::error;
32
33/// `Eth` pubsub RPC implementation.
34///
35/// This handles `eth_subscribe` RPC calls.
36#[derive(Clone)]
37pub struct EthPubSub<Eth> {
38    /// All nested fields bundled together.
39    inner: Arc<EthPubSubInner<Eth>>,
40}
41
42// === impl EthPubSub ===
43
44impl<Eth> EthPubSub<Eth> {
45    /// Creates a new, shareable instance.
46    pub fn new(eth_api: Eth, subscription_task_spawner: Runtime) -> Self {
47        let inner = EthPubSubInner { eth_api, subscription_task_spawner };
48        Self { inner: Arc::new(inner) }
49    }
50}
51
52impl<Eth> EthPubSub<Eth>
53where
54    Eth: EthSubscriptions,
55{
56    /// Returns the current sync status for the `syncing` subscription
57    pub fn sync_status(&self, is_syncing: bool) -> PubSubSyncStatus {
58        self.inner.sync_status(is_syncing)
59    }
60
61    /// Returns a stream that yields all transaction hashes emitted by the txpool.
62    pub fn pending_transaction_hashes_stream(&self) -> impl Stream<Item = TxHash> {
63        self.inner.pending_transaction_hashes_stream()
64    }
65
66    /// Returns a stream that yields all transactions emitted by the txpool.
67    pub fn full_pending_transaction_stream(
68        &self,
69    ) -> impl Stream<Item = NewTransactionEvent<<Eth::Pool as TransactionPool>::Transaction>> {
70        self.inner.full_pending_transaction_stream()
71    }
72
73    /// Returns a stream that yields new block headers.
74    pub fn new_headers_stream(&self) -> impl Stream<Item = RpcHeader<Eth::NetworkTypes>> {
75        self.inner.eth_api.header_stream()
76    }
77
78    /// Returns a stream that yields matching logs.
79    pub fn log_stream(&self, filter: Filter) -> impl Stream<Item = Log> {
80        self.inner.eth_api.log_stream(filter)
81    }
82
83    /// The actual handler for an accepted [`EthPubSub::subscribe`] call.
84    pub async fn handle_accepted(
85        &self,
86        accepted_sink: SubscriptionSink,
87        kind: SubscriptionKind,
88        params: Option<Params>,
89    ) -> Result<(), ErrorObject<'static>> {
90        #[allow(unreachable_patterns)]
91        match kind {
92            SubscriptionKind::NewHeads => {
93                pipe_from_stream(accepted_sink, self.new_headers_stream()).await
94            }
95            SubscriptionKind::Logs => {
96                // if no params are provided, used default filter params
97                let filter = match params {
98                    Some(Params::Logs(filter)) => *filter,
99                    Some(Params::Bool(_)) => {
100                        return Err(invalid_params_rpc_err("Invalid params for logs"))
101                    }
102                    _ => Default::default(),
103                };
104                pipe_from_stream(accepted_sink, self.log_stream(filter)).await
105            }
106            SubscriptionKind::NewPendingTransactions => {
107                if let Some(params) = params {
108                    match params {
109                        Params::Bool(true) => {
110                            // full transaction objects requested
111                            let stream = self.full_pending_transaction_stream().filter_map(|tx| {
112                                let tx_value = match self
113                                    .inner
114                                    .eth_api
115                                    .converter()
116                                    .fill_pending(tx.transaction.to_consensus())
117                                {
118                                    Ok(tx) => Some(tx),
119                                    Err(err) => {
120                                        error!(target = "rpc",
121                                            %err,
122                                            "Failed to fill transaction with block context"
123                                        );
124                                        None
125                                    }
126                                };
127                                std::future::ready(tx_value)
128                            });
129                            return pipe_from_stream(accepted_sink, stream).await
130                        }
131                        Params::Bool(false) | Params::None => {
132                            // only hashes requested
133                        }
134                        _ => {
135                            return Err(invalid_params_rpc_err(
136                                "Invalid params for newPendingTransactions",
137                            ))
138                        }
139                    }
140                }
141
142                pipe_from_stream(accepted_sink, self.pending_transaction_hashes_stream()).await
143            }
144            SubscriptionKind::Syncing => {
145                // get new block subscription
146                let mut canon_state = BroadcastStream::new(
147                    self.inner.eth_api.provider().subscribe_to_canonical_state(),
148                );
149                // get current sync status
150                let mut initial_sync_status = self.inner.eth_api.network().is_syncing();
151                let current_sub_res = self.sync_status(initial_sync_status);
152
153                // send the current status immediately
154                let msg = SubscriptionMessage::new(
155                    accepted_sink.method_name(),
156                    accepted_sink.subscription_id(),
157                    &current_sub_res,
158                )
159                .map_err(SubscriptionSerializeError::new)?;
160
161                if accepted_sink.send(msg).await.is_err() {
162                    return Ok(())
163                }
164
165                while canon_state.next().await.is_some() {
166                    let current_syncing = self.inner.eth_api.network().is_syncing();
167                    // Only send a new response if the sync status has changed
168                    if current_syncing != initial_sync_status {
169                        // Update the sync status on each new block
170                        initial_sync_status = current_syncing;
171
172                        // send a new message now that the status changed
173                        let sync_status = self.sync_status(current_syncing);
174                        let msg = SubscriptionMessage::new(
175                            accepted_sink.method_name(),
176                            accepted_sink.subscription_id(),
177                            &sync_status,
178                        )
179                        .map_err(SubscriptionSerializeError::new)?;
180
181                        if accepted_sink.send(msg).await.is_err() {
182                            break
183                        }
184                    }
185                }
186
187                Ok(())
188            }
189            SubscriptionKind::TransactionReceipts => {
190                let filter = match params {
191                    Some(Params::TransactionReceipts(filter)) => filter,
192                    None | Some(Params::None) => TransactionReceiptsParams::default(),
193                    _ => {
194                        return Err(invalid_params_rpc_err("Invalid params for transactionReceipts"))
195                    }
196                };
197
198                pipe_from_stream(
199                    accepted_sink,
200                    self.inner.eth_api.transaction_receipts_stream(filter),
201                )
202                .await
203            }
204            _ => Err(invalid_params_rpc_err("Unsupported subscription kind")),
205        }
206    }
207}
208
209#[async_trait::async_trait]
210impl<Eth> EthPubSubApiServer<RpcTransaction<Eth::NetworkTypes>> for EthPubSub<Eth>
211where
212    Eth: EthSubscriptions,
213{
214    /// Handler for `eth_subscribe`
215    async fn subscribe(
216        &self,
217        pending: PendingSubscriptionSink,
218        kind: SubscriptionKind,
219        params: Option<Params>,
220    ) -> jsonrpsee::core::SubscriptionResult {
221        let sink = pending.accept().await?;
222        let pubsub = self.clone();
223        self.inner.subscription_task_spawner.spawn_task(async move {
224            let _ = pubsub.handle_accepted(sink, kind, params).await;
225        });
226
227        Ok(())
228    }
229}
230
231/// Helper to convert a serde error into an [`ErrorObject`]
232#[derive(Debug, thiserror::Error)]
233#[error("Failed to serialize subscription item: {0}")]
234pub struct SubscriptionSerializeError(#[from] serde_json::Error);
235
236impl SubscriptionSerializeError {
237    const fn new(err: serde_json::Error) -> Self {
238        Self(err)
239    }
240}
241
242impl From<SubscriptionSerializeError> for ErrorObject<'static> {
243    fn from(value: SubscriptionSerializeError) -> Self {
244        internal_rpc_err(value.to_string())
245    }
246}
247
248/// Pipes all stream items to the subscription sink.
249async fn pipe_from_stream<T, St>(
250    sink: SubscriptionSink,
251    mut stream: St,
252) -> Result<(), ErrorObject<'static>>
253where
254    St: Stream<Item = T> + Unpin,
255    T: Serialize,
256{
257    loop {
258        tokio::select! {
259            _ = sink.closed() => {
260                // connection dropped
261                break Ok(())
262            },
263            maybe_item = stream.next() => {
264                let item = match maybe_item {
265                    Some(item) => item,
266                    None => {
267                        // stream ended
268                        break  Ok(())
269                    },
270                };
271                let msg = SubscriptionMessage::new(
272                    sink.method_name(),
273                    sink.subscription_id(),
274                    &item
275                ).map_err(SubscriptionSerializeError::new)?;
276
277                if sink.send(msg).await.is_err() {
278                    break Ok(());
279                }
280            }
281        }
282    }
283}
284
285impl<Eth> std::fmt::Debug for EthPubSub<Eth> {
286    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        f.debug_struct("EthPubSub").finish_non_exhaustive()
288    }
289}
290
291/// Container type `EthPubSub`
292#[derive(Clone)]
293struct EthPubSubInner<EthApi> {
294    /// The `eth` API.
295    eth_api: EthApi,
296    /// The type that's used to spawn subscription tasks.
297    subscription_task_spawner: Runtime,
298}
299
300// == impl EthPubSubInner ===
301
302impl<Eth> EthPubSubInner<Eth>
303where
304    Eth: RpcNodeCore<Provider: BlockNumReader>,
305{
306    /// Returns the current sync status for the `syncing` subscription
307    fn sync_status(&self, is_syncing: bool) -> PubSubSyncStatus {
308        if is_syncing {
309            let current_block = self
310                .eth_api
311                .provider()
312                .chain_info()
313                .map(|info| info.best_number)
314                .unwrap_or_default();
315            PubSubSyncStatus::Detailed(SyncStatusMetadata {
316                syncing: true,
317                starting_block: 0,
318                current_block,
319                highest_block: Some(current_block),
320            })
321        } else {
322            PubSubSyncStatus::Simple(false)
323        }
324    }
325}
326
327impl<Eth> EthPubSubInner<Eth>
328where
329    Eth: RpcNodeCore<Pool: TransactionPool>,
330{
331    /// Returns a stream that yields all transaction hashes emitted by the txpool.
332    fn pending_transaction_hashes_stream(&self) -> impl Stream<Item = TxHash> {
333        ReceiverStream::new(self.eth_api.pool().pending_transactions_listener())
334    }
335
336    /// Returns a stream that yields all transactions emitted by the txpool.
337    fn full_pending_transaction_stream(
338        &self,
339    ) -> impl Stream<Item = NewTransactionEvent<<Eth::Pool as TransactionPool>::Transaction>> {
340        self.eth_api.pool().new_pending_pool_transactions_listener()
341    }
342}