use bitcoin::Amount;
use serde::Serialize;
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub enum ConfirmationLevel {
Unconfirmed,
OneConfirmation,
ThreeConfirmations,
SixConfirmations,
}
impl ConfirmationLevel {
pub fn from_count(confirmations: u32) -> Self {
match confirmations {
0 => Self::Unconfirmed,
1..=2 => Self::OneConfirmation,
3..=5 => Self::ThreeConfirmations,
_ => Self::SixConfirmations,
}
}
pub fn min_confirmations(&self) -> u32 {
match self {
Self::Unconfirmed => 0,
Self::OneConfirmation => 1,
Self::ThreeConfirmations => 3,
Self::SixConfirmations => 6,
}
}
pub fn meets_threshold(&self, required: ConfirmationLevel) -> bool {
*self >= required
}
pub fn description(&self) -> &'static str {
match self {
Self::Unconfirmed => "Unconfirmed (waiting for first block)",
Self::OneConfirmation => "1 confirmation (initial confirmation)",
Self::ThreeConfirmations => "3+ confirmations (reasonably secure)",
Self::SixConfirmations => "6+ confirmations (highly secure)",
}
}
}
#[derive(Debug, Clone)]
pub struct TrackedOrder {
pub order_id: Uuid,
pub address: String,
pub expected_amount: Amount,
pub txid: Option<String>,
pub confirmations: u32,
pub level: ConfirmationLevel,
pub previous_level: ConfirmationLevel,
pub received_amount: Option<Amount>,
}
impl TrackedOrder {
pub fn new(order_id: Uuid, address: String, expected_amount: Amount) -> Self {
Self {
order_id,
address,
expected_amount,
txid: None,
confirmations: 0,
level: ConfirmationLevel::Unconfirmed,
previous_level: ConfirmationLevel::Unconfirmed,
received_amount: None,
}
}
pub fn update_confirmations(&mut self, confirmations: u32) -> Option<ConfirmationEvent> {
self.confirmations = confirmations;
let new_level = ConfirmationLevel::from_count(confirmations);
if new_level != self.level {
let event = ConfirmationEvent {
order_id: self.order_id,
previous_level: self.level,
new_level,
confirmations,
txid: self.txid.clone(),
};
self.previous_level = self.level;
self.level = new_level;
Some(event)
} else {
None
}
}
pub fn is_confirmed(&self, required: ConfirmationLevel) -> bool {
self.level.meets_threshold(required)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfirmationEvent {
pub order_id: Uuid,
pub previous_level: ConfirmationLevel,
pub new_level: ConfirmationLevel,
pub confirmations: u32,
pub txid: Option<String>,
}
impl ConfirmationEvent {
pub fn notification_message(&self) -> String {
match self.new_level {
ConfirmationLevel::Unconfirmed => {
format!("Payment detected in mempool for order {}", self.order_id)
}
ConfirmationLevel::OneConfirmation => {
format!(
"Payment received first confirmation for order {} (tx: {})",
self.order_id,
self.txid.as_deref().unwrap_or("unknown")
)
}
ConfirmationLevel::ThreeConfirmations => {
format!(
"Payment reached 3 confirmations for order {} - reasonably secure",
self.order_id
)
}
ConfirmationLevel::SixConfirmations => {
format!(
"Payment fully confirmed for order {} - 6+ confirmations",
self.order_id
)
}
}
}
pub fn is_significant(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Serialize)]
pub enum NotificationType {
PaymentDetected,
FirstConfirmation,
ReasonablySecure,
FullyConfirmed,
}
impl From<ConfirmationLevel> for NotificationType {
fn from(level: ConfirmationLevel) -> Self {
match level {
ConfirmationLevel::Unconfirmed => Self::PaymentDetected,
ConfirmationLevel::OneConfirmation => Self::FirstConfirmation,
ConfirmationLevel::ThreeConfirmations => Self::ReasonablySecure,
ConfirmationLevel::SixConfirmations => Self::FullyConfirmed,
}
}
}
pub struct ConfirmationTracker {
orders: HashMap<Uuid, TrackedOrder>,
required_level: ConfirmationLevel,
}
impl ConfirmationTracker {
pub fn new(required_level: ConfirmationLevel) -> Self {
Self {
orders: HashMap::new(),
required_level,
}
}
pub fn with_defaults() -> Self {
Self::new(ConfirmationLevel::ThreeConfirmations)
}
pub fn track(&mut self, order_id: Uuid, address: String, expected_amount: Amount) {
let order = TrackedOrder::new(order_id, address, expected_amount);
self.orders.insert(order_id, order);
}
pub fn untrack(&mut self, order_id: Uuid) -> Option<TrackedOrder> {
self.orders.remove(&order_id)
}
pub fn update(
&mut self,
order_id: Uuid,
confirmations: u32,
txid: Option<String>,
received_amount: Option<Amount>,
) -> Option<ConfirmationEvent> {
if let Some(order) = self.orders.get_mut(&order_id) {
if let Some(txid) = txid {
order.txid = Some(txid);
}
if let Some(amount) = received_amount {
order.received_amount = Some(amount);
}
order.update_confirmations(confirmations)
} else {
None
}
}
pub fn get(&self, order_id: Uuid) -> Option<&TrackedOrder> {
self.orders.get(&order_id)
}
pub fn is_confirmed(&self, order_id: Uuid) -> bool {
self.orders
.get(&order_id)
.map(|o| o.is_confirmed(self.required_level))
.unwrap_or(false)
}
pub fn orders_at_level(&self, level: ConfirmationLevel) -> Vec<&TrackedOrder> {
self.orders.values().filter(|o| o.level == level).collect()
}
pub fn confirmed_orders(&self) -> Vec<&TrackedOrder> {
self.orders
.values()
.filter(|o| o.is_confirmed(self.required_level))
.collect()
}
pub fn pending_orders(&self) -> Vec<&TrackedOrder> {
self.orders
.values()
.filter(|o| !o.is_confirmed(self.required_level))
.collect()
}
pub fn stats(&self) -> ConfirmationStats {
let mut stats = ConfirmationStats::default();
for order in self.orders.values() {
match order.level {
ConfirmationLevel::Unconfirmed => stats.unconfirmed += 1,
ConfirmationLevel::OneConfirmation => stats.one_confirmation += 1,
ConfirmationLevel::ThreeConfirmations => stats.three_confirmations += 1,
ConfirmationLevel::SixConfirmations => stats.six_confirmations += 1,
}
}
stats.total = self.orders.len();
stats
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ConfirmationStats {
pub total: usize,
pub unconfirmed: usize,
pub one_confirmation: usize,
pub three_confirmations: usize,
pub six_confirmations: usize,
}
impl Default for ConfirmationTracker {
fn default() -> Self {
Self::with_defaults()
}
}