use crate::client::endpoints::{AccountEndpoints, TokenEndpoints, TransactionEndpoints};
use crate::client::BscScanClient;
use crate::error::{Error, Result};
use crate::payment::models::{Currency, PaymentRequest};
use crate::payment::utils::{amount_sufficient, is_valid_address};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
pub struct PaymentVerifier {
client: BscScanClient,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum VerificationResult {
NotFound,
Pending {
tx_hash: String,
confirmations: u64,
},
Confirmed {
tx_hash: String,
confirmations: u64,
},
Failed {
reason: String,
},
}
impl PaymentVerifier {
pub fn new(client: BscScanClient) -> Self {
Self { client }
}
pub async fn verify_payment(&self, request: &PaymentRequest) -> Result<VerificationResult> {
if !is_valid_address(&request.recipient_address) {
return Err(Error::InvalidAddress(request.recipient_address.clone()));
}
let matching_tx = match &request.currency {
Currency::ETH => self.find_eth_transaction(request).await?,
Currency::ERC20 {
contract_address,
decimals,
} => {
self.find_token_transaction(request, contract_address, *decimals)
.await?
}
};
let (tx_hash, confirmations, actual_amount) = match matching_tx {
Some(data) => data,
None => return Ok(VerificationResult::NotFound),
};
let min_percent = Decimal::from_str_radix("99.9", 10).unwrap();
if !amount_sufficient(request.amount, actual_amount, min_percent) {
return Ok(VerificationResult::Failed {
reason: format!(
"Amount mismatch: expected {}, got {}",
request.amount, actual_amount
),
});
}
if confirmations >= request.required_confirmations {
Ok(VerificationResult::Confirmed {
tx_hash,
confirmations,
})
} else {
Ok(VerificationResult::Pending {
tx_hash,
confirmations,
})
}
}
async fn find_eth_transaction(
&self,
request: &PaymentRequest,
) -> Result<Option<(String, u64, Decimal)>> {
let transactions = self
.client
.get_transactions(&request.recipient_address, 0, 99999999, 1, 100, "desc")
.await?;
for tx in transactions {
if !tx.is_successful() {
continue;
}
let tx_value = tx.value_bnb();
if amount_sufficient(request.amount, tx_value, Decimal::new(999, 1)) {
let confirmations = tx.confirmations_u64();
return Ok(Some((tx.hash, confirmations, tx_value)));
}
}
Ok(None)
}
async fn find_token_transaction(
&self,
request: &PaymentRequest,
contract_address: &str,
_decimals: u8,
) -> Result<Option<(String, u64, Decimal)>> {
let transfers = self
.client
.get_token_transfers(
&request.recipient_address,
Some(contract_address),
0,
99999999,
1,
100,
"desc",
)
.await?;
for transfer in transfers {
let tx_value = transfer.value_tokens();
if amount_sufficient(request.amount, tx_value, Decimal::new(999, 1)) {
let confirmations = transfer.confirmations_u64();
return Ok(Some((transfer.hash, confirmations, tx_value)));
}
}
Ok(None)
}
pub async fn check_confirmations(&self, tx_hash: &str) -> Result<u64> {
self.client.get_confirmations(tx_hash).await
}
pub async fn find_matching_transaction(&self, request: &PaymentRequest) -> Result<Option<String>> {
let result = self.verify_payment(request).await?;
match result {
VerificationResult::Confirmed { tx_hash, .. } => Ok(Some(tx_hash)),
VerificationResult::Pending { tx_hash, .. } => Ok(Some(tx_hash)),
_ => Ok(None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verification_result() {
let result = VerificationResult::Confirmed {
tx_hash: "0x123".to_string(),
confirmations: 15,
};
match result {
VerificationResult::Confirmed {
confirmations,
..
} => {
assert_eq!(confirmations, 15);
}
_ => panic!("Expected Confirmed"),
}
}
}