Skip to content

Commit aceb75d

Browse files
decofegrandizzy
andcommitted
feat: add Transport trait abstraction for server and client
Foundation for WebSocket support (TOOLS-323). Adds transport traits matching mppx's Transport interface: - server::transport::Transport — get_credential, respond_challenge, respond_receipt with associated Input/Output types - server::transport::HttpTransport — HTTP impl using http crate types - client::transport::Transport — is_payment_required, get_challenge, set_credential with associated Request/Response types - client::transport::HttpTransport — reqwest impl Existing HTTP logic (fetch.rs, axum.rs, middleware.rs) is unchanged; the traits provide a parallel abstraction that WebSocket/MCP transports will implement in follow-up PRs. Co-Authored-By: grandizzy <38490174+grandizzy@users.noreply.github.com>
1 parent cdc32a9 commit aceb75d

5 files changed

Lines changed: 400 additions & 1 deletion

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ default = ["reqwest-default-tls"]
1616

1717
# Side selection
1818
client = ["dep:reqwest"]
19-
server = ["tokio", "futures-core", "async-stream"]
19+
server = ["tokio", "futures-core", "async-stream", "http-types"]
2020

2121
# Method implementations
2222
evm = ["alloy", "hex", "rand"]

src/client/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
2020
mod error;
2121
mod provider;
22+
pub mod transport;
2223

2324
#[cfg(feature = "tempo")]
2425
pub mod tempo;

src/client/transport.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//! Client-side transport abstraction.
2+
//!
3+
//! Abstracts how challenges are received and credentials are sent
4+
//! across different transport protocols (HTTP, WebSocket, MCP, etc.).
5+
//!
6+
//! This matches the mppx `Transport` interface from `mppx/client`.
7+
//!
8+
//! # Built-in transports
9+
//!
10+
//! - [`http()`]: HTTP transport (Authorization/WWW-Authenticate headers)
11+
//!
12+
//! # Custom transports
13+
//!
14+
//! Implement [`Transport`] for custom protocols:
15+
//!
16+
//! ```ignore
17+
//! use mpp::client::transport::{Transport};
18+
//! use mpp::protocol::core::PaymentChallenge;
19+
//!
20+
//! struct MyTransport;
21+
//!
22+
//! impl Transport for MyTransport {
23+
//! type Request = MyRequest;
24+
//! type Response = MyResponse;
25+
//!
26+
//! fn name(&self) -> &str { "custom" }
27+
//! // ...
28+
//! }
29+
//! ```
30+
31+
use crate::error::MppError;
32+
use crate::protocol::core::PaymentChallenge;
33+
34+
/// Client-side transport trait.
35+
///
36+
/// Abstracts how the client detects payment-required responses, extracts
37+
/// challenges, and attaches credentials to requests.
38+
pub trait Transport: Send + Sync {
39+
/// The outgoing request type.
40+
type Request;
41+
/// The incoming response type.
42+
type Response;
43+
44+
/// Transport name for identification (e.g., "http", "ws", "mcp").
45+
fn name(&self) -> &str;
46+
47+
/// Check if a response indicates payment is required.
48+
fn is_payment_required(&self, response: &Self::Response) -> bool;
49+
50+
/// Extract the payment challenge from a payment-required response.
51+
fn get_challenge(&self, response: &Self::Response) -> Result<PaymentChallenge, MppError>;
52+
53+
/// Attach a credential string to a request.
54+
fn set_credential(&self, request: Self::Request, credential: &str) -> Self::Request;
55+
}
56+
57+
/// Reqwest HTTP transport for client-side payment handling.
58+
///
59+
/// - Detects payment required via 402 status
60+
/// - Extracts challenges from `WWW-Authenticate` header
61+
/// - Sends credentials via `Authorization` header
62+
///
63+
/// This is the default transport, matching mppx's `Transport.http()`.
64+
#[cfg(feature = "client")]
65+
pub struct HttpTransport;
66+
67+
/// Create an HTTP transport instance.
68+
#[cfg(feature = "client")]
69+
pub fn http() -> HttpTransport {
70+
HttpTransport
71+
}
72+
73+
#[cfg(feature = "client")]
74+
impl Transport for HttpTransport {
75+
type Request = reqwest::RequestBuilder;
76+
type Response = reqwest::Response;
77+
78+
fn name(&self) -> &str {
79+
"http"
80+
}
81+
82+
fn is_payment_required(&self, response: &Self::Response) -> bool {
83+
response.status() == reqwest::StatusCode::PAYMENT_REQUIRED
84+
}
85+
86+
fn get_challenge(&self, response: &Self::Response) -> Result<PaymentChallenge, MppError> {
87+
let header = response
88+
.headers()
89+
.get(reqwest::header::WWW_AUTHENTICATE)
90+
.ok_or_else(|| MppError::MissingHeader("WWW-Authenticate".to_string()))?;
91+
92+
let header_str = header.to_str().map_err(|e| {
93+
MppError::MalformedCredential(Some(format!("invalid WWW-Authenticate header: {e}")))
94+
})?;
95+
96+
crate::protocol::core::parse_www_authenticate(header_str)
97+
}
98+
99+
fn set_credential(&self, request: Self::Request, credential: &str) -> Self::Request {
100+
request.header(reqwest::header::AUTHORIZATION, credential)
101+
}
102+
}
103+
104+
#[cfg(all(test, feature = "client"))]
105+
mod tests {
106+
use super::*;
107+
108+
#[test]
109+
fn test_http_transport_name() {
110+
let transport = http();
111+
assert_eq!(transport.name(), "http");
112+
}
113+
}

src/server/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
mod amount;
3030
mod mpp;
3131
pub mod sse;
32+
pub mod transport;
3233

3334
#[cfg(feature = "tower")]
3435
pub mod middleware;

0 commit comments

Comments
 (0)