Skip to main content

r402_core/
resource_server.rs

1//! Resource-server orchestration shared by HTTP and MCP transports.
2//!
3//! Mirrors the Go `X402ResourceServer` surface used by `go/mcp`:
4//! `FindMatchingRequirements`, `VerifyPayment`, `SettlePayment`.
5//!
6//! This type does **not** own pricing or route maps; it only sequences
7//! wire construction against a [`Facilitator`].
8
9use std::sync::Arc;
10
11use serde::Serialize;
12use serde_json::Value;
13
14use crate::error::FacilitatorError;
15use crate::facilitator::{DynFacilitator, Facilitator};
16use crate::wire::{
17    PaymentPayload, PaymentRequirements, SettleRequest, SettleResponse, TypedVerifyRequest, V2,
18    VerifyRequest, VerifyResponse, find_matching_requirements,
19};
20
21/// Wire payment payload with typed requirements and opaque scheme body.
22pub type WirePaymentPayload = PaymentPayload<PaymentRequirements, Value>;
23
24/// Server-side payment orchestrator (Go `X402ResourceServer` subset).
25pub struct ResourceServer {
26    facilitator: Arc<dyn DynFacilitator>,
27}
28
29impl Clone for ResourceServer {
30    fn clone(&self) -> Self {
31        Self {
32            facilitator: Arc::clone(&self.facilitator),
33        }
34    }
35}
36
37impl std::fmt::Debug for ResourceServer {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("ResourceServer").finish_non_exhaustive()
40    }
41}
42
43impl ResourceServer {
44    /// Creates a resource server over any [`Facilitator`].
45    #[must_use]
46    pub fn new<F>(facilitator: Arc<F>) -> Self
47    where
48        F: Facilitator + 'static,
49    {
50        let erased: Arc<dyn DynFacilitator> = facilitator;
51        Self {
52            facilitator: erased,
53        }
54    }
55
56    /// Creates from an already-erased facilitator handle.
57    #[must_use]
58    pub fn from_dyn(facilitator: Arc<dyn DynFacilitator>) -> Self {
59        Self { facilitator }
60    }
61
62    /// Returns a clone of the inner facilitator handle.
63    #[must_use]
64    pub fn facilitator(&self) -> Arc<dyn DynFacilitator> {
65        Arc::clone(&self.facilitator)
66    }
67
68    /// Go `FindMatchingRequirements`.
69    ///
70    /// Method form matches the official Go `X402ResourceServer` surface even
71    /// though matching is pure over the arguments.
72    #[must_use]
73    #[allow(
74        clippy::unused_self,
75        reason = "API parity with Go X402ResourceServer.FindMatchingRequirements"
76    )]
77    pub fn find_matching_requirements<'a>(
78        &self,
79        available: &'a [PaymentRequirements],
80        payload: &WirePaymentPayload,
81    ) -> Option<&'a PaymentRequirements> {
82        find_matching_requirements(available, &payload.accepted)
83    }
84
85    /// Go `VerifyPayment`: builds a verify request and calls the facilitator.
86    ///
87    /// # Errors
88    ///
89    /// Propagates facilitator transport / internal errors.
90    pub async fn verify_payment(
91        &self,
92        payload: &WirePaymentPayload,
93        requirements: &PaymentRequirements,
94    ) -> Result<VerifyResponse, FacilitatorError> {
95        let request = build_verify_request(payload, requirements)?;
96        DynFacilitator::verify(self.facilitator.as_ref(), request).await
97    }
98
99    /// Go `SettlePayment`: builds a settle request and calls the facilitator.
100    ///
101    /// # Errors
102    ///
103    /// Propagates facilitator transport / internal errors.
104    pub async fn settle_payment(
105        &self,
106        payload: &WirePaymentPayload,
107        requirements: &PaymentRequirements,
108    ) -> Result<SettleResponse, FacilitatorError> {
109        let request = build_settle_request(payload, requirements)?;
110        DynFacilitator::settle(self.facilitator.as_ref(), request).await
111    }
112}
113
114fn build_verify_request(
115    payload: &WirePaymentPayload,
116    requirements: &PaymentRequirements,
117) -> Result<VerifyRequest, FacilitatorError> {
118    let typed = TypedVerifyRequest {
119        x402_version: V2,
120        payment_payload: payload.clone(),
121        payment_requirements: requirements.clone(),
122    };
123    to_verify_request(&typed)
124}
125
126fn build_settle_request(
127    payload: &WirePaymentPayload,
128    requirements: &PaymentRequirements,
129) -> Result<SettleRequest, FacilitatorError> {
130    let typed = TypedVerifyRequest {
131        x402_version: V2,
132        payment_payload: payload.clone(),
133        payment_requirements: requirements.clone(),
134    };
135    let verify = to_verify_request(&typed)?;
136    Ok(SettleRequest::from(verify.into_json()))
137}
138
139fn to_verify_request<T: Serialize>(typed: &T) -> Result<VerifyRequest, FacilitatorError> {
140    let json = serde_json::to_value(typed).map_err(FacilitatorError::internal)?;
141    Ok(VerifyRequest::from(json))
142}
143
144#[cfg(test)]
145mod tests {
146    use std::future::Future;
147    use std::sync::atomic::{AtomicUsize, Ordering};
148
149    use super::*;
150    use crate::wire::SupportedResponse;
151
152    struct MockFacilitator {
153        verifies: AtomicUsize,
154        settles: AtomicUsize,
155    }
156
157    impl Facilitator for MockFacilitator {
158        fn verify(
159            &self,
160            _request: VerifyRequest,
161        ) -> impl Future<Output = Result<VerifyResponse, FacilitatorError>> + Send {
162            self.verifies.fetch_add(1, Ordering::SeqCst);
163            std::future::ready(Ok(VerifyResponse::valid("0xpayer")))
164        }
165
166        fn settle(
167            &self,
168            _request: SettleRequest,
169        ) -> impl Future<Output = Result<SettleResponse, FacilitatorError>> + Send {
170            self.settles.fetch_add(1, Ordering::SeqCst);
171            std::future::ready(Ok(SettleResponse::Success {
172                payer: "0xpayer".into(),
173                transaction: "0xtx".into(),
174                network: "eip155:1".into(),
175                amount: Some("1".into()),
176                extensions: crate::wire::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    fn sample_payload() -> WirePaymentPayload {
188        let req = PaymentRequirements::new(
189            "exact".into(),
190            "eip155:1".parse().unwrap(),
191            "1".into(),
192            "0xa".into(),
193            "0xb".into(),
194            60,
195        );
196        WirePaymentPayload::new(req, serde_json::json!({"k": 1}))
197    }
198
199    #[tokio::test]
200    async fn verify_and_settle_invoke_facilitator() {
201        let mock = Arc::new(MockFacilitator {
202            verifies: AtomicUsize::new(0),
203            settles: AtomicUsize::new(0),
204        });
205        let rs = ResourceServer::new(Arc::clone(&mock));
206        let payload = sample_payload();
207        let req = payload.accepted.clone();
208        assert!(rs.verify_payment(&payload, &req).await.unwrap().is_valid());
209        assert!(
210            rs.settle_payment(&payload, &req)
211                .await
212                .unwrap()
213                .is_success()
214        );
215        assert_eq!(mock.verifies.load(Ordering::SeqCst), 1);
216        assert_eq!(mock.settles.load(Ordering::SeqCst), 1);
217    }
218
219    #[test]
220    fn find_matching_delegates_to_core() {
221        let mock = Arc::new(MockFacilitator {
222            verifies: AtomicUsize::new(0),
223            settles: AtomicUsize::new(0),
224        });
225        let rs = ResourceServer::new(mock);
226        let payload = sample_payload();
227        let available = [payload.accepted.clone()];
228        assert!(
229            rs.find_matching_requirements(&available, &payload)
230                .is_some()
231        );
232    }
233}