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 let valid = verify_payment(
239 facilitator,
240 &x_payment_header,
241 &selected,
242 &payment_requirements,
243 )
244 .await?;
245
246 #[cfg(feature = "tracing")]
247 tracing::debug!("Payment verified for payer: {}", valid.payer);
248
249 let settle_response = settle_payment(
250 facilitator,
251 &x_payment_header,
252 &selected,
253 &payment_requirements,
254 )
255 .await?;
256
257 #[cfg(feature = "tracing")]
258 tracing::debug!(
259 "Payment settled: payer='{}', network='{}', transaction='{}'",
260 settle_response.payer,
261 settle_response.network,
262 settle_response.transaction
263 );
264
265 let payer = if settle_response.payer.is_empty() && !valid.payer.is_empty() {
266 valid.payer
267 } else {
268 settle_response.payer
269 };
270
271 Ok(PaymentResponse {
272 success: true,
273 transaction: settle_response.transaction,
274 network: settle_response.network,
275 payer,
276 })
277}
278
279pub async fn process_payment_no_verify<F: Facilitator>(
281 facilitator: &F,
282 headers: &HeaderMap,
283 payment_requirements: Vec<PaymentRequirements>,
284) -> Result<PaymentResponse, ErrorResponse> {
285 let updated_requirements = update_supported_kinds(facilitator, payment_requirements).await?;
286 let x_payment_header = extract_payment_payload(headers, &updated_requirements)?;
287 let selected = select_payment_with_payload(&updated_requirements, &x_payment_header)?;
288
289 let settle_response = settle_payment(
290 facilitator,
291 &x_payment_header,
292 &selected,
293 &updated_requirements,
294 )
295 .await?;
296
297 Ok(PaymentResponse {
298 success: true,
299 transaction: settle_response.transaction,
300 network: settle_response.network,
301 payer: settle_response.payer,
302 })
303}
304
305pub async fn process_payment_no_settle<F: Facilitator>(
307 facilitator: &F,
308 headers: &HeaderMap,
309 payment_requirements: Vec<PaymentRequirements>,
310) -> Result<FacilitatorVerifyValid, ErrorResponse> {
311 let updated_requirements = update_supported_kinds(facilitator, payment_requirements).await?;
312 let x_payment_header = extract_payment_payload(headers, &updated_requirements)?;
313 let selected = select_payment_with_payload(&updated_requirements, &x_payment_header)?;
314
315 let verify_response = verify_payment(
316 facilitator,
317 &x_payment_header,
318 &selected,
319 &updated_requirements,
320 )
321 .await?;
322
323 Ok(verify_response)
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn test_filter_supported_kinds() {
332 let supported = serde_json::from_str(
333 "{
334 \"kinds\": [
335 {
336 \"x402Version\": 1,
337 \"scheme\": \"exact\",
338 \"network\": \"base-sepolia\"
339 },
340 {
341 \"x402Version\": 1,
342 \"scheme\": \"exact\",
343 \"network\": \"base\"
344 },
345 {
346 \"x402Version\": 1,
347 \"scheme\": \"exact\",
348 \"network\": \"my-mock-network\",
349 \"extra\": {
350 \"mockField\": \"mockValue\"
351 }
352 },
353 {
354 \"x402Version\": 1,
355 \"scheme\": \"exact\",
356 \"network\": \"solana-devnet\",
357 \"extra\": {
358 \"feePayer\": \"2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4\"
359 }
360 },
361 {
362 \"x402Version\": 1,
363 \"scheme\": \"exact\",
364 \"network\": \"solana\",
365 \"extra\": {
366 \"feePayer\": \"2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4\"
367 }
368 }
369 ]
370}",
371 )
372 .unwrap();
373
374 let payment_requirements = serde_json::from_value(serde_json::json!([{
376 "scheme": "exact",
377 "network": "base",
378 "maxAmountRequired": "100",
379 "resource": "https://devnet.aimo.network/api/v1/chat/completions",
380 "description": "LLM Generation endpoint",
381 "mimeType": "application/json",
382 "payTo": "0xD14cE79C13CE71a853eF3E8BD75969d4BDEE39c1",
383 "maxTimeoutSeconds": 60,
384 "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
385 "outputSchema": {
386 "input": {
387 "discoverable": true,
388 "type": "http",
389 "method": "POST"
390 }
391 },
392 "extra": {
393 "name": "USD Coin",
394 "version": "2"
395 }
396 }]))
397 .unwrap();
398 let filtered = filter_supported_kinds(&supported, payment_requirements);
400 assert_eq!(filtered.len(), 1);
401 assert_eq!(
402 filtered[0].extra,
403 Some(serde_json::json!({"name": "USD Coin", "version": "2"}))
404 );
405
406 let payment_requirements_solana = serde_json::from_str(
408 "[
409 {
410 \"scheme\": \"exact\",
411 \"network\": \"solana\",
412 \"maxAmountRequired\": \"100\",
413 \"resource\": \"https://devnet.aimo.network/api/v1/chat/completions\",
414 \"description\": \"LLM Generation endpoint\",
415 \"mimeType\": \"application/json\",
416 \"payTo\": \"Ge3jkza5KRfXvaq3GELNLh6V1pjjdEKNpEdGXJgjjKUR\",
417 \"maxTimeoutSeconds\": 60,
418 \"asset\": \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\",
419 \"outputSchema\": {
420 \"input\": {
421 \"discoverable\": true,
422 \"type\": \"http\",
423 \"method\": \"POST\"
424 }
425 }
426 }
427 ]",
428 )
429 .unwrap();
430
431 let filtered = filter_supported_kinds(&supported, payment_requirements_solana);
433 assert_eq!(filtered.len(), 1);
434 assert_eq!(
435 filtered[0].extra,
436 Some(serde_json::json!({"feePayer": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4"}))
437 );
438
439 let payment_requirements_mock = serde_json::from_str(
441 "[
442 {
443 \"scheme\": \"exact\",
444 \"network\": \"my-mock-network\",
445 \"maxAmountRequired\": \"100\",
446 \"resource\": \"https://devnet.aimo.network/api/v1/chat/completions\",
447 \"description\": \"LLM Generation endpoint\",
448 \"mimeType\": \"application/json\",
449 \"payTo\": \"0xD14cE79C13CE71a853eF3E8BD75969d4BDEE39c1\",
450 \"maxTimeoutSeconds\": 60,
451 \"asset\": \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",
452 \"outputSchema\": {
453 \"input\": {
454 \"discoverable\": true,
455 \"type\": \"http\",
456 \"method\": \"POST\"
457 }
458 },
459 \"extra\": {
460 \"name\": \"USD Coin\",
461 \"version\": \"2\"
462 }
463 }
464 ]",
465 )
466 .unwrap();
467
468 let filtered = filter_supported_kinds(&supported, payment_requirements_mock);
470 assert_eq!(filtered.len(), 1);
471 assert_eq!(
472 filtered[0].extra,
473 Some(serde_json::json!({"mockField": "mockValue"}))
474 );
475 }
476}