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