1use crate::{
10 blockchain::{BlockchainClient, BlockchainClientFactory, TransactionStatus},
11 types::{PaymentPayload, PaymentRequirements, SettleResponse, VerifyResponse},
12 Result, X402Error,
13};
14use serde::{Deserialize, Serialize};
15use std::time::{Duration, SystemTime, UNIX_EPOCH};
16
17pub struct BlockchainFacilitatorClient {
19 blockchain_client: BlockchainClient,
21 #[allow(dead_code)]
23 network: String,
24 #[allow(dead_code)]
26 verification_timeout: Duration,
27 #[allow(dead_code)]
29 confirmation_blocks: u64,
30}
31
32#[derive(Debug, Clone)]
34pub struct BlockchainFacilitatorConfig {
35 pub rpc_url: Option<String>,
37 pub network: String,
39 pub verification_timeout: Duration,
41 pub confirmation_blocks: u64,
43 pub max_retries: u32,
45 pub retry_delay: Duration,
47}
48
49impl Default for BlockchainFacilitatorConfig {
50 fn default() -> Self {
51 Self {
52 rpc_url: None,
53 network: "base-sepolia".to_string(),
54 verification_timeout: Duration::from_secs(30),
55 confirmation_blocks: 1,
56 max_retries: 3,
57 retry_delay: Duration::from_secs(1),
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct TransactionVerification {
65 pub is_valid: bool,
66 pub transaction_hash: Option<String>,
67 pub block_number: Option<u64>,
68 pub gas_used: Option<u64>,
69 pub error_reason: Option<String>,
70}
71
72impl BlockchainFacilitatorClient {
73 pub fn new(config: BlockchainFacilitatorConfig) -> Result<Self> {
75 let blockchain_client = if let Some(rpc_url) = config.rpc_url {
76 BlockchainClient::new(rpc_url, config.network.clone())
77 } else {
78 match config.network.as_str() {
79 "base-sepolia" => BlockchainClientFactory::base_sepolia(),
80 "base" => BlockchainClientFactory::base(),
81 "avalanche-fuji" => BlockchainClientFactory::avalanche_fuji(),
82 "avalanche" => BlockchainClientFactory::avalanche(),
83 _ => {
84 return Err(X402Error::invalid_network(format!(
85 "Unsupported network: {}",
86 config.network
87 )))
88 }
89 }
90 };
91
92 Ok(Self {
93 blockchain_client,
94 network: config.network,
95 verification_timeout: config.verification_timeout,
96 confirmation_blocks: config.confirmation_blocks,
97 })
98 }
99
100 pub async fn verify(
102 &self,
103 payment_payload: &PaymentPayload,
104 requirements: &PaymentRequirements,
105 ) -> Result<VerifyResponse> {
106 if payment_payload.network != requirements.network {
108 return Ok(VerifyResponse {
109 is_valid: false,
110 invalid_reason: Some(format!(
111 "Network mismatch: payment network {} != requirements network {}",
112 payment_payload.network, requirements.network
113 )),
114 payer: Some(payment_payload.payload.authorization.from.clone()),
115 });
116 }
117
118 if payment_payload.scheme != requirements.scheme {
120 return Ok(VerifyResponse {
121 is_valid: false,
122 invalid_reason: Some(format!(
123 "Scheme mismatch: payment scheme {} != requirements scheme {}",
124 payment_payload.scheme, requirements.scheme
125 )),
126 payer: Some(payment_payload.payload.authorization.from.clone()),
127 });
128 }
129
130 if !payment_payload.payload.authorization.is_valid_now()? {
132 return Ok(VerifyResponse {
133 is_valid: false,
134 invalid_reason: Some("Authorization expired or not yet valid".to_string()),
135 payer: Some(payment_payload.payload.authorization.from.clone()),
136 });
137 }
138
139 let payment_amount: u128 = payment_payload
141 .payload
142 .authorization
143 .value
144 .parse()
145 .map_err(|_| {
146 X402Error::invalid_payment_requirements("Invalid payment amount format")
147 })?;
148
149 let required_amount: u128 = requirements.max_amount_required.parse().map_err(|_| {
150 X402Error::invalid_payment_requirements("Invalid required amount format")
151 })?;
152
153 if payment_amount < required_amount {
154 return Ok(VerifyResponse {
155 is_valid: false,
156 invalid_reason: Some(format!(
157 "Insufficient amount: {} < {}",
158 payment_amount, required_amount
159 )),
160 payer: Some(payment_payload.payload.authorization.from.clone()),
161 });
162 }
163
164 if payment_payload.payload.authorization.to != requirements.pay_to {
166 return Ok(VerifyResponse {
167 is_valid: false,
168 invalid_reason: Some(format!(
169 "Recipient mismatch: {} != {}",
170 payment_payload.payload.authorization.to, requirements.pay_to
171 )),
172 payer: Some(payment_payload.payload.authorization.from.clone()),
173 });
174 }
175
176 let balance_info = self
178 .blockchain_client
179 .get_usdc_balance(&payment_payload.payload.authorization.from)
180 .await?;
181
182 if let Some(token_balance) = balance_info.token_balance {
183 let balance: u128 = u128::from_str_radix(token_balance.trim_start_matches("0x"), 16)
184 .map_err(|_| X402Error::invalid_payment_requirements("Invalid balance format"))?;
185
186 if balance < payment_amount {
187 return Ok(VerifyResponse {
188 is_valid: false,
189 invalid_reason: Some(format!(
190 "Insufficient balance: {} < {}",
191 balance, payment_amount
192 )),
193 payer: Some(payment_payload.payload.authorization.from.clone()),
194 });
195 }
196 }
197
198 Ok(VerifyResponse {
200 is_valid: true,
201 invalid_reason: None,
202 payer: Some(payment_payload.payload.authorization.from.clone()),
203 })
204 }
205
206 pub async fn settle(
208 &self,
209 payment_payload: &PaymentPayload,
210 requirements: &PaymentRequirements,
211 ) -> Result<SettleResponse> {
212 let verification = self.verify(payment_payload, requirements).await?;
214 if !verification.is_valid {
215 return Ok(SettleResponse {
216 success: false,
217 error_reason: Some(
218 verification
219 .invalid_reason
220 .unwrap_or("Verification failed".to_string()),
221 ),
222 transaction: "".to_string(),
223 network: payment_payload.network.clone(),
224 payer: verification.payer,
225 });
226 }
227
228 let transaction_hash = self
237 .create_settlement_transaction(payment_payload, requirements)
238 .await?;
239
240 let confirmation_result = self.wait_for_confirmation(&transaction_hash).await?;
242
243 if confirmation_result.success {
244 Ok(SettleResponse {
245 success: true,
246 error_reason: None,
247 transaction: transaction_hash,
248 network: payment_payload.network.clone(),
249 payer: Some(payment_payload.payload.authorization.from.clone()),
250 })
251 } else {
252 Ok(SettleResponse {
253 success: false,
254 error_reason: Some(
255 confirmation_result
256 .error_reason
257 .unwrap_or("Transaction failed".to_string()),
258 ),
259 transaction: transaction_hash,
260 network: payment_payload.network.clone(),
261 payer: Some(payment_payload.payload.authorization.from.clone()),
262 })
263 }
264 }
265
266 async fn create_settlement_transaction(
268 &self,
269 payment_payload: &PaymentPayload,
270 _requirements: &PaymentRequirements,
271 ) -> Result<String> {
272 let auth = &payment_payload.payload.authorization;
279 let usdc_contract = self.blockchain_client.get_usdc_contract_address()?;
280
281 let function_selector = "0x4000aea0"; let encoded_params = self.encode_transfer_with_authorization_params(auth)?;
286 let data = format!("{}{}", function_selector, encoded_params);
287
288 let tx_request = crate::blockchain::TransactionRequest {
290 from: auth.from.clone(),
291 to: usdc_contract,
292 value: None, data: Some(data),
294 gas: Some("0x5208".to_string()), gas_price: Some("0x3b9aca00".to_string()), };
297
298 let estimated_gas = self.blockchain_client.estimate_gas(&tx_request).await?;
300
301 let mut final_tx = tx_request;
303 final_tx.gas = Some(format!("0x{:x}", estimated_gas));
304
305 let tx_hash = self.simulate_transaction_broadcast(&final_tx, auth).await?;
313
314 Ok(tx_hash)
315 }
316
317 fn encode_transfer_with_authorization_params(
319 &self,
320 auth: &crate::types::ExactEvmPayloadAuthorization,
321 ) -> Result<String> {
322 use std::str::FromStr;
323
324 let mut encoded = String::new();
341
342 encoded.push_str(&format!("{:064x}", 0)); encoded.push_str(auth.from.trim_start_matches("0x"));
345 encoded.push_str(auth.to.trim_start_matches("0x"));
346 encoded.push_str(&format!("{:064x}", u128::from_str(&auth.value)?));
347 encoded.push_str(&format!("{:064x}", u128::from_str(&auth.valid_after)?));
348 encoded.push_str(&format!("{:064x}", u128::from_str(&auth.valid_before)?));
349 encoded.push_str(auth.nonce.trim_start_matches("0x"));
350 encoded.push_str(&format!("{:02x}", 0)); encoded.push_str(&format!("{:064x}", 0)); encoded.push_str(&format!("{:064x}", 0)); Ok(encoded)
355 }
356
357 async fn simulate_transaction_broadcast(
359 &self,
360 _tx_request: &crate::blockchain::TransactionRequest,
361 _auth: &crate::types::ExactEvmPayloadAuthorization,
362 ) -> Result<String> {
363 let timestamp = SystemTime::now()
371 .duration_since(UNIX_EPOCH)
372 .unwrap()
373 .as_secs();
374
375 let mut hash_bytes = [0u8; 32];
377 hash_bytes[0..8].copy_from_slice(×tamp.to_be_bytes());
378 hash_bytes[8..16].copy_from_slice(&(timestamp % 1000000).to_be_bytes());
379
380 use sha2::{Digest, Sha256};
382 let mut hasher = Sha256::new();
383 hasher.update(_auth.from.as_bytes());
384 hasher.update(_auth.to.as_bytes());
385 hasher.update(_auth.value.as_bytes());
386 hasher.update(_auth.nonce.as_bytes());
387 let hash_result = hasher.finalize();
388 hash_bytes[16..32].copy_from_slice(&hash_result[16..32]);
389
390 Ok(format!("0x{}", hex::encode(hash_bytes)))
391 }
392
393 async fn wait_for_confirmation(&self, transaction_hash: &str) -> Result<ConfirmationResult> {
395 let mut attempts = 0;
396 let max_attempts = 30; while attempts < max_attempts {
399 match self
400 .blockchain_client
401 .get_transaction_status(transaction_hash)
402 .await
403 {
404 Ok(tx_info) => {
405 match tx_info.status {
406 TransactionStatus::Confirmed => {
407 return Ok(ConfirmationResult {
408 success: true,
409 error_reason: None,
410 block_number: tx_info.block_number,
411 gas_used: tx_info.gas_used,
412 });
413 }
414 TransactionStatus::Failed => {
415 return Ok(ConfirmationResult {
416 success: false,
417 error_reason: Some("Transaction failed on blockchain".to_string()),
418 block_number: None,
419 gas_used: None,
420 });
421 }
422 TransactionStatus::Pending => {
423 }
425 TransactionStatus::Unknown => {
426 }
428 }
429 }
430 Err(e) => {
431 eprintln!("Error checking transaction status: {}", e);
433 }
434 }
435
436 tokio::time::sleep(Duration::from_secs(1)).await;
437 attempts += 1;
438 }
439
440 Ok(ConfirmationResult {
441 success: false,
442 error_reason: Some("Transaction confirmation timeout".to_string()),
443 block_number: None,
444 gas_used: None,
445 })
446 }
447
448 pub async fn get_network_info(&self) -> Result<crate::blockchain::NetworkInfo> {
450 self.blockchain_client.get_network_info().await
451 }
452
453 pub async fn is_transaction_confirmed(&self, transaction_hash: &str) -> Result<bool> {
455 let tx_info = self
456 .blockchain_client
457 .get_transaction_status(transaction_hash)
458 .await?;
459 Ok(tx_info.status == TransactionStatus::Confirmed)
460 }
461}
462
463#[derive(Debug, Clone)]
465struct ConfirmationResult {
466 success: bool,
467 error_reason: Option<String>,
468 #[allow(dead_code)]
469 block_number: Option<u64>,
470 #[allow(dead_code)]
471 gas_used: Option<u64>,
472}
473
474pub struct BlockchainFacilitatorFactory;
476
477impl BlockchainFacilitatorFactory {
478 pub fn base_sepolia() -> Result<BlockchainFacilitatorClient> {
480 BlockchainFacilitatorClient::new(BlockchainFacilitatorConfig {
481 network: "base-sepolia".to_string(),
482 ..Default::default()
483 })
484 }
485
486 pub fn base() -> Result<BlockchainFacilitatorClient> {
488 BlockchainFacilitatorClient::new(BlockchainFacilitatorConfig {
489 network: "base".to_string(),
490 ..Default::default()
491 })
492 }
493
494 pub fn avalanche_fuji() -> Result<BlockchainFacilitatorClient> {
496 BlockchainFacilitatorClient::new(BlockchainFacilitatorConfig {
497 network: "avalanche-fuji".to_string(),
498 ..Default::default()
499 })
500 }
501
502 pub fn avalanche() -> Result<BlockchainFacilitatorClient> {
504 BlockchainFacilitatorClient::new(BlockchainFacilitatorConfig {
505 network: "avalanche".to_string(),
506 ..Default::default()
507 })
508 }
509
510 pub fn custom(config: BlockchainFacilitatorConfig) -> Result<BlockchainFacilitatorClient> {
512 BlockchainFacilitatorClient::new(config)
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_facilitator_config_default() {
522 let config = BlockchainFacilitatorConfig::default();
523 assert_eq!(config.network, "base-sepolia");
524 assert_eq!(config.confirmation_blocks, 1);
525 }
526
527 #[test]
528 fn test_facilitator_factory() {
529 let facilitator = BlockchainFacilitatorFactory::base_sepolia();
530 assert!(facilitator.is_ok());
531 }
532}