1use std::collections::{HashMap, HashSet};
2
3use bsv::primitives::public_key::PublicKey;
4use bsv::remittance::types::PeerMessage;
5use bsv::wallet::interfaces::{InternalizeActionArgs, InternalizeOutput, Payment, WalletInterface};
6use bsv::wallet::types::BooleanDefaultTrue;
7use futures_util::future::join_all;
8
9use crate::client::MessageBoxClient;
10use crate::client::{check_status_error, is_duplicate_message_rejection};
11use crate::encryption;
12use crate::error::MessageBoxError;
13use crate::types::{
14 AcknowledgeMessageParams, FailedRecipient, ListMessagesParams, ListMessagesResponse,
15 MessagePayment, MessagePaymentOutput, SendListParams, SendListResult, SendMessageParams,
16 SendMessageRequest, SendMessageResponse, SentRecipient, ServerPeerMessage,
17};
18
19pub(crate) fn dedup_messages(results: Vec<Vec<PeerMessage>>) -> Vec<PeerMessage> {
25 let mut seen = HashSet::new();
26 let mut out = Vec::new();
27 for host_messages in results {
28 for msg in host_messages {
29 if seen.insert(msg.message_id.clone()) {
30 out.push(msg);
31 }
32 }
33 }
34 out
35}
36
37#[derive(serde::Deserialize)]
41pub(crate) struct WrappedMessageBody {
42 pub message: Option<serde_json::Value>,
43 pub payment: Option<ServerPayment>,
44}
45
46#[derive(serde::Deserialize)]
47pub(crate) struct ServerPayment {
48 pub tx: Option<Vec<u8>>,
49 pub outputs: Option<Vec<ServerPaymentOutput>>,
50 pub description: Option<String>,
51}
52
53#[derive(serde::Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub(crate) struct ServerPaymentOutput {
58 pub output_index: Option<u32>,
59 pub protocol: Option<String>,
61 pub derivation_prefix: Option<Vec<u8>>,
63 pub derivation_suffix: Option<Vec<u8>>,
65 pub sender_identity_key: Option<String>,
67}
68
69impl<W: WalletInterface + Clone + 'static + Send + Sync> MessageBoxClient<W> {
70 #[allow(clippy::too_many_arguments)]
82 pub async fn send_message(
83 &self,
84 recipient: &str,
85 message_box: &str,
86 body: &str,
87 skip_encryption: bool,
88 check_permissions: bool,
89 message_id: Option<&str>,
90 override_host: Option<&str>,
91 ) -> Result<String, MessageBoxError> {
92 self.assert_initialized().await?;
93 let host = match override_host {
94 Some(h) => h.to_string(),
95 None => self.resolve_host_for_recipient(recipient).await?,
96 };
97 self.send_message_to_host(
98 &host,
99 recipient,
100 message_box,
101 body,
102 skip_encryption,
103 check_permissions,
104 message_id,
105 None,
106 )
107 .await
108 }
109
110 #[allow(clippy::too_many_arguments)]
123 pub(crate) async fn send_message_to_host(
124 &self,
125 host: &str,
126 recipient: &str,
127 message_box: &str,
128 body: &str,
129 skip_encryption: bool,
130 check_permissions: bool,
131 message_id: Option<&str>,
132 payment: Option<MessagePayment>,
133 ) -> Result<String, MessageBoxError> {
134 let wire_body = if skip_encryption {
136 body.to_string()
137 } else {
138 encryption::encrypt_body(self.wallet(), body, recipient, self.originator()).await?
139 };
140
141 let resolved_message_id = if let Some(id) = message_id {
145 id.to_string()
146 } else {
147 encryption::generate_message_id(self.wallet(), body, recipient, self.originator())
148 .await?
149 };
150
151 let payment = if check_permissions && payment.is_none() {
154 let quote = self
155 .get_message_box_quote(recipient, message_box, None)
156 .await?;
157 if quote.delivery_fee > 0 || quote.recipient_fee > 0 {
158 let p = self.create_message_payment(recipient, "e, None).await?;
159 Some(p)
160 } else {
161 None
162 }
163 } else {
164 payment
165 };
166
167 let request = SendMessageRequest {
169 message: SendMessageParams {
170 recipient: recipient.to_string(),
171 message_box: message_box.to_string(),
172 body: wire_body,
173 message_id: resolved_message_id.clone(),
174 },
175 payment,
176 };
177
178 let body_bytes = serde_json::to_vec(&request)?;
179 let url = format!("{host}/sendMessage");
180 let response = self.post_json_raw(&url, body_bytes).await?;
185
186 if is_duplicate_message_rejection(&response.body) {
199 return Ok(resolved_message_id);
200 }
201
202 if response.status < 200 || response.status >= 300 {
205 return Err(MessageBoxError::Http(response.status, url));
206 }
207
208 check_status_error(&response.body)?;
210
211 if let Ok(resp) = serde_json::from_slice::<SendMessageResponse>(&response.body) {
213 if let Some(server_id) = resp.message_id {
214 return Ok(server_id);
215 }
216 }
217 Ok(resolved_message_id)
218 }
219
220 async fn create_message_payment(
230 &self,
231 recipient: &str,
232 quote: &crate::types::MessageBoxQuote,
233 description: Option<&str>,
234 ) -> Result<MessagePayment, MessageBoxError> {
235 use base64::Engine;
236 use bsv::primitives::public_key::PublicKey;
237 use bsv::primitives::utils::from_hex;
238 use bsv::script::templates::{ScriptTemplateLock, P2PKH};
239 use bsv::wallet::interfaces::{
240 CreateActionArgs, CreateActionOptions, CreateActionOutput, GetPublicKeyArgs,
241 };
242 use bsv::wallet::proto_wallet::ProtoWallet;
243 use bsv::wallet::types::{BooleanDefaultTrue, Counterparty, CounterpartyType, Protocol};
244
245 let desc = description.unwrap_or("MessageBox delivery fee");
246 let sender_identity_key = self.get_identity_key().await?;
247
248 let mut output_index: u32 = 0;
249 let mut outputs = Vec::new();
250 let mut payment_outputs = Vec::new();
251
252 if quote.delivery_fee > 0 {
254 let prefix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
256 let suffix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
257 let prefix = base64::engine::general_purpose::STANDARD.encode(&prefix_bytes);
258 let suffix = base64::engine::general_purpose::STANDARD.encode(&suffix_bytes);
259
260 let agent_pk = PublicKey::from_string("e.delivery_agent_identity_key)
261 .map_err(|e| MessageBoxError::Wallet(format!("agent key: {e}")))?;
262
263 let delivery_key = self
265 .wallet()
266 .get_public_key(
267 GetPublicKeyArgs {
268 identity_key: false,
269 protocol_id: Some(Protocol {
270 security_level: 2,
271 protocol: "3241645161d8".to_string(),
272 }),
273 key_id: Some(format!("{prefix} {suffix}")),
274 counterparty: Some(Counterparty {
275 counterparty_type: CounterpartyType::Other,
276 public_key: Some(agent_pk),
277 }),
278 privileged: false,
279 privileged_reason: None,
280 for_self: None,
281 seek_permission: None,
282 },
283 self.originator(),
284 )
285 .await
286 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
287
288 let hash_vec = delivery_key.public_key.to_hash();
289 let mut hash = [0u8; 20];
290 hash.copy_from_slice(&hash_vec);
291 let lock = P2PKH::from_public_key_hash(hash)
292 .lock()
293 .map_err(|e| MessageBoxError::Wallet(format!("P2PKH lock: {e}")))?;
294 let lock_bytes = from_hex(&lock.to_hex())
295 .map_err(|e| MessageBoxError::Wallet(format!("hex decode: {e}")))?;
296
297 outputs.push(CreateActionOutput {
298 locking_script: Some(lock_bytes),
299 satoshis: quote.delivery_fee as u64,
300 output_description: "MessageBox server delivery fee".to_string(),
301 basket: None,
302 custom_instructions: None,
303 tags: None,
304 });
305
306 payment_outputs.push(MessagePaymentOutput {
308 output_index,
309 derivation_prefix: prefix.as_bytes().to_vec(),
310 derivation_suffix: suffix.as_bytes().to_vec(),
311 sender_identity_key: sender_identity_key.clone(),
312 });
313 output_index += 1;
314 }
315
316 if quote.recipient_fee > 0 {
318 let prefix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
319 let suffix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
320 let prefix = base64::engine::general_purpose::STANDARD.encode(&prefix_bytes);
321 let suffix = base64::engine::general_purpose::STANDARD.encode(&suffix_bytes);
322
323 let anyone_wallet = ProtoWallet::anyone();
327
328 let recipient_pk = PublicKey::from_string(recipient)
329 .map_err(|e| MessageBoxError::Wallet(format!("recipient key: {e}")))?;
330
331 let recv_key = anyone_wallet
333 .get_public_key(
334 GetPublicKeyArgs {
335 identity_key: false,
336 protocol_id: Some(Protocol {
337 security_level: 2,
338 protocol: "3241645161d8".to_string(),
339 }),
340 key_id: Some(format!("{prefix} {suffix}")),
341 counterparty: Some(Counterparty {
342 counterparty_type: CounterpartyType::Other,
343 public_key: Some(recipient_pk),
344 }),
345 privileged: false,
346 privileged_reason: None,
347 for_self: None,
348 seek_permission: None,
349 },
350 None,
351 )
352 .await
353 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
354
355 let hash_vec2 = recv_key.public_key.to_hash();
356 let mut hash2 = [0u8; 20];
357 hash2.copy_from_slice(&hash_vec2);
358 let lock_recv = P2PKH::from_public_key_hash(hash2)
359 .lock()
360 .map_err(|e| MessageBoxError::Wallet(format!("P2PKH lock: {e}")))?;
361 let lock_recv_bytes = from_hex(&lock_recv.to_hex())
362 .map_err(|e| MessageBoxError::Wallet(format!("hex decode: {e}")))?;
363
364 outputs.push(CreateActionOutput {
365 locking_script: Some(lock_recv_bytes),
366 satoshis: quote.recipient_fee as u64,
367 output_description: "Recipient message fee".to_string(),
368 basket: None,
369 custom_instructions: None,
370 tags: None,
371 });
372
373 let anyone_id = anyone_wallet
375 .get_public_key(
376 GetPublicKeyArgs {
377 identity_key: true,
378 protocol_id: None,
379 key_id: None,
380 counterparty: None,
381 privileged: false,
382 privileged_reason: None,
383 for_self: None,
384 seek_permission: None,
385 },
386 None,
387 )
388 .await
389 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
390
391 payment_outputs.push(MessagePaymentOutput {
392 output_index,
393 derivation_prefix: prefix.as_bytes().to_vec(),
394 derivation_suffix: suffix.as_bytes().to_vec(),
395 sender_identity_key: anyone_id.public_key.to_der_hex(),
396 });
397 }
398
399 let create_result = self
400 .wallet()
401 .create_action(
402 CreateActionArgs {
403 description: desc.to_string(),
404 input_beef: None,
405 inputs: None,
406 outputs: Some(outputs),
407 lock_time: None,
408 version: None,
409 labels: Some(vec!["messagebox".to_string()]),
410 options: Some(CreateActionOptions {
411 randomize_outputs: BooleanDefaultTrue(Some(false)),
412 ..Default::default()
413 }),
414 reference: None,
415 },
416 self.originator(),
417 )
418 .await
419 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
420
421 let tx = create_result
422 .tx
423 .ok_or_else(|| MessageBoxError::Wallet("create_action returned no tx".to_string()))?;
424
425 Ok(MessagePayment {
426 tx,
427 outputs: payment_outputs,
428 })
429 }
430
431 pub async fn send_message_to_recipients(
438 &self,
439 params: &SendListParams,
440 override_host: Option<&str>,
441 ) -> Result<SendListResult, MessageBoxError> {
442 self.assert_initialized().await?;
443
444 let skip_enc = params.skip_encryption.unwrap_or(false);
445
446 let recipient_refs: Vec<&str> = params.recipients.iter().map(|s| s.as_str()).collect();
447 let multi_quote = self
448 .get_message_box_quote_multi(&recipient_refs, ¶ms.message_box, override_host)
449 .await?;
450
451 let blocked: Vec<String> = multi_quote.blocked_recipients.clone();
453 let sendable: Vec<&crate::types::RecipientQuote> = multi_quote
454 .quotes_by_recipient
455 .iter()
456 .filter(|rq| rq.status != "blocked")
457 .collect();
458
459 let mut recipient_hosts: HashMap<String, String> = HashMap::new();
461 for rq in &sendable {
462 let host = if let Some(h) = override_host {
463 h.to_string()
464 } else {
465 self.resolve_host_for_recipient(&rq.recipient)
466 .await
467 .unwrap_or_else(|_| self.host().to_string())
468 };
469 recipient_hosts.insert(rq.recipient.clone(), host);
470 }
471
472 let needs_payment = sendable
474 .iter()
475 .any(|rq| rq.delivery_fee > 0 || rq.recipient_fee > 0);
476
477 let batch_payment = if needs_payment {
478 let pairs_for_payment: Vec<(String, i64, i64, String)> = sendable
480 .iter()
481 .map(|rq| {
482 let host = recipient_hosts
483 .get(&rq.recipient)
484 .cloned()
485 .unwrap_or_else(|| self.host().to_string());
486 let agent_key = multi_quote
487 .delivery_agent_identity_key_by_host
488 .get(&host)
489 .cloned()
490 .unwrap_or_default();
491 (
492 rq.recipient.clone(),
493 rq.delivery_fee,
494 rq.recipient_fee,
495 agent_key,
496 )
497 })
498 .collect();
499
500 match self
501 .create_message_payment_batch_from_tuples(&pairs_for_payment, None)
502 .await
503 {
504 Ok(p) => Some(p),
505 Err(e) => {
506 let failed_entries: Vec<FailedRecipient> = sendable
508 .iter()
509 .map(|rq| FailedRecipient {
510 recipient: rq.recipient.clone(),
511 error: e.to_string(),
512 })
513 .collect();
514 return Ok(SendListResult {
515 status: "error".to_string(),
516 description: "Batch payment creation failed".to_string(),
517 sent: vec![],
518 blocked,
519 failed: failed_entries,
520 totals: None,
521 });
522 }
523 }
524 } else {
525 None
526 };
527
528 let mut sent: Vec<SentRecipient> = Vec::new();
530 let mut failed: Vec<FailedRecipient> = Vec::new();
531
532 for rq in &sendable {
533 let host = recipient_hosts
534 .get(&rq.recipient)
535 .cloned()
536 .unwrap_or_else(|| self.host().to_string());
537
538 match self
539 .send_message_to_host(
540 &host,
541 &rq.recipient,
542 ¶ms.message_box,
543 ¶ms.body,
544 skip_enc,
545 false, None,
547 batch_payment.clone(),
548 )
549 .await
550 {
551 Ok(msg_id) => sent.push(SentRecipient {
552 recipient: rq.recipient.clone(),
553 message_id: msg_id,
554 }),
555 Err(e) => failed.push(FailedRecipient {
556 recipient: rq.recipient.clone(),
557 error: e.to_string(),
558 }),
559 }
560 }
561
562 Ok(SendListResult {
563 status: "success".to_string(),
564 description: format!("Sent to {} recipients", sent.len()),
565 sent,
566 blocked,
567 failed,
568 totals: multi_quote.totals,
569 })
570 }
571
572 async fn create_message_payment_batch_from_tuples(
580 &self,
581 tuples: &[(String, i64, i64, String)],
582 description: Option<&str>,
583 ) -> Result<MessagePayment, MessageBoxError> {
584 use base64::Engine;
585 use bsv::primitives::public_key::PublicKey;
586 use bsv::primitives::utils::from_hex;
587 use bsv::script::templates::{ScriptTemplateLock, P2PKH};
588 use bsv::wallet::interfaces::{
589 CreateActionArgs, CreateActionOptions, CreateActionOutput, GetPublicKeyArgs,
590 };
591 use bsv::wallet::proto_wallet::ProtoWallet;
592 use bsv::wallet::types::{BooleanDefaultTrue, Counterparty, CounterpartyType, Protocol};
593
594 let desc = description.unwrap_or("MessageBox batch delivery fee");
595 let sender_identity_key = self.get_identity_key().await?;
596 let anyone_wallet = ProtoWallet::anyone();
597 let anyone_id = anyone_wallet
598 .get_public_key(
599 GetPublicKeyArgs {
600 identity_key: true,
601 protocol_id: None,
602 key_id: None,
603 counterparty: None,
604 privileged: false,
605 privileged_reason: None,
606 for_self: None,
607 seek_permission: None,
608 },
609 None,
610 )
611 .await
612 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
613 let anyone_id_hex = anyone_id.public_key.to_der_hex();
614
615 let mut outputs: Vec<CreateActionOutput> = Vec::new();
616 let mut payment_outputs: Vec<MessagePaymentOutput> = Vec::new();
617
618 for (recipient, delivery_fee, recipient_fee, agent_key) in tuples {
619 if *delivery_fee > 0 && !agent_key.is_empty() {
621 let prefix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
622 let suffix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
623 let prefix = base64::engine::general_purpose::STANDARD.encode(&prefix_bytes);
624 let suffix = base64::engine::general_purpose::STANDARD.encode(&suffix_bytes);
625
626 let agent_pk = PublicKey::from_string(agent_key)
627 .map_err(|e| MessageBoxError::Wallet(format!("agent key: {e}")))?;
628
629 let key = self
630 .wallet()
631 .get_public_key(
632 GetPublicKeyArgs {
633 identity_key: false,
634 protocol_id: Some(Protocol {
635 security_level: 2,
636 protocol: "3241645161d8".to_string(),
637 }),
638 key_id: Some(format!("{prefix} {suffix}")),
639 counterparty: Some(Counterparty {
640 counterparty_type: CounterpartyType::Other,
641 public_key: Some(agent_pk),
642 }),
643 privileged: false,
644 privileged_reason: None,
645 for_self: None,
646 seek_permission: None,
647 },
648 self.originator(),
649 )
650 .await
651 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
652
653 let hash_vec = key.public_key.to_hash();
654 let mut hash = [0u8; 20];
655 hash.copy_from_slice(&hash_vec);
656 let lock = P2PKH::from_public_key_hash(hash)
657 .lock()
658 .map_err(|e| MessageBoxError::Wallet(format!("P2PKH lock: {e}")))?;
659 let lock_bytes = from_hex(&lock.to_hex())
660 .map_err(|e| MessageBoxError::Wallet(format!("hex decode: {e}")))?;
661
662 let output_index = outputs.len() as u32;
663 outputs.push(CreateActionOutput {
664 locking_script: Some(lock_bytes),
665 satoshis: *delivery_fee as u64,
666 output_description: format!("Delivery fee for {}", recipient),
667 basket: None,
668 custom_instructions: None,
669 tags: None,
670 });
671 payment_outputs.push(MessagePaymentOutput {
672 output_index,
673 derivation_prefix: prefix.as_bytes().to_vec(),
674 derivation_suffix: suffix.as_bytes().to_vec(),
675 sender_identity_key: sender_identity_key.clone(),
676 });
677 }
678
679 if *recipient_fee > 0 {
681 let prefix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
682 let suffix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
683 let prefix = base64::engine::general_purpose::STANDARD.encode(&prefix_bytes);
684 let suffix = base64::engine::general_purpose::STANDARD.encode(&suffix_bytes);
685
686 let recipient_pk = PublicKey::from_string(recipient)
687 .map_err(|e| MessageBoxError::Wallet(format!("recipient key: {e}")))?;
688
689 let key = anyone_wallet
691 .get_public_key(
692 GetPublicKeyArgs {
693 identity_key: false,
694 protocol_id: Some(Protocol {
695 security_level: 2,
696 protocol: "3241645161d8".to_string(),
697 }),
698 key_id: Some(format!("{prefix} {suffix}")),
699 counterparty: Some(Counterparty {
700 counterparty_type: CounterpartyType::Other,
701 public_key: Some(recipient_pk),
702 }),
703 privileged: false,
704 privileged_reason: None,
705 for_self: None,
706 seek_permission: None,
707 },
708 None,
709 )
710 .await
711 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
712
713 let hash_vec = key.public_key.to_hash();
714 let mut hash = [0u8; 20];
715 hash.copy_from_slice(&hash_vec);
716 let lock = P2PKH::from_public_key_hash(hash)
717 .lock()
718 .map_err(|e| MessageBoxError::Wallet(format!("P2PKH lock: {e}")))?;
719 let lock_bytes = from_hex(&lock.to_hex())
720 .map_err(|e| MessageBoxError::Wallet(format!("hex decode: {e}")))?;
721
722 let output_index = outputs.len() as u32;
723 outputs.push(CreateActionOutput {
724 locking_script: Some(lock_bytes),
725 satoshis: *recipient_fee as u64,
726 output_description: format!("Recipient fee for {}", recipient),
727 basket: None,
728 custom_instructions: None,
729 tags: None,
730 });
731 payment_outputs.push(MessagePaymentOutput {
732 output_index,
733 derivation_prefix: prefix.as_bytes().to_vec(),
734 derivation_suffix: suffix.as_bytes().to_vec(),
735 sender_identity_key: anyone_id_hex.clone(),
736 });
737 }
738 }
739
740 if outputs.is_empty() {
741 return Ok(MessagePayment {
742 tx: vec![],
743 outputs: vec![],
744 });
745 }
746
747 let create_result = self
748 .wallet()
749 .create_action(
750 CreateActionArgs {
751 description: desc.to_string(),
752 input_beef: None,
753 inputs: None,
754 outputs: Some(outputs),
755 lock_time: None,
756 version: None,
757 labels: Some(vec!["messagebox".to_string()]),
758 options: Some(CreateActionOptions {
759 randomize_outputs: BooleanDefaultTrue(Some(false)),
760 ..Default::default()
761 }),
762 reference: None,
763 },
764 self.originator(),
765 )
766 .await
767 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
768
769 let tx = create_result
770 .tx
771 .ok_or_else(|| MessageBoxError::Wallet("create_action returned no tx".to_string()))?;
772
773 Ok(MessagePayment {
774 tx,
775 outputs: payment_outputs,
776 })
777 }
778
779 pub async fn list_messages_lite(
786 &self,
787 message_box: &str,
788 override_host: Option<&str>,
789 ) -> Result<Vec<ServerPeerMessage>, MessageBoxError> {
790 self.assert_initialized().await?;
791
792 let host = override_host.unwrap_or_else(|| self.host());
793 let params = ListMessagesParams {
794 message_box: message_box.to_string(),
795 };
796 let body_bytes = serde_json::to_vec(¶ms)?;
797 let url = format!("{host}/listMessages");
798 let response = self.post_json(&url, body_bytes).await?;
799 check_status_error(&response.body)?;
800
801 let mut list_response: ListMessagesResponse = serde_json::from_slice(&response.body)?;
802
803 for msg in &mut list_response.messages {
806 let outcome = encryption::try_decrypt_message_typed(
810 self.wallet(),
811 &msg.body,
812 &msg.sender,
813 None, )
815 .await;
816 msg.authenticated_decrypt = outcome.is_authenticated();
817 msg.body = outcome.into_body();
818 }
819
820 Ok(list_response.messages)
821 }
822
823 pub async fn list_messages(
839 &self,
840 message_box: &str,
841 accept_payments: bool,
842 override_host: Option<&str>,
843 ) -> Result<Vec<PeerMessage>, MessageBoxError> {
844 self.assert_initialized().await?;
845
846 if let Some(host) = override_host {
848 return self
849 .list_messages_from_host(host, message_box, accept_payments)
850 .await;
851 }
852
853 let identity_key = self.get_identity_key().await?;
855 let ads = self
856 .query_advertisements(Some(&identity_key), None)
857 .await
858 .unwrap_or_default();
859
860 let mut host_set: HashSet<String> = ads.into_iter().map(|ad| ad.host).collect();
862 host_set.insert(self.host().to_string());
863
864 if host_set.len() == 1 {
865 return self
867 .list_messages_from_host(self.host(), message_box, accept_payments)
868 .await;
869 }
870
871 let futures: Vec<_> = host_set
873 .iter()
874 .map(|h| self.list_messages_from_host(h, message_box, accept_payments))
875 .collect();
876
877 let outcomes = join_all(futures).await;
878 let successful: Vec<Vec<PeerMessage>> =
879 outcomes.into_iter().filter_map(|r| r.ok()).collect();
880
881 if successful.is_empty() {
882 return Err(MessageBoxError::Http(
883 0,
884 format!("list_messages: all {} hosts failed", host_set.len()),
885 ));
886 }
887
888 Ok(dedup_messages(successful))
889 }
890
891 async fn list_messages_from_host(
896 &self,
897 host: &str,
898 message_box: &str,
899 accept_payments: bool,
900 ) -> Result<Vec<PeerMessage>, MessageBoxError> {
901 let identity_key = self.get_identity_key().await?;
903
904 let params = ListMessagesParams {
905 message_box: message_box.to_string(),
906 };
907 let body_bytes = serde_json::to_vec(¶ms)?;
908 let url = format!("{host}/listMessages");
909 let response = self.post_json(&url, body_bytes).await?;
910 check_status_error(&response.body)?;
911
912 let list_response: ListMessagesResponse = serde_json::from_slice(&response.body)?;
913
914 let mut result = Vec::with_capacity(list_response.messages.len());
915 for msg in list_response.messages {
916 let plain_body: String =
918 if let Ok(wrapped) = serde_json::from_str::<WrappedMessageBody>(&msg.body) {
919 if accept_payments {
921 if let Some(payment) = &wrapped.payment {
922 if let Some(tx_bytes) = &payment.tx {
923 let description = payment
924 .description
925 .clone()
926 .unwrap_or_else(|| "Server delivery fee".to_string());
927
928 let outputs: Vec<InternalizeOutput> = payment
931 .outputs
932 .as_deref()
933 .unwrap_or(&[])
934 .iter()
935 .filter_map(|o| {
936 if o.protocol.as_deref() != Some("wallet payment")
938 && o.protocol.is_some()
939 {
940 return None;
941 }
942 let sender_pk = o
944 .sender_identity_key
945 .as_deref()
946 .and_then(|k| PublicKey::from_string(k).ok())?;
947 Some(InternalizeOutput::WalletPayment {
948 output_index: o.output_index.unwrap_or(0),
949 payment: Payment {
950 derivation_prefix: o
951 .derivation_prefix
952 .clone()
953 .unwrap_or_default(),
954 derivation_suffix: o
955 .derivation_suffix
956 .clone()
957 .unwrap_or_default(),
958 sender_identity_key: sender_pk,
959 },
960 })
961 })
962 .collect();
963
964 let args = InternalizeActionArgs {
965 tx: tx_bytes.clone(),
966 description,
967 labels: Some(vec!["server-delivery-fee".to_string()]),
968 seek_permission: BooleanDefaultTrue(Some(false)),
969 outputs: outputs,
970 };
971 let _ = self
973 .wallet()
974 .internalize_action(args, self.originator())
975 .await;
976 }
977 }
978 }
979
980 match wrapped.message {
982 Some(serde_json::Value::String(s)) => s,
983 Some(v) => v.to_string(),
984 None => msg.body.clone(),
985 }
986 } else {
987 msg.body.clone()
989 };
990
991 let decrypted = encryption::try_decrypt_message(
993 self.wallet(),
994 &plain_body,
995 &msg.sender,
996 self.originator(),
997 )
998 .await;
999
1000 result.push(PeerMessage {
1001 message_id: msg.message_id,
1002 sender: msg.sender,
1003 recipient: identity_key.clone(),
1004 message_box: message_box.to_string(),
1005 body: decrypted,
1006 });
1007 }
1008
1009 Ok(result)
1010 }
1011
1012 pub async fn acknowledge_message(
1018 &self,
1019 message_ids: Vec<String>,
1020 override_host: Option<&str>,
1021 ) -> Result<(), MessageBoxError> {
1022 self.assert_initialized().await?;
1023
1024 if let Some(host) = override_host {
1025 return self.acknowledge_message_on_host(host, &message_ids).await;
1026 }
1027
1028 let identity_key = self.get_identity_key().await?;
1030 let ads = self
1031 .query_advertisements(Some(&identity_key), None)
1032 .await
1033 .unwrap_or_default();
1034
1035 let mut host_set: HashSet<String> = ads.into_iter().map(|ad| ad.host).collect();
1036 host_set.insert(self.host().to_string());
1037
1038 if host_set.len() == 1 {
1039 return self
1040 .acknowledge_message_on_host(self.host(), &message_ids)
1041 .await;
1042 }
1043
1044 let futures: Vec<_> = host_set
1046 .iter()
1047 .map(|h| self.acknowledge_message_on_host(h, &message_ids))
1048 .collect();
1049
1050 let outcomes = join_all(futures).await;
1051 let any_ok = outcomes.iter().any(|r| r.is_ok());
1052
1053 if any_ok {
1054 Ok(())
1055 } else {
1056 Err(MessageBoxError::Http(
1057 0,
1058 format!("acknowledge_message: all {} hosts failed", host_set.len()),
1059 ))
1060 }
1061 }
1062
1063 async fn acknowledge_message_on_host(
1065 &self,
1066 host: &str,
1067 message_ids: &[String],
1068 ) -> Result<(), MessageBoxError> {
1069 let params = AcknowledgeMessageParams {
1070 message_ids: message_ids.to_vec(),
1071 };
1072 let body_bytes = serde_json::to_vec(¶ms)?;
1073 let url = format!("{host}/acknowledgeMessage");
1074 let response = self.post_json(&url, body_bytes).await?;
1075 check_status_error(&response.body)?;
1076 Ok(())
1077 }
1078}
1079
1080#[cfg(test)]
1085mod tests {
1086 use crate::encryption::generate_message_id;
1087 use crate::types::{
1088 AcknowledgeMessageParams, ListMessagesResponse, SendMessageParams, SendMessageRequest,
1089 };
1090 use bsv::primitives::private_key::PrivateKey;
1091 use bsv::wallet::error::WalletError;
1092 use bsv::wallet::interfaces::*;
1093 use bsv::wallet::proto_wallet::ProtoWallet;
1094 use std::sync::Arc;
1095
1096 #[derive(Clone)]
1098 struct ArcWallet(Arc<ProtoWallet>);
1099
1100 impl ArcWallet {
1101 fn new() -> Self {
1102 let key = PrivateKey::from_random().expect("random key");
1103 ArcWallet(Arc::new(ProtoWallet::new(key)))
1104 }
1105 }
1106
1107 #[async_trait::async_trait]
1108 impl WalletInterface for ArcWallet {
1109 async fn create_action(
1110 &self,
1111 args: CreateActionArgs,
1112 orig: Option<&str>,
1113 ) -> Result<CreateActionResult, WalletError> {
1114 self.0.create_action(args, orig).await
1115 }
1116 async fn sign_action(
1117 &self,
1118 args: SignActionArgs,
1119 orig: Option<&str>,
1120 ) -> Result<SignActionResult, WalletError> {
1121 self.0.sign_action(args, orig).await
1122 }
1123 async fn abort_action(
1124 &self,
1125 args: AbortActionArgs,
1126 orig: Option<&str>,
1127 ) -> Result<AbortActionResult, WalletError> {
1128 self.0.abort_action(args, orig).await
1129 }
1130 async fn list_actions(
1131 &self,
1132 args: ListActionsArgs,
1133 orig: Option<&str>,
1134 ) -> Result<ListActionsResult, WalletError> {
1135 self.0.list_actions(args, orig).await
1136 }
1137 async fn internalize_action(
1138 &self,
1139 args: InternalizeActionArgs,
1140 orig: Option<&str>,
1141 ) -> Result<InternalizeActionResult, WalletError> {
1142 self.0.internalize_action(args, orig).await
1143 }
1144 async fn list_outputs(
1145 &self,
1146 args: ListOutputsArgs,
1147 orig: Option<&str>,
1148 ) -> Result<ListOutputsResult, WalletError> {
1149 self.0.list_outputs(args, orig).await
1150 }
1151 async fn relinquish_output(
1152 &self,
1153 args: RelinquishOutputArgs,
1154 orig: Option<&str>,
1155 ) -> Result<RelinquishOutputResult, WalletError> {
1156 self.0.relinquish_output(args, orig).await
1157 }
1158 async fn get_public_key(
1159 &self,
1160 args: GetPublicKeyArgs,
1161 orig: Option<&str>,
1162 ) -> Result<GetPublicKeyResult, WalletError> {
1163 self.0.get_public_key(args, orig).await
1164 }
1165 async fn reveal_counterparty_key_linkage(
1166 &self,
1167 args: RevealCounterpartyKeyLinkageArgs,
1168 orig: Option<&str>,
1169 ) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> {
1170 self.0.reveal_counterparty_key_linkage(args, orig).await
1171 }
1172 async fn reveal_specific_key_linkage(
1173 &self,
1174 args: RevealSpecificKeyLinkageArgs,
1175 orig: Option<&str>,
1176 ) -> Result<RevealSpecificKeyLinkageResult, WalletError> {
1177 self.0.reveal_specific_key_linkage(args, orig).await
1178 }
1179 async fn encrypt(
1180 &self,
1181 args: EncryptArgs,
1182 orig: Option<&str>,
1183 ) -> Result<EncryptResult, WalletError> {
1184 self.0.encrypt(args, orig).await
1185 }
1186 async fn decrypt(
1187 &self,
1188 args: DecryptArgs,
1189 orig: Option<&str>,
1190 ) -> Result<DecryptResult, WalletError> {
1191 self.0.decrypt(args, orig).await
1192 }
1193 async fn create_hmac(
1194 &self,
1195 args: CreateHmacArgs,
1196 orig: Option<&str>,
1197 ) -> Result<CreateHmacResult, WalletError> {
1198 self.0.create_hmac(args, orig).await
1199 }
1200 async fn verify_hmac(
1201 &self,
1202 args: VerifyHmacArgs,
1203 orig: Option<&str>,
1204 ) -> Result<VerifyHmacResult, WalletError> {
1205 self.0.verify_hmac(args, orig).await
1206 }
1207 async fn create_signature(
1208 &self,
1209 args: CreateSignatureArgs,
1210 orig: Option<&str>,
1211 ) -> Result<CreateSignatureResult, WalletError> {
1212 self.0.create_signature(args, orig).await
1213 }
1214 async fn verify_signature(
1215 &self,
1216 args: VerifySignatureArgs,
1217 orig: Option<&str>,
1218 ) -> Result<VerifySignatureResult, WalletError> {
1219 self.0.verify_signature(args, orig).await
1220 }
1221 async fn acquire_certificate(
1222 &self,
1223 args: AcquireCertificateArgs,
1224 orig: Option<&str>,
1225 ) -> Result<Certificate, WalletError> {
1226 self.0.acquire_certificate(args, orig).await
1227 }
1228 async fn list_certificates(
1229 &self,
1230 args: ListCertificatesArgs,
1231 orig: Option<&str>,
1232 ) -> Result<ListCertificatesResult, WalletError> {
1233 self.0.list_certificates(args, orig).await
1234 }
1235 async fn prove_certificate(
1236 &self,
1237 args: ProveCertificateArgs,
1238 orig: Option<&str>,
1239 ) -> Result<ProveCertificateResult, WalletError> {
1240 self.0.prove_certificate(args, orig).await
1241 }
1242 async fn relinquish_certificate(
1243 &self,
1244 args: RelinquishCertificateArgs,
1245 orig: Option<&str>,
1246 ) -> Result<RelinquishCertificateResult, WalletError> {
1247 self.0.relinquish_certificate(args, orig).await
1248 }
1249 async fn discover_by_identity_key(
1250 &self,
1251 args: DiscoverByIdentityKeyArgs,
1252 orig: Option<&str>,
1253 ) -> Result<DiscoverCertificatesResult, WalletError> {
1254 self.0.discover_by_identity_key(args, orig).await
1255 }
1256 async fn discover_by_attributes(
1257 &self,
1258 args: DiscoverByAttributesArgs,
1259 orig: Option<&str>,
1260 ) -> Result<DiscoverCertificatesResult, WalletError> {
1261 self.0.discover_by_attributes(args, orig).await
1262 }
1263 async fn is_authenticated(
1264 &self,
1265 orig: Option<&str>,
1266 ) -> Result<AuthenticatedResult, WalletError> {
1267 self.0.is_authenticated(orig).await
1268 }
1269 async fn wait_for_authentication(
1270 &self,
1271 orig: Option<&str>,
1272 ) -> Result<AuthenticatedResult, WalletError> {
1273 self.0.wait_for_authentication(orig).await
1274 }
1275 async fn get_height(&self, orig: Option<&str>) -> Result<GetHeightResult, WalletError> {
1276 self.0.get_height(orig).await
1277 }
1278 async fn get_header_for_height(
1279 &self,
1280 args: GetHeaderArgs,
1281 orig: Option<&str>,
1282 ) -> Result<GetHeaderResult, WalletError> {
1283 self.0.get_header_for_height(args, orig).await
1284 }
1285 async fn get_network(&self, orig: Option<&str>) -> Result<GetNetworkResult, WalletError> {
1286 self.0.get_network(orig).await
1287 }
1288 async fn get_version(&self, orig: Option<&str>) -> Result<GetVersionResult, WalletError> {
1289 self.0.get_version(orig).await
1290 }
1291 }
1292
1293 #[test]
1299 fn test_send_message_request_format() {
1300 let req = SendMessageRequest {
1301 message: SendMessageParams {
1302 recipient: "03abc123".to_string(),
1303 message_box: "payment_inbox".to_string(),
1304 body: r#"{"encryptedMessage":"abc=="}"#.to_string(),
1305 message_id: "deadbeef01234567".to_string(),
1306 },
1307 payment: None,
1308 };
1309 let json = serde_json::to_string(&req).unwrap();
1310 assert!(
1312 json.starts_with(r#"{"message":"#),
1313 "must have message wrapper"
1314 );
1315 assert!(json.contains("\"recipient\""), "camelCase recipient");
1316 assert!(json.contains("\"messageBox\""), "camelCase messageBox");
1317 assert!(json.contains("\"messageId\""), "camelCase messageId");
1318 assert!(
1319 json.contains("\"payment_inbox\""),
1320 "messageBox value preserved"
1321 );
1322 assert!(!json.contains("message_box"), "no snake_case leakage");
1323 assert!(!json.contains("message_id"), "no snake_case leakage");
1324 }
1325
1326 #[test]
1328 fn test_acknowledge_request_format() {
1329 let params = AcknowledgeMessageParams {
1330 message_ids: vec!["id1".to_string(), "id2".to_string()],
1331 };
1332 let json = serde_json::to_string(¶ms).unwrap();
1333 assert_eq!(json, r#"{"messageIds":["id1","id2"]}"#);
1334 }
1335
1336 #[test]
1338 fn test_list_messages_response_parsing() {
1339 let raw = r#"{
1340 "status": "success",
1341 "messages": [
1342 {
1343 "messageId": "abc123",
1344 "body": "hello world",
1345 "sender": "03xyz",
1346 "created_at": "2024-01-01T00:00:00Z",
1347 "updated_at": "2024-01-01T00:01:00Z"
1348 }
1349 ]
1350 }"#;
1351 let resp: ListMessagesResponse = serde_json::from_str(raw).unwrap();
1352 assert_eq!(resp.status, "success");
1353 assert_eq!(resp.messages.len(), 1);
1354 assert_eq!(resp.messages[0].message_id, "abc123");
1355 assert_eq!(resp.messages[0].body, "hello world");
1356 assert_eq!(resp.messages[0].sender, "03xyz");
1357 }
1358
1359 #[tokio::test]
1365 async fn test_message_id_is_64_hex_chars() {
1366 let wallet = ArcWallet::new();
1367 let other = ArcWallet::new();
1369 let other_pk = other
1370 .get_public_key(
1371 GetPublicKeyArgs {
1372 identity_key: true,
1373 protocol_id: None,
1374 key_id: None,
1375 counterparty: None,
1376 privileged: false,
1377 privileged_reason: None,
1378 for_self: None,
1379 seek_permission: None,
1380 },
1381 None,
1382 )
1383 .await
1384 .expect("get_public_key")
1385 .public_key
1386 .to_der_hex();
1387
1388 let id = generate_message_id(&wallet, "test body", &other_pk, None)
1389 .await
1390 .expect("generate_message_id");
1391
1392 assert_eq!(id.len(), 64, "HMAC hex must be 64 chars (32 bytes)");
1393 assert!(
1394 id.chars().all(|c| c.is_ascii_hexdigit()),
1395 "all characters must be hex"
1396 );
1397 assert!(
1398 id.chars().all(|c| !c.is_uppercase()),
1399 "hex must be lowercase"
1400 );
1401 }
1402
1403 #[test]
1409 fn list_messages_parses_wrapped_body() {
1410 use super::WrappedMessageBody;
1411 let raw = r#"{"message": "hello world", "payment": {"tx": [1,2,3]}}"#;
1412 let wrapped: WrappedMessageBody = serde_json::from_str(raw).unwrap();
1413 assert!(
1414 wrapped.message.is_some(),
1415 "message sub-field must be present"
1416 );
1417 assert!(
1418 wrapped.payment.is_some(),
1419 "payment sub-field must be present"
1420 );
1421 let msg_val = wrapped.message.unwrap();
1423 assert_eq!(msg_val.as_str().unwrap(), "hello world");
1424 }
1425
1426 #[test]
1428 fn list_messages_plain_body_passthrough() {
1429 use super::WrappedMessageBody;
1430 let plain = "plain body text";
1432 let result = serde_json::from_str::<WrappedMessageBody>(plain);
1433 assert!(result.is_err(), "plain text must not parse as wrapped body");
1434 }
1435
1436 #[test]
1438 fn list_messages_missing_payment_no_crash() {
1439 use super::WrappedMessageBody;
1440 let raw = r#"{"message": "the content", "payment": null}"#;
1441 let wrapped: WrappedMessageBody = serde_json::from_str(raw).unwrap();
1442 assert!(wrapped.message.is_some(), "message present");
1443 assert!(wrapped.payment.is_none(), "payment is none when null");
1444 }
1445
1446 #[test]
1448 fn test_list_messages_dedup_by_id() {
1449 use super::dedup_messages;
1450 use bsv::remittance::types::PeerMessage;
1451
1452 let msg_a = PeerMessage {
1453 message_id: "id-1".to_string(),
1454 sender: "03sender".to_string(),
1455 recipient: "03me".to_string(),
1456 message_box: "inbox".to_string(),
1457 body: "first".to_string(),
1458 };
1459 let msg_a_dup = PeerMessage {
1460 message_id: "id-1".to_string(), sender: "03sender".to_string(),
1462 recipient: "03me".to_string(),
1463 message_box: "inbox".to_string(),
1464 body: "duplicate".to_string(), };
1466 let msg_b = PeerMessage {
1467 message_id: "id-2".to_string(),
1468 sender: "03sender".to_string(),
1469 recipient: "03me".to_string(),
1470 message_box: "inbox".to_string(),
1471 body: "second".to_string(),
1472 };
1473
1474 let results = vec![vec![msg_a.clone(), msg_b.clone()], vec![msg_a_dup]];
1476 let deduped = dedup_messages(results);
1477
1478 assert_eq!(deduped.len(), 2, "must deduplicate to 2 unique messages");
1479 let first = deduped.iter().find(|m| m.message_id == "id-1").unwrap();
1481 assert_eq!(first.body, "first", "first-seen must win on deduplication");
1482 }
1483
1484 #[tokio::test]
1486 async fn test_message_id_deterministic() {
1487 let wallet = ArcWallet::new();
1488 let other = ArcWallet::new();
1489 let other_pk = other
1490 .get_public_key(
1491 GetPublicKeyArgs {
1492 identity_key: true,
1493 protocol_id: None,
1494 key_id: None,
1495 counterparty: None,
1496 privileged: false,
1497 privileged_reason: None,
1498 for_self: None,
1499 seek_permission: None,
1500 },
1501 None,
1502 )
1503 .await
1504 .expect("get_public_key")
1505 .public_key
1506 .to_der_hex();
1507
1508 let id1 = generate_message_id(&wallet, "same body", &other_pk, None)
1509 .await
1510 .expect("first call");
1511 let id2 = generate_message_id(&wallet, "same body", &other_pk, None)
1512 .await
1513 .expect("second call");
1514
1515 assert_eq!(id1, id2, "same inputs must produce the same HMAC");
1516 }
1517}