kaccy_bitcoin/
monitor.rs

1//! Transaction monitoring
2
3use bitcoin::Amount;
4use serde::Serialize;
5use std::sync::Arc;
6
7use crate::client::BitcoinClient;
8use crate::error::{BitcoinError, Result};
9
10/// Payment status with details
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12pub enum PaymentStatus {
13    /// No payment received yet
14    NotReceived,
15    /// Payment received but waiting for confirmations
16    Pending {
17        received_amount: Amount,
18        confirmations: u32,
19    },
20    /// Payment received but amount is incorrect
21    Underpaid { expected: Amount, received: Amount },
22    /// Payment received but amount is more than expected
23    Overpaid { expected: Amount, received: Amount },
24    /// Payment confirmed with required confirmations
25    Confirmed {
26        received_amount: Amount,
27        confirmations: u32,
28    },
29}
30
31impl PaymentStatus {
32    /// Check if payment can proceed (confirmed or overpaid with enough confirmations)
33    pub fn is_actionable(&self) -> bool {
34        matches!(
35            self,
36            PaymentStatus::Confirmed { .. } | PaymentStatus::Overpaid { .. }
37        )
38    }
39}
40
41/// Monitors payments for pending orders
42pub struct PaymentMonitor {
43    client: Arc<BitcoinClient>,
44    required_confirmations: u32,
45}
46
47impl PaymentMonitor {
48    pub fn new(client: Arc<BitcoinClient>, required_confirmations: u32) -> Self {
49        Self {
50            client,
51            required_confirmations,
52        }
53    }
54
55    /// Check payment status for an address
56    pub fn check_payment(&self, address: &str, expected_amount: Amount) -> Result<PaymentStatus> {
57        let addr: bitcoin::Address<bitcoin::address::NetworkUnchecked> = address
58            .parse()
59            .map_err(|e| BitcoinError::InvalidAddress(format!("{:?}", e)))?;
60
61        let checked_addr = addr.assume_checked();
62
63        // Check with 0 confirmations first (mempool)
64        let unconfirmed = self
65            .client
66            .get_received_by_address(&checked_addr, Some(0))?;
67
68        if unconfirmed == Amount::ZERO {
69            return Ok(PaymentStatus::NotReceived);
70        }
71
72        // Check with required confirmations
73        let confirmed = self
74            .client
75            .get_received_by_address(&checked_addr, Some(self.required_confirmations))?;
76
77        // Estimate confirmations based on difference
78        let estimated_confirmations = if confirmed >= unconfirmed {
79            self.required_confirmations
80        } else if confirmed > Amount::ZERO {
81            // Some confirmations but not enough
82            self.required_confirmations / 2 // Rough estimate
83        } else {
84            0
85        };
86
87        // Determine status based on amount and confirmations
88        if confirmed >= expected_amount {
89            if confirmed > expected_amount {
90                Ok(PaymentStatus::Overpaid {
91                    expected: expected_amount,
92                    received: confirmed,
93                })
94            } else {
95                Ok(PaymentStatus::Confirmed {
96                    received_amount: confirmed,
97                    confirmations: self.required_confirmations,
98                })
99            }
100        } else if unconfirmed >= expected_amount {
101            // Received full amount but waiting for confirmations
102            Ok(PaymentStatus::Pending {
103                received_amount: unconfirmed,
104                confirmations: estimated_confirmations,
105            })
106        } else if unconfirmed > Amount::ZERO {
107            // Received some but not enough
108            Ok(PaymentStatus::Underpaid {
109                expected: expected_amount,
110                received: unconfirmed,
111            })
112        } else {
113            Ok(PaymentStatus::NotReceived)
114        }
115    }
116
117    /// Check payment and get detailed info including transaction ID
118    pub fn check_payment_detailed(
119        &self,
120        address: &str,
121        expected_amount: Amount,
122    ) -> Result<PaymentDetails> {
123        let status = self.check_payment(address, expected_amount)?;
124
125        Ok(PaymentDetails {
126            address: address.to_string(),
127            expected_amount,
128            status,
129            txid: None, // Would need additional lookup for txid
130        })
131    }
132
133    /// Get required confirmations setting
134    pub fn required_confirmations(&self) -> u32 {
135        self.required_confirmations
136    }
137}
138
139/// Detailed payment information
140#[derive(Debug, Clone, Serialize)]
141pub struct PaymentDetails {
142    pub address: String,
143    pub expected_amount: Amount,
144    pub status: PaymentStatus,
145    pub txid: Option<String>,
146}
147
148/// Pending order for monitoring
149#[derive(Debug, Clone)]
150pub struct PendingOrder {
151    pub order_id: uuid::Uuid,
152    pub address: String,
153    pub expected_amount: Amount,
154    pub created_at: chrono::DateTime<chrono::Utc>,
155}
156
157/// Payment event emitted by the monitor
158#[derive(Debug, Clone)]
159pub enum PaymentEvent {
160    /// Payment received and confirmed
161    Confirmed {
162        order_id: uuid::Uuid,
163        amount: Amount,
164        confirmations: u32,
165    },
166    /// Payment pending (in mempool or insufficient confirmations)
167    Pending {
168        order_id: uuid::Uuid,
169        amount: Amount,
170        confirmations: u32,
171    },
172    /// Underpayment detected
173    Underpaid {
174        order_id: uuid::Uuid,
175        expected: Amount,
176        received: Amount,
177    },
178    /// Overpayment detected
179    Overpaid {
180        order_id: uuid::Uuid,
181        expected: Amount,
182        received: Amount,
183    },
184    /// Order expired without payment
185    Expired { order_id: uuid::Uuid },
186}
187
188/// Configuration for payment monitor background task
189#[derive(Debug, Clone)]
190pub struct MonitorConfig {
191    /// Polling interval in seconds
192    pub poll_interval_secs: u64,
193    /// Order expiry time in hours
194    pub order_expiry_hours: u32,
195    /// Required confirmations for payment to be confirmed
196    pub required_confirmations: u32,
197}
198
199impl Default for MonitorConfig {
200    fn default() -> Self {
201        Self {
202            poll_interval_secs: 30,
203            order_expiry_hours: 24,
204            required_confirmations: 3,
205        }
206    }
207}
208
209/// Background payment monitoring task
210pub struct PaymentMonitorTask {
211    client: Arc<BitcoinClient>,
212    config: MonitorConfig,
213    /// Channel to send payment events
214    event_sender: Option<tokio::sync::mpsc::Sender<PaymentEvent>>,
215    /// Shutdown signal
216    shutdown: tokio::sync::watch::Receiver<bool>,
217}
218
219impl PaymentMonitorTask {
220    /// Create a new payment monitor task
221    pub fn new(
222        client: Arc<BitcoinClient>,
223        config: MonitorConfig,
224        shutdown: tokio::sync::watch::Receiver<bool>,
225    ) -> Self {
226        Self {
227            client,
228            config,
229            event_sender: None,
230            shutdown,
231        }
232    }
233
234    /// Set the event sender channel
235    pub fn with_event_sender(mut self, sender: tokio::sync::mpsc::Sender<PaymentEvent>) -> Self {
236        self.event_sender = Some(sender);
237        self
238    }
239
240    /// Start the monitoring loop (call in a spawned task)
241    pub async fn run<F, Fut>(&mut self, get_pending_orders: F)
242    where
243        F: Fn() -> Fut,
244        Fut: std::future::Future<Output = Vec<PendingOrder>>,
245    {
246        let poll_interval = std::time::Duration::from_secs(self.config.poll_interval_secs);
247
248        tracing::info!(
249            poll_interval_secs = self.config.poll_interval_secs,
250            required_confirmations = self.config.required_confirmations,
251            "Payment monitor started"
252        );
253
254        loop {
255            // Check shutdown signal
256            if *self.shutdown.borrow() {
257                tracing::info!("Payment monitor shutting down");
258                break;
259            }
260
261            // Get pending orders
262            let orders = get_pending_orders().await;
263
264            if !orders.is_empty() {
265                tracing::debug!(count = orders.len(), "Checking pending orders");
266            }
267
268            // Check each order
269            for order in orders {
270                if let Err(e) = self.check_order(&order).await {
271                    tracing::warn!(
272                        order_id = %order.order_id,
273                        error = %e,
274                        "Error checking payment"
275                    );
276                }
277            }
278
279            // Wait for next poll interval or shutdown
280            tokio::select! {
281                _ = tokio::time::sleep(poll_interval) => {}
282                _ = self.shutdown.changed() => {
283                    if *self.shutdown.borrow() {
284                        tracing::info!("Payment monitor received shutdown signal");
285                        break;
286                    }
287                }
288            }
289        }
290    }
291
292    /// Check payment status for a single order
293    async fn check_order(&self, order: &PendingOrder) -> Result<()> {
294        let monitor = PaymentMonitor::new(self.client.clone(), self.config.required_confirmations);
295
296        // Check for expiry first
297        let age = chrono::Utc::now() - order.created_at;
298        if age.num_hours() >= self.config.order_expiry_hours as i64 {
299            self.emit_event(PaymentEvent::Expired {
300                order_id: order.order_id,
301            })
302            .await;
303            return Ok(());
304        }
305
306        let status = monitor.check_payment(&order.address, order.expected_amount)?;
307
308        let event = match status {
309            PaymentStatus::Confirmed {
310                received_amount,
311                confirmations,
312            } => Some(PaymentEvent::Confirmed {
313                order_id: order.order_id,
314                amount: received_amount,
315                confirmations,
316            }),
317            PaymentStatus::Pending {
318                received_amount,
319                confirmations,
320            } => Some(PaymentEvent::Pending {
321                order_id: order.order_id,
322                amount: received_amount,
323                confirmations,
324            }),
325            PaymentStatus::Underpaid { expected, received } => Some(PaymentEvent::Underpaid {
326                order_id: order.order_id,
327                expected,
328                received,
329            }),
330            PaymentStatus::Overpaid { expected, received } => Some(PaymentEvent::Overpaid {
331                order_id: order.order_id,
332                expected,
333                received,
334            }),
335            PaymentStatus::NotReceived => None,
336        };
337
338        if let Some(event) = event {
339            self.emit_event(event).await;
340        }
341
342        Ok(())
343    }
344
345    /// Emit a payment event
346    async fn emit_event(&self, event: PaymentEvent) {
347        tracing::info!(
348            event = ?event,
349            "Payment event"
350        );
351
352        if let Some(ref sender) = self.event_sender {
353            if let Err(e) = sender.send(event.clone()).await {
354                tracing::error!(error = %e, "Failed to send payment event");
355            }
356        }
357    }
358}
359
360/// Create a shutdown signal pair for the monitor
361pub fn create_shutdown_signal() -> (
362    tokio::sync::watch::Sender<bool>,
363    tokio::sync::watch::Receiver<bool>,
364) {
365    tokio::sync::watch::channel(false)
366}