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}