From 6eb38f9cd4ab061eb7e2cc4bfa0ec37538de69c9 Mon Sep 17 00:00:00 2001 From: Duy Do Date: Fri, 16 Jan 2026 18:04:09 +0700 Subject: [PATCH 01/11] Traits --- x402-paywall/src/lib.rs | 48 +++++++++++++++++++++++++++++++++ x402-paywall/src/paywall.rs | 21 ++++++++------- x402-paywall/src/processor.rs | 51 ++++++++++++++--------------------- 3 files changed, 79 insertions(+), 41 deletions(-) diff --git a/x402-paywall/src/lib.rs b/x402-paywall/src/lib.rs index 295fa85..fb1c464 100644 --- a/x402-paywall/src/lib.rs +++ b/x402-paywall/src/lib.rs @@ -40,6 +40,54 @@ //! - `400 Bad Request`: Invalid payment payload or unsupported requirements. //! - `500 Internal Server Error`: Facilitator communication failures. +use std::fmt::Display; + pub mod errors; pub mod paywall; pub mod processor; + +pub trait HttpRequest { + fn get_header(&self, name: &str) -> Option<&[u8]>; + fn insert_extension(&mut self, ext: T) -> Option; +} + +pub trait HttpResponse { + fn is_success(&self) -> bool; + fn insert_header(&mut self, name: &'static str, value: &[u8]) + -> Result<(), InvalidHeaderValue>; +} + +impl HttpRequest for http::Request { + fn get_header(&self, name: &str) -> Option<&[u8]> { + self.headers().get(name).map(|v| v.as_bytes()) + } + + fn insert_extension(&mut self, ext: T) -> Option { + self.extensions_mut().insert(ext) + } +} + +#[derive(Debug)] +pub struct InvalidHeaderValue; + +impl Display for InvalidHeaderValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("invalid header value") + } +} + +impl HttpResponse for http::Response { + fn is_success(&self) -> bool { + self.status().is_success() + } + + fn insert_header( + &mut self, + name: &'static str, + value: &[u8], + ) -> Result<(), InvalidHeaderValue> { + let value = http::HeaderValue::from_bytes(value).map_err(|_| InvalidHeaderValue)?; + self.headers_mut().insert(name, value); + Ok(()) + } +} diff --git a/x402-paywall/src/paywall.rs b/x402-paywall/src/paywall.rs index 4fe97e0..9dd6db9 100644 --- a/x402-paywall/src/paywall.rs +++ b/x402-paywall/src/paywall.rs @@ -5,7 +5,6 @@ use std::fmt::Display; use bon::Builder; -use http::{Request, Response}; use x402_core::{ core::Resource, facilitator::{Facilitator, SupportedResponse}, @@ -14,6 +13,7 @@ use x402_core::{ }; use crate::{ + HttpRequest, HttpResponse, errors::ErrorResponse, processor::{PaymentState, RequestProcessor}, }; @@ -77,16 +77,15 @@ impl PayWall { /// Process an incoming request and extract payment information. /// /// Returns a [`RequestProcessor`] on success for further processing. - pub fn process_request<'pw, Req>( + pub fn process_request<'pw, Req: HttpRequest>( &'pw self, - request: Request, + request: Req, ) -> Result, ErrorResponse> { let payment_signature = request - .headers() - .get("PAYMENT-SIGNATURE") + .get_header("PAYMENT-SIGNATURE") .ok_or_else(|| self.payment_required()) .and_then(|h| { - h.to_str().map_err(|err| { + str::from_utf8(h).map_err(|err| { self.invalid_payment(format!( "Failed to decode PAYMENT-SIGNATURE header: {err}" )) @@ -128,12 +127,14 @@ impl PayWall { /// **verify** the payment, **run** the provided resource handler, and **settle** the payment on success. pub async fn handle_payment( self, - request: Request, + request: Req, handler: Fun, - ) -> Result, ErrorResponse> + ) -> Result where - Fun: FnOnce(Request) -> Fut, - Fut: Future>, + Fun: FnOnce(Req) -> Fut, + Fut: Future, + Req: HttpRequest, + Res: HttpResponse, { let response = self .update_accepts() diff --git a/x402-paywall/src/processor.rs b/x402-paywall/src/processor.rs index 43f5e0d..8e4ca1d 100644 --- a/x402-paywall/src/processor.rs +++ b/x402-paywall/src/processor.rs @@ -1,4 +1,3 @@ -use http::{HeaderValue, Request, Response}; use x402_core::{ facilitator::{ Facilitator, PaymentRequest, SettleResult, SettleSuccess, VerifyResult, VerifyValid, @@ -7,7 +6,7 @@ use x402_core::{ types::{Base64EncodedHeader, Extension, Record}, }; -use crate::{errors::ErrorResponse, paywall::PayWall}; +use crate::{HttpRequest, HttpResponse, errors::ErrorResponse, paywall::PayWall}; /// The state of a payment processed by the paywall when accessing the resource handler. /// @@ -45,15 +44,15 @@ pub struct PaymentState { /// Payment processing state before running the resource handler. /// /// See [`PayWall`] for usage in the full payment processing flow. -pub struct RequestProcessor<'pw, F: Facilitator, Req> { +pub struct RequestProcessor<'pw, F: Facilitator, Req: HttpRequest> { pub paywall: &'pw PayWall, - pub request: Request, + pub request: Req, pub payload: PaymentPayload, pub selected: PaymentRequirements, pub payment_state: PaymentState, } -impl<'pw, F: Facilitator, Req> RequestProcessor<'pw, F, Req> { +impl<'pw, F: Facilitator, Req: HttpRequest> RequestProcessor<'pw, F, Req> { /// Verify the payment with the facilitator. /// /// `self.payment_state.verified` will be populated on success. @@ -131,12 +130,10 @@ impl<'pw, F: Facilitator, Req> RequestProcessor<'pw, F, Req> { handler: Fun, ) -> Result, ErrorResponse> where - Fun: FnOnce(Request) -> Fut, - Fut: Future>, + Fun: FnOnce(Req) -> Fut, + Fut: Future, { - self.request - .extensions_mut() - .insert(self.payment_state.clone()); + self.request.insert_extension(self.payment_state.clone()); let response = handler(self.request).await; Ok(ResponseProcessor { @@ -152,13 +149,13 @@ impl<'pw, F: Facilitator, Req> RequestProcessor<'pw, F, Req> { /// Payment processing state after running the resource handler. pub struct ResponseProcessor<'pw, F: Facilitator, Res> { pub paywall: &'pw PayWall, - pub response: Response, + pub response: Res, pub payload: PaymentPayload, pub selected: PaymentRequirements, pub payment_state: PaymentState, } -impl<'pw, F: Facilitator, Res> ResponseProcessor<'pw, F, Res> { +impl<'pw, F: Facilitator, Res: HttpResponse> ResponseProcessor<'pw, F, Res> { /// Settle the payment with the facilitator after running the resource handler. /// /// After settlement, `self.payment_state.settled` will be populated on success. @@ -199,10 +196,7 @@ impl<'pw, F: Facilitator, Res> ResponseProcessor<'pw, F, Res> { /// Conditionally settle the payment based on the provided prediction function. /// /// After settlement, `self.payment_state.settled` will be populated on success. - pub async fn settle_on( - self, - predicate: impl Fn(&Response) -> bool, - ) -> Result { + pub async fn settle_on(self, predicate: impl Fn(&Res) -> bool) -> Result { if predicate(&self.response) { self.settle().await } else { @@ -214,11 +208,11 @@ impl<'pw, F: Facilitator, Res> ResponseProcessor<'pw, F, Res> { /// /// After settlement, `self.payment_state.settled` will be populated on success. pub async fn settle_on_success(self) -> Result { - self.settle_on(|resp| resp.status().is_success()).await + self.settle_on(|resp| resp.is_success()).await } /// Generate the final response, including the `PAYMENT-RESPONSE` header if settled. - pub fn response(self) -> Response { + pub fn response(self) -> Res { let mut response = self.response; if let Some(settled) = &self.payment_state.settled { @@ -234,20 +228,15 @@ impl<'pw, F: Facilitator, Res> ResponseProcessor<'pw, F, Res> { #[cfg(feature = "tracing")] tracing::warn!("Failed to encode PAYMENT-RESPONSE header: {err}; skipping") }) - .ok() - .and_then(|h| { - HeaderValue::from_str(&h.0) - .inspect_err(|err| { - #[cfg(feature = "tracing")] - tracing::warn!( - "Failed to encode PAYMENT-RESPONSE header: {err}; skipping" - ) - }) - .ok() - }); - + .ok(); if let Some(header) = header { - response.headers_mut().insert("PAYMENT-RESPONSE", header); + response + .insert_header("PAYMENT-RESPONSE", header.0.as_bytes()) + .inspect_err(|err| { + #[cfg(feature = "tracing")] + tracing::warn!("Failed to encode PAYMENT-RESPONSE header: {err}; skipping") + }) + .ok(); } } From 91a304d1df5f9b5a3bd14adb86c926c07e86b173 Mon Sep 17 00:00:00 2001 From: Duy Do Date: Fri, 16 Jan 2026 20:00:10 +0700 Subject: [PATCH 02/11] Actix --- Cargo.lock | 491 ++++++++++++++++++++++++++++++++++++++-- x402-kit/Cargo.toml | 1 + x402-paywall/Cargo.toml | 4 +- x402-paywall/src/lib.rs | 34 +++ 4 files changed, 510 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a321d09..586aeeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,219 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash 0.1.5", + "futures-core", + "h2 0.3.27", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.110", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1654a77ba142e37f049637a3e5685f864514af11fcbc51cb51eb6596afe5b8d6" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash 0.1.5", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.6.1", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -230,7 +443,7 @@ checksum = "003f46c54f22854a32b9cc7972660a476968008ad505427eabab49225309ec40" dependencies = [ "alloy-primitives", "alloy-sol-types", - "http", + "http 1.4.0", "serde", "serde_json", "thiserror", @@ -885,7 +1098,7 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -916,7 +1129,7 @@ checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", - "http", + "http 1.4.0", "http-body", "http-body-util", "mime", @@ -1083,6 +1296,27 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -1110,6 +1344,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bytestring" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +dependencies = [ + "bytes", +] + [[package]] name = "c-kzg" version = "2.1.5" @@ -1132,6 +1375,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -1197,6 +1442,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1237,6 +1493,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1613,6 +1878,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1811,6 +2086,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.12" @@ -1822,7 +2116,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.4.0", "indexmap 2.12.1", "slab", "tokio", @@ -1900,6 +2194,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.4.0" @@ -1917,7 +2222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.4.0", ] [[package]] @@ -1928,7 +2233,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", + "http 1.4.0", "http-body", "pin-project-lite", ] @@ -1955,8 +2260,8 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", - "http", + "h2 0.4.12", + "http 1.4.0", "http-body", "httparse", "httpdate", @@ -1974,7 +2279,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http", + "http 1.4.0", "hyper", "hyper-util", "rustls", @@ -2012,14 +2317,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http", + "http 1.4.0", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.1", "system-configuration", "tokio", "tower-service", @@ -2168,6 +2473,12 @@ dependencies = [ "parity-scale-codec", ] +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + [[package]] name = "impl-trait-for-tuples" version = "0.2.3" @@ -2242,6 +2553,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.82" @@ -2285,6 +2606,12 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.5.0" @@ -2315,6 +2642,23 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + [[package]] name = "lock_api" version = "0.4.14" @@ -2374,6 +2718,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.0" @@ -2381,6 +2735,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -2785,7 +3140,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2", + "socket2 0.6.1", "thiserror", "tokio", "tracing", @@ -2822,7 +3177,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.1", "tracing", "windows-sys 0.60.2", ] @@ -2948,6 +3303,35 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" + [[package]] name = "regex-syntax" version = "0.8.8" @@ -2964,8 +3348,8 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", - "h2", - "http", + "h2 0.4.12", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -3398,6 +3782,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3444,6 +3839,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -3454,6 +3859,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "slab" version = "0.4.11" @@ -3469,6 +3880,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.1" @@ -3802,8 +4223,10 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", - "socket2", + "signal-hook-registry", + "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", ] @@ -3919,7 +4342,7 @@ dependencies = [ "bitflags", "bytes", "futures-util", - "http", + "http 1.4.0", "http-body", "iri-string", "pin-project-lite", @@ -4520,6 +4943,7 @@ version = "0.1.0" name = "x402-kit" version = "2.2.1" dependencies = [ + "actix-web", "alloy", "alloy-core", "alloy-primitives", @@ -4529,7 +4953,7 @@ dependencies = [ "bon", "futures-util", "hex", - "http", + "http 1.4.0", "rand 0.9.2", "reqwest", "serde", @@ -4551,9 +4975,10 @@ dependencies = [ name = "x402-paywall" version = "2.2.1" dependencies = [ + "actix-web", "axum", "bon", - "http", + "http 1.4.0", "serde_json", "tracing", "x402-core", @@ -4675,3 +5100,31 @@ dependencies = [ "quote", "syn 2.0.110", ] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/x402-kit/Cargo.toml b/x402-kit/Cargo.toml index 002768d..56a8ff9 100644 --- a/x402-kit/Cargo.toml +++ b/x402-kit/Cargo.toml @@ -56,3 +56,4 @@ tower-http = { version = "0.6", features = ["trace"] } futures-util = { version = "0.3" } solana-pubkey = { version = "4" } tracing = { version = "0.1" } +actix-web = "4" diff --git a/x402-paywall/Cargo.toml b/x402-paywall/Cargo.toml index f9287b3..e9e201d 100644 --- a/x402-paywall/Cargo.toml +++ b/x402-paywall/Cargo.toml @@ -8,9 +8,10 @@ license = "MIT" description = "(V2 Supported) A fully modular SDK for building complex X402 payment integrations." [features] -default = ["tracing", "axum"] +default = ["tracing", "axum", "actix-web"] tracing = ["dep:tracing"] axum = ["dep:axum"] +actix-web = ["dep:actix-web"] [dependencies] x402-core = { version = "2.2.1", path = "../x402-core" } @@ -19,3 +20,4 @@ bon = { version = "3.8" } tracing = { version = "0.1", optional = true } serde_json = { version = "1.0" } axum = { version = "0.8", optional = true } +actix-web = { version = "4", optional = true, default-features = false } diff --git a/x402-paywall/src/lib.rs b/x402-paywall/src/lib.rs index fb1c464..2ddfc65 100644 --- a/x402-paywall/src/lib.rs +++ b/x402-paywall/src/lib.rs @@ -91,3 +91,37 @@ impl HttpResponse for http::Response { Ok(()) } } + +#[cfg(feature = "actix-web")] +mod actix_impl { + use actix_web::HttpMessage; + + use super::*; + impl HttpRequest for actix_web::HttpRequest { + fn get_header(&self, name: &str) -> Option<&[u8]> { + self.headers().get(name).map(|v| v.as_bytes()) + } + + fn insert_extension(&mut self, ext: T) -> Option { + self.extensions_mut().insert(ext) + } + } + + impl HttpResponse for actix_web::HttpResponse { + fn is_success(&self) -> bool { + self.status().is_success() + } + + fn insert_header( + &mut self, + name: &'static str, + value: &[u8], + ) -> Result<(), InvalidHeaderValue> { + let value = actix_web::http::header::HeaderValue::from_bytes(value) + .map_err(|_| InvalidHeaderValue)?; + let name = actix_web::http::header::HeaderName::from_static(name); + self.headers_mut().insert(name, value); + Ok(()) + } + } +} From db5e63f123d81b09d357139073b227d7329c13ff Mon Sep 17 00:00:00 2001 From: Duy Do Date: Fri, 16 Jan 2026 20:17:15 +0700 Subject: [PATCH 03/11] features --- x402-kit/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x402-kit/Cargo.toml b/x402-kit/Cargo.toml index 56a8ff9..11ea717 100644 --- a/x402-kit/Cargo.toml +++ b/x402-kit/Cargo.toml @@ -8,12 +8,13 @@ license = "MIT" description = "(V2 Supported) A fully modular SDK for building complex X402 payment integrations." [features] -default = ["facilitator-client", "evm-signer", "svm-signer", "axum"] +default = ["facilitator-client", "evm-signer", "svm-signer", "axum", "actix-web"] facilitator-client = ["dep:reqwest", "dep:http"] evm-signer = ["dep:alloy-core", "dep:alloy-signer", "dep:rand"] svm-signer = ["dep:bincode"] paywall = ["dep:x402-paywall"] axum = ["paywall", "x402-paywall/axum"] +actix-web = ["paywall", "x402-paywall/actix-web"] [dependencies] # === Core Deps === From 8172f3707ce37700f1f91469fc6a3cc3da10b450 Mon Sep 17 00:00:00 2001 From: Duy Do Date: Sat, 17 Jan 2026 14:56:21 +0700 Subject: [PATCH 04/11] request-middleware --- Cargo.lock | 276 ++++++----------------------- x402-kit/Cargo.toml | 4 +- x402-kit/src/facilitator_client.rs | 8 +- 3 files changed, 66 insertions(+), 222 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 586aeeb..a6a1c38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,7 +39,7 @@ dependencies = [ "flate2", "foldhash 0.1.5", "futures-core", - "h2 0.3.27", + "h2", "http 0.2.12", "httparse", "httpdate", @@ -545,7 +545,7 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest", + "reqwest 0.12.24", "serde", "serde_json", "thiserror", @@ -589,7 +589,7 @@ dependencies = [ "alloy-transport-http", "futures", "pin-project", - "reqwest", + "reqwest 0.12.24", "serde", "serde_json", "tokio", @@ -790,7 +790,7 @@ checksum = "90aa6825760905898c106aba9c804b131816a15041523e80b6d4fe7af6380ada" dependencies = [ "alloy-json-rpc", "alloy-transport", - "reqwest", + "reqwest 0.12.24", "serde_json", "tower", "tracing", @@ -834,6 +834,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "ark-ff" version = "0.3.0" @@ -1453,16 +1459,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1906,21 +1902,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -2105,25 +2086,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "h2" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.4.0", - "indexmap 2.12.1", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -2260,7 +2222,6 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.12", "http 1.4.0", "http-body", "httparse", @@ -2290,22 +2251,6 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.18" @@ -2325,11 +2270,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.1", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -2740,23 +2683,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2852,50 +2778,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -3346,20 +3228,15 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", - "encoding_rs", "futures-core", - "h2 0.4.12", "http 1.4.0", "http-body", "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", - "mime", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -3370,7 +3247,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", "tower", "tower-http", @@ -3382,6 +3258,52 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest-middleware" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42e2f48018a33d679ee7f477a446b697663a14e91ab0b3a0206792a22dd3aa8" +dependencies = [ + "anyhow", + "async-trait", + "http 1.4.0", + "reqwest 0.13.1", + "serde", + "thiserror", + "tower-service", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -3552,15 +3474,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "schemars" version = "0.9.0" @@ -3627,29 +3540,6 @@ dependencies = [ "cc", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "0.11.0" @@ -4071,27 +3961,6 @@ dependencies = [ "syn 2.0.110", ] -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tap" version = "1.0.1" @@ -4242,16 +4111,6 @@ dependencies = [ "syn 2.0.110", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -4335,9 +4194,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", @@ -4515,12 +4374,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -4708,17 +4561,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -4955,7 +4797,7 @@ dependencies = [ "hex", "http 1.4.0", "rand 0.9.2", - "reqwest", + "reqwest-middleware", "serde", "serde_json", "solana-pubkey", diff --git a/x402-kit/Cargo.toml b/x402-kit/Cargo.toml index 11ea717..fbcc468 100644 --- a/x402-kit/Cargo.toml +++ b/x402-kit/Cargo.toml @@ -9,7 +9,7 @@ description = "(V2 Supported) A fully modular SDK for building complex X402 paym [features] default = ["facilitator-client", "evm-signer", "svm-signer", "axum", "actix-web"] -facilitator-client = ["dep:reqwest", "dep:http"] +facilitator-client = ["dep:http", "dep:reqwest-middleware"] evm-signer = ["dep:alloy-core", "dep:alloy-signer", "dep:rand"] svm-signer = ["dep:bincode"] paywall = ["dep:x402-paywall"] @@ -30,7 +30,7 @@ solana-pubkey = { version = "4.0" } solana-signature = { version = "3.1" } # === Feature "facilitator-client" === -reqwest = { version = "0.12", optional = true, features = ["json"] } +reqwest-middleware = { version = "0.5.0", optional = true, features = ["json"] } # === Feature "evm-signer" === alloy-core = { version = "1.4", features = ["sol-types"], optional = true } diff --git a/x402-kit/src/facilitator_client.rs b/x402-kit/src/facilitator_client.rs index 62958b3..0e64ff3 100644 --- a/x402-kit/src/facilitator_client.rs +++ b/x402-kit/src/facilitator_client.rs @@ -29,7 +29,7 @@ where SRes: IntoSettleResponse + for<'de> Deserialize<'de>, { pub base_url: Url, - pub client: reqwest::Client, + pub client: reqwest_middleware::ClientWithMiddleware, pub supported_headers: HeaderMap, pub verify_headers: HeaderMap, pub settle_headers: HeaderMap, @@ -130,7 +130,7 @@ where pub fn new_from_url(base_url: Url) -> Self { FacilitatorClient { base_url, - client: reqwest::Client::new(), + client: Default::default(), supported_headers: HeaderMap::new(), verify_headers: HeaderMap::new(), settle_headers: HeaderMap::new(), @@ -235,7 +235,9 @@ pub enum FacilitatorClientError { #[error("URL parse error: {0}")] UrlParseError(#[from] url::ParseError), #[error("HTTP request error: {0}")] - HttpRequestError(#[from] reqwest::Error), + HttpRequestError(#[from] reqwest_middleware::reqwest::Error), + #[error("HTTP request error: {0}")] + HttpRequestMiddlewareError(#[from] reqwest_middleware::Error), #[error("JSON Serialization/Deserialization error: {0}")] SerdeJsonError(#[from] serde_json::Error), } From 4831ae2b0d8680e1cb56552235222a11ee34cdc1 Mon Sep 17 00:00:00 2001 From: Duy Do Date: Sat, 17 Jan 2026 15:45:12 +0700 Subject: [PATCH 05/11] Downgrade --- Cargo.lock | 99 ++++++++++++++++++++------------------------- x402-kit/Cargo.toml | 2 +- 2 files changed, 45 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6a1c38..2f43335 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -315,7 +315,7 @@ dependencies = [ "futures", "futures-util", "serde_json", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -357,7 +357,7 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -382,7 +382,7 @@ dependencies = [ "alloy-rlp", "borsh", "serde", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -405,7 +405,7 @@ dependencies = [ "serde", "serde_with", "sha2", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -446,7 +446,7 @@ dependencies = [ "http 1.4.0", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "tracing", ] @@ -473,7 +473,7 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -545,10 +545,10 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest 0.12.24", + "reqwest", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", "url", @@ -589,7 +589,7 @@ dependencies = [ "alloy-transport-http", "futures", "pin-project", - "reqwest 0.12.24", + "reqwest", "serde", "serde_json", "tokio", @@ -641,7 +641,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -667,7 +667,7 @@ dependencies = [ "either", "elliptic-curve", "k256", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -683,7 +683,7 @@ dependencies = [ "async-trait", "k256", "rand 0.8.5", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -774,7 +774,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "tokio", "tower", "tracing", @@ -790,7 +790,7 @@ checksum = "90aa6825760905898c106aba9c804b131816a15041523e80b6d4fe7af6380ada" dependencies = [ "alloy-json-rpc", "alloy-transport", - "reqwest 0.12.24", + "reqwest", "serde_json", "tower", "tracing", @@ -3023,7 +3023,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.6.1", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -3044,7 +3044,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -3258,49 +3258,18 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "reqwest" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" -dependencies = [ - "base64", - "bytes", - "futures-core", - "http 1.4.0", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "reqwest-middleware" -version = "0.5.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f42e2f48018a33d679ee7f477a446b697663a14e91ab0b3a0206792a22dd3aa8" +checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" dependencies = [ "anyhow", "async-trait", "http 1.4.0", - "reqwest 0.13.1", + "reqwest", "serde", - "thiserror", + "thiserror 1.0.69", "tower-service", ] @@ -3980,13 +3949,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", ] [[package]] @@ -4773,7 +4762,7 @@ dependencies = [ "bon", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "url", ] @@ -4802,7 +4791,7 @@ dependencies = [ "serde_json", "solana-pubkey", "solana-signature", - "thiserror", + "thiserror 2.0.17", "tokio", "tower-http", "tracing", diff --git a/x402-kit/Cargo.toml b/x402-kit/Cargo.toml index fbcc468..1f910a5 100644 --- a/x402-kit/Cargo.toml +++ b/x402-kit/Cargo.toml @@ -30,7 +30,7 @@ solana-pubkey = { version = "4.0" } solana-signature = { version = "3.1" } # === Feature "facilitator-client" === -reqwest-middleware = { version = "0.5.0", optional = true, features = ["json"] } +reqwest-middleware = { version = "0.4.2", optional = true, features = ["json"] } # === Feature "evm-signer" === alloy-core = { version = "1.4", features = ["sol-types"], optional = true } From f2ea7d39fbafc1ab2dc0e7a247ae73cf45fa0fe8 Mon Sep 17 00:00:00 2001 From: Duy Do Date: Tue, 20 Jan 2026 15:32:24 +0700 Subject: [PATCH 06/11] B --- x402-paywall/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x402-paywall/src/lib.rs b/x402-paywall/src/lib.rs index 2ddfc65..c1fd7a5 100644 --- a/x402-paywall/src/lib.rs +++ b/x402-paywall/src/lib.rs @@ -107,7 +107,7 @@ mod actix_impl { } } - impl HttpResponse for actix_web::HttpResponse { + impl HttpResponse for actix_web::HttpResponse { fn is_success(&self) -> bool { self.status().is_success() } From bb82ea5fc0be073414dbdeec46b18ef15cf47f85 Mon Sep 17 00:00:00 2001 From: Duy Do Date: Wed, 21 Jan 2026 00:36:28 +0700 Subject: [PATCH 07/11] Error --- x402-paywall/src/errors.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/x402-paywall/src/errors.rs b/x402-paywall/src/errors.rs index a5350df..e85b9ba 100644 --- a/x402-paywall/src/errors.rs +++ b/x402-paywall/src/errors.rs @@ -21,6 +21,12 @@ pub struct ErrorResponse { pub body: Box, } +impl Display for ErrorResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("payment required") + } +} + impl ErrorResponse { /// Payment needed to access resource pub fn payment_required( @@ -165,3 +171,30 @@ impl axum::response::IntoResponse for ErrorResponse { response } } + +#[cfg(feature = "actix-web")] +impl ErrorResponse { + fn actix_header(&self) -> (&'static str, &str) { + match &self.header { + ErrorResponseHeader::PaymentRequired(base64_encoded_header) => { + ("payment-required", &base64_encoded_header.0) + } + ErrorResponseHeader::PaymentResponse(base64_encoded_header) => { + ("payment-required", &base64_encoded_header.0) + } + } + } +} + +#[cfg(feature = "actix-web")] +impl actix_web::ResponseError for ErrorResponse { + fn status_code(&self) -> actix_web::http::StatusCode { + actix_web::http::StatusCode::from_u16(self.status.as_u16()).unwrap() + } + + fn error_response(&self) -> actix_web::HttpResponse { + actix_web::HttpResponseBuilder::new(self.status_code()) + .insert_header(self.actix_header()) + .json(&self.body) + } +} From 20d4ca47eb03ca838a532c96cca16c7a37782a29 Mon Sep 17 00:00:00 2001 From: Duy Do Date: Wed, 21 Jan 2026 11:44:21 +0700 Subject: [PATCH 08/11] Fix header --- x402-paywall/src/errors.rs | 2 +- x402-paywall/src/processor.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x402-paywall/src/errors.rs b/x402-paywall/src/errors.rs index e85b9ba..ae91d43 100644 --- a/x402-paywall/src/errors.rs +++ b/x402-paywall/src/errors.rs @@ -180,7 +180,7 @@ impl ErrorResponse { ("payment-required", &base64_encoded_header.0) } ErrorResponseHeader::PaymentResponse(base64_encoded_header) => { - ("payment-required", &base64_encoded_header.0) + ("payment-response", &base64_encoded_header.0) } } } diff --git a/x402-paywall/src/processor.rs b/x402-paywall/src/processor.rs index 8e4ca1d..aaaf0e1 100644 --- a/x402-paywall/src/processor.rs +++ b/x402-paywall/src/processor.rs @@ -231,7 +231,7 @@ impl<'pw, F: Facilitator, Res: HttpResponse> ResponseProcessor<'pw, F, Res> { .ok(); if let Some(header) = header { response - .insert_header("PAYMENT-RESPONSE", header.0.as_bytes()) + .insert_header("payment-response", header.0.as_bytes()) .inspect_err(|err| { #[cfg(feature = "tracing")] tracing::warn!("Failed to encode PAYMENT-RESPONSE header: {err}; skipping") From f05aaac0c187bd811346f4f03b2a408d8d4f7478 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:08:25 +0800 Subject: [PATCH 09/11] Add actix-web seller example to validate actix integration (#7) * Initial plan * Add actix-web seller example to validate actix integration Co-authored-by: takasaki404 <193405421+takasaki404@users.noreply.github.com> * Fix resource URL for multi_payments_paywall in actix-web example Co-authored-by: takasaki404 <193405421+takasaki404@users.noreply.github.com> * chore(style): fix code format --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: takasaki404 <193405421+takasaki404@users.noreply.github.com> Co-authored-by: takasaki404 --- x402-kit/examples/actix_web_seller.rs | 237 ++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 x402-kit/examples/actix_web_seller.rs diff --git a/x402-kit/examples/actix_web_seller.rs b/x402-kit/examples/actix_web_seller.rs new file mode 100644 index 0000000..e45e072 --- /dev/null +++ b/x402-kit/examples/actix_web_seller.rs @@ -0,0 +1,237 @@ +use actix_web::{ + App, Error, HttpMessage, HttpResponse, HttpServer, + body::MessageBody, + dev::{ServiceRequest, ServiceResponse}, + middleware::{self, Next}, + web, +}; +use alloy::primitives::address; +use serde_json::json; +use solana_pubkey::pubkey; +use url::Url; +use url_macro::url; +use x402_kit::{ + core::Resource, + facilitator_client::{FacilitatorClient, StandardFacilitatorClient}, + networks::{evm::assets::UsdcBaseSepolia, svm::assets::UsdcSolanaDevnet}, + paywall::{paywall::PayWall, processor::PaymentState}, + schemes::{exact_evm::ExactEvm, exact_svm::ExactSvm}, + transport::Accepts, +}; + +struct AppState { + facilitator: StandardFacilitatorClient, +} + +async fn standard_paywall( + req: ServiceRequest, + next: Next, +) -> Result, Error> { + let state = req + .app_data::>() + .expect("AppState not configured") + .clone(); + + let (http_req, payload) = req.into_parts(); + let http_req_clone = http_req.clone(); + + let paywall = PayWall::builder() + .facilitator(state.facilitator.clone()) + .accepts( + ExactEvm::builder() + .amount(1000) + .asset(UsdcBaseSepolia) + .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20")) + .build(), + ) + .resource( + Resource::builder() + .url(url!("https://example.com/resource/standard")) + .description("X402 payment protected resource") + .mime_type("application/json") + .build(), + ) + .build(); + + // Run the paywall + let response = paywall + .handle_payment(http_req, |http_req| async move { + let srv_req = ServiceRequest::from_parts(http_req, payload); + match next.call(srv_req).await { + Ok(res) => res.map_into_boxed_body().into_parts().1, + Err(err) => err.error_response().map_into_boxed_body(), + } + }) + .await + .map_err(Error::from)?; + + Ok(ServiceResponse::new(http_req_clone, response)) +} + +async fn custom_paywall( + req: ServiceRequest, + next: Next, +) -> Result, Error> { + let state = req + .app_data::>() + .expect("AppState not configured") + .clone(); + + let (http_req, payload) = req.into_parts(); + let http_req_clone = http_req.clone(); + + let paywall = PayWall::builder() + .facilitator(state.facilitator.clone()) + .accepts( + ExactEvm::builder() + .amount(1000) + .asset(UsdcBaseSepolia) + .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20")) + .build(), + ) + .resource( + Resource::builder() + .url(url!("https://example.com/resource/custom")) + .description("X402 payment protected resource") + .mime_type("application/json") + .build(), + ) + .build(); + + // Skip updating accepts from facilitator, skip verifying, and settle payment before running handler + let response = paywall + .process_request(http_req) + .map_err(Error::from)? + .settle() + .await + .map_err(Error::from)? + .run_handler(|http_req| async move { + let srv_req = ServiceRequest::from_parts(http_req, payload); + match next.call(srv_req).await { + Ok(res) => res.map_into_boxed_body().into_parts().1, + Err(err) => err.error_response().map_into_boxed_body(), + } + }) + .await + .map_err(Error::from)? + .response(); + + Ok(ServiceResponse::new(http_req_clone, response)) +} + +async fn multi_payments_paywall( + req: ServiceRequest, + next: Next, +) -> Result, Error> { + let state = req + .app_data::>() + .expect("AppState not configured") + .clone(); + + let (http_req, payload) = req.into_parts(); + let http_req_clone = http_req.clone(); + + let paywall = PayWall::builder() + .facilitator(state.facilitator.clone()) + .accepts( + Accepts::new() + .push( + ExactEvm::builder() + .amount(1000) + .asset(UsdcBaseSepolia) + .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20")) + .build(), + ) + .push( + ExactSvm::builder() + .amount(1000) + .asset(UsdcSolanaDevnet) + .pay_to(pubkey!("Ge3jkza5KRfXvaq3GELNLh6V1pjjdEKNpEdGXJgjjKUR")) + .build(), + ), + ) + .resource( + Resource::builder() + .url(url!("https://example.com/resource/multi_payments")) + .description("X402 payment protected resource") + .mime_type("application/json") + .build(), + ) + .build(); + + // Run the paywall + let response = paywall + .handle_payment(http_req, |http_req| async move { + let srv_req = ServiceRequest::from_parts(http_req, payload); + match next.call(srv_req).await { + Ok(res) => res.map_into_boxed_body().into_parts().1, + Err(err) => err.error_response().map_into_boxed_body(), + } + }) + .await + .map_err(Error::from)?; + + Ok(ServiceResponse::new(http_req_clone, response)) +} + +/// Example handler for a protected resource. +/// +/// The `PayWall` middleware will inject the `PaymentState` into the request extensions. +async fn example_handler(req: actix_web::HttpRequest) -> HttpResponse { + let extensions = req.extensions(); + let payment_state = extensions.get::(); + + HttpResponse::Ok().json(json!({ + "message": "You have accessed a protected resource!", + "verify_state": payment_state + .and_then(|ps| serde_json::to_value(&ps.verified).ok()) + .unwrap_or(json!(null)), + "settle_state": payment_state + .and_then(|ps| serde_json::to_value(&ps.settled).ok()) + .unwrap_or(json!(null)), + })) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + tracing_subscriber::fmt::init(); + + let facilitator_url = std::env::var("FACILITATOR_URL") + .expect("Please set `FACILITATOR_URL` in environment variables"); + let facilitator_url = + Url::parse(&facilitator_url).expect("FACILITATOR_URL must be a valid URL"); + tracing::info!("Using facilitator at {}", facilitator_url); + let facilitator = FacilitatorClient::from_url(facilitator_url); + + let port = std::env::var("PORT") + .unwrap_or_else(|_| "3000".to_string()) + .parse::() + .expect("PORT must be a valid u16 integer"); + + tracing::info!("Starting server on 0.0.0.0:{}", port); + + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(AppState { + facilitator: facilitator.clone(), + })) + .service( + web::resource("/resource/standard") + .wrap(middleware::from_fn(standard_paywall)) + .route(web::post().to(example_handler)), + ) + .service( + web::resource("/resource/custom") + .wrap(middleware::from_fn(custom_paywall)) + .route(web::post().to(example_handler)), + ) + .service( + web::resource("/resource/multi_payments") + .wrap(middleware::from_fn(multi_payments_paywall)) + .route(web::post().to(example_handler)), + ) + }) + .bind(("0.0.0.0", port))? + .run() + .await +} From 3424e54d8b1e35009c8542073bb3b091bf3a3a0d Mon Sep 17 00:00:00 2001 From: Archer Date: Tue, 24 Feb 2026 23:14:04 +0800 Subject: [PATCH 10/11] chore: add actix-web feature to CI (#8) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 903e08c..a427016 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,7 @@ jobs: - "" # no extra features - "tracing" - "axum" + - "actix-web" - "all" # marker for all features steps: - uses: actions/checkout@v6 From 27582bb4c5357367fbbb33c1f8c4e3db00c3af30 Mon Sep 17 00:00:00 2001 From: takasaki404 Date: Tue, 24 Feb 2026 23:16:10 +0800 Subject: [PATCH 11/11] chore: add actix web to x402-kit build CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a427016..501195a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: - "svm-signer" - "paywall" - "axum" + - "actix-web" - "all" # marker for all features steps: - uses: actions/checkout@v6