Skip to main content

x402_paywall/
processor.rs

1use x402_core::{
2    facilitator::{
3        Facilitator, PaymentRequest, SettleResult, SettleSuccess, VerifyResult, VerifyValid,
4    },
5    transport::{PaymentPayload, PaymentRequirements, SettlementResponse},
6    types::{Base64EncodedHeader, Extension, Record},
7};
8
9use crate::{HttpRequest, HttpResponse, errors::ErrorResponse, paywall::PayWall};
10
11/// The state of a payment processed by the paywall when accessing the resource handler.
12///
13/// This state is attached to the request extensions before running the resource handler,
14/// and can be accessed within the handler to inspect the payment status.
15///
16/// # Example
17///
18/// ```rust
19/// use axum::{extract::Extension, Json};
20/// use serde_json::{json, Value};
21/// use x402_core::facilitator::{VerifyValid, SettleSuccess};
22/// use x402_paywall::processor::PaymentState;
23///
24/// async fn example_handler(Extension(payment_state): Extension<PaymentState>) -> Json<Value> {
25///     Json(json!({
26///         "message": "You have accessed a protected resource!",
27///         "verify_state": serde_json::to_value(&payment_state.verified).unwrap_or(json!(null)),
28///         "settle_state": serde_json::to_value(&payment_state.settled).unwrap_or(json!(null)),
29///     }))
30/// }
31/// ```
32#[derive(Debug, Clone)]
33pub struct PaymentState {
34    /// Verification result, if verification was performed.
35    pub verified: Option<VerifyValid>,
36    /// Settlement result, if settlement was performed.
37    pub settled: Option<SettleSuccess>,
38    /// All extensions info provided by the paywall.
39    pub required_extensions: Record<Extension>,
40    /// All extensions info provided by the signer.
41    pub payload_extensions: Record<Extension>,
42}
43
44/// Payment processing state before running the resource handler.
45///
46/// See [`PayWall`] for usage in the full payment processing flow.
47pub struct RequestProcessor<'pw, F: Facilitator, Req: HttpRequest> {
48    pub paywall: &'pw PayWall<F>,
49    pub request: Req,
50    pub payload: PaymentPayload,
51    pub selected: PaymentRequirements,
52    pub payment_state: PaymentState,
53}
54
55impl<'pw, F: Facilitator, Req: HttpRequest> RequestProcessor<'pw, F, Req> {
56    /// Verify the payment with the facilitator.
57    ///
58    /// `self.payment_state.verified` will be populated on success.
59    pub async fn verify(mut self) -> Result<Self, ErrorResponse> {
60        let response = self
61            .paywall
62            .facilitator
63            .verify(PaymentRequest {
64                payment_payload: self.payload.clone(),
65                payment_requirements: self.selected.clone(),
66            })
67            .await
68            .map_err(|err| {
69                self.paywall
70                    .server_error(format!("Failed to verify payment: {err}"))
71            })?;
72
73        let valid = match response {
74            VerifyResult::Valid(v) => v,
75            VerifyResult::Invalid(iv) => {
76                return Err(self.paywall.payment_failed(iv.invalid_reason));
77            }
78        };
79
80        #[cfg(feature = "tracing")]
81        tracing::debug!("Payment verified: payer='{}'", valid.payer);
82
83        self.payment_state.verified = Some(valid);
84
85        Ok(self)
86    }
87
88    /// Settle the payment with the facilitator.
89    ///
90    /// `self.payment_state.settled` will be populated on success.
91    pub async fn settle(mut self) -> Result<Self, ErrorResponse> {
92        let settlement = self
93            .paywall
94            .facilitator
95            .settle(PaymentRequest {
96                payment_payload: self.payload.clone(),
97                payment_requirements: self.selected.clone(),
98            })
99            .await
100            .map_err(|err| {
101                self.paywall
102                    .server_error(format!("Failed to settle payment: {err}"))
103            })?;
104
105        let settled = match settlement {
106            SettleResult::Success(s) => s,
107            SettleResult::Failed(f) => {
108                return Err(self.paywall.payment_failed(f.error_reason));
109            }
110        };
111
112        #[cfg(feature = "tracing")]
113        tracing::debug!(
114            "Payment settled: payer='{}', transaction='{}', network='{}'",
115            settled.payer,
116            settled.transaction,
117            settled.network
118        );
119
120        self.payment_state.settled = Some(settled);
121
122        Ok(self)
123    }
124
125    /// Run the resource handler with the payment state attached to the request extensions.
126    ///
127    /// After running the handler, returns a [`ResponseProcessor`] for further processing.
128    pub async fn run_handler<Fun, Fut, Res>(
129        mut self,
130        handler: Fun,
131    ) -> Result<ResponseProcessor<'pw, F, Res>, ErrorResponse>
132    where
133        Fun: FnOnce(Req) -> Fut,
134        Fut: Future<Output = Res>,
135    {
136        self.request.insert_extension(self.payment_state.clone());
137
138        let response = handler(self.request).await;
139        Ok(ResponseProcessor {
140            paywall: self.paywall,
141            response,
142            payload: self.payload,
143            selected: self.selected,
144            payment_state: self.payment_state,
145        })
146    }
147}
148
149/// Payment processing state after running the resource handler.
150pub struct ResponseProcessor<'pw, F: Facilitator, Res> {
151    pub paywall: &'pw PayWall<F>,
152    pub response: Res,
153    pub payload: PaymentPayload,
154    pub selected: PaymentRequirements,
155    pub payment_state: PaymentState,
156}
157
158impl<'pw, F: Facilitator, Res: HttpResponse> ResponseProcessor<'pw, F, Res> {
159    /// Settle the payment with the facilitator after running the resource handler.
160    ///
161    /// After settlement, `self.payment_state.settled` will be populated on success.
162    pub async fn settle(mut self) -> Result<Self, ErrorResponse> {
163        // Settle payment with facilitator
164        let settlement = self
165            .paywall
166            .facilitator
167            .settle(PaymentRequest {
168                payment_payload: self.payload.clone(),
169                payment_requirements: self.selected.clone(),
170            })
171            .await
172            .map_err(|err| {
173                self.paywall
174                    .server_error(format!("Failed to settle payment: {err}"))
175            })?;
176
177        let settled = match settlement {
178            SettleResult::Success(s) => s,
179            SettleResult::Failed(f) => {
180                return Err(self.paywall.payment_failed(f.error_reason));
181            }
182        };
183
184        #[cfg(feature = "tracing")]
185        tracing::debug!(
186            "Payment settled: payer='{}', transaction='{}', network='{}'",
187            settled.payer,
188            settled.transaction,
189            settled.network
190        );
191
192        self.payment_state.settled = Some(settled);
193        Ok(self)
194    }
195
196    /// Conditionally settle the payment based on the provided prediction function.
197    ///
198    /// After settlement, `self.payment_state.settled` will be populated on success.
199    pub async fn settle_on(self, predicate: impl Fn(&Res) -> bool) -> Result<Self, ErrorResponse> {
200        if predicate(&self.response) {
201            self.settle().await
202        } else {
203            Ok(self)
204        }
205    }
206
207    /// Settle the payment if the response status is a success (2xx).
208    ///
209    /// After settlement, `self.payment_state.settled` will be populated on success.
210    pub async fn settle_on_success(self) -> Result<Self, ErrorResponse> {
211        self.settle_on(|resp| resp.is_success()).await
212    }
213
214    /// Generate the final response, including the `PAYMENT-RESPONSE` header if settled.
215    pub fn response(self) -> Res {
216        let mut response = self.response;
217
218        if let Some(settled) = &self.payment_state.settled {
219            let settlement_response = SettlementResponse {
220                success: true,
221                payer: settled.payer.clone(),
222                transaction: settled.transaction.clone(),
223                network: settled.network.clone(),
224            };
225
226            let header = Base64EncodedHeader::try_from(settlement_response)
227                .inspect_err(|_err| {
228                    #[cfg(feature = "tracing")]
229                    tracing::warn!("Failed to encode PAYMENT-RESPONSE header: {_err}; skipping")
230                })
231                .ok();
232            if let Some(header) = header {
233                response
234                    .insert_header("payment-response", header.0.as_bytes())
235                    .inspect_err(|_err| {
236                        #[cfg(feature = "tracing")]
237                        tracing::warn!("Failed to encode PAYMENT-RESPONSE header: {_err}; skipping")
238                    })
239                    .ok();
240            }
241        }
242
243        response
244    }
245}