Skip to main content

r402_http/server/
upto.rs

1//! HTTP-level support for the x402 "upto" scheme.
2//!
3//! The upto scheme lets a buyer authorise a **maximum** payment and lets the
4//! resource server decide the actual charge at request time (e.g. for
5//! usage-based pricing). This module defines the
6//! [`UptoActualAmount`] response extension that handlers use to communicate
7//! the final charge back to the r402 middleware.
8//!
9//! # Flow
10//!
11//! ```text
12//! client          middleware           handler
13//!   |                 |                    |
14//!   | POST + sig ---->| verify             |
15//!   |                 |  ok                |
16//!   |                 |---- request ------>|
17//!   |                 |                    |  (compute usage,
18//!   |                 |                    |   set extension)
19//!   |                 |<--- response + ext |
20//!   |                 | read UptoActualAmount
21//!   |                 |    → override
22//!   |                 |    → settle(actual)
23//!   |<------- resp ---|
24//! ```
25//!
26//! # Example
27//!
28//! ```ignore
29//! use axum::response::IntoResponse;
30//! use r402_http::server::UptoActualAmount;
31//!
32//! async fn handler() -> impl IntoResponse {
33//!     let mut response = "Hello".into_response();
34//!     response
35//!         .extensions_mut()
36//!         .insert(UptoActualAmount::new("125000")); // 0.125 USDC
37//!     response
38//! }
39//! ```
40//!
41//! # Compatibility
42//!
43//! Only [`SettlementMode::Sequential`](super::SettlementMode::Sequential)
44//! honours this extension: concurrent and background modes spawn settlement
45//! before the handler returns, so the override has nowhere to land. Mixing
46//! upto with those modes silently charges the signed maximum.
47
48use compact_str::CompactString;
49
50/// Response extension instructing the r402 middleware to settle the upto
51/// payment for this specific amount (base units as a decimal string).
52///
53/// Inserted by application handlers into [`Response::extensions_mut`] so the
54/// middleware can patch `paymentRequirements.amount` before forwarding the
55/// settle request to the facilitator.
56///
57/// The value MUST be less than or equal to the authorised maximum from the
58/// buyer's signed payload; otherwise the facilitator returns
59/// [`ErrorReason::InvalidUptoEvmPayloadSettlementExceedsAmount`](r402_core::error_reason::ErrorReason::InvalidUptoEvmPayloadSettlementExceedsAmount).
60///
61/// [`Response::extensions_mut`]: axum_core::response::Response::extensions_mut
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct UptoActualAmount(CompactString);
64
65impl UptoActualAmount {
66    /// Creates a new override from a decimal-string amount.
67    pub fn new<S: Into<CompactString>>(amount: S) -> Self {
68        Self(amount.into())
69    }
70
71    /// Returns the wrapped amount as a string slice.
72    #[must_use]
73    pub fn as_str(&self) -> &str {
74        self.0.as_str()
75    }
76
77    /// Consumes the wrapper and returns the inner [`CompactString`].
78    #[must_use]
79    pub fn into_inner(self) -> CompactString {
80        self.0
81    }
82}
83
84impl AsRef<str> for UptoActualAmount {
85    fn as_ref(&self) -> &str {
86        self.0.as_str()
87    }
88}
89
90impl From<CompactString> for UptoActualAmount {
91    fn from(value: CompactString) -> Self {
92        Self(value)
93    }
94}
95
96impl From<&str> for UptoActualAmount {
97    fn from(value: &str) -> Self {
98        Self(value.into())
99    }
100}
101
102impl From<String> for UptoActualAmount {
103    fn from(value: String) -> Self {
104        Self(value.into())
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn roundtrips_through_str_constructors() {
114        let a = UptoActualAmount::new("125000");
115        assert_eq!(a.as_str(), "125000");
116        assert_eq!(a.as_ref(), "125000");
117        assert_eq!(a.into_inner().as_str(), "125000");
118    }
119
120    #[test]
121    fn is_constructible_from_string_like() {
122        let _a: UptoActualAmount = "1".into();
123        let _b: UptoActualAmount = String::from("2").into();
124        let _c: UptoActualAmount = CompactString::from("3").into();
125    }
126}