Skip to main content

x402_sdk/
core.rs

1/// x402 Core module.
2use crate::config::{ConfigError, ConfigManager};
3use crate::types::{
4    ChainType, Currency, PaymentRequest, PaymentVerification, VerificationResult,
5    X402ProtocolResponse,
6};
7use crate::verifier::{PaymentVerifier, VerificationError, VerifierRegistry};
8use std::collections::HashMap;
9use std::sync::{Arc, RwLock};
10use uuid::Uuid;
11
12/// Core for handling x402 Payment Required protocol.
13///
14/// # Examples
15///
16/// ```rust
17/// use x402::core::X402;
18///
19/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
20/// let mut engine = X402::from_config_file("config.toml")?;
21///
22/// engine.register_chain_verifier(
23///     ChainType::Evm(EvmChain::Ethereum),
24///     "https://mainnet.infura.io/v3/your-key".to_string()
25/// ).await?;
26///
27/// // Handle access request
28/// let result = engine.handle_access_request(
29///     "0x742E4D6c9Ff68c6E355B069E2775D3Dd6876b4a5",
30///     "/premium/content",
31///     None,
32///     None
33/// ).await?;
34///
35/// if result.should_serve_content {
36///     // Return 402 Payment Required with payment details
37///     println!("Payment required: {:?}", result.x402_response);
38/// }
39/// # Ok(())
40/// # }
41/// ```
42pub struct X402 {
43    config_manager: ConfigManager,
44    verifier_registry: VerifierRegistry,
45    payment_sessions_cache: Arc<RwLock<HashMap<String, PaymentSession>>>,
46}
47
48impl X402 {
49    pub fn new(config_manager: ConfigManager) -> Result<Self, EngineError> {
50        Ok(Self {
51            config_manager,
52            verifier_registry: VerifierRegistry::new(),
53            payment_sessions_cache: Arc::new(RwLock::new(HashMap::new())),
54        })
55    }
56
57    pub fn from_config_file(path: &str) -> Result<Self, EngineError> {
58        let config_manager = ConfigManager::from_file(path)?;
59        Self::new(config_manager)
60    }
61
62    pub fn from_default_config() -> Result<Self, EngineError> {
63        let config_manager = ConfigManager::new()?;
64        Self::new(config_manager)
65    }
66
67    /// register chain verifier by chain type
68    pub async fn register_chain_verifier(
69        &mut self,
70        chain_type: ChainType,
71        rpc_url: String,
72    ) -> Result<(), EngineError> {
73        let chain_config = self
74            .config_manager
75            .get_chain_config(&chain_type)
76            .ok_or_else(|| EngineError::ChainNotSupported(chain_type.clone()))?;
77        let verifier: Box<dyn PaymentVerifier> = match &chain_type {
78            ChainType::Evm(_) => {
79                use crate::verifier::evm::EvmVerifier;
80                let evm_verifier = EvmVerifier::new(rpc_url, chain_type.clone())
81                    .await
82                    .map_err(EngineError::VerificationError)?;
83                Box::new(evm_verifier)
84            }
85            ChainType::Solana(_) => {
86                use crate::verifier::solana::SolanaVerifier;
87                let solana_verifier = SolanaVerifier::new();
88                Box::new(solana_verifier)
89            }
90            _ => {
91                return Err(EngineError::ChainNotSupported(chain_type));
92            }
93        };
94        self.verifier_registry
95            .register_verifier(chain_type, verifier);
96        Ok(())
97    }
98
99    pub async fn verify_payment(
100        &self,
101        user_address: &str,
102        payment_nonce: &str,
103    ) -> Result<PaymentVerification, EngineError> {
104        let (chain_type, payment_request) = {
105            let sessions = self.payment_sessions_cache.read().unwrap();
106            let session = sessions
107                .get(payment_nonce)
108                .ok_or(EngineError::InvalidSession)?;
109
110            if session.user_address != user_address {
111                return Err(EngineError::AddressMismatch);
112            }
113
114            (
115                session.payment_request.chain.chain_type.clone(),
116                session.payment_request.clone(),
117            )
118        };
119        let verifier = self
120            .verifier_registry
121            .get_verifier(&chain_type)
122            .ok_or(EngineError::ChainNotSupported(chain_type))?;
123        let verification = verifier
124            .verify_payment(&payment_request, user_address)
125            .await
126            .map_err(EngineError::VerificationFailed)?;
127        if verification.is_paid {
128            let mut sessions = self.payment_sessions_cache.write().unwrap();
129            if let Some(session) = sessions.get_mut(payment_nonce) {
130                session.verified = true;
131            }
132        }
133        Ok(verification)
134    }
135
136    fn create_payment_request(
137        &self,
138        user_address: &str,
139        resource_path: &str,
140        custom_amount: Option<&str>,
141    ) -> Result<PaymentRequest, EngineError> {
142        let config = self.config_manager.get_config();
143        let default_chain = self.config_manager.get_default_chain_config()?;
144
145        let amount = custom_amount
146            .map(|s| s.to_string())
147            .unwrap_or_else(|| config.payments.default_amount.clone());
148        let currency = match &config.service.default_currency {
149            crate::config::CurrencyConfig {
150                currency_type,
151                address,
152                decimals,
153            } => match currency_type {
154                crate::config::CurrencyType::Native => Currency::Native,
155                crate::config::CurrencyType::Erc20 => {
156                    let token_address =
157                        address.clone().ok_or(EngineError::InvalidCurrencyConfig)?;
158                    Currency::Token {
159                        address: token_address,
160                        decimals: *decimals,
161                    }
162                }
163                _ => Currency::Native,
164            },
165        };
166        Ok(PaymentRequest {
167            amount,
168            currency,
169            recipient: self.config_manager.get_service_address(),
170            chain: default_chain.clone(),
171            description: Some(format!("Access to: {}", resource_path)),
172            expires_at: Some(
173                std::time::SystemTime::now()
174                    .duration_since(std::time::UNIX_EPOCH)
175                    .unwrap()
176                    .as_secs()
177                    + config.payments.expiration_time_secs,
178            ),
179            nonce: Uuid::new_v4().to_string(),
180        })
181    }
182
183    fn store_payment_session(&self, user_address: &str, payment_request: PaymentRequest) {
184        let session = PaymentSession {
185            user_address: user_address.to_string(),
186            payment_request,
187            created_at: std::time::SystemTime::now()
188                .duration_since(std::time::UNIX_EPOCH)
189                .unwrap()
190                .as_secs(),
191            verified: false,
192        };
193
194        let mut sessions = self.payment_sessions_cache.write().unwrap();
195        sessions.insert(session.payment_request.nonce.clone(), session);
196    }
197
198    /// Handles an access request and returns appropriate payment verification result.
199    ///
200    /// # Payment Flow
201    ///
202    /// First Request (no payment_nonce): Returns 402 Payment Required with payment details
203    /// Subsequent Request (with payment_nonce): Verifies payment and grants access if paid
204    /// Payment Failed/Insufficient: Returns new payment request for retry
205    ///
206    /// # Params
207    ///
208    /// user_address - Blockchain address of the user requesting access
209    /// resource_path - Path identifier for the requested resource (used in payment description)
210    /// payment_nonce - Optional payment session identifier from previous 402 response
211    /// custom_amount - Optional custom payment amount overriding default configuration
212    ///
213    /// # Examples
214    ///
215    /// ```rust
216    /// // First request - returns 402 with payment details
217    /// let result = engine.handle_access_request(
218    ///     "0x1234...",
219    ///     "/premium/content",
220    ///     None,
221    ///     None
222    /// ).await?;
223    ///
224    /// // Subsequent request with payment nonce - returns 200 if paid
225    /// let result = engine.handle_access_request(
226    ///     "0x1234...",
227    ///     "/premium/content",
228    ///     Some("payment-nonce-from-402-response"),
229    ///     None
230    /// ).await?;
231    ///
232    /// if result.should_serve_content {
233    ///     // Serve the paid content to user
234    /// } else {
235    ///     // Return 402 response with payment details
236    ///     let payment_response = result.x402_response.unwrap();
237    /// }
238    /// ```
239    pub async fn handle_access_request(
240        &self,
241        user_address: &str,
242        resource_path: &str,
243        payment_nonce: Option<&str>,
244        custom_amount: Option<&str>,
245    ) -> Result<VerificationResult, EngineError> {
246        if let Some(nonce) = payment_nonce {
247            if let Ok(verification) = self.verify_payment(user_address, nonce).await {
248                if verification.is_paid {
249                    return Ok(VerificationResult {
250                        should_serve_content: true,
251                        http_status: 200,
252                        x402_response: None,
253                        verification: Some(verification),
254                    });
255                }
256            }
257        }
258        let payment_request =
259            self.create_payment_request(user_address, resource_path, custom_amount)?;
260        let config = self.config_manager.get_config();
261        let x402_response = X402ProtocolResponse {
262            status: 402,
263            payment_required: payment_request.clone(),
264            verification_url: Some(format!(
265                "{}/{}",
266                config.service.base_verification_url, payment_request.nonce
267            )),
268        };
269        self.store_payment_session(user_address, payment_request);
270        Ok(VerificationResult {
271            should_serve_content: false,
272            http_status: 402,
273            x402_response: Some(x402_response),
274            verification: None,
275        })
276    }
277
278    pub fn config_manager(&self) -> &ConfigManager {
279        &self.config_manager
280    }
281
282    pub fn verifier_registry(&self) -> &VerifierRegistry {
283        &self.verifier_registry
284    }
285
286    pub fn verifier_registry_mut(&mut self) -> &mut VerifierRegistry {
287        &mut self.verifier_registry
288    }
289}
290
291#[derive(Debug)]
292pub enum EngineError {
293    ConfigError(ConfigError),
294    VerificationError(VerificationError),
295    InvalidSession,
296    AddressMismatch,
297    ChainNotSupported(ChainType),
298    VerificationFailed(VerificationError),
299    InvalidCurrencyConfig,
300}
301
302impl std::fmt::Display for EngineError {
303    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304        match self {
305            Self::ConfigError(err) => write!(f, "Configuration error: {}", err),
306            Self::VerificationError(err) => write!(f, "Verification error: {}", err),
307            Self::InvalidSession => write!(f, "Payment session not found"),
308            Self::AddressMismatch => write!(f, "User address mismatch"),
309            Self::ChainNotSupported(chain_type) => {
310                write!(f, "Chain not supported: {:?}", chain_type)
311            }
312            Self::VerificationFailed(err) => write!(f, "Verification failed: {}", err),
313            Self::InvalidCurrencyConfig => write!(f, "Invalid currency configuration"),
314        }
315    }
316}
317
318impl std::error::Error for EngineError {}
319
320impl From<ConfigError> for EngineError {
321    fn from(err: ConfigError) -> Self {
322        Self::ConfigError(err)
323    }
324}
325
326impl From<VerificationError> for EngineError {
327    fn from(err: VerificationError) -> Self {
328        Self::VerificationError(err)
329    }
330}
331
332struct PaymentSession {
333    user_address: String,
334    payment_request: PaymentRequest,
335    created_at: u64,
336    verified: bool,
337}