From 3c5370288bb5f7513899be8956e2284f7b85f4f6 Mon Sep 17 00:00:00 2001 From: steven Date: Wed, 25 Mar 2026 09:51:16 -0600 Subject: [PATCH 1/4] feat: add MPP payment layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds MPP 402 payment handling to alloy's HTTP and WS transports. - MppLayer

+ MppService — tower middleware for hyper that intercepts 402 responses, parses the Payment challenge, calls provider.pay(), and retries with an Authorization header - MppReqwestClient

— reqwest transport with built-in 402 interception - MppWsConnect

— wraps WsConnect, handles 402 during WS upgrade handshake - mpp-tempo feature adds with_tempo_signer / with_tempo_access_key convenience constructors on all three types Amp-Thread-ID: https://ampcode.com/threads/T-019d2306-2d81-77e8-84af-41a35f908648 Co-authored-by: Amp --- crates/transport-http/Cargo.toml | 15 ++ crates/transport-http/src/layers/mod.rs | 5 + crates/transport-http/src/layers/mpp.rs | 175 +++++++++++++++++++ crates/transport-http/src/lib.rs | 7 + crates/transport-http/src/mpp_reqwest.rs | 207 +++++++++++++++++++++++ crates/transport-ws/Cargo.toml | 11 ++ crates/transport-ws/src/lib.rs | 5 + crates/transport-ws/src/mpp.rs | 141 +++++++++++++++ 8 files changed, 566 insertions(+) create mode 100644 crates/transport-http/src/layers/mpp.rs create mode 100644 crates/transport-http/src/mpp_reqwest.rs create mode 100644 crates/transport-ws/src/mpp.rs diff --git a/crates/transport-http/Cargo.toml b/crates/transport-http/Cargo.toml index 8a27693ec43..d2b19268ca5 100644 --- a/crates/transport-http/Cargo.toml +++ b/crates/transport-http/Cargo.toml @@ -49,6 +49,9 @@ opentelemetry = { version = "0.31.0", optional = true } opentelemetry-http = { version = "0.31.0", optional = true} tracing-opentelemetry = { version = "0.32.0", optional = true} +# mpp payment layer +mpp = { version = "0.7", default-features = false, features = ["client"], optional = true } + [features] default = ["reqwest", "reqwest-default-tls", "traceparent"] reqwest = [ @@ -87,3 +90,15 @@ traceparent = [ reqwest-default-tls = ["reqwest?/default-tls"] reqwest-native-tls = ["reqwest?/native-tls"] reqwest-rustls-tls = ["reqwest?/rustls"] +mpp = [ + "dep:mpp", + "dep:alloy-json-rpc", + "dep:serde_json", + "dep:tower", + "dep:tracing", +] +mpp-tempo = [ + "mpp", + "mpp/tempo", +] + diff --git a/crates/transport-http/src/layers/mod.rs b/crates/transport-http/src/layers/mod.rs index 877c86627e3..c3ae1d5b561 100644 --- a/crates/transport-http/src/layers/mod.rs +++ b/crates/transport-http/src/layers/mod.rs @@ -11,3 +11,8 @@ pub use auth::{AuthLayer, AuthService}; mod trace; #[cfg(feature = "traceparent")] pub use trace::{TraceParentLayer, TraceParentService}; + +#[cfg(all(feature = "mpp", feature = "hyper"))] +mod mpp; +#[cfg(all(feature = "mpp", feature = "hyper"))] +pub use self::mpp::{MppLayer, MppService}; diff --git a/crates/transport-http/src/layers/mpp.rs b/crates/transport-http/src/layers/mpp.rs new file mode 100644 index 00000000000..022750b72aa --- /dev/null +++ b/crates/transport-http/src/layers/mpp.rs @@ -0,0 +1,175 @@ +use crate::hyper::{header, Request, Response}; +use alloy_transport::{TransportError, TransportErrorKind}; +use hyper::header::HeaderValue; +use mpp::{client::PaymentProvider, format_authorization, PaymentChallenge}; +use std::{future::Future, pin::Pin, task}; +use tower::{Layer, Service}; +use tracing::debug; + +/// A tower [`Layer`] that intercepts HTTP 402 responses and automatically +/// handles payment challenges using the [Machine Payments Protocol (MPP)]. +/// +/// When the upstream service returns a `402 Payment Required` response with +/// a `WWW-Authenticate: Payment ...` header, this layer: +/// 1. Parses the [`PaymentChallenge`] from the header +/// 2. Calls [`PaymentProvider::pay`] to obtain a [`PaymentCredential`] +/// 3. Retries the original request with an `Authorization: Payment ...` header +/// +/// Non-402 responses pass through unchanged. +/// +/// # Example +/// +/// ```ignore +/// use alloy_transport_http::{HyperClient, MppLayer}; +/// +/// let client = HyperClient::new() +/// .layer(MppLayer::new(my_provider)); +/// ``` +/// +/// [Machine Payments Protocol (MPP)]: https://github.com/tempoxyz/mpp-rs +/// [`PaymentCredential`]: mpp::PaymentCredential +#[derive(Clone, Debug)] +pub struct MppLayer

{ + provider: P, +} + +impl

MppLayer

{ + /// Create a new [`MppLayer`] with the given [`PaymentProvider`]. + pub const fn new(provider: P) -> Self { + Self { provider } + } +} + +#[cfg(feature = "mpp-tempo")] +impl MppLayer { + /// Create an [`MppLayer`] backed by a `TempoProvider` in direct signing mode. + /// + /// # Errors + /// + /// Returns an error if `rpc_url` is not a valid URL. + pub fn with_tempo_signer( + signer: mpp::PrivateKeySigner, + rpc_url: impl AsRef, + ) -> Result { + Ok(Self::new(mpp::client::TempoProvider::new(signer, rpc_url)?)) + } + + /// Create an [`MppLayer`] backed by a `TempoProvider` in keychain + /// (access key) signing mode. + /// + /// # Errors + /// + /// Returns an error if `rpc_url` is not a valid URL. + pub fn with_tempo_access_key( + signer: mpp::PrivateKeySigner, + wallet_address: mpp::Address, + rpc_url: impl AsRef, + ) -> Result { + use mpp::client::tempo::signing::{KeychainVersion, TempoSigningMode}; + + Ok(Self::new(mpp::client::TempoProvider::new(signer, rpc_url)?.with_signing_mode( + TempoSigningMode::Keychain { + wallet: wallet_address, + key_authorization: None, + version: KeychainVersion::V2, + }, + ))) + } +} + +impl Layer for MppLayer

{ + type Service = MppService; + + fn layer(&self, inner: S) -> Self::Service { + MppService { inner, provider: self.provider.clone() } + } +} + +/// A service that handles MPP 402 payment challenges automatically. +/// +/// See [`MppLayer`] for details. +#[derive(Clone, Debug)] +pub struct MppService { + inner: S, + provider: P, +} + +impl Service> for MppService +where + S: Service, Response = Response> + Clone + Send + Sync + 'static, + S::Future: Send, + S::Error: std::error::Error + Send + Sync + 'static, + B: From> + Send + 'static + Clone + Sync, + ResBody: hyper::body::Body + Send + 'static, + ResBody::Error: std::error::Error + Send + Sync + 'static, + ResBody::Data: Send, + P: PaymentProvider + 'static, +{ + type Response = Response; + type Error = TransportError; + type Future = + Pin, Self::Error>> + Send + 'static>>; + + fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> task::Poll> { + self.inner.poll_ready(cx).map_err(TransportErrorKind::custom) + } + + fn call(&mut self, req: Request) -> Self::Future { + let mut service = self.inner.clone(); + let provider = self.provider.clone(); + let body = req.body().clone(); + let original_parts = req.uri().clone(); + let original_headers = req.headers().clone(); + let original_method = req.method().clone(); + + Box::pin(async move { + let resp = service.call(req).await.map_err(TransportErrorKind::custom)?; + + if resp.status() != hyper::StatusCode::PAYMENT_REQUIRED { + return Ok(resp); + } + + debug!("received 402, attempting MPP payment"); + + let www_auth = resp + .headers() + .get(header::WWW_AUTHENTICATE) + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| { + TransportErrorKind::custom_str("402 response missing WWW-Authenticate header") + })? + .to_owned(); + + let challenge = PaymentChallenge::from_header(&www_auth).map_err(|e| { + TransportErrorKind::custom_str(&format!("failed to parse MPP challenge: {e}")) + })?; + + let credential = provider + .pay(&challenge) + .await + .map_err(|e| TransportErrorKind::custom_str(&format!("MPP payment failed: {e}")))?; + + let auth_value = format_authorization(&credential).map_err(|e| { + TransportErrorKind::custom_str(&format!("failed to format MPP authorization: {e}")) + })?; + + debug!("MPP payment succeeded, retrying request"); + + let mut retry = Request::builder().method(original_method).uri(original_parts); + + for (name, value) in original_headers.iter() { + retry = retry.header(name, value); + } + + let retry = retry + .header( + header::AUTHORIZATION, + HeaderValue::from_str(&auth_value).map_err(TransportErrorKind::custom)?, + ) + .body(body) + .map_err(TransportErrorKind::custom)?; + + service.call(retry).await.map_err(TransportErrorKind::custom) + }) + } +} diff --git a/crates/transport-http/src/lib.rs b/crates/transport-http/src/lib.rs index 6f2a2f072ee..5f42baff30c 100644 --- a/crates/transport-http/src/lib.rs +++ b/crates/transport-http/src/lib.rs @@ -23,9 +23,16 @@ pub use hyper_util; mod layers; #[cfg(all(not(target_family = "wasm"), feature = "jwt-auth"))] pub use layers::{AuthLayer, AuthService}; +#[cfg(all(not(target_family = "wasm"), feature = "mpp", feature = "hyper"))] +pub use layers::{MppLayer, MppService}; #[cfg(all(not(target_family = "wasm"), feature = "traceparent"))] pub use layers::{TraceParentLayer, TraceParentService}; +#[cfg(all(feature = "mpp", feature = "reqwest"))] +mod mpp_reqwest; +#[cfg(all(feature = "mpp", feature = "reqwest"))] +pub use mpp_reqwest::{MppReqwestClient, MppReqwestConnect, MppReqwestTransport}; + #[cfg(all(not(target_family = "wasm"), feature = "hyper"))] mod hyper_transport; #[cfg(all(not(target_family = "wasm"), feature = "hyper"))] diff --git a/crates/transport-http/src/mpp_reqwest.rs b/crates/transport-http/src/mpp_reqwest.rs new file mode 100644 index 00000000000..886265ab4a1 --- /dev/null +++ b/crates/transport-http/src/mpp_reqwest.rs @@ -0,0 +1,207 @@ +use crate::{Http, HttpConnect}; +use alloy_json_rpc::{RequestPacket, ResponsePacket}; +use alloy_transport::{TransportError, TransportErrorKind, TransportFut, TransportResult}; +use itertools::Itertools; +use mpp::{client::PaymentProvider, format_authorization, PaymentChallenge}; +use std::task; +use tower::Service; +use tracing::{debug, debug_span, instrument, trace, Instrument}; +use url::Url; + +/// A reqwest-based HTTP client that automatically handles MPP 402 payment +/// challenges. +/// +/// This wraps a [`reqwest::Client`] together with a [`PaymentProvider`]. +/// When a request receives a `402 Payment Required` response with a +/// `WWW-Authenticate: Payment ...` header, the client: +/// 1. Parses the [`PaymentChallenge`] +/// 2. Calls [`PaymentProvider::pay`] to obtain a credential +/// 3. Retries the request with an `Authorization: Payment ...` header +/// +/// # Example +/// +/// ```ignore +/// use alloy_transport_http::{Http, MppReqwestClient}; +/// +/// let client = Http::mpp_reqwest("https://rpc.example.com".parse()?, provider); +/// ``` +#[derive(Clone, Debug)] +pub struct MppReqwestClient

{ + client: reqwest::Client, + provider: P, +} + +impl

MppReqwestClient

{ + /// Create a new [`MppReqwestClient`] with the default reqwest client. + pub fn new(provider: P) -> Self { + Self { client: reqwest::Client::new(), provider } + } + + /// Create a new [`MppReqwestClient`] with a custom reqwest client. + pub const fn with_client(client: reqwest::Client, provider: P) -> Self { + Self { client, provider } + } +} + +#[cfg(feature = "mpp-tempo")] +impl MppReqwestClient { + /// Create an [`MppReqwestClient`] backed by a `TempoProvider` in direct + /// signing mode. + /// + /// # Errors + /// + /// Returns an error if `rpc_url` is not a valid URL. + pub fn with_tempo_signer( + signer: mpp::PrivateKeySigner, + rpc_url: impl AsRef, + ) -> Result { + Ok(Self::new(mpp::client::TempoProvider::new(signer, rpc_url)?)) + } + + /// Create an [`MppReqwestClient`] backed by a `TempoProvider` in keychain + /// (access key) signing mode. + /// + /// # Errors + /// + /// Returns an error if `rpc_url` is not a valid URL. + pub fn with_tempo_access_key( + signer: mpp::PrivateKeySigner, + wallet_address: mpp::Address, + rpc_url: impl AsRef, + ) -> Result { + use mpp::client::tempo::signing::{KeychainVersion, TempoSigningMode}; + + Ok(Self::new(mpp::client::TempoProvider::new(signer, rpc_url)?.with_signing_mode( + TempoSigningMode::Keychain { + wallet: wallet_address, + key_authorization: None, + version: KeychainVersion::V2, + }, + ))) + } +} + +/// An [`Http`] transport using [`reqwest`] with automatic MPP payment handling. +pub type MppReqwestTransport

= Http>; + +/// Connection details for an [`MppReqwestTransport`]. +pub type MppReqwestConnect

= HttpConnect>; + +impl Http> { + /// Create a new [`Http`] transport with MPP payment support. + pub fn mpp_reqwest(url: Url, provider: P) -> Self { + Self::with_client(MppReqwestClient::new(provider), url) + } + + #[instrument(name = "request", skip_all, fields(method_names = %req.method_names().take(3).format(", ").to_string()))] + async fn do_mpp_reqwest(self, req: RequestPacket) -> TransportResult { + let resp = self + .client + .client + .post(self.url.clone()) + .json(&req) + .headers(req.headers()) + .send() + .await + .map_err(TransportErrorKind::custom)?; + + let status = resp.status(); + debug!(%status, "received response from server"); + + if status == reqwest::StatusCode::PAYMENT_REQUIRED { + debug!("received 402, attempting MPP payment"); + + let www_auth = resp + .headers() + .get(reqwest::header::WWW_AUTHENTICATE) + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| { + TransportErrorKind::custom_str("402 response missing WWW-Authenticate header") + })? + .to_owned(); + + let challenge = PaymentChallenge::from_header(&www_auth).map_err(|e| { + TransportErrorKind::custom_str(&format!("failed to parse MPP challenge: {e}")) + })?; + + let credential = + self.client.provider.pay(&challenge).await.map_err(|e| { + TransportErrorKind::custom_str(&format!("MPP payment failed: {e}")) + })?; + + let auth_value = format_authorization(&credential).map_err(|e| { + TransportErrorKind::custom_str(&format!("failed to format MPP authorization: {e}")) + })?; + + debug!("MPP payment succeeded, retrying request"); + + let retry_resp = self + .client + .client + .post(self.url) + .json(&req) + .headers(req.headers()) + .header(reqwest::header::AUTHORIZATION, &auth_value) + .send() + .await + .map_err(TransportErrorKind::custom)?; + + let retry_status = retry_resp.status(); + debug!(%retry_status, "received retry response from server"); + + let body = retry_resp.bytes().await.map_err(TransportErrorKind::custom)?; + + if tracing::enabled!(tracing::Level::TRACE) { + trace!(body = %String::from_utf8_lossy(&body), "response body"); + } else { + debug!(bytes = body.len(), "retrieved response body"); + } + + if !retry_status.is_success() { + return Err(TransportErrorKind::http_error( + retry_status.as_u16(), + String::from_utf8_lossy(&body).into_owned(), + )); + } + + return serde_json::from_slice(&body) + .map_err(|err| TransportError::deser_err(err, String::from_utf8_lossy(&body))); + } + + let body = resp.bytes().await.map_err(TransportErrorKind::custom)?; + + if tracing::enabled!(tracing::Level::TRACE) { + trace!(body = %String::from_utf8_lossy(&body), "response body"); + } else { + debug!(bytes = body.len(), "retrieved response body"); + } + + if !status.is_success() { + return Err(TransportErrorKind::http_error( + status.as_u16(), + String::from_utf8_lossy(&body).into_owned(), + )); + } + + serde_json::from_slice(&body) + .map_err(|err| TransportError::deser_err(err, String::from_utf8_lossy(&body))) + } +} + +impl Service for Http> { + type Response = ResponsePacket; + type Error = TransportError; + type Future = TransportFut<'static>; + + #[inline] + fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll> { + task::Poll::Ready(Ok(())) + } + + #[inline] + fn call(&mut self, req: RequestPacket) -> Self::Future { + let this = self.clone(); + let span = debug_span!("MppReqwestTransport", url = %this.url); + Box::pin(this.do_mpp_reqwest(req).instrument(span.or_current())) + } +} diff --git a/crates/transport-ws/Cargo.toml b/crates/transport-ws/Cargo.toml index 6d052715dfb..514db0758de 100644 --- a/crates/transport-ws/Cargo.toml +++ b/crates/transport-ws/Cargo.toml @@ -39,6 +39,17 @@ tokio = { workspace = true, features = ["sync", "rt", "time"] } rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs"] } tokio-tungstenite = { workspace = true, features = ["rustls-tls-webpki-roots"] } +# mpp payment support +mpp = { version = "0.7", default-features = false, features = ["client"], optional = true } + # WASM only [target.'cfg(target_family = "wasm")'.dependencies] ws_stream_wasm = "0.7.4" + +[features] +mpp = ["dep:mpp"] +mpp-tempo = [ + "mpp", + "mpp/tempo", +] + diff --git a/crates/transport-ws/src/lib.rs b/crates/transport-ws/src/lib.rs index 0ca203b5e96..e5de68a2ece 100644 --- a/crates/transport-ws/src/lib.rs +++ b/crates/transport-ws/src/lib.rs @@ -17,6 +17,11 @@ mod native; #[cfg(not(target_family = "wasm"))] pub use native::{WebSocketConfig, WsConnect}; +#[cfg(all(not(target_family = "wasm"), feature = "mpp"))] +mod mpp; +#[cfg(all(not(target_family = "wasm"), feature = "mpp"))] +pub use self::mpp::MppWsConnect; + #[cfg(target_family = "wasm")] mod wasm; #[cfg(target_family = "wasm")] diff --git a/crates/transport-ws/src/mpp.rs b/crates/transport-ws/src/mpp.rs new file mode 100644 index 00000000000..0a5126c5f93 --- /dev/null +++ b/crates/transport-ws/src/mpp.rs @@ -0,0 +1,141 @@ +use crate::WsConnect; +use alloy_pubsub::PubSubConnect; +use alloy_transport::{Authorization, TransportErrorKind, TransportResult}; +use mpp::{client::PaymentProvider, format_authorization, PaymentChallenge}; +use tokio_tungstenite::tungstenite; +use tracing::debug; + +/// A wrapper around [`WsConnect`] that handles MPP 402 payment challenges +/// during the WebSocket upgrade handshake. +/// +/// If the server responds with `402 Payment Required` and a +/// `WWW-Authenticate: Payment ...` header during connection, this wrapper: +/// 1. Parses the [`PaymentChallenge`] +/// 2. Calls [`PaymentProvider::pay`] to obtain a credential +/// 3. Retries the connection with an `Authorization: Payment ...` header +/// +/// # Example +/// +/// ```ignore +/// use alloy_transport_ws::{WsConnect, MppWsConnect}; +/// +/// let ws = WsConnect::new("wss://example.com/rpc"); +/// let client = ClientBuilder::default() +/// .pubsub(MppWsConnect::new(ws, my_provider)) +/// .await?; +/// ``` +/// +/// [`PaymentChallenge`]: mpp::PaymentChallenge +#[derive(Clone, Debug)] +pub struct MppWsConnect

{ + inner: WsConnect, + provider: P, +} + +impl

MppWsConnect

{ + /// Create a new [`MppWsConnect`] wrapping the given [`WsConnect`] and + /// [`PaymentProvider`]. + pub const fn new(inner: WsConnect, provider: P) -> Self { + Self { inner, provider } + } +} + +#[cfg(feature = "mpp-tempo")] +impl MppWsConnect { + /// Create an [`MppWsConnect`] backed by a `TempoProvider` in direct signing mode. + /// + /// # Errors + /// + /// Returns an error if `rpc_url` is not a valid URL. + pub fn with_tempo_signer( + inner: WsConnect, + signer: mpp::PrivateKeySigner, + rpc_url: impl AsRef, + ) -> Result { + Ok(Self::new(inner, mpp::client::TempoProvider::new(signer, rpc_url)?)) + } + + /// Create an [`MppWsConnect`] backed by a `TempoProvider` in keychain + /// (access key) signing mode. + /// + /// # Errors + /// + /// Returns an error if `rpc_url` is not a valid URL. + pub fn with_tempo_access_key( + inner: WsConnect, + signer: mpp::PrivateKeySigner, + wallet_address: mpp::Address, + rpc_url: impl AsRef, + ) -> Result { + use mpp::client::tempo::signing::{KeychainVersion, TempoSigningMode}; + + Ok(Self::new( + inner, + mpp::client::TempoProvider::new(signer, rpc_url)?.with_signing_mode( + TempoSigningMode::Keychain { + wallet: wallet_address, + key_authorization: None, + version: KeychainVersion::V2, + }, + ), + )) + } +} + +impl

PubSubConnect for MppWsConnect

+where + P: PaymentProvider + 'static, +{ + fn is_local(&self) -> bool { + self.inner.is_local() + } + + async fn connect(&self) -> TransportResult { + match self.inner.connect().await { + Ok(handle) => Ok(handle), + Err(err) => { + // Extract the tungstenite HTTP error from the transport error chain. + let challenge = extract_402_challenge(&err).ok_or(err)?; + + debug!("received 402 during WS upgrade, attempting MPP payment"); + + let credential = self.provider.pay(&challenge).await.map_err(|e| { + TransportErrorKind::custom_str(&format!("MPP payment failed: {e}")) + })?; + + let auth_value = format_authorization(&credential).map_err(|e| { + TransportErrorKind::custom_str(&format!( + "failed to format MPP authorization: {e}" + )) + })?; + + debug!("MPP payment succeeded, retrying WS connection"); + + let retry = self.inner.clone().with_auth(Authorization::raw(auth_value)); + retry.connect().await + } + } + } +} + +/// Try to extract a [`PaymentChallenge`] from a transport error that wraps a +/// tungstenite HTTP 402 response. +fn extract_402_challenge(err: &alloy_transport::TransportError) -> Option { + use std::error::Error; + + // Walk the error source chain to find a tungstenite::Error. + let mut source: Option<&(dyn Error + 'static)> = Some(err); + while let Some(current) = source { + if let Some(tungstenite::Error::Http(response)) = + current.downcast_ref::() + { + if response.status() == http::StatusCode::PAYMENT_REQUIRED { + let www_auth = + response.headers().get(http::header::WWW_AUTHENTICATE)?.to_str().ok()?; + return PaymentChallenge::from_header(www_auth).ok(); + } + } + source = current.source(); + } + None +} From ea184444ab2d58f7305acebc9781d5cc0c9ca863 Mon Sep 17 00:00:00 2001 From: steven Date: Wed, 25 Mar 2026 17:31:16 -0600 Subject: [PATCH 2/4] update --- crates/transport-http/Cargo.toml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/transport-http/Cargo.toml b/crates/transport-http/Cargo.toml index 73cad866fd7..5a100e5298c 100644 --- a/crates/transport-http/Cargo.toml +++ b/crates/transport-http/Cargo.toml @@ -90,12 +90,4 @@ traceparent = [ reqwest-default-tls = ["reqwest?/default-tls"] reqwest-native-tls = ["reqwest?/native-tls"] reqwest-rustls-tls = ["reqwest?/rustls"] -mpp = [ - "dep:mpp", - "dep:alloy-json-rpc", - "dep:serde_json", - "dep:tower", - "dep:tracing", -] - - +mpp = ["dep:mpp"] From 787576ae4974431dd0042bb16484c889fdbd45b1 Mon Sep 17 00:00:00 2001 From: steven Date: Thu, 26 Mar 2026 09:26:49 -0600 Subject: [PATCH 3/4] rr From b59ae9270f06f7be4f6537d0f5db2d4ff0a1a134 Mon Sep 17 00:00:00 2001 From: steven Date: Thu, 26 Mar 2026 09:33:16 -0600 Subject: [PATCH 4/4] dep --- crates/transport-http/Cargo.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/transport-http/Cargo.toml b/crates/transport-http/Cargo.toml index 5a100e5298c..d5e1dcd4174 100644 --- a/crates/transport-http/Cargo.toml +++ b/crates/transport-http/Cargo.toml @@ -90,4 +90,10 @@ traceparent = [ reqwest-default-tls = ["reqwest?/default-tls"] reqwest-native-tls = ["reqwest?/native-tls"] reqwest-rustls-tls = ["reqwest?/rustls"] -mpp = ["dep:mpp"] +mpp = [ + "dep:mpp", + "dep:alloy-json-rpc", + "dep:serde_json", + "dep:tower", + "dep:tracing", +]