Skip to main content

r402_core/payment/
mod.rs

1//! Typestate lifecycle for a single x402 payment.
2//!
3//! A payment moves through three well-defined states:
4//!
5//! ```text
6//! Payment<Unverified> ──verify()──▶ Payment<Verified> ──settle()──▶ Payment<Settled>
7//! ```
8//!
9//! Each transition is a method on the corresponding state; the compiler
10//! enforces the order (you cannot call `settle` on an `Unverified` payment).
11//! All state types are zero-sized so the `Payment<S>` wrapper compiles to the
12//! same memory layout as its payload.
13//!
14//! # Example
15//!
16//! ```no_run
17//! use r402_core::payment::Payment;
18//! use r402_core::Facilitator;
19//! use r402_core::wire::VerifyRequest;
20//!
21//! # async fn run<F: Facilitator>(facilitator: F, payload: VerifyRequest) {
22//! let payment = Payment::new(payload.clone());
23//! let (verified, _vr) = payment.verify(&facilitator, payload.clone()).await.unwrap();
24//! let (_settled, _sr) = verified.settle(&facilitator, payload.into()).await.unwrap();
25//! # }
26//! ```
27
28use std::marker::PhantomData;
29
30use crate::error::FacilitatorError;
31use crate::facilitator::Facilitator;
32use crate::wire::{SettleRequest, SettleResponse, VerifyRequest, VerifyResponse};
33
34/// Marker indicating the payment has not yet been verified.
35#[derive(Debug, Clone, Copy, Default)]
36pub struct Unverified;
37
38/// Marker indicating the payment has passed verification.
39#[derive(Debug, Clone, Copy, Default)]
40pub struct Verified;
41
42/// Marker indicating the payment has been settled.
43#[derive(Debug, Clone, Copy, Default)]
44pub struct Settled;
45
46/// A payment moving through the verify → settle lifecycle.
47///
48/// The type parameter `S` tracks the current state at compile time. The
49/// struct is transparent (zero overhead) around the underlying request.
50#[derive(Debug, Clone)]
51pub struct Payment<S> {
52    request: VerifyRequest,
53    state: PhantomData<fn() -> S>,
54}
55
56impl<S> Payment<S> {
57    /// Returns a reference to the wire-level verify request this payment
58    /// was constructed from.
59    #[must_use]
60    pub const fn request(&self) -> &VerifyRequest {
61        &self.request
62    }
63}
64
65impl Payment<Unverified> {
66    /// Wraps a raw verify request as an unverified payment.
67    #[must_use]
68    pub const fn new(request: VerifyRequest) -> Self {
69        Self {
70            request,
71            state: PhantomData,
72        }
73    }
74
75    /// Runs facilitator verification.
76    ///
77    /// On success, returns the verified payment together with the
78    /// facilitator's [`VerifyResponse`]. On failure, the payment is
79    /// consumed and a [`FacilitatorError`] is returned.
80    ///
81    /// # Errors
82    ///
83    /// Propagates any [`FacilitatorError`] raised by the underlying
84    /// facilitator. A `Valid` verification that returns `VerifyResponse::Invalid`
85    /// is surfaced as `Err(FacilitatorError::Verification(VerificationError::InvalidFormat))`.
86    pub async fn verify<F: Facilitator>(
87        self,
88        facilitator: &F,
89        request: VerifyRequest,
90    ) -> Result<(Payment<Verified>, VerifyResponse), FacilitatorError> {
91        let response = facilitator.verify(request).await?;
92        match &response {
93            VerifyResponse::Valid { .. } => Ok((
94                Payment {
95                    request: self.request,
96                    state: PhantomData,
97                },
98                response,
99            )),
100            VerifyResponse::Invalid {
101                reason, message, ..
102            } => Err(FacilitatorError::Verification(
103                crate::error::VerificationError::InvalidFormat(format!(
104                    "{reason}: {}",
105                    message.as_deref().unwrap_or(""),
106                )),
107            )),
108        }
109    }
110}
111
112impl Payment<Verified> {
113    /// Runs facilitator settlement on a previously verified payment.
114    ///
115    /// # Errors
116    ///
117    /// Propagates any [`FacilitatorError`] raised by the facilitator; a
118    /// `SettleResponse::Failure` is surfaced as
119    /// `Err(FacilitatorError::Onchain(...))`.
120    pub async fn settle<F: Facilitator>(
121        self,
122        facilitator: &F,
123        request: SettleRequest,
124    ) -> Result<(Payment<Settled>, SettleResponse), FacilitatorError> {
125        let response = facilitator.settle(request).await?;
126        match &response {
127            SettleResponse::Success { .. } => Ok((
128                Payment {
129                    request: self.request,
130                    state: PhantomData,
131                },
132                response,
133            )),
134            SettleResponse::Failure { message, .. } => Err(FacilitatorError::Onchain(
135                message.as_deref().unwrap_or("").to_owned(),
136            )),
137        }
138    }
139}
140
141impl Payment<Settled> {
142    /// Extracts the underlying wire-level request.
143    #[must_use]
144    pub fn into_request(self) -> VerifyRequest {
145        self.request
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use std::future::Future;
152
153    use super::*;
154    use crate::error_reason::ErrorReason;
155    use crate::wire::{Extensions, SettleResponse, SupportedResponse, VerifyResponse};
156
157    struct AlwaysValid;
158
159    impl Facilitator for AlwaysValid {
160        fn verify(
161            &self,
162            _request: VerifyRequest,
163        ) -> impl Future<Output = Result<VerifyResponse, FacilitatorError>> + Send {
164            std::future::ready(Ok(VerifyResponse::valid("0xPAYER")))
165        }
166
167        fn settle(
168            &self,
169            _request: SettleRequest,
170        ) -> impl Future<Output = Result<SettleResponse, FacilitatorError>> + Send {
171            std::future::ready(Ok(SettleResponse::Success {
172                payer: "0xPAYER".into(),
173                transaction: "0xTX".into(),
174                network: "eip155:1".into(),
175                amount: Some("1000000".into()),
176                extensions: Extensions::new(),
177            }))
178        }
179
180        fn supported(
181            &self,
182        ) -> impl Future<Output = Result<SupportedResponse, FacilitatorError>> + Send {
183            std::future::ready(Ok(SupportedResponse::default()))
184        }
185    }
186
187    struct AlwaysInvalid;
188
189    impl Facilitator for AlwaysInvalid {
190        fn verify(
191            &self,
192            _request: VerifyRequest,
193        ) -> impl Future<Output = Result<VerifyResponse, FacilitatorError>> + Send {
194            std::future::ready(Ok(VerifyResponse::invalid(
195                None,
196                ErrorReason::InvalidPayload,
197            )))
198        }
199
200        fn settle(
201            &self,
202            _request: SettleRequest,
203        ) -> impl Future<Output = Result<SettleResponse, FacilitatorError>> + Send {
204            std::future::ready(Err(FacilitatorError::Onchain("unreachable".into())))
205        }
206
207        fn supported(
208            &self,
209        ) -> impl Future<Output = Result<SupportedResponse, FacilitatorError>> + Send {
210            std::future::ready(Ok(SupportedResponse::default()))
211        }
212    }
213
214    fn dummy_request() -> VerifyRequest {
215        serde_json::json!({}).into()
216    }
217
218    #[tokio::test]
219    async fn verify_then_settle_succeeds() {
220        let facilitator = AlwaysValid;
221        let payment = Payment::new(dummy_request());
222        let (verified, verify_resp) = payment.verify(&facilitator, dummy_request()).await.unwrap();
223        assert!(verify_resp.is_valid());
224        let (_settled, settle_resp) = verified
225            .settle(&facilitator, dummy_request().into())
226            .await
227            .unwrap();
228        assert!(settle_resp.is_success());
229    }
230
231    #[tokio::test]
232    async fn verify_invalid_propagates() {
233        let facilitator = AlwaysInvalid;
234        let payment = Payment::new(dummy_request());
235        let err = payment
236            .verify(&facilitator, dummy_request())
237            .await
238            .unwrap_err();
239        assert!(matches!(err, FacilitatorError::Verification(_)));
240    }
241}