Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
aceb75d
feat: add Transport trait abstraction for server and client
decofe Mar 24, 2026
8a92119
feat: add WebSocket server transport
decofe Mar 24, 2026
808989b
feat: add WebSocket client transport
decofe Mar 24, 2026
3f95c58
feat: add Axum WebSocket handler for payment-gated sessions
decofe Mar 24, 2026
7846e9a
test: add WebSocket integration tests
decofe Mar 24, 2026
15d30fb
docs: add WebSocket payment example
decofe Mar 24, 2026
218a77c
docs: add ws feature to README
decofe Mar 24, 2026
697bdf0
ci: run WebSocket integration tests
decofe Mar 24, 2026
6f78f1a
feat: add WebSocket session handler with metered streaming
decofe Mar 24, 2026
4d15804
fix: HTTP credential parsing, WS challenge binding, doc mismatch
decofe Mar 24, 2026
58732ab
test: add challenge mismatch and wire type cross-compat tests
decofe Mar 24, 2026
1a67a7c
fix: use mock charge method in ws example for working local demo
grandizzy Mar 24, 2026
c39a0fc
refactor: extract send_error helper in axum_ws
grandizzy Mar 24, 2026
31f8104
feat: promote axum to optional dep, ungating axum_ws from test-only
grandizzy Mar 24, 2026
9f52c3b
remove unused axum_ws module, revert axum to axum-core dep
grandizzy Mar 24, 2026
575c8f2
refactor: gate ws_session module on tempo feature at mod level
grandizzy Mar 24, 2026
42cf613
refactor: remove redundant client feature gates from client/transport.rs
grandizzy Mar 24, 2026
c33170c
refactor: clean up ws_session imports, remove now_iso8601 and next_it…
grandizzy Mar 24, 2026
e2c7163
refactor: use let-else early returns across WS code
grandizzy Mar 24, 2026
cd1ebbd
merge: resolve conflicts with main (ws + stripe)
decofe Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ jobs:
- run: cargo update -p native-tls
- uses: taiki-e/install-action@cargo-hack
- name: Tests
run: cargo test --features tempo,stripe,server,client,axum,middleware,tower,utils,integration-stripe
run: cargo test --features tempo,stripe,ws,server,client,axum,middleware,tower,utils,integration-stripe,integration-ws
env:
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
- run: cargo hack check --each-feature --no-dev-deps --skip integration
- run: cargo hack check --each-feature --no-dev-deps --skip integration,integration-stripe,integration-ws
- name: Check examples
run: find examples -name Cargo.toml -exec cargo check --manifest-path {} \;

Expand Down
14 changes: 12 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ default = ["reqwest-default-tls"]

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

# Method implementations
evm = ["alloy", "hex", "rand"]
Expand All @@ -35,12 +35,16 @@ tower = ["dep:tower-layer", "dep:tower-service", "http-types", "dep:http-body",
# Axum middleware support (server-side convenience)
axum = ["dep:axum-core", "server", "http-types"]

# WebSocket support
ws = ["server", "client", "dep:tokio-tungstenite", "dep:futures-util"]

reqwest-default-tls = ["reqwest?/default-tls"]
reqwest-native-tls = ["reqwest?/native-tls"]
reqwest-rustls-tls = ["reqwest?/rustls-tls"]

# Integration tests (requires a running Tempo localnet)
integration = ["tempo", "server", "client", "axum"]
integration-ws = ["ws", "tempo", "server", "client", "axum"]
integration-stripe = ["stripe", "server", "client", "axum"]

[dependencies]
Expand Down Expand Up @@ -82,9 +86,15 @@ http-body = { version = "1", optional = true }
# Axum dependencies (optional)
axum-core = { version = "0.5", optional = true }

# WebSocket dependencies (optional)
tokio-tungstenite = { version = "0.26", optional = true }
futures-util = { version = "0.3", optional = true }

[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"] }
axum = { version = "0.8" }
axum = { version = "0.8", features = ["ws"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
hex = "0.4"
tokio-tungstenite = "0.26"
futures-util = "0.3"

34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,39 @@ let resp = reqwest::Client::new()
.await?;
```

### WebSocket

```rust
use mpp::server::ws::{WsMessage, WsResponse};

// Server: parse incoming WS message, send challenge/receipt
let msg: WsMessage = serde_json::from_str(&text)?;
if let WsMessage::Credential { credential } = msg {
let parsed = mpp::parse_authorization(&credential)?;
let receipt = mpp.verify_credential(&parsed).await?;
let resp = WsResponse::Receipt {
receipt: serde_json::to_value(&receipt)?,
};
socket.send(resp.to_text()).await;
}

// Client: detect challenge, send credential
let msg: mpp::client::ws::WsServerMessage = serde_json::from_str(&text)?;
if let WsServerMessage::Challenge { challenge, .. } = msg {
let cred_msg = serde_json::json!({
"type": "credential",
"credential": auth_string,
});
ws.send(cred_msg.to_string()).await;
}
```

WSS (WebSocket Secure) is handled at the connection layer — the transport itself is protocol-agnostic. On the server, terminate TLS via a reverse proxy (nginx, Cloudflare) or use `axum-server` with rustls. On the client, `tokio-tungstenite` supports `wss://` URLs via its `native-tls` or `rustls` features:

```toml
tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] }
```

## Feature Flags

| Feature | Description |
Expand All @@ -114,6 +147,7 @@ let resp = reqwest::Client::new()
| `middleware` | reqwest-middleware support with `PaymentMiddleware` (implies `client`) |
| `tower` | Tower middleware for server-side integration |
| `axum` | Axum extractor support for server-side convenience |
| `ws` | WebSocket transport for bidirectional session payments |
| `utils` | Hex/random utilities for development and testing |

## Payment Methods
Expand Down
21 changes: 21 additions & 0 deletions examples/ws/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "ws-example"
version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
name = "ws-server"
path = "src/server.rs"

[[bin]]
name = "ws-client"
path = "src/client.rs"

[dependencies]
mpp = { path = "../..", features = ["server", "client", "ws", "tempo"] }
axum = { version = "0.8", features = ["ws"] }
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.26"
futures-util = "0.3"
serde_json = "1"
25 changes: 25 additions & 0 deletions examples/ws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# WebSocket Payment Example

Demonstrates the MPP WebSocket payment flow with a server that streams
fortunes after payment verification.

## Running

```bash
# Start the server
cargo run --bin ws-server

# In another terminal, start the client
cargo run --bin ws-client
```

## Protocol

1. Client connects via WebSocket
2. Server sends `{ "type": "challenge", ... }`
3. Client responds with `{ "type": "credential", "credential": "Payment ..." }`
4. Server verifies payment and streams data as `{ "type": "message", "data": "..." }`
5. Server sends final `{ "type": "receipt", ... }` and closes

**Note:** This example uses a mock credential. In production, use
`TempoProvider` to sign real transactions.
111 changes: 111 additions & 0 deletions examples/ws/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! # WebSocket Payment Client
//!
//! Connects to the WS payment server, handles the challenge/credential
//! flow, and prints received fortunes.
//!
//! ## Running
//!
//! ```bash
//! # First start the server:
//! cargo run --bin ws-server
//!
//! # Then in another terminal:
//! cargo run --bin ws-client
//! ```

use futures_util::{SinkExt, StreamExt};
use mpp::client::ws::WsServerMessage;
use mpp::protocol::core::{format_authorization, PaymentPayload};
use tokio_tungstenite::tungstenite;

#[tokio::main]
async fn main() {
let url = std::env::args()
.nth(1)
.unwrap_or_else(|| "ws://127.0.0.1:3000/ws".to_string());

println!("Connecting to {url} ...");

let (mut ws, _) = tokio_tungstenite::connect_async(&url)
.await
.expect("failed to connect");

println!("Connected!");

while let Some(msg) = ws.next().await {
let msg = match msg {
Ok(tungstenite::Message::Text(text)) => text,
Ok(tungstenite::Message::Close(_)) => {
println!("Server closed connection");
break;
}
Err(e) => {
eprintln!("WS error: {e}");
break;
}
_ => continue,
};

let server_msg: WsServerMessage = match serde_json::from_str(&msg) {
Ok(m) => m,
Err(e) => {
eprintln!("Failed to parse server message: {e}");
continue;
}
};

match server_msg {
WsServerMessage::Challenge { challenge, .. } => {
println!("Received payment challenge");

// Parse the challenge
let parsed: mpp::PaymentChallenge =
serde_json::from_value(challenge).expect("parse challenge");

// Create a mock credential (in real use, sign a transaction)
let credential = mpp::PaymentCredential::new(
parsed.to_echo(),
PaymentPayload::hash(
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
),
);
let auth_str = format_authorization(&credential).unwrap();

// Send credential
let cred_msg = serde_json::json!({
"type": "credential",
"credential": auth_str,
});
ws.send(tungstenite::Message::Text(cred_msg.to_string().into()))
.await
.unwrap();
println!("Sent credential");
}
WsServerMessage::Data { data } => {
println!(" {data}");
}
WsServerMessage::NeedVoucher {
channel_id,
required_cumulative,
..
} => {
println!(
"Server needs voucher for channel {channel_id} (required: {required_cumulative})"
);
// In real use: sign and send a new voucher
}
WsServerMessage::Receipt { receipt } => {
println!("\nPayment receipt:");
println!(" Status: {}", receipt["status"]);
println!(" Reference: {}", receipt["reference"]);
break;
}
WsServerMessage::Error { error } => {
eprintln!("Server error: {error}");
// In this demo, the mock credential will fail verification.
// A real client would use TempoProvider to sign a transaction.
break;
}
}
}
}
Loading
Loading