use bitcoin::Amount;
use std::collections::HashMap;
use uuid::Uuid;
use crate::client::BitcoinClient;
use crate::error::Result;
#[derive(Debug, Clone)]
pub struct OrderMatch {
pub order_id: Uuid,
pub address: String,
pub expected_amount_sats: u64,
}
#[derive(Debug, Clone)]
pub struct MatchResult {
pub order_id: Uuid,
pub txid: String,
pub received_amount_sats: u64,
pub expected_amount_sats: u64,
pub confirmations: i32,
pub status: MatchStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchStatus {
Exact,
Underpaid,
Overpaid,
}
impl MatchResult {
pub fn is_sufficient(&self) -> bool {
matches!(self.status, MatchStatus::Exact | MatchStatus::Overpaid)
}
pub fn shortfall(&self) -> Option<u64> {
if self.status == MatchStatus::Underpaid {
Some(
self.expected_amount_sats
.saturating_sub(self.received_amount_sats),
)
} else {
None
}
}
pub fn excess(&self) -> Option<u64> {
if self.status == MatchStatus::Overpaid {
Some(
self.received_amount_sats
.saturating_sub(self.expected_amount_sats),
)
} else {
None
}
}
}
pub struct TransactionMatcher {
client: std::sync::Arc<BitcoinClient>,
min_confirmations: i32,
}
impl TransactionMatcher {
pub fn new(client: std::sync::Arc<BitcoinClient>, min_confirmations: i32) -> Self {
Self {
client,
min_confirmations,
}
}
pub fn match_transactions(
&self,
orders: &[OrderMatch],
since_block: Option<&bitcoin::BlockHash>,
) -> Result<Vec<MatchResult>> {
if orders.is_empty() {
return Ok(Vec::new());
}
let order_by_address: HashMap<&str, &OrderMatch> =
orders.iter().map(|o| (o.address.as_str(), o)).collect();
let result = self.client.list_since_block(since_block, None)?;
let mut matches = Vec::new();
for tx in &result.transactions {
if tx.category != "Receive" {
continue;
}
if let Some(address) = &tx.address {
if let Some(order) = order_by_address.get(address.as_str()) {
let received_sats = tx.amount.unsigned_abs();
let status = match received_sats.cmp(&order.expected_amount_sats) {
std::cmp::Ordering::Equal => MatchStatus::Exact,
std::cmp::Ordering::Less => MatchStatus::Underpaid,
std::cmp::Ordering::Greater => MatchStatus::Overpaid,
};
matches.push(MatchResult {
order_id: order.order_id,
txid: tx.txid.to_string(),
received_amount_sats: received_sats,
expected_amount_sats: order.expected_amount_sats,
confirmations: tx.confirmations,
status,
});
}
}
}
Ok(matches)
}
pub fn check_order_payment(&self, order: &OrderMatch) -> Result<Option<MatchResult>> {
let address: bitcoin::Address<bitcoin::address::NetworkUnchecked> =
order
.address
.parse()
.map_err(|e| crate::error::BitcoinError::InvalidAddress(format!("{:?}", e)))?;
let checked_addr = address.assume_checked();
let unconfirmed = self
.client
.get_received_by_address(&checked_addr, Some(0))?;
if unconfirmed == Amount::ZERO {
return Ok(None);
}
let confirmed = self
.client
.get_received_by_address(&checked_addr, Some(self.min_confirmations as u32))?;
let received_sats = unconfirmed.to_sat();
let confirmations = if confirmed >= unconfirmed {
self.min_confirmations
} else if confirmed > Amount::ZERO {
self.min_confirmations / 2
} else {
0
};
let status = match received_sats.cmp(&order.expected_amount_sats) {
std::cmp::Ordering::Equal => MatchStatus::Exact,
std::cmp::Ordering::Less => MatchStatus::Underpaid,
std::cmp::Ordering::Greater => MatchStatus::Overpaid,
};
Ok(Some(MatchResult {
order_id: order.order_id,
txid: String::new(), received_amount_sats: received_sats,
expected_amount_sats: order.expected_amount_sats,
confirmations,
status,
}))
}
pub fn filter_confirmed<'a>(&self, matches: &'a [MatchResult]) -> Vec<&'a MatchResult> {
matches
.iter()
.filter(|m| m.confirmations >= self.min_confirmations)
.collect()
}
pub fn group_by_status(&self, matches: &[MatchResult]) -> MatchGroups {
let mut groups = MatchGroups::default();
for m in matches {
match m.status {
MatchStatus::Exact => groups.exact.push(m.clone()),
MatchStatus::Underpaid => groups.underpaid.push(m.clone()),
MatchStatus::Overpaid => groups.overpaid.push(m.clone()),
}
}
groups
}
}
#[derive(Debug, Default)]
pub struct MatchGroups {
pub exact: Vec<MatchResult>,
pub underpaid: Vec<MatchResult>,
pub overpaid: Vec<MatchResult>,
}
impl MatchGroups {
pub fn is_empty(&self) -> bool {
self.exact.is_empty() && self.underpaid.is_empty() && self.overpaid.is_empty()
}
pub fn total_count(&self) -> usize {
self.exact.len() + self.underpaid.len() + self.overpaid.len()
}
pub fn successful(&self) -> impl Iterator<Item = &MatchResult> {
self.exact.iter().chain(self.overpaid.iter())
}
}