1use std::sync::Arc;
12
13use bsv::auth::utils::create_nonce;
14use bsv::primitives::public_key::PublicKey;
15use bsv::remittance::types::PeerMessage;
16use bsv::wallet::interfaces::{CreateHmacArgs, VerifyHmacArgs, WalletInterface};
17use bsv::wallet::types::{Counterparty, CounterpartyType, Protocol};
18
19use crate::client::MessageBoxClient;
20use crate::error::MessageBoxError;
21use crate::types::{
22 IncomingPaymentRequest, PaymentRequestLimits, PaymentRequestMessage, PaymentRequestResponse,
23 PaymentRequestResult, PAYMENT_REQUESTS_MESSAGEBOX, PAYMENT_REQUEST_RESPONSES_MESSAGEBOX,
24};
25
26fn payment_request_auth_protocol() -> Protocol {
28 Protocol {
29 security_level: 2,
30 protocol: "payment request auth".to_string(),
31 }
32}
33
34fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, MessageBoxError> {
36 hex::decode(hex).map_err(|e| MessageBoxError::Auth(format!("hex decode: {e}")))
37}
38
39fn bytes_to_hex(bytes: &[u8]) -> String {
41 hex::encode(bytes)
42}
43
44impl<W: WalletInterface + Clone + 'static + Send + Sync> MessageBoxClient<W> {
45 pub async fn request_payment(
57 &self,
58 recipient: &str,
59 amount: u64,
60 description: &str,
61 expires_at: u64,
62 ) -> Result<PaymentRequestResult, MessageBoxError> {
63 if amount == 0 {
64 return Err(MessageBoxError::Validation(
65 "Payment request amount must be greater than 0".to_string(),
66 ));
67 }
68
69 let request_id = create_nonce(self.wallet())
71 .await
72 .map_err(|e| MessageBoxError::Auth(format!("create_nonce request_id: {e}")))?;
73
74 let sender_identity_key = self.get_identity_key().await?;
75
76 let proof_data = format!("{}{}", request_id, recipient);
78 let hmac_result = self
79 .wallet()
80 .create_hmac(
81 CreateHmacArgs {
82 data: proof_data.as_bytes().to_vec(),
83 protocol_id: payment_request_auth_protocol(),
84 key_id: request_id.clone(),
85 counterparty: Counterparty {
86 counterparty_type: CounterpartyType::Other,
87 public_key: Some(PublicKey::from_string(recipient).map_err(|e| {
88 MessageBoxError::Auth(format!("invalid recipient key: {e}"))
89 })?),
90 },
91 privileged: false,
92 privileged_reason: None,
93 seek_permission: None,
94 },
95 self.originator(),
96 )
97 .await
98 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
99
100 let request_proof = bytes_to_hex(&hmac_result.hmac);
101
102 let message = PaymentRequestMessage {
103 request_id: request_id.clone(),
104 sender_identity_key,
105 request_proof: request_proof.clone(),
106 amount: Some(amount),
107 description: Some(description.to_string()),
108 expires_at: Some(expires_at),
109 cancelled: None,
110 };
111
112 let body = serde_json::to_string(&message)?;
113
114 match self
115 .send_message(
116 recipient,
117 PAYMENT_REQUESTS_MESSAGEBOX,
118 &body,
119 false,
120 false,
121 None,
122 None,
123 )
124 .await
125 {
126 Ok(_) => {}
127 Err(MessageBoxError::Http(403, _)) => {
128 return Err(MessageBoxError::Validation(
129 "Payment request blocked — you are not on the recipient's whitelist."
130 .to_string(),
131 ));
132 }
133 Err(e) => return Err(e),
134 }
135
136 Ok(PaymentRequestResult {
137 request_id,
138 request_proof,
139 })
140 }
141
142 pub async fn cancel_payment_request(
150 &self,
151 recipient: &str,
152 request_id: &str,
153 request_proof: &str,
154 host_override: Option<&str>,
155 ) -> Result<(), MessageBoxError> {
156 let sender_identity_key = self.get_identity_key().await?;
157
158 let message = PaymentRequestMessage {
159 request_id: request_id.to_string(),
160 sender_identity_key,
161 request_proof: request_proof.to_string(),
162 amount: None,
163 description: None,
164 expires_at: None,
165 cancelled: Some(true),
166 };
167
168 let body = serde_json::to_string(&message)?;
169 self.send_message(
170 recipient,
171 PAYMENT_REQUESTS_MESSAGEBOX,
172 &body,
173 false,
174 false,
175 None,
176 host_override,
177 )
178 .await?;
179
180 Ok(())
181 }
182
183 pub async fn list_incoming_payment_requests(
197 &self,
198 host_override: Option<&str>,
199 limits: Option<PaymentRequestLimits>,
200 ) -> Result<Vec<IncomingPaymentRequest>, MessageBoxError> {
201 let limits = limits.unwrap_or_default();
202 let my_identity_key = self.get_identity_key().await?;
203
204 let messages = self
205 .list_messages(PAYMENT_REQUESTS_MESSAGEBOX, false, host_override)
206 .await?;
207
208 let mut parsed: Vec<(PeerMessage, PaymentRequestMessage)> = Vec::new();
210 let mut malformed_ids: Vec<String> = Vec::new();
211
212 for msg in &messages {
213 match serde_json::from_str::<PaymentRequestMessage>(&msg.body) {
214 Ok(body) if is_valid_payment_request(&body) => {
215 parsed.push((msg.clone(), body));
216 }
217 _ => {
218 malformed_ids.push(msg.message_id.clone());
219 }
220 }
221 }
222
223 let mut cancelled_requests: std::collections::HashMap<String, String> =
225 std::collections::HashMap::new();
226 let mut cancellation_ids: Vec<String> = Vec::new();
227
228 for (msg, body) in &parsed {
229 if body.cancelled == Some(true) {
230 let proof_data = format!("{}{}", body.request_id, my_identity_key);
232 let proof_bytes = match hex_to_bytes(&body.request_proof) {
233 Ok(b) => b,
234 Err(_) => {
235 malformed_ids.push(msg.message_id.clone());
236 continue;
237 }
238 };
239
240 let verify_result = self
241 .wallet()
242 .verify_hmac(
243 VerifyHmacArgs {
244 data: proof_data.as_bytes().to_vec(),
245 hmac: proof_bytes,
246 protocol_id: payment_request_auth_protocol(),
247 key_id: body.request_id.clone(),
248 counterparty: Counterparty {
249 counterparty_type: CounterpartyType::Other,
250 public_key: PublicKey::from_string(&msg.sender).ok(),
251 },
252 privileged: false,
253 privileged_reason: None,
254 seek_permission: None,
255 },
256 self.originator(),
257 )
258 .await;
259
260 match verify_result {
261 Ok(r) if r.valid => {
262 cancelled_requests.insert(body.request_id.clone(), msg.sender.clone());
263 cancellation_ids.push(msg.message_id.clone());
264 }
265 _ => {
266 malformed_ids.push(msg.message_id.clone());
267 }
268 }
269 }
270 }
271
272 let now_ms = std::time::SystemTime::now()
274 .duration_since(std::time::UNIX_EPOCH)
275 .unwrap_or_default()
276 .as_millis() as u64;
277
278 let mut result: Vec<IncomingPaymentRequest> = Vec::new();
279 let mut expired_ids: Vec<String> = Vec::new();
280 let mut cancelled_original_ids: Vec<String> = Vec::new();
281 let mut out_of_range_ids: Vec<String> = Vec::new();
282
283 for (msg, body) in &parsed {
284 if body.cancelled == Some(true) {
286 continue;
287 }
288
289 let amount = match body.amount {
290 Some(a) => a,
291 None => {
292 malformed_ids.push(msg.message_id.clone());
293 continue;
294 }
295 };
296 let description = match &body.description {
297 Some(d) => d.clone(),
298 None => {
299 malformed_ids.push(msg.message_id.clone());
300 continue;
301 }
302 };
303 let expires_at = match body.expires_at {
304 Some(e) => e,
305 None => {
306 malformed_ids.push(msg.message_id.clone());
307 continue;
308 }
309 };
310
311 if expires_at < now_ms {
313 expired_ids.push(msg.message_id.clone());
314 continue;
315 }
316
317 if let Some(cancel_sender) = cancelled_requests.get(&body.request_id) {
319 if cancel_sender == &msg.sender {
320 cancelled_original_ids.push(msg.message_id.clone());
321 continue;
322 }
323 }
324
325 if amount < limits.min_amount || amount > limits.max_amount {
327 out_of_range_ids.push(msg.message_id.clone());
328 continue;
329 }
330
331 let proof_data = format!("{}{}", body.request_id, my_identity_key);
333 let proof_bytes = match hex_to_bytes(&body.request_proof) {
334 Ok(b) => b,
335 Err(_) => {
336 malformed_ids.push(msg.message_id.clone());
337 continue;
338 }
339 };
340
341 let verify_result = self
342 .wallet()
343 .verify_hmac(
344 VerifyHmacArgs {
345 data: proof_data.as_bytes().to_vec(),
346 hmac: proof_bytes,
347 protocol_id: payment_request_auth_protocol(),
348 key_id: body.request_id.clone(),
349 counterparty: Counterparty {
350 counterparty_type: CounterpartyType::Other,
351 public_key: PublicKey::from_string(&msg.sender).ok(),
352 },
353 privileged: false,
354 privileged_reason: None,
355 seek_permission: None,
356 },
357 self.originator(),
358 )
359 .await;
360
361 match verify_result {
362 Ok(r) if r.valid => {
363 result.push(IncomingPaymentRequest {
364 message_id: msg.message_id.clone(),
365 sender: msg.sender.clone(),
366 request_id: body.request_id.clone(),
367 amount,
368 description,
369 expires_at,
370 });
371 }
372 _ => {
373 malformed_ids.push(msg.message_id.clone());
374 }
375 }
376 }
377
378 let mut ack_ids: Vec<String> = Vec::new();
380 ack_ids.extend(expired_ids);
381 ack_ids.extend(cancelled_original_ids);
382 ack_ids.extend(cancellation_ids);
383 ack_ids.extend(out_of_range_ids);
384 ack_ids.extend(malformed_ids);
385
386 if !ack_ids.is_empty() {
387 let _ = self.acknowledge_message(ack_ids, host_override).await;
389 }
390
391 Ok(result)
392 }
393
394 pub async fn fulfill_payment_request(
402 &self,
403 request: &IncomingPaymentRequest,
404 note: Option<&str>,
405 host_override: Option<&str>,
406 ) -> Result<(), MessageBoxError> {
407 self.send_payment(&request.sender, request.amount).await?;
409
410 let mut response = PaymentRequestResponse {
412 request_id: request.request_id.clone(),
413 status: "paid".to_string(),
414 amount_paid: Some(request.amount),
415 note: None,
416 };
417 if let Some(n) = note {
418 response.note = Some(n.to_string());
419 }
420
421 let body = serde_json::to_string(&response)?;
422 self.send_message(
423 &request.sender,
424 PAYMENT_REQUEST_RESPONSES_MESSAGEBOX,
425 &body,
426 false,
427 false,
428 None,
429 host_override,
430 )
431 .await?;
432
433 self.acknowledge_message(vec![request.message_id.clone()], host_override)
435 .await?;
436
437 Ok(())
438 }
439
440 pub async fn decline_payment_request(
444 &self,
445 request: &IncomingPaymentRequest,
446 note: Option<&str>,
447 host_override: Option<&str>,
448 ) -> Result<(), MessageBoxError> {
449 let mut response = PaymentRequestResponse {
450 request_id: request.request_id.clone(),
451 status: "declined".to_string(),
452 amount_paid: None,
453 note: None,
454 };
455 if let Some(n) = note {
456 response.note = Some(n.to_string());
457 }
458
459 let body = serde_json::to_string(&response)?;
460 self.send_message(
461 &request.sender,
462 PAYMENT_REQUEST_RESPONSES_MESSAGEBOX,
463 &body,
464 false,
465 false,
466 None,
467 host_override,
468 )
469 .await?;
470
471 self.acknowledge_message(vec![request.message_id.clone()], host_override)
473 .await?;
474
475 Ok(())
476 }
477
478 pub async fn listen_for_live_payment_requests(
485 &self,
486 on_request: Arc<dyn Fn(IncomingPaymentRequest) + Send + Sync>,
487 override_host: Option<&str>,
488 ) -> Result<(), MessageBoxError> {
489 let wrapper: Arc<dyn Fn(PeerMessage) + Send + Sync> = Arc::new(move |msg: PeerMessage| {
490 if let Ok(body) = serde_json::from_str::<PaymentRequestMessage>(&msg.body) {
491 if body.cancelled == Some(true) {
493 return;
494 }
495 if let (Some(amount), Some(description), Some(expires_at)) =
497 (body.amount, body.description.clone(), body.expires_at)
498 {
499 on_request(IncomingPaymentRequest {
500 message_id: msg.message_id,
501 sender: msg.sender,
502 request_id: body.request_id,
503 amount,
504 description,
505 expires_at,
506 });
507 }
508 }
509 });
510
511 self.listen_for_live_messages(PAYMENT_REQUESTS_MESSAGEBOX, wrapper, override_host)
512 .await
513 }
514
515 pub async fn list_payment_request_responses(
522 &self,
523 host_override: Option<&str>,
524 ) -> Result<Vec<PaymentRequestResponse>, MessageBoxError> {
525 let messages = self
526 .list_messages(PAYMENT_REQUEST_RESPONSES_MESSAGEBOX, false, host_override)
527 .await?;
528
529 let responses = messages
530 .into_iter()
531 .filter_map(|msg| serde_json::from_str::<PaymentRequestResponse>(&msg.body).ok())
532 .collect();
533
534 Ok(responses)
535 }
536
537 pub async fn listen_for_live_payment_request_responses(
544 &self,
545 on_response: Arc<dyn Fn(PaymentRequestResponse) + Send + Sync>,
546 override_host: Option<&str>,
547 ) -> Result<(), MessageBoxError> {
548 let wrapper: Arc<dyn Fn(PeerMessage) + Send + Sync> = Arc::new(move |msg: PeerMessage| {
549 if let Ok(response) = serde_json::from_str::<PaymentRequestResponse>(&msg.body) {
550 on_response(response);
551 }
552 });
553
554 self.listen_for_live_messages(PAYMENT_REQUEST_RESPONSES_MESSAGEBOX, wrapper, override_host)
555 .await
556 }
557
558 pub async fn allow_payment_requests_from(
568 &self,
569 identity_key: &str,
570 host_override: Option<&str>,
571 ) -> Result<(), MessageBoxError> {
572 self.set_message_box_permission(
573 crate::types::SetPermissionParams {
574 message_box: PAYMENT_REQUESTS_MESSAGEBOX.to_string(),
575 sender: Some(identity_key.to_string()),
576 recipient_fee: 0,
577 },
578 host_override,
579 )
580 .await
581 }
582
583 pub async fn block_payment_requests_from(
589 &self,
590 identity_key: &str,
591 host_override: Option<&str>,
592 ) -> Result<(), MessageBoxError> {
593 self.set_message_box_permission(
594 crate::types::SetPermissionParams {
595 message_box: PAYMENT_REQUESTS_MESSAGEBOX.to_string(),
596 sender: Some(identity_key.to_string()),
597 recipient_fee: -1,
598 },
599 host_override,
600 )
601 .await
602 }
603
604 pub async fn list_payment_request_permissions(
611 &self,
612 host_override: Option<&str>,
613 ) -> Result<Vec<(String, bool)>, MessageBoxError> {
614 let permissions = self
615 .list_message_box_permissions(
616 Some(PAYMENT_REQUESTS_MESSAGEBOX),
617 None,
618 None,
619 host_override,
620 )
621 .await?;
622
623 let result = permissions
624 .into_iter()
625 .filter(|p| p.sender.is_some() && !p.sender.as_ref().unwrap().is_empty())
626 .map(|p| {
627 let allowed = p.status() != "blocked";
628 (p.sender.unwrap_or_default(), allowed)
629 })
630 .collect();
631
632 Ok(result)
633 }
634}
635
636fn is_valid_payment_request(msg: &PaymentRequestMessage) -> bool {
642 if msg.request_id.is_empty()
643 || msg.sender_identity_key.is_empty()
644 || msg.request_proof.is_empty()
645 {
646 return false;
647 }
648
649 if msg.cancelled == Some(true) {
650 return true;
651 }
652
653 msg.amount.is_some() && msg.description.is_some() && msg.expires_at.is_some()
654}
655
656#[cfg(test)]
661mod tests {
662 use super::*;
663
664 #[test]
665 fn payment_request_message_serializes_camel_case() {
666 let msg = PaymentRequestMessage {
667 request_id: "req-123".to_string(),
668 sender_identity_key: "03abc".to_string(),
669 request_proof: "deadbeef".to_string(),
670 amount: Some(5000),
671 description: Some("test payment".to_string()),
672 expires_at: Some(1700000000000),
673 cancelled: None,
674 };
675 let json = serde_json::to_string(&msg).unwrap();
676 assert!(json.contains("\"requestId\""));
677 assert!(json.contains("\"senderIdentityKey\""));
678 assert!(json.contains("\"requestProof\""));
679 assert!(json.contains("\"expiresAt\""));
680 assert!(!json.contains("request_id"));
681 assert!(!json.contains("sender_identity_key"));
682 assert!(!json.contains("cancelled"));
683 }
684
685 #[test]
686 fn cancellation_message_serializes_correctly() {
687 let msg = PaymentRequestMessage {
688 request_id: "req-456".to_string(),
689 sender_identity_key: "03def".to_string(),
690 request_proof: "cafebabe".to_string(),
691 amount: None,
692 description: None,
693 expires_at: None,
694 cancelled: Some(true),
695 };
696 let json = serde_json::to_string(&msg).unwrap();
697 assert!(json.contains("\"cancelled\":true"));
698 assert!(!json.contains("amount"));
699 assert!(!json.contains("description"));
700 assert!(!json.contains("expiresAt"));
701 }
702
703 #[test]
704 fn payment_request_response_round_trip() {
705 let resp = PaymentRequestResponse {
706 request_id: "req-789".to_string(),
707 status: "paid".to_string(),
708 note: Some("done".to_string()),
709 amount_paid: Some(5000),
710 };
711 let json = serde_json::to_string(&resp).unwrap();
712 let back: PaymentRequestResponse = serde_json::from_str(&json).unwrap();
713 assert_eq!(back.request_id, "req-789");
714 assert_eq!(back.status, "paid");
715 assert_eq!(back.amount_paid, Some(5000));
716 assert_eq!(back.note, Some("done".to_string()));
717 }
718
719 #[test]
720 fn declined_response_omits_amount_paid() {
721 let resp = PaymentRequestResponse {
722 request_id: "req-abc".to_string(),
723 status: "declined".to_string(),
724 note: None,
725 amount_paid: None,
726 };
727 let json = serde_json::to_string(&resp).unwrap();
728 assert!(!json.contains("amountPaid"));
729 assert!(!json.contains("note"));
730 }
731
732 #[test]
733 fn is_valid_payment_request_validates_correctly() {
734 let valid = PaymentRequestMessage {
736 request_id: "r1".to_string(),
737 sender_identity_key: "03abc".to_string(),
738 request_proof: "proof".to_string(),
739 amount: Some(1000),
740 description: Some("test".to_string()),
741 expires_at: Some(999999),
742 cancelled: None,
743 };
744 assert!(is_valid_payment_request(&valid));
745
746 let cancel = PaymentRequestMessage {
748 request_id: "r2".to_string(),
749 sender_identity_key: "03def".to_string(),
750 request_proof: "proof".to_string(),
751 amount: None,
752 description: None,
753 expires_at: None,
754 cancelled: Some(true),
755 };
756 assert!(is_valid_payment_request(&cancel));
757
758 let bad = PaymentRequestMessage {
760 request_id: "".to_string(),
761 sender_identity_key: "03abc".to_string(),
762 request_proof: "proof".to_string(),
763 amount: Some(1000),
764 description: Some("test".to_string()),
765 expires_at: Some(999999),
766 cancelled: None,
767 };
768 assert!(!is_valid_payment_request(&bad));
769
770 let bad2 = PaymentRequestMessage {
772 request_id: "r3".to_string(),
773 sender_identity_key: "03abc".to_string(),
774 request_proof: "proof".to_string(),
775 amount: None,
776 description: Some("test".to_string()),
777 expires_at: Some(999999),
778 cancelled: None,
779 };
780 assert!(!is_valid_payment_request(&bad2));
781 }
782
783 #[test]
784 fn payment_request_limits_default() {
785 let limits = PaymentRequestLimits::default();
786 assert_eq!(limits.min_amount, 1000);
787 assert_eq!(limits.max_amount, 10_000_000);
788 }
789}