-
Notifications
You must be signed in to change notification settings - Fork 194
feat(api): add pending transaction support in the eth subscription API
#6941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2214c87
2291945
0d4eca0
709de17
0571359
c76e413
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,14 +59,20 @@ | |
| //! ``` | ||
| //! | ||
|
|
||
| use crate::rpc::eth::pubsub_trait::{ | ||
| EthPubSubApiServer, LogFilter, SubscriptionKind, SubscriptionParams, | ||
| use crate::blocks::Tipset; | ||
| use crate::message_pool::MpoolUpdate; | ||
| use crate::prelude::ShallowClone; | ||
| use crate::rpc::RPCState; | ||
| use crate::rpc::eth::pubsub_trait::{EthPubSubApiServer, SubscriptionKind, SubscriptionParams}; | ||
| use crate::rpc::eth::types::{ApiHeaders, EthFilterSpec}; | ||
| use crate::rpc::eth::{ | ||
| Block as EthBlock, TxInfo, eth_logs_with_filter, eth_tx_hash_from_signed_message, | ||
| }; | ||
| use crate::rpc::{RPCState, chain}; | ||
| use jsonrpsee::PendingSubscriptionSink; | ||
| use jsonrpsee::core::{SubscriptionError, SubscriptionResult}; | ||
| use crate::utils::broadcast::subscription_stream; | ||
| use futures::{Stream, StreamExt as _}; | ||
| use jsonrpsee::core::SubscriptionResult; | ||
| use jsonrpsee::{PendingSubscriptionSink, SubscriptionSink}; | ||
| use std::sync::Arc; | ||
| use tokio::sync::broadcast::{Receiver as Subscriber, error::RecvError}; | ||
|
|
||
| #[derive(derive_more::Constructor)] | ||
| pub struct EthPubSub { | ||
|
|
@@ -82,93 +88,109 @@ impl EthPubSubApiServer for EthPubSub { | |
| params: Option<SubscriptionParams>, | ||
| ) -> SubscriptionResult { | ||
| let sink = pending.accept().await?; | ||
| let ctx = self.ctx.clone(); | ||
|
|
||
| let ctx = self.ctx.shallow_clone(); | ||
| match kind { | ||
| SubscriptionKind::NewHeads => self.handle_new_heads_subscription(sink, ctx).await, | ||
| SubscriptionKind::PendingTransactions => { | ||
| return Err(SubscriptionError::from( | ||
| jsonrpsee::types::ErrorObjectOwned::owned( | ||
| jsonrpsee::types::error::METHOD_NOT_FOUND_CODE, | ||
| "pendingTransactions subscription not yet implemented", | ||
| None::<()>, | ||
| ), | ||
| )); | ||
| } | ||
| SubscriptionKind::NewHeads => spawn_new_heads(sink, ctx), | ||
| SubscriptionKind::PendingTransactions => spawn_pending_transactions(sink, ctx), | ||
| SubscriptionKind::Logs => { | ||
| let filter = params.and_then(|p| p.filter); | ||
| self.handle_logs_subscription(sink, ctx, filter).await | ||
| let filter = params.and_then(|p| p.filter).map(EthFilterSpec::from); | ||
| spawn_logs(sink, ctx, filter); | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| impl EthPubSub { | ||
| async fn handle_new_heads_subscription( | ||
| &self, | ||
| accepted_sink: jsonrpsee::SubscriptionSink, | ||
| ctx: Arc<RPCState>, | ||
| ) { | ||
| let (subscriber, handle) = chain::new_heads(ctx); | ||
| tokio::spawn(async move { | ||
| handle_subscription(subscriber, accepted_sink, handle).await; | ||
| }); | ||
| } | ||
| /// Stream of tipsets as they are applied to the chain head. Reverts are | ||
| /// ignored; lagged events are dropped (and logged) by [`subscription_stream`]. | ||
| fn head_applied_tipsets(ctx: &Arc<RPCState>) -> impl Stream<Item = Tipset> + Send + use<> { | ||
| subscription_stream(ctx.chain_store().subscribe_head_changes()) | ||
| .flat_map(|changes| futures::stream::iter(changes.applies)) | ||
| } | ||
|
|
||
| async fn handle_logs_subscription( | ||
| &self, | ||
| accepted_sink: jsonrpsee::SubscriptionSink, | ||
| ctx: Arc<RPCState>, | ||
| filter_spec: Option<LogFilter>, | ||
| ) { | ||
| let filter_spec = filter_spec.map(Into::into); | ||
| let (logs, handle) = chain::logs(&ctx, filter_spec); | ||
| tokio::spawn(async move { | ||
| handle_subscription(logs, accepted_sink, handle).await; | ||
| }); | ||
| } | ||
| fn spawn_new_heads(sink: SubscriptionSink, ctx: Arc<RPCState>) { | ||
| let stream = head_applied_tipsets(&ctx) | ||
| .filter_map(move |ts| { | ||
| let state_mngr = ctx.state_manager.shallow_clone(); | ||
| async move { | ||
| EthBlock::from_filecoin_tipset(&state_mngr, ts, TxInfo::Full) | ||
| .await | ||
| .inspect_err(|e| { | ||
| tracing::error!("Failed to convert tipset to eth block: {e:#}") | ||
| }) | ||
| .ok() | ||
| .map(ApiHeaders) | ||
| } | ||
| }) | ||
| .boxed(); | ||
| tokio::spawn(pipe_stream_to_sink(stream, sink)); | ||
| } | ||
|
|
||
| async fn handle_subscription<T>( | ||
| mut subscriber: Subscriber<T>, | ||
| sink: jsonrpsee::SubscriptionSink, | ||
| handle: tokio::task::JoinHandle<()>, | ||
| ) where | ||
| T: serde::Serialize + Clone, | ||
| fn spawn_logs(sink: SubscriptionSink, ctx: Arc<RPCState>, filter: Option<EthFilterSpec>) { | ||
| let stream = head_applied_tipsets(&ctx) | ||
| .filter_map(move |ts| { | ||
| let ctx = ctx.shallow_clone(); | ||
| let filter = filter.clone(); | ||
| async move { | ||
| eth_logs_with_filter(&ctx, &ts, filter) | ||
| .await | ||
| .inspect_err(|e| { | ||
| tracing::error!("Failed to fetch logs for tipset {}: {e:#}", ts.key()) | ||
| }) | ||
| .ok() | ||
| } | ||
| }) | ||
| .flat_map(futures::stream::iter) | ||
| .boxed(); | ||
| tokio::spawn(pipe_stream_to_sink(stream, sink)); | ||
| } | ||
|
|
||
| fn spawn_pending_transactions(sink: SubscriptionSink, ctx: Arc<RPCState>) { | ||
| let mpool_rx = ctx.mpool.subscribe_to_updates(); | ||
| let eth_chain_id = ctx.chain_config().eth_chain_id; | ||
| let stream = subscription_stream(mpool_rx) | ||
| .filter_map(move |update| async move { | ||
| let MpoolUpdate::Add(msg) = update else { | ||
| return None; | ||
| }; | ||
| eth_tx_hash_from_signed_message(&msg, eth_chain_id).ok() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of dropping failures silently we could log them |
||
| }) | ||
| .boxed(); | ||
| tokio::spawn(pipe_stream_to_sink(stream, sink)); | ||
| } | ||
|
|
||
| /// Forward stream items to the subscription sink until the sink is closed, | ||
| /// the client disconnects, or the upstream stream ends. The stream is | ||
| /// expected to absorb upstream backpressure (e.g. `Lagged`) on its own; this | ||
| /// helper only cares about the sink side. | ||
| async fn pipe_stream_to_sink<S, T>(mut stream: S, sink: SubscriptionSink) | ||
| where | ||
| S: Stream<Item = T> + Unpin + Send, | ||
| T: serde::Serialize + Send, | ||
| { | ||
| loop { | ||
| tokio::select! { | ||
| action = subscriber.recv() => { | ||
| match action { | ||
| Ok(v) => { | ||
| match jsonrpsee::SubscriptionMessage::new(sink.method_name(), sink.subscription_id(), &v) { | ||
| Ok(msg) => { | ||
| if let Err(e) = sink.send(msg).await { | ||
| tracing::error!("Failed to send message: {:?}", e); | ||
| break; | ||
| } | ||
| } | ||
| Err(e) => { | ||
| tracing::error!("Failed to serialize message: {:?}", e); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| Err(RecvError::Closed) => { | ||
| _ = sink.closed() => break, | ||
| maybe = stream.next() => { | ||
| let Some(item) = maybe else { break }; | ||
| let msg = match jsonrpsee::SubscriptionMessage::new( | ||
| sink.method_name(), | ||
| sink.subscription_id(), | ||
| &item, | ||
| ) { | ||
| Ok(m) => m, | ||
| Err(e) => { | ||
| tracing::error!("Failed to serialize subscription message: {e:?}"); | ||
| break; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so because we are only sending specific data (logs, hashes, tipset) to the stream, if json is not able to serialised then even if we continue it will fail again. |
||
| } | ||
| Err(RecvError::Lagged(_)) => { | ||
| } | ||
| }; | ||
| if let Err(e) = sink.send(msg).await { | ||
| tracing::debug!("Subscription sink send failed (client disconnected): {e:?}"); | ||
| break; | ||
| } | ||
| } | ||
| _ = sink.closed() => { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| handle.abort(); | ||
|
|
||
| tracing::info!("Subscription task ended (id: {:?})", sink.subscription_id()); | ||
| tracing::debug!("Subscription task ended (id: {:?})", sink.subscription_id()); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the linked issue number instead of PR number in this changelog entry.
Line 46 should reference issue
#6031(the tracked objective) rather than PR#6941, to match the project’s changelog convention.Suggested edit
Based on learnings: “In CHANGELOG.md entries, when both an issue and a PR exist for a change, reference the issue number… Use PR numbers only if there is no corresponding issue.”
🤖 Prompt for AI Agents