1use actix_web::{
43 dev::Payload, error::ResponseError, http::StatusCode, Error, FromRequest, HttpRequest,
44 HttpResponse,
45};
46use chrono::{Duration, Utc};
47use fly402_core::{PaymentAuthorization, PaymentRequest};
48use std::future::{ready, Ready};
49use uuid::Uuid;
50
51#[derive(Debug, Clone)]
53pub struct X402Config {
54 pub payment_address: String,
56
57 pub token_mint: String,
59
60 pub network: String,
62
63 pub rpc_url: Option<String>,
65
66 pub auto_verify: bool,
68}
69
70#[derive(Debug, Clone)]
72pub struct X402State {
73 pub config: X402Config,
74}
75
76#[derive(Debug, Clone)]
78pub struct PaymentRequirement {
79 pub amount: String,
81
82 pub description: Option<String>,
84
85 pub expires_in: i64,
87}
88
89impl PaymentRequirement {
90 pub fn new(amount: &str) -> Self {
92 Self {
93 amount: amount.to_string(),
94 description: None,
95 expires_in: 300,
96 }
97 }
98
99 pub fn with_description(mut self, description: &str) -> Self {
101 self.description = Some(description.to_string());
102 self
103 }
104
105 pub fn with_expires_in(mut self, seconds: i64) -> Self {
107 self.expires_in = seconds;
108 self
109 }
110}
111
112pub struct PaymentExtractor {
114 pub authorization: PaymentAuthorization,
115}
116
117impl FromRequest for PaymentExtractor {
118 type Error = Error;
119 type Future = Ready<Result<Self, Self::Error>>;
120
121 fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
122 let auth_header = match req.headers().get("X-Payment-Authorization") {
124 Some(h) => match h.to_str() {
125 Ok(s) => s,
126 Err(_) => {
127 return ready(Err(PaymentError::InvalidHeader.into()));
128 }
129 },
130 None => {
131 return ready(Err(PaymentError::Required.into()));
132 }
133 };
134
135 match PaymentAuthorization::from_header_value(auth_header) {
137 Ok(auth) => {
138 ready(Ok(PaymentExtractor { authorization: auth }))
140 }
141 Err(e) => ready(Err(PaymentError::InvalidAuthorization(e.to_string()).into())),
142 }
143 }
144}
145
146#[derive(Debug)]
148pub enum PaymentError {
149 Required,
150 InvalidHeader,
151 InvalidAuthorization(String),
152}
153
154impl std::fmt::Display for PaymentError {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 match self {
157 PaymentError::Required => write!(f, "Payment required"),
158 PaymentError::InvalidHeader => write!(f, "Invalid authorization header"),
159 PaymentError::InvalidAuthorization(msg) => {
160 write!(f, "Invalid payment authorization: {}", msg)
161 }
162 }
163 }
164}
165
166impl ResponseError for PaymentError {
167 fn status_code(&self) -> StatusCode {
168 match self {
169 PaymentError::Required => StatusCode::PAYMENT_REQUIRED,
170 PaymentError::InvalidHeader | PaymentError::InvalidAuthorization(_) => {
171 StatusCode::BAD_REQUEST
172 }
173 }
174 }
175
176 fn error_response(&self) -> HttpResponse {
177 match self {
178 PaymentError::Required => {
179 HttpResponse::PaymentRequired().json(serde_json::json!({
180 "error": "Payment required",
181 "message": "This endpoint requires payment"
182 }))
183 }
184 _ => HttpResponse::BadRequest().json(serde_json::json!({
185 "error": self.to_string()
186 })),
187 }
188 }
189}
190
191pub fn create_payment_request(
193 config: &X402Config,
194 requirement: &PaymentRequirement,
195 resource: &str,
196) -> PaymentRequest {
197 let expires_at = Utc::now() + Duration::seconds(requirement.expires_in);
198 let payment_id = Uuid::new_v4().to_string();
199 let nonce = Uuid::new_v4().to_string();
200
201 let mut request = PaymentRequest::new(
202 requirement.amount.clone(),
203 config.token_mint.clone(),
204 config.payment_address.clone(),
205 config.network.clone(),
206 expires_at,
207 nonce,
208 payment_id,
209 resource.to_string(),
210 );
211
212 if let Some(desc) = &requirement.description {
213 request = request.with_description(desc.clone());
214 }
215
216 request
217}
218
219pub fn payment_required_response(payment_request: PaymentRequest) -> HttpResponse {
221 HttpResponse::PaymentRequired().json(payment_request)
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_payment_requirement() {
230 let req = PaymentRequirement::new("0.10")
231 .with_description("Premium data access")
232 .with_expires_in(600);
233
234 assert_eq!(req.amount, "0.10");
235 assert_eq!(req.description, Some("Premium data access".to_string()));
236 assert_eq!(req.expires_in, 600);
237 }
238
239 #[test]
240 fn test_create_payment_request() {
241 let config = X402Config {
242 payment_address: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".to_string(),
243 token_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
244 network: "solana-devnet".to_string(),
245 rpc_url: None,
246 auto_verify: false,
247 };
248
249 let requirement = PaymentRequirement::new("0.10");
250 let request = create_payment_request(&config, &requirement, "/api/premium");
251
252 assert_eq!(request.max_amount_required, "0.10");
253 assert_eq!(request.payment_address, config.payment_address);
254 assert_eq!(request.asset_address, config.token_mint);
255 }
256}