use crate::{
blockchain::{BlockchainClient, BlockchainClientFactory, TransactionStatus},
types::{PaymentPayload, PaymentRequirements, SettleResponse, VerifyResponse},
Result, X402Error,
};
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub struct BlockchainFacilitatorClient {
blockchain_client: BlockchainClient,
#[allow(dead_code)]
network: String,
#[allow(dead_code)]
verification_timeout: Duration,
#[allow(dead_code)]
confirmation_blocks: u64,
}
#[derive(Debug, Clone)]
pub struct BlockchainFacilitatorConfig {
pub rpc_url: Option<String>,
pub network: String,
pub verification_timeout: Duration,
pub confirmation_blocks: u64,
pub max_retries: u32,
pub retry_delay: Duration,
}
impl Default for BlockchainFacilitatorConfig {
fn default() -> Self {
Self {
rpc_url: None,
network: "base-sepolia".to_string(),
verification_timeout: Duration::from_secs(30),
confirmation_blocks: 1,
max_retries: 3,
retry_delay: Duration::from_secs(1),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionVerification {
pub is_valid: bool,
pub transaction_hash: Option<String>,
pub block_number: Option<u64>,
pub gas_used: Option<u64>,
pub error_reason: Option<String>,
}
impl BlockchainFacilitatorClient {
pub fn new(config: BlockchainFacilitatorConfig) -> Result<Self> {
let blockchain_client = if let Some(rpc_url) = config.rpc_url {
BlockchainClient::new(rpc_url, config.network.clone())
} else {
match config.network.as_str() {
"base-sepolia" => BlockchainClientFactory::base_sepolia(),
"base" => BlockchainClientFactory::base(),
"avalanche-fuji" => BlockchainClientFactory::avalanche_fuji(),
"avalanche" => BlockchainClientFactory::avalanche(),
_ => {
return Err(X402Error::invalid_network(format!(
"Unsupported network: {}",
config.network
)))
}
}
};
Ok(Self {
blockchain_client,
network: config.network,
verification_timeout: config.verification_timeout,
confirmation_blocks: config.confirmation_blocks,
})
}
pub async fn verify(
&self,
payment_payload: &PaymentPayload,
requirements: &PaymentRequirements,
) -> Result<VerifyResponse> {
if payment_payload.network != requirements.network {
return Ok(VerifyResponse {
is_valid: false,
invalid_reason: Some(format!(
"Network mismatch: payment network {} != requirements network {}",
payment_payload.network, requirements.network
)),
payer: Some(payment_payload.payload.authorization.from.clone()),
});
}
if payment_payload.scheme != requirements.scheme {
return Ok(VerifyResponse {
is_valid: false,
invalid_reason: Some(format!(
"Scheme mismatch: payment scheme {} != requirements scheme {}",
payment_payload.scheme, requirements.scheme
)),
payer: Some(payment_payload.payload.authorization.from.clone()),
});
}
if !payment_payload.payload.authorization.is_valid_now()? {
return Ok(VerifyResponse {
is_valid: false,
invalid_reason: Some("Authorization expired or not yet valid".to_string()),
payer: Some(payment_payload.payload.authorization.from.clone()),
});
}
let payment_amount: u128 = payment_payload
.payload
.authorization
.value
.parse()
.map_err(|_| {
X402Error::invalid_payment_requirements("Invalid payment amount format")
})?;
let required_amount: u128 = requirements.max_amount_required.parse().map_err(|_| {
X402Error::invalid_payment_requirements("Invalid required amount format")
})?;
if payment_amount < required_amount {
return Ok(VerifyResponse {
is_valid: false,
invalid_reason: Some(format!(
"Insufficient amount: {} < {}",
payment_amount, required_amount
)),
payer: Some(payment_payload.payload.authorization.from.clone()),
});
}
if payment_payload.payload.authorization.to != requirements.pay_to {
return Ok(VerifyResponse {
is_valid: false,
invalid_reason: Some(format!(
"Recipient mismatch: {} != {}",
payment_payload.payload.authorization.to, requirements.pay_to
)),
payer: Some(payment_payload.payload.authorization.from.clone()),
});
}
let balance_info = self
.blockchain_client
.get_usdc_balance(&payment_payload.payload.authorization.from)
.await?;
if let Some(token_balance) = balance_info.token_balance {
let balance: u128 = u128::from_str_radix(token_balance.trim_start_matches("0x"), 16)
.map_err(|_| X402Error::invalid_payment_requirements("Invalid balance format"))?;
if balance < payment_amount {
return Ok(VerifyResponse {
is_valid: false,
invalid_reason: Some(format!(
"Insufficient balance: {} < {}",
balance, payment_amount
)),
payer: Some(payment_payload.payload.authorization.from.clone()),
});
}
}
Ok(VerifyResponse {
is_valid: true,
invalid_reason: None,
payer: Some(payment_payload.payload.authorization.from.clone()),
})
}
pub async fn settle(
&self,
payment_payload: &PaymentPayload,
requirements: &PaymentRequirements,
) -> Result<SettleResponse> {
let verification = self.verify(payment_payload, requirements).await?;
if !verification.is_valid {
return Ok(SettleResponse {
success: false,
error_reason: Some(
verification
.invalid_reason
.unwrap_or("Verification failed".to_string()),
),
transaction: "".to_string(),
network: payment_payload.network.clone(),
payer: verification.payer,
});
}
let transaction_hash = self
.create_settlement_transaction(payment_payload, requirements)
.await?;
let confirmation_result = self.wait_for_confirmation(&transaction_hash).await?;
if confirmation_result.success {
Ok(SettleResponse {
success: true,
error_reason: None,
transaction: transaction_hash,
network: payment_payload.network.clone(),
payer: Some(payment_payload.payload.authorization.from.clone()),
})
} else {
Ok(SettleResponse {
success: false,
error_reason: Some(
confirmation_result
.error_reason
.unwrap_or("Transaction failed".to_string()),
),
transaction: transaction_hash,
network: payment_payload.network.clone(),
payer: Some(payment_payload.payload.authorization.from.clone()),
})
}
}
async fn create_settlement_transaction(
&self,
payment_payload: &PaymentPayload,
_requirements: &PaymentRequirements,
) -> Result<String> {
let auth = &payment_payload.payload.authorization;
let usdc_contract = self.blockchain_client.get_usdc_contract_address()?;
let function_selector = "0x4000aea0";
let encoded_params = self.encode_transfer_with_authorization_params(auth)?;
let data = format!("{}{}", function_selector, encoded_params);
let tx_request = crate::blockchain::TransactionRequest {
from: auth.from.clone(),
to: usdc_contract,
value: None, data: Some(data),
gas: Some("0x5208".to_string()), gas_price: Some("0x3b9aca00".to_string()), };
let estimated_gas = self.blockchain_client.estimate_gas(&tx_request).await?;
let mut final_tx = tx_request;
final_tx.gas = Some(format!("0x{:x}", estimated_gas));
let tx_hash = self.simulate_transaction_broadcast(&final_tx, auth).await?;
Ok(tx_hash)
}
fn encode_transfer_with_authorization_params(
&self,
auth: &crate::types::ExactEvmPayloadAuthorization,
) -> Result<String> {
use std::str::FromStr;
let mut encoded = String::new();
encoded.push_str(&format!("{:064x}", 0)); encoded.push_str(auth.from.trim_start_matches("0x"));
encoded.push_str(auth.to.trim_start_matches("0x"));
encoded.push_str(&format!("{:064x}", u128::from_str(&auth.value)?));
encoded.push_str(&format!("{:064x}", u128::from_str(&auth.valid_after)?));
encoded.push_str(&format!("{:064x}", u128::from_str(&auth.valid_before)?));
encoded.push_str(auth.nonce.trim_start_matches("0x"));
encoded.push_str(&format!("{:02x}", 0)); encoded.push_str(&format!("{:064x}", 0)); encoded.push_str(&format!("{:064x}", 0));
Ok(encoded)
}
async fn simulate_transaction_broadcast(
&self,
_tx_request: &crate::blockchain::TransactionRequest,
_auth: &crate::types::ExactEvmPayloadAuthorization,
) -> Result<String> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let mut hash_bytes = [0u8; 32];
hash_bytes[0..8].copy_from_slice(×tamp.to_be_bytes());
hash_bytes[8..16].copy_from_slice(&(timestamp % 1000000).to_be_bytes());
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(_auth.from.as_bytes());
hasher.update(_auth.to.as_bytes());
hasher.update(_auth.value.as_bytes());
hasher.update(_auth.nonce.as_bytes());
let hash_result = hasher.finalize();
hash_bytes[16..32].copy_from_slice(&hash_result[16..32]);
Ok(format!("0x{}", hex::encode(hash_bytes)))
}
async fn wait_for_confirmation(&self, transaction_hash: &str) -> Result<ConfirmationResult> {
let mut attempts = 0;
let max_attempts = 30;
while attempts < max_attempts {
match self
.blockchain_client
.get_transaction_status(transaction_hash)
.await
{
Ok(tx_info) => {
match tx_info.status {
TransactionStatus::Confirmed => {
return Ok(ConfirmationResult {
success: true,
error_reason: None,
block_number: tx_info.block_number,
gas_used: tx_info.gas_used,
});
}
TransactionStatus::Failed => {
return Ok(ConfirmationResult {
success: false,
error_reason: Some("Transaction failed on blockchain".to_string()),
block_number: None,
gas_used: None,
});
}
TransactionStatus::Pending => {
}
TransactionStatus::Unknown => {
}
}
}
Err(e) => {
eprintln!("Error checking transaction status: {}", e);
}
}
tokio::time::sleep(Duration::from_secs(1)).await;
attempts += 1;
}
Ok(ConfirmationResult {
success: false,
error_reason: Some("Transaction confirmation timeout".to_string()),
block_number: None,
gas_used: None,
})
}
pub async fn get_network_info(&self) -> Result<crate::blockchain::NetworkInfo> {
self.blockchain_client.get_network_info().await
}
pub async fn is_transaction_confirmed(&self, transaction_hash: &str) -> Result<bool> {
let tx_info = self
.blockchain_client
.get_transaction_status(transaction_hash)
.await?;
Ok(tx_info.status == TransactionStatus::Confirmed)
}
}
#[derive(Debug, Clone)]
struct ConfirmationResult {
success: bool,
error_reason: Option<String>,
#[allow(dead_code)]
block_number: Option<u64>,
#[allow(dead_code)]
gas_used: Option<u64>,
}
pub struct BlockchainFacilitatorFactory;
impl BlockchainFacilitatorFactory {
pub fn base_sepolia() -> Result<BlockchainFacilitatorClient> {
BlockchainFacilitatorClient::new(BlockchainFacilitatorConfig {
network: "base-sepolia".to_string(),
..Default::default()
})
}
pub fn base() -> Result<BlockchainFacilitatorClient> {
BlockchainFacilitatorClient::new(BlockchainFacilitatorConfig {
network: "base".to_string(),
..Default::default()
})
}
pub fn avalanche_fuji() -> Result<BlockchainFacilitatorClient> {
BlockchainFacilitatorClient::new(BlockchainFacilitatorConfig {
network: "avalanche-fuji".to_string(),
..Default::default()
})
}
pub fn avalanche() -> Result<BlockchainFacilitatorClient> {
BlockchainFacilitatorClient::new(BlockchainFacilitatorConfig {
network: "avalanche".to_string(),
..Default::default()
})
}
pub fn custom(config: BlockchainFacilitatorConfig) -> Result<BlockchainFacilitatorClient> {
BlockchainFacilitatorClient::new(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_facilitator_config_default() {
let config = BlockchainFacilitatorConfig::default();
assert_eq!(config.network, "base-sepolia");
assert_eq!(config.confirmation_blocks, 1);
}
#[test]
fn test_facilitator_factory() {
let facilitator = BlockchainFacilitatorFactory::base_sepolia();
assert!(facilitator.is_ok());
}
}