1use std::fmt::Display;
2
3use http::{HeaderMap, StatusCode};
4
5use crate::{
6 concepts::Facilitator,
7 transport::{
8 Base64EncodedHeader, FacilitatorPaymentRequest, FacilitatorPaymentRequestPayload,
9 FacilitatorSettleFailed, FacilitatorSettleResponse, FacilitatorSettleSuccess,
10 FacilitatorSupportedResponse, FacilitatorVerifyInvalid, FacilitatorVerifyResponse,
11 FacilitatorVerifyValid, PaymentPayload, PaymentRequirements, PaymentRequirementsResponse,
12 PaymentResponse,
13 },
14 types::X402Version,
15};
16
17#[derive(Debug, Clone)]
19pub struct ErrorResponse {
20 pub status: StatusCode,
21 pub error: String,
22 pub accepts: Vec<PaymentRequirements>,
23}
24
25impl ErrorResponse {
26 pub fn into_payment_requirements_response(self) -> PaymentRequirementsResponse {
27 PaymentRequirementsResponse {
28 x402_version: X402Version::V1,
29 error: self.error,
30 accepts: self.accepts,
31 }
32 }
33
34 pub fn payment_required(accepts: &[PaymentRequirements]) -> Self {
35 ErrorResponse {
36 status: StatusCode::PAYMENT_REQUIRED,
37 error: "X-PAYMENT header is required".to_string(),
38 accepts: accepts.to_owned(),
39 }
40 }
41
42 pub fn invalid_payment(error: impl Display, accepts: &[PaymentRequirements]) -> Self {
43 ErrorResponse {
44 status: StatusCode::BAD_REQUEST,
45 error: error.to_string(),
46 accepts: accepts.to_owned(),
47 }
48 }
49
50 pub fn payment_failed(error: impl Display, accepts: &[PaymentRequirements]) -> Self {
51 ErrorResponse {
52 status: StatusCode::PAYMENT_REQUIRED,
53 error: error.to_string(),
54 accepts: accepts.to_owned(),
55 }
56 }
57
58 pub fn server_error(error: impl Display, accepts: &[PaymentRequirements]) -> Self {
59 ErrorResponse {
60 status: StatusCode::INTERNAL_SERVER_ERROR,
61 error: error.to_string(),
62 accepts: accepts.to_owned(),
63 }
64 }
65}
66
67pub fn extract_payment_payload(
69 headers: &HeaderMap,
70 payment_requirements: &[PaymentRequirements],
71) -> Result<Base64EncodedHeader, ErrorResponse> {
72 Ok(Base64EncodedHeader(
73 headers
74 .get("X-Payment")
75 .ok_or(ErrorResponse::payment_required(payment_requirements))?
76 .to_str()
77 .map_err(|err| {
78 ErrorResponse::invalid_payment(
79 format!("Failed to parse X-Payment header: {}", err),
80 payment_requirements,
81 )
82 })?
83 .to_string(),
84 ))
85}
86
87pub async fn update_supported_kinds<F: Facilitator>(
89 facilitator: &F,
90 payment_requirements: Vec<PaymentRequirements>,
91) -> Result<Vec<PaymentRequirements>, ErrorResponse> {
92 let supported = facilitator
93 .supported()
94 .await
95 .map_err(|err| ErrorResponse::server_error(err, &payment_requirements))?;
96
97 Ok(filter_supported_kinds(&supported, payment_requirements))
98}
99
100pub fn filter_supported_kinds(
104 supported: &FacilitatorSupportedResponse,
105 payment_requirements: Vec<PaymentRequirements>,
106) -> Vec<PaymentRequirements> {
107 payment_requirements
108 .into_iter()
109 .filter_map(|mut pr| {
110 supported
111 .kinds
112 .iter()
113 .find(|kind| kind.scheme == pr.scheme && kind.network == pr.network)
114 .map(|s| {
115 if s.extra.is_some() {
117 pr.extra = s.extra.clone();
118 }
119 pr
120 })
121 })
122 .collect()
123}
124
125pub fn select_payment_with_payload(
127 payment_requirements: &[PaymentRequirements],
128 x_payment_header: &Base64EncodedHeader,
129) -> Result<PaymentRequirements, ErrorResponse> {
130 let payment_payload = PaymentPayload::try_from(x_payment_header.clone())
131 .map_err(|err| ErrorResponse::invalid_payment(err, payment_requirements))?;
132
133 payment_requirements
134 .iter()
135 .find(|pr| pr.network == payment_payload.network && pr.scheme == payment_payload.scheme)
136 .cloned()
137 .ok_or(ErrorResponse::invalid_payment(
138 "Payment payload does not match any accepted payment requirements",
139 payment_requirements,
140 ))
141}
142
143pub async fn verify_payment<F: Facilitator>(
145 facilitator: &F,
146 x_payment_header: &Base64EncodedHeader,
147 selected: &PaymentRequirements,
148 payment_requirements: &[PaymentRequirements],
149) -> Result<FacilitatorVerifyValid, ErrorResponse> {
150 let payment_payload = x_payment_header
151 .clone()
152 .try_into()
153 .map_err(|err| ErrorResponse::invalid_payment(err, payment_requirements))?;
154
155 #[cfg(feature = "tracing")]
156 tracing::debug!(
157 "Verifying payment for scheme={}, network={}",
158 selected.scheme,
159 selected.network,
160 );
161
162 let request = FacilitatorPaymentRequest {
163 payload: FacilitatorPaymentRequestPayload {
164 payment_payload,
165 payment_requirements: selected.clone(),
166 },
167 x_payment_header: x_payment_header.clone(),
168 };
169
170 let verify_response = facilitator
171 .verify(request)
172 .await
173 .map_err(|err| ErrorResponse::server_error(err, payment_requirements))?;
174
175 match verify_response {
176 FacilitatorVerifyResponse::Valid(valid) => Ok(valid),
177 FacilitatorVerifyResponse::Invalid(FacilitatorVerifyInvalid {
178 invalid_reason,
179 payer,
180 }) => Err(ErrorResponse::invalid_payment(
181 format!(
182 "Invalid payment: reason='{invalid_reason}', payer={}",
183 payer.unwrap_or("[Unknown]".to_string())
184 ),
185 payment_requirements,
186 )),
187 }
188}
189
190pub async fn settle_payment<F: Facilitator>(
192 facilitator: &F,
193 x_payment_header: &Base64EncodedHeader,
194 selected: &PaymentRequirements,
195 payment_requirements: &[PaymentRequirements],
196) -> Result<FacilitatorSettleSuccess, ErrorResponse> {
197 let payment_payload = x_payment_header
198 .clone()
199 .try_into()
200 .map_err(|err| ErrorResponse::invalid_payment(err, payment_requirements))?;
201
202 let settle_response: FacilitatorSettleResponse = facilitator
203 .settle(FacilitatorPaymentRequest {
204 payload: FacilitatorPaymentRequestPayload {
205 payment_payload,
206 payment_requirements: selected.clone(),
207 },
208 x_payment_header: x_payment_header.clone(),
209 })
210 .await
211 .map_err(|err| ErrorResponse::server_error(err, payment_requirements))?;
212
213 match settle_response {
214 FacilitatorSettleResponse::Success(success) => Ok(success),
215 FacilitatorSettleResponse::Failed(FacilitatorSettleFailed {
216 error_reason,
217 payer,
218 }) => Err(ErrorResponse::payment_failed(
219 format!(
220 "Payment settlement failed: reason='{}', payer={}",
221 error_reason,
222 payer.unwrap_or("[Unknown]".to_string())
223 ),
224 payment_requirements,
225 )),
226 }
227}
228
229pub async fn process_payment<F: Facilitator>(
231 facilitator: &F,
232 headers: &HeaderMap,
233 payment_requirements: Vec<PaymentRequirements>,
234) -> Result<PaymentResponse, ErrorResponse> {
235 let x_payment_header = extract_payment_payload(headers, &payment_requirements)?;
236 let selected = select_payment_with_payload(&payment_requirements, &x_payment_header)?;
237
238 #[allow(unused_variables)]
240 let valid = verify_payment(
241 facilitator,
242 &x_payment_header,
243 &selected,
244 &payment_requirements,
245 )
246 .await?;
247
248 #[cfg(feature = "tracing")]
249 tracing::debug!("Payment verified for payer: {}", valid.payer);
250
251 let settle_response = settle_payment(
252 facilitator,
253 &x_payment_header,
254 &selected,
255 &payment_requirements,
256 )
257 .await?;
258
259 #[cfg(feature = "tracing")]
260 tracing::debug!(
261 "Payment settled: payer='{}', network='{}', transaction='{}'",
262 settle_response.payer,
263 settle_response.network,
264 settle_response.transaction
265 );
266
267 Ok(PaymentResponse {
268 success: true,
269 transaction: settle_response.transaction,
270 network: settle_response.network,
271 payer: settle_response.payer,
272 })
273}
274
275pub async fn process_payment_no_verify<F: Facilitator>(
277 facilitator: &F,
278 headers: &HeaderMap,
279 payment_requirements: Vec<PaymentRequirements>,
280) -> Result<PaymentResponse, ErrorResponse> {
281 let updated_requirements = update_supported_kinds(facilitator, payment_requirements).await?;
282 let x_payment_header = extract_payment_payload(headers, &updated_requirements)?;
283 let selected = select_payment_with_payload(&updated_requirements, &x_payment_header)?;
284
285 let settle_response = settle_payment(
286 facilitator,
287 &x_payment_header,
288 &selected,
289 &updated_requirements,
290 )
291 .await?;
292
293 Ok(PaymentResponse {
294 success: true,
295 transaction: settle_response.transaction,
296 network: settle_response.network,
297 payer: settle_response.payer,
298 })
299}
300
301pub async fn process_payment_no_settle<F: Facilitator>(
303 facilitator: &F,
304 headers: &HeaderMap,
305 payment_requirements: Vec<PaymentRequirements>,
306) -> Result<FacilitatorVerifyValid, ErrorResponse> {
307 let updated_requirements = update_supported_kinds(facilitator, payment_requirements).await?;
308 let x_payment_header = extract_payment_payload(headers, &updated_requirements)?;
309 let selected = select_payment_with_payload(&updated_requirements, &x_payment_header)?;
310
311 let verify_response = verify_payment(
312 facilitator,
313 &x_payment_header,
314 &selected,
315 &updated_requirements,
316 )
317 .await?;
318
319 Ok(verify_response)
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_filter_supported_kinds() {
328 let supported = serde_json::from_str(
329 "{
330 \"kinds\": [
331 {
332 \"x402Version\": 1,
333 \"scheme\": \"exact\",
334 \"network\": \"base-sepolia\"
335 },
336 {
337 \"x402Version\": 1,
338 \"scheme\": \"exact\",
339 \"network\": \"base\"
340 },
341 {
342 \"x402Version\": 1,
343 \"scheme\": \"exact\",
344 \"network\": \"my-mock-network\",
345 \"extra\": {
346 \"mockField\": \"mockValue\"
347 }
348 },
349 {
350 \"x402Version\": 1,
351 \"scheme\": \"exact\",
352 \"network\": \"solana-devnet\",
353 \"extra\": {
354 \"feePayer\": \"2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4\"
355 }
356 },
357 {
358 \"x402Version\": 1,
359 \"scheme\": \"exact\",
360 \"network\": \"solana\",
361 \"extra\": {
362 \"feePayer\": \"2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4\"
363 }
364 }
365 ]
366}",
367 )
368 .unwrap();
369
370 let payment_requirements = serde_json::from_str(
372 "[
373 {
374 \"scheme\": \"exact\",
375 \"network\": \"base\",
376 \"maxAmountRequired\": \"100\",
377 \"resource\": \"https://devnet.aimo.network/api/v1/chat/completions\",
378 \"description\": \"LLM Generation endpoint\",
379 \"mimeType\": \"application/json\",
380 \"payTo\": \"0xD14cE79C13CE71a853eF3E8BD75969d4BDEE39c1\",
381 \"maxTimeoutSeconds\": 60,
382 \"asset\": \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",
383 \"outputSchema\": {
384 \"input\": {
385 \"discoverable\": true,
386 \"type\": \"http\",
387 \"method\": \"post\"
388 }
389 },
390 \"extra\": {
391 \"name\": \"USD Coin\",
392 \"version\": \"2\"
393 }
394 }
395 ]",
396 )
397 .unwrap();
398
399 let filtered = filter_supported_kinds(&supported, payment_requirements);
401 assert_eq!(filtered.len(), 1);
402 assert_eq!(
403 filtered[0].extra,
404 Some(serde_json::json!({"name": "USD Coin", "version": "2"}))
405 );
406
407 let payment_requirements_solana = serde_json::from_str(
409 "[
410 {
411 \"scheme\": \"exact\",
412 \"network\": \"solana\",
413 \"maxAmountRequired\": \"100\",
414 \"resource\": \"https://devnet.aimo.network/api/v1/chat/completions\",
415 \"description\": \"LLM Generation endpoint\",
416 \"mimeType\": \"application/json\",
417 \"payTo\": \"Ge3jkza5KRfXvaq3GELNLh6V1pjjdEKNpEdGXJgjjKUR\",
418 \"maxTimeoutSeconds\": 60,
419 \"asset\": \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\",
420 \"outputSchema\": {
421 \"input\": {
422 \"discoverable\": true,
423 \"type\": \"http\",
424 \"method\": \"post\"
425 }
426 }
427 }
428 ]",
429 )
430 .unwrap();
431
432 let filtered = filter_supported_kinds(&supported, payment_requirements_solana);
434 assert_eq!(filtered.len(), 1);
435 assert_eq!(
436 filtered[0].extra,
437 Some(serde_json::json!({"feePayer": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4"}))
438 );
439
440 let payment_requirements_mock = serde_json::from_str(
442 "[
443 {
444 \"scheme\": \"exact\",
445 \"network\": \"my-mock-network\",
446 \"maxAmountRequired\": \"100\",
447 \"resource\": \"https://devnet.aimo.network/api/v1/chat/completions\",
448 \"description\": \"LLM Generation endpoint\",
449 \"mimeType\": \"application/json\",
450 \"payTo\": \"0xD14cE79C13CE71a853eF3E8BD75969d4BDEE39c1\",
451 \"maxTimeoutSeconds\": 60,
452 \"asset\": \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",
453 \"outputSchema\": {
454 \"input\": {
455 \"discoverable\": true,
456 \"type\": \"http\",
457 \"method\": \"post\"
458 }
459 },
460 \"extra\": {
461 \"name\": \"USD Coin\",
462 \"version\": \"2\"
463 }
464 }
465 ]",
466 )
467 .unwrap();
468
469 let filtered = filter_supported_kinds(&supported, payment_requirements_mock);
471 assert_eq!(filtered.len(), 1);
472 assert_eq!(
473 filtered[0].extra,
474 Some(serde_json::json!({"mockField": "mockValue"}))
475 );
476 }
477}