1use bitcoin::Amount;
4use serde::Serialize;
5use std::sync::Arc;
6
7use crate::client::BitcoinClient;
8use crate::error::{BitcoinError, Result};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12pub enum PaymentStatus {
13 NotReceived,
15 Pending {
17 received_amount: Amount,
18 confirmations: u32,
19 },
20 Underpaid { expected: Amount, received: Amount },
22 Overpaid { expected: Amount, received: Amount },
24 Confirmed {
26 received_amount: Amount,
27 confirmations: u32,
28 },
29}
30
31impl PaymentStatus {
32 pub fn is_actionable(&self) -> bool {
34 matches!(
35 self,
36 PaymentStatus::Confirmed { .. } | PaymentStatus::Overpaid { .. }
37 )
38 }
39}
40
41pub 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 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 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 let confirmed = self
74 .client
75 .get_received_by_address(&checked_addr, Some(self.required_confirmations))?;
76
77 let estimated_confirmations = if confirmed >= unconfirmed {
79 self.required_confirmations
80 } else if confirmed > Amount::ZERO {
81 self.required_confirmations / 2 } else {
84 0
85 };
86
87 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 Ok(PaymentStatus::Pending {
103 received_amount: unconfirmed,
104 confirmations: estimated_confirmations,
105 })
106 } else if unconfirmed > Amount::ZERO {
107 Ok(PaymentStatus::Underpaid {
109 expected: expected_amount,
110 received: unconfirmed,
111 })
112 } else {
113 Ok(PaymentStatus::NotReceived)
114 }
115 }
116
117 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, })
131 }
132
133 pub fn required_confirmations(&self) -> u32 {
135 self.required_confirmations
136 }
137}
138
139#[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#[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#[derive(Debug, Clone)]
159pub enum PaymentEvent {
160 Confirmed {
162 order_id: uuid::Uuid,
163 amount: Amount,
164 confirmations: u32,
165 },
166 Pending {
168 order_id: uuid::Uuid,
169 amount: Amount,
170 confirmations: u32,
171 },
172 Underpaid {
174 order_id: uuid::Uuid,
175 expected: Amount,
176 received: Amount,
177 },
178 Overpaid {
180 order_id: uuid::Uuid,
181 expected: Amount,
182 received: Amount,
183 },
184 Expired { order_id: uuid::Uuid },
186}
187
188#[derive(Debug, Clone)]
190pub struct MonitorConfig {
191 pub poll_interval_secs: u64,
193 pub order_expiry_hours: u32,
195 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
209pub struct PaymentMonitorTask {
211 client: Arc<BitcoinClient>,
212 config: MonitorConfig,
213 event_sender: Option<tokio::sync::mpsc::Sender<PaymentEvent>>,
215 shutdown: tokio::sync::watch::Receiver<bool>,
217}
218
219impl PaymentMonitorTask {
220 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 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 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 if *self.shutdown.borrow() {
257 tracing::info!("Payment monitor shutting down");
258 break;
259 }
260
261 let orders = get_pending_orders().await;
263
264 if !orders.is_empty() {
265 tracing::debug!(count = orders.len(), "Checking pending orders");
266 }
267
268 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 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 async fn check_order(&self, order: &PendingOrder) -> Result<()> {
294 let monitor = PaymentMonitor::new(self.client.clone(), self.config.required_confirmations);
295
296 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 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
360pub fn create_shutdown_signal() -> (
362 tokio::sync::watch::Sender<bool>,
363 tokio::sync::watch::Receiver<bool>,
364) {
365 tokio::sync::watch::channel(false)
366}