1use std::collections::{HashMap, HashSet};
2
3use bsv::remittance::types::PeerMessage;
4use bsv::wallet::interfaces::{InternalizeActionArgs, InternalizeOutput, Payment, WalletInterface};
5use bsv::wallet::types::BooleanDefaultTrue;
6use bsv::primitives::public_key::PublicKey;
7use futures_util::future::join_all;
8
9use crate::client::MessageBoxClient;
10use crate::error::MessageBoxError;
11use crate::client::{check_status_error, is_duplicate_message_rejection};
12use crate::types::{AcknowledgeMessageParams, FailedRecipient, ListMessagesParams, ListMessagesResponse, MessagePayment, MessagePaymentOutput, SendListParams, SendListResult, SentRecipient, SendMessageParams, SendMessageRequest, SendMessageResponse, ServerPeerMessage};
13use crate::encryption;
14
15pub(crate) fn dedup_messages(results: Vec<Vec<PeerMessage>>) -> Vec<PeerMessage> {
21 let mut seen = HashSet::new();
22 let mut out = Vec::new();
23 for host_messages in results {
24 for msg in host_messages {
25 if seen.insert(msg.message_id.clone()) {
26 out.push(msg);
27 }
28 }
29 }
30 out
31}
32
33#[derive(serde::Deserialize)]
37pub(crate) struct WrappedMessageBody {
38 pub message: Option<serde_json::Value>,
39 pub payment: Option<ServerPayment>,
40}
41
42#[derive(serde::Deserialize)]
43pub(crate) struct ServerPayment {
44 pub tx: Option<Vec<u8>>,
45 pub outputs: Option<Vec<ServerPaymentOutput>>,
46 pub description: Option<String>,
47}
48
49#[derive(serde::Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub(crate) struct ServerPaymentOutput {
54 pub output_index: Option<u32>,
55 pub protocol: Option<String>,
57 pub derivation_prefix: Option<Vec<u8>>,
59 pub derivation_suffix: Option<Vec<u8>>,
61 pub sender_identity_key: Option<String>,
63}
64
65impl<W: WalletInterface + Clone + 'static + Send + Sync> MessageBoxClient<W> {
66 #[allow(clippy::too_many_arguments)]
78 pub async fn send_message(
79 &self,
80 recipient: &str,
81 message_box: &str,
82 body: &str,
83 skip_encryption: bool,
84 check_permissions: bool,
85 message_id: Option<&str>,
86 override_host: Option<&str>,
87 ) -> Result<String, MessageBoxError> {
88 self.assert_initialized().await?;
89 let host = match override_host {
90 Some(h) => h.to_string(),
91 None => self.resolve_host_for_recipient(recipient).await?,
92 };
93 self.send_message_to_host(
94 &host,
95 recipient,
96 message_box,
97 body,
98 skip_encryption,
99 check_permissions,
100 message_id,
101 None,
102 )
103 .await
104 }
105
106 #[allow(clippy::too_many_arguments)]
119 pub(crate) async fn send_message_to_host(
120 &self,
121 host: &str,
122 recipient: &str,
123 message_box: &str,
124 body: &str,
125 skip_encryption: bool,
126 check_permissions: bool,
127 message_id: Option<&str>,
128 payment: Option<MessagePayment>,
129 ) -> Result<String, MessageBoxError> {
130 let wire_body = if skip_encryption {
132 body.to_string()
133 } else {
134 encryption::encrypt_body(
135 self.wallet(),
136 body,
137 recipient,
138 self.originator(),
139 )
140 .await?
141 };
142
143 let resolved_message_id = if let Some(id) = message_id {
147 id.to_string()
148 } else {
149 encryption::generate_message_id(
150 self.wallet(),
151 body,
152 recipient,
153 self.originator(),
154 )
155 .await?
156 };
157
158 let payment = if check_permissions && payment.is_none() {
161 let quote = self.get_message_box_quote(recipient, message_box, None).await?;
162 if quote.delivery_fee > 0 || quote.recipient_fee > 0 {
163 let p = self.create_message_payment(recipient, "e, None).await?;
164 Some(p)
165 } else {
166 None
167 }
168 } else {
169 payment
170 };
171
172 let request = SendMessageRequest {
174 message: SendMessageParams {
175 recipient: recipient.to_string(),
176 message_box: message_box.to_string(),
177 body: wire_body,
178 message_id: resolved_message_id.clone(),
179 },
180 payment,
181 };
182
183 let body_bytes = serde_json::to_vec(&request)?;
184 let url = format!("{host}/sendMessage");
185 let response = self.post_json_raw(&url, body_bytes).await?;
190
191 if is_duplicate_message_rejection(&response.body) {
204 return Ok(resolved_message_id);
205 }
206
207 if response.status < 200 || response.status >= 300 {
210 return Err(MessageBoxError::Http(response.status, url));
211 }
212
213 check_status_error(&response.body)?;
215
216 if let Ok(resp) = serde_json::from_slice::<SendMessageResponse>(&response.body) {
218 if let Some(server_id) = resp.message_id {
219 return Ok(server_id);
220 }
221 }
222 Ok(resolved_message_id)
223 }
224
225 async fn create_message_payment(
235 &self,
236 recipient: &str,
237 quote: &crate::types::MessageBoxQuote,
238 description: Option<&str>,
239 ) -> Result<MessagePayment, MessageBoxError> {
240 use bsv::wallet::interfaces::{
241 CreateActionArgs, CreateActionOptions, CreateActionOutput, GetPublicKeyArgs,
242 };
243 use bsv::wallet::types::{BooleanDefaultTrue, Counterparty, CounterpartyType, Protocol};
244 use bsv::primitives::public_key::PublicKey;
245 use bsv::primitives::utils::from_hex;
246 use bsv::script::templates::{P2PKH, ScriptTemplateLock};
247 use bsv::wallet::proto_wallet::ProtoWallet;
248 use base64::Engine;
249
250 let desc = description.unwrap_or("MessageBox delivery fee");
251 let sender_identity_key = self.get_identity_key().await?;
252
253 let mut output_index: u32 = 0;
254 let mut outputs = Vec::new();
255 let mut payment_outputs = Vec::new();
256
257 if quote.delivery_fee > 0 {
259 let prefix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
261 let suffix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
262 let prefix = base64::engine::general_purpose::STANDARD.encode(&prefix_bytes);
263 let suffix = base64::engine::general_purpose::STANDARD.encode(&suffix_bytes);
264
265 let agent_pk = PublicKey::from_string("e.delivery_agent_identity_key)
266 .map_err(|e| MessageBoxError::Wallet(format!("agent key: {e}")))?;
267
268 let delivery_key = self
270 .wallet()
271 .get_public_key(
272 GetPublicKeyArgs {
273 identity_key: false,
274 protocol_id: Some(Protocol {
275 security_level: 2,
276 protocol: "3241645161d8".to_string(),
277 }),
278 key_id: Some(format!("{prefix} {suffix}")),
279 counterparty: Some(Counterparty {
280 counterparty_type: CounterpartyType::Other,
281 public_key: Some(agent_pk),
282 }),
283 privileged: false,
284 privileged_reason: None,
285 for_self: None,
286 seek_permission: None,
287 },
288 self.originator(),
289 )
290 .await
291 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
292
293 let hash_vec = delivery_key.public_key.to_hash();
294 let mut hash = [0u8; 20];
295 hash.copy_from_slice(&hash_vec);
296 let lock = P2PKH::from_public_key_hash(hash)
297 .lock()
298 .map_err(|e| MessageBoxError::Wallet(format!("P2PKH lock: {e}")))?;
299 let lock_bytes = from_hex(&lock.to_hex())
300 .map_err(|e| MessageBoxError::Wallet(format!("hex decode: {e}")))?;
301
302 outputs.push(CreateActionOutput {
303 locking_script: Some(lock_bytes),
304 satoshis: quote.delivery_fee as u64,
305 output_description: "MessageBox server delivery fee".to_string(),
306 basket: None,
307 custom_instructions: None,
308 tags: vec![],
309 });
310
311 payment_outputs.push(MessagePaymentOutput {
313 output_index,
314 derivation_prefix: prefix.as_bytes().to_vec(),
315 derivation_suffix: suffix.as_bytes().to_vec(),
316 sender_identity_key: sender_identity_key.clone(),
317 });
318 output_index += 1;
319 }
320
321 if quote.recipient_fee > 0 {
323 let prefix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
324 let suffix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
325 let prefix = base64::engine::general_purpose::STANDARD.encode(&prefix_bytes);
326 let suffix = base64::engine::general_purpose::STANDARD.encode(&suffix_bytes);
327
328 let anyone_wallet = ProtoWallet::anyone();
332
333 let recipient_pk = PublicKey::from_string(recipient)
334 .map_err(|e| MessageBoxError::Wallet(format!("recipient key: {e}")))?;
335
336 let recv_key = anyone_wallet
338 .get_public_key(
339 GetPublicKeyArgs {
340 identity_key: false,
341 protocol_id: Some(Protocol {
342 security_level: 2,
343 protocol: "3241645161d8".to_string(),
344 }),
345 key_id: Some(format!("{prefix} {suffix}")),
346 counterparty: Some(Counterparty {
347 counterparty_type: CounterpartyType::Other,
348 public_key: Some(recipient_pk),
349 }),
350 privileged: false,
351 privileged_reason: None,
352 for_self: None,
353 seek_permission: None,
354 },
355 None,
356 )
357 .await
358 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
359
360 let hash_vec2 = recv_key.public_key.to_hash();
361 let mut hash2 = [0u8; 20];
362 hash2.copy_from_slice(&hash_vec2);
363 let lock_recv = P2PKH::from_public_key_hash(hash2)
364 .lock()
365 .map_err(|e| MessageBoxError::Wallet(format!("P2PKH lock: {e}")))?;
366 let lock_recv_bytes = from_hex(&lock_recv.to_hex())
367 .map_err(|e| MessageBoxError::Wallet(format!("hex decode: {e}")))?;
368
369 outputs.push(CreateActionOutput {
370 locking_script: Some(lock_recv_bytes),
371 satoshis: quote.recipient_fee as u64,
372 output_description: "Recipient message fee".to_string(),
373 basket: None,
374 custom_instructions: None,
375 tags: vec![],
376 });
377
378 let anyone_id = anyone_wallet
380 .get_public_key(
381 GetPublicKeyArgs {
382 identity_key: true,
383 protocol_id: None,
384 key_id: None,
385 counterparty: None,
386 privileged: false,
387 privileged_reason: None,
388 for_self: None,
389 seek_permission: None,
390 },
391 None,
392 )
393 .await
394 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
395
396 payment_outputs.push(MessagePaymentOutput {
397 output_index,
398 derivation_prefix: prefix.as_bytes().to_vec(),
399 derivation_suffix: suffix.as_bytes().to_vec(),
400 sender_identity_key: anyone_id.public_key.to_der_hex(),
401 });
402 }
403
404 let create_result = self
405 .wallet()
406 .create_action(
407 CreateActionArgs {
408 description: desc.to_string(),
409 input_beef: None,
410 inputs: vec![],
411 outputs,
412 lock_time: None,
413 version: None,
414 labels: vec!["messagebox".to_string()],
415 options: Some(CreateActionOptions {
416 randomize_outputs: BooleanDefaultTrue(Some(false)),
417 ..Default::default()
418 }),
419 reference: None,
420 },
421 self.originator(),
422 )
423 .await
424 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
425
426 let tx = create_result
427 .tx
428 .ok_or_else(|| MessageBoxError::Wallet("create_action returned no tx".to_string()))?;
429
430 Ok(MessagePayment {
431 tx,
432 outputs: payment_outputs,
433 })
434 }
435
436
437 pub async fn send_message_to_recipients(
444 &self,
445 params: &SendListParams,
446 override_host: Option<&str>,
447 ) -> Result<SendListResult, MessageBoxError> {
448 self.assert_initialized().await?;
449
450 let skip_enc = params.skip_encryption.unwrap_or(false);
451
452 let recipient_refs: Vec<&str> = params.recipients.iter().map(|s| s.as_str()).collect();
453 let multi_quote = self
454 .get_message_box_quote_multi(&recipient_refs, ¶ms.message_box, override_host)
455 .await?;
456
457 let blocked: Vec<String> = multi_quote.blocked_recipients.clone();
459 let sendable: Vec<&crate::types::RecipientQuote> = multi_quote
460 .quotes_by_recipient
461 .iter()
462 .filter(|rq| rq.status != "blocked")
463 .collect();
464
465 let mut recipient_hosts: HashMap<String, String> = HashMap::new();
467 for rq in &sendable {
468 let host = if let Some(h) = override_host {
469 h.to_string()
470 } else {
471 self.resolve_host_for_recipient(&rq.recipient).await.unwrap_or_else(|_| self.host().to_string())
472 };
473 recipient_hosts.insert(rq.recipient.clone(), host);
474 }
475
476 let needs_payment = sendable
478 .iter()
479 .any(|rq| rq.delivery_fee > 0 || rq.recipient_fee > 0);
480
481 let batch_payment = if needs_payment {
482 let pairs_for_payment: Vec<(String, i64, i64, String)> = sendable
484 .iter()
485 .map(|rq| {
486 let host = recipient_hosts.get(&rq.recipient).cloned().unwrap_or_else(|| self.host().to_string());
487 let agent_key = multi_quote.delivery_agent_identity_key_by_host.get(&host).cloned().unwrap_or_default();
488 (rq.recipient.clone(), rq.delivery_fee, rq.recipient_fee, agent_key)
489 })
490 .collect();
491
492 match self.create_message_payment_batch_from_tuples(&pairs_for_payment, None).await {
493 Ok(p) => Some(p),
494 Err(e) => {
495 let failed_entries: Vec<FailedRecipient> = sendable
497 .iter()
498 .map(|rq| FailedRecipient {
499 recipient: rq.recipient.clone(),
500 error: e.to_string(),
501 })
502 .collect();
503 return Ok(SendListResult {
504 status: "error".to_string(),
505 description: "Batch payment creation failed".to_string(),
506 sent: vec![],
507 blocked,
508 failed: failed_entries,
509 totals: None,
510 });
511 }
512 }
513 } else {
514 None
515 };
516
517 let mut sent: Vec<SentRecipient> = Vec::new();
519 let mut failed: Vec<FailedRecipient> = Vec::new();
520
521 for rq in &sendable {
522 let host = recipient_hosts
523 .get(&rq.recipient)
524 .cloned()
525 .unwrap_or_else(|| self.host().to_string());
526
527 match self
528 .send_message_to_host(
529 &host,
530 &rq.recipient,
531 ¶ms.message_box,
532 ¶ms.body,
533 skip_enc,
534 false, None,
536 batch_payment.clone(),
537 )
538 .await
539 {
540 Ok(msg_id) => sent.push(SentRecipient {
541 recipient: rq.recipient.clone(),
542 message_id: msg_id,
543 }),
544 Err(e) => failed.push(FailedRecipient {
545 recipient: rq.recipient.clone(),
546 error: e.to_string(),
547 }),
548 }
549 }
550
551 Ok(SendListResult {
552 status: "success".to_string(),
553 description: format!("Sent to {} recipients", sent.len()),
554 sent,
555 blocked,
556 failed,
557 totals: multi_quote.totals,
558 })
559 }
560
561 async fn create_message_payment_batch_from_tuples(
569 &self,
570 tuples: &[(String, i64, i64, String)],
571 description: Option<&str>,
572 ) -> Result<MessagePayment, MessageBoxError> {
573 use bsv::wallet::interfaces::{
574 CreateActionArgs, CreateActionOptions, CreateActionOutput, GetPublicKeyArgs,
575 };
576 use bsv::wallet::types::{BooleanDefaultTrue, Counterparty, CounterpartyType, Protocol};
577 use bsv::primitives::public_key::PublicKey;
578 use bsv::primitives::utils::from_hex;
579 use bsv::script::templates::{P2PKH, ScriptTemplateLock};
580 use bsv::wallet::proto_wallet::ProtoWallet;
581 use base64::Engine;
582
583 let desc = description.unwrap_or("MessageBox batch delivery fee");
584 let sender_identity_key = self.get_identity_key().await?;
585 let anyone_wallet = ProtoWallet::anyone();
586 let anyone_id = anyone_wallet
587 .get_public_key(
588 GetPublicKeyArgs {
589 identity_key: true,
590 protocol_id: None,
591 key_id: None,
592 counterparty: None,
593 privileged: false,
594 privileged_reason: None,
595 for_self: None,
596 seek_permission: None,
597 },
598 None,
599 )
600 .await
601 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
602 let anyone_id_hex = anyone_id.public_key.to_der_hex();
603
604 let mut outputs: Vec<CreateActionOutput> = Vec::new();
605 let mut payment_outputs: Vec<MessagePaymentOutput> = Vec::new();
606
607 for (recipient, delivery_fee, recipient_fee, agent_key) in tuples {
608 if *delivery_fee > 0 && !agent_key.is_empty() {
610 let prefix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
611 let suffix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
612 let prefix = base64::engine::general_purpose::STANDARD.encode(&prefix_bytes);
613 let suffix = base64::engine::general_purpose::STANDARD.encode(&suffix_bytes);
614
615 let agent_pk = PublicKey::from_string(agent_key)
616 .map_err(|e| MessageBoxError::Wallet(format!("agent key: {e}")))?;
617
618 let key = self
619 .wallet()
620 .get_public_key(
621 GetPublicKeyArgs {
622 identity_key: false,
623 protocol_id: Some(Protocol {
624 security_level: 2,
625 protocol: "3241645161d8".to_string(),
626 }),
627 key_id: Some(format!("{prefix} {suffix}")),
628 counterparty: Some(Counterparty {
629 counterparty_type: CounterpartyType::Other,
630 public_key: Some(agent_pk),
631 }),
632 privileged: false,
633 privileged_reason: None,
634 for_self: None,
635 seek_permission: None,
636 },
637 self.originator(),
638 )
639 .await
640 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
641
642 let hash_vec = key.public_key.to_hash();
643 let mut hash = [0u8; 20];
644 hash.copy_from_slice(&hash_vec);
645 let lock = P2PKH::from_public_key_hash(hash)
646 .lock()
647 .map_err(|e| MessageBoxError::Wallet(format!("P2PKH lock: {e}")))?;
648 let lock_bytes = from_hex(&lock.to_hex())
649 .map_err(|e| MessageBoxError::Wallet(format!("hex decode: {e}")))?;
650
651 let output_index = outputs.len() as u32;
652 outputs.push(CreateActionOutput {
653 locking_script: Some(lock_bytes),
654 satoshis: *delivery_fee as u64,
655 output_description: format!("Delivery fee for {}", recipient),
656 basket: None,
657 custom_instructions: None,
658 tags: vec![],
659 });
660 payment_outputs.push(MessagePaymentOutput {
661 output_index,
662 derivation_prefix: prefix.as_bytes().to_vec(),
663 derivation_suffix: suffix.as_bytes().to_vec(),
664 sender_identity_key: sender_identity_key.clone(),
665 });
666 }
667
668 if *recipient_fee > 0 {
670 let prefix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
671 let suffix_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
672 let prefix = base64::engine::general_purpose::STANDARD.encode(&prefix_bytes);
673 let suffix = base64::engine::general_purpose::STANDARD.encode(&suffix_bytes);
674
675 let recipient_pk = PublicKey::from_string(recipient)
676 .map_err(|e| MessageBoxError::Wallet(format!("recipient key: {e}")))?;
677
678 let key = anyone_wallet
680 .get_public_key(
681 GetPublicKeyArgs {
682 identity_key: false,
683 protocol_id: Some(Protocol {
684 security_level: 2,
685 protocol: "3241645161d8".to_string(),
686 }),
687 key_id: Some(format!("{prefix} {suffix}")),
688 counterparty: Some(Counterparty {
689 counterparty_type: CounterpartyType::Other,
690 public_key: Some(recipient_pk),
691 }),
692 privileged: false,
693 privileged_reason: None,
694 for_self: None,
695 seek_permission: None,
696 },
697 None,
698 )
699 .await
700 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
701
702 let hash_vec = key.public_key.to_hash();
703 let mut hash = [0u8; 20];
704 hash.copy_from_slice(&hash_vec);
705 let lock = P2PKH::from_public_key_hash(hash)
706 .lock()
707 .map_err(|e| MessageBoxError::Wallet(format!("P2PKH lock: {e}")))?;
708 let lock_bytes = from_hex(&lock.to_hex())
709 .map_err(|e| MessageBoxError::Wallet(format!("hex decode: {e}")))?;
710
711 let output_index = outputs.len() as u32;
712 outputs.push(CreateActionOutput {
713 locking_script: Some(lock_bytes),
714 satoshis: *recipient_fee as u64,
715 output_description: format!("Recipient fee for {}", recipient),
716 basket: None,
717 custom_instructions: None,
718 tags: vec![],
719 });
720 payment_outputs.push(MessagePaymentOutput {
721 output_index,
722 derivation_prefix: prefix.as_bytes().to_vec(),
723 derivation_suffix: suffix.as_bytes().to_vec(),
724 sender_identity_key: anyone_id_hex.clone(),
725 });
726 }
727 }
728
729 if outputs.is_empty() {
730 return Ok(MessagePayment { tx: vec![], outputs: vec![] });
731 }
732
733 let create_result = self
734 .wallet()
735 .create_action(
736 CreateActionArgs {
737 description: desc.to_string(),
738 input_beef: None,
739 inputs: vec![],
740 outputs,
741 lock_time: None,
742 version: None,
743 labels: vec!["messagebox".to_string()],
744 options: Some(CreateActionOptions {
745 randomize_outputs: BooleanDefaultTrue(Some(false)),
746 ..Default::default()
747 }),
748 reference: None,
749 },
750 self.originator(),
751 )
752 .await
753 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
754
755 let tx = create_result
756 .tx
757 .ok_or_else(|| MessageBoxError::Wallet("create_action returned no tx".to_string()))?;
758
759 Ok(MessagePayment {
760 tx,
761 outputs: payment_outputs,
762 })
763 }
764
765 pub async fn list_messages_lite(
772 &self,
773 message_box: &str,
774 override_host: Option<&str>,
775 ) -> Result<Vec<ServerPeerMessage>, MessageBoxError> {
776 self.assert_initialized().await?;
777
778 let host = override_host.unwrap_or_else(|| self.host());
779 let params = ListMessagesParams {
780 message_box: message_box.to_string(),
781 };
782 let body_bytes = serde_json::to_vec(¶ms)?;
783 let url = format!("{host}/listMessages");
784 let response = self.post_json(&url, body_bytes).await?;
785 check_status_error(&response.body)?;
786
787 let mut list_response: ListMessagesResponse =
788 serde_json::from_slice(&response.body)?;
789
790 for msg in &mut list_response.messages {
793 let outcome = encryption::try_decrypt_message_typed(
797 self.wallet(),
798 &msg.body,
799 &msg.sender,
800 None, )
802 .await;
803 msg.authenticated_decrypt = outcome.is_authenticated();
804 msg.body = outcome.into_body();
805 }
806
807 Ok(list_response.messages)
808 }
809
810 pub async fn list_messages(
826 &self,
827 message_box: &str,
828 accept_payments: bool,
829 override_host: Option<&str>,
830 ) -> Result<Vec<PeerMessage>, MessageBoxError> {
831 self.assert_initialized().await?;
832
833 if let Some(host) = override_host {
835 return self.list_messages_from_host(host, message_box, accept_payments).await;
836 }
837
838 let identity_key = self.get_identity_key().await?;
840 let ads = self.query_advertisements(Some(&identity_key), None).await.unwrap_or_default();
841
842 let mut host_set: HashSet<String> = ads.into_iter().map(|ad| ad.host).collect();
844 host_set.insert(self.host().to_string());
845
846 if host_set.len() == 1 {
847 return self.list_messages_from_host(self.host(), message_box, accept_payments).await;
849 }
850
851 let futures: Vec<_> = host_set
853 .iter()
854 .map(|h| self.list_messages_from_host(h, message_box, accept_payments))
855 .collect();
856
857 let outcomes = join_all(futures).await;
858 let successful: Vec<Vec<PeerMessage>> = outcomes
859 .into_iter()
860 .filter_map(|r| r.ok())
861 .collect();
862
863 if successful.is_empty() {
864 return Err(MessageBoxError::Http(0, format!("list_messages: all {} hosts failed", host_set.len())));
865 }
866
867 Ok(dedup_messages(successful))
868 }
869
870 async fn list_messages_from_host(
875 &self,
876 host: &str,
877 message_box: &str,
878 accept_payments: bool,
879 ) -> Result<Vec<PeerMessage>, MessageBoxError> {
880 let identity_key = self.get_identity_key().await?;
882
883 let params = ListMessagesParams {
884 message_box: message_box.to_string(),
885 };
886 let body_bytes = serde_json::to_vec(¶ms)?;
887 let url = format!("{host}/listMessages");
888 let response = self.post_json(&url, body_bytes).await?;
889 check_status_error(&response.body)?;
890
891 let list_response: ListMessagesResponse = serde_json::from_slice(&response.body)?;
892
893 let mut result = Vec::with_capacity(list_response.messages.len());
894 for msg in list_response.messages {
895 let plain_body: String = if let Ok(wrapped) = serde_json::from_str::<WrappedMessageBody>(&msg.body) {
897 if accept_payments {
899 if let Some(payment) = &wrapped.payment {
900 if let Some(tx_bytes) = &payment.tx {
901 let description = payment
902 .description
903 .clone()
904 .unwrap_or_else(|| "Server delivery fee".to_string());
905
906 let outputs: Vec<InternalizeOutput> = payment
909 .outputs
910 .as_deref()
911 .unwrap_or(&[])
912 .iter()
913 .filter_map(|o| {
914 if o.protocol.as_deref() != Some("wallet payment") && o.protocol.is_some() {
916 return None;
917 }
918 let sender_pk = o.sender_identity_key
920 .as_deref()
921 .and_then(|k| PublicKey::from_string(k).ok())?;
922 Some(InternalizeOutput::WalletPayment {
923 output_index: o.output_index.unwrap_or(0),
924 payment: Payment {
925 derivation_prefix: o.derivation_prefix.clone().unwrap_or_default(),
926 derivation_suffix: o.derivation_suffix.clone().unwrap_or_default(),
927 sender_identity_key: sender_pk,
928 },
929 })
930 })
931 .collect();
932
933 let args = InternalizeActionArgs {
934 tx: tx_bytes.clone(),
935 description,
936 labels: vec!["server-delivery-fee".to_string()],
937 seek_permission: BooleanDefaultTrue(Some(false)),
938 outputs,
939 };
940 let _ = self.wallet().internalize_action(args, self.originator()).await;
942 }
943 }
944 }
945
946 match wrapped.message {
948 Some(serde_json::Value::String(s)) => s,
949 Some(v) => v.to_string(),
950 None => msg.body.clone(),
951 }
952 } else {
953 msg.body.clone()
955 };
956
957 let decrypted = encryption::try_decrypt_message(
959 self.wallet(),
960 &plain_body,
961 &msg.sender,
962 self.originator(),
963 )
964 .await;
965
966 result.push(PeerMessage {
967 message_id: msg.message_id,
968 sender: msg.sender,
969 recipient: identity_key.clone(),
970 message_box: message_box.to_string(),
971 body: decrypted,
972 });
973 }
974
975 Ok(result)
976 }
977
978 pub async fn acknowledge_message(
984 &self,
985 message_ids: Vec<String>,
986 override_host: Option<&str>,
987 ) -> Result<(), MessageBoxError> {
988 self.assert_initialized().await?;
989
990 if let Some(host) = override_host {
991 return self.acknowledge_message_on_host(host, &message_ids).await;
992 }
993
994 let identity_key = self.get_identity_key().await?;
996 let ads = self.query_advertisements(Some(&identity_key), None).await.unwrap_or_default();
997
998 let mut host_set: HashSet<String> = ads.into_iter().map(|ad| ad.host).collect();
999 host_set.insert(self.host().to_string());
1000
1001 if host_set.len() == 1 {
1002 return self.acknowledge_message_on_host(self.host(), &message_ids).await;
1003 }
1004
1005 let futures: Vec<_> = host_set
1007 .iter()
1008 .map(|h| self.acknowledge_message_on_host(h, &message_ids))
1009 .collect();
1010
1011 let outcomes = join_all(futures).await;
1012 let any_ok = outcomes.iter().any(|r| r.is_ok());
1013
1014 if any_ok {
1015 Ok(())
1016 } else {
1017 Err(MessageBoxError::Http(0, format!("acknowledge_message: all {} hosts failed", host_set.len())))
1018 }
1019 }
1020
1021 async fn acknowledge_message_on_host(
1023 &self,
1024 host: &str,
1025 message_ids: &[String],
1026 ) -> Result<(), MessageBoxError> {
1027 let params = AcknowledgeMessageParams {
1028 message_ids: message_ids.to_vec(),
1029 };
1030 let body_bytes = serde_json::to_vec(¶ms)?;
1031 let url = format!("{host}/acknowledgeMessage");
1032 let response = self.post_json(&url, body_bytes).await?;
1033 check_status_error(&response.body)?;
1034 Ok(())
1035 }
1036}
1037
1038#[cfg(test)]
1043mod tests {
1044 use crate::encryption::generate_message_id;
1045 use crate::types::{
1046 AcknowledgeMessageParams, ListMessagesResponse, SendMessageParams, SendMessageRequest,
1047 };
1048 use bsv::primitives::private_key::PrivateKey;
1049 use bsv::wallet::error::WalletError;
1050 use bsv::wallet::interfaces::*;
1051 use bsv::wallet::proto_wallet::ProtoWallet;
1052 use std::sync::Arc;
1053
1054 #[derive(Clone)]
1056 struct ArcWallet(Arc<ProtoWallet>);
1057
1058 impl ArcWallet {
1059 fn new() -> Self {
1060 let key = PrivateKey::from_random().expect("random key");
1061 ArcWallet(Arc::new(ProtoWallet::new(key)))
1062 }
1063 }
1064
1065 #[async_trait::async_trait]
1066 impl WalletInterface for ArcWallet {
1067 async fn create_action(&self, args: CreateActionArgs, orig: Option<&str>) -> Result<CreateActionResult, WalletError> { self.0.create_action(args, orig).await }
1068 async fn sign_action(&self, args: SignActionArgs, orig: Option<&str>) -> Result<SignActionResult, WalletError> { self.0.sign_action(args, orig).await }
1069 async fn abort_action(&self, args: AbortActionArgs, orig: Option<&str>) -> Result<AbortActionResult, WalletError> { self.0.abort_action(args, orig).await }
1070 async fn list_actions(&self, args: ListActionsArgs, orig: Option<&str>) -> Result<ListActionsResult, WalletError> { self.0.list_actions(args, orig).await }
1071 async fn internalize_action(&self, args: InternalizeActionArgs, orig: Option<&str>) -> Result<InternalizeActionResult, WalletError> { self.0.internalize_action(args, orig).await }
1072 async fn list_outputs(&self, args: ListOutputsArgs, orig: Option<&str>) -> Result<ListOutputsResult, WalletError> { self.0.list_outputs(args, orig).await }
1073 async fn relinquish_output(&self, args: RelinquishOutputArgs, orig: Option<&str>) -> Result<RelinquishOutputResult, WalletError> { self.0.relinquish_output(args, orig).await }
1074 async fn get_public_key(&self, args: GetPublicKeyArgs, orig: Option<&str>) -> Result<GetPublicKeyResult, WalletError> { self.0.get_public_key(args, orig).await }
1075 async fn reveal_counterparty_key_linkage(&self, args: RevealCounterpartyKeyLinkageArgs, orig: Option<&str>) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> { self.0.reveal_counterparty_key_linkage(args, orig).await }
1076 async fn reveal_specific_key_linkage(&self, args: RevealSpecificKeyLinkageArgs, orig: Option<&str>) -> Result<RevealSpecificKeyLinkageResult, WalletError> { self.0.reveal_specific_key_linkage(args, orig).await }
1077 async fn encrypt(&self, args: EncryptArgs, orig: Option<&str>) -> Result<EncryptResult, WalletError> { self.0.encrypt(args, orig).await }
1078 async fn decrypt(&self, args: DecryptArgs, orig: Option<&str>) -> Result<DecryptResult, WalletError> { self.0.decrypt(args, orig).await }
1079 async fn create_hmac(&self, args: CreateHmacArgs, orig: Option<&str>) -> Result<CreateHmacResult, WalletError> { self.0.create_hmac(args, orig).await }
1080 async fn verify_hmac(&self, args: VerifyHmacArgs, orig: Option<&str>) -> Result<VerifyHmacResult, WalletError> { self.0.verify_hmac(args, orig).await }
1081 async fn create_signature(&self, args: CreateSignatureArgs, orig: Option<&str>) -> Result<CreateSignatureResult, WalletError> { self.0.create_signature(args, orig).await }
1082 async fn verify_signature(&self, args: VerifySignatureArgs, orig: Option<&str>) -> Result<VerifySignatureResult, WalletError> { self.0.verify_signature(args, orig).await }
1083 async fn acquire_certificate(&self, args: AcquireCertificateArgs, orig: Option<&str>) -> Result<Certificate, WalletError> { self.0.acquire_certificate(args, orig).await }
1084 async fn list_certificates(&self, args: ListCertificatesArgs, orig: Option<&str>) -> Result<ListCertificatesResult, WalletError> { self.0.list_certificates(args, orig).await }
1085 async fn prove_certificate(&self, args: ProveCertificateArgs, orig: Option<&str>) -> Result<ProveCertificateResult, WalletError> { self.0.prove_certificate(args, orig).await }
1086 async fn relinquish_certificate(&self, args: RelinquishCertificateArgs, orig: Option<&str>) -> Result<RelinquishCertificateResult, WalletError> { self.0.relinquish_certificate(args, orig).await }
1087 async fn discover_by_identity_key(&self, args: DiscoverByIdentityKeyArgs, orig: Option<&str>) -> Result<DiscoverCertificatesResult, WalletError> { self.0.discover_by_identity_key(args, orig).await }
1088 async fn discover_by_attributes(&self, args: DiscoverByAttributesArgs, orig: Option<&str>) -> Result<DiscoverCertificatesResult, WalletError> { self.0.discover_by_attributes(args, orig).await }
1089 async fn is_authenticated(&self, orig: Option<&str>) -> Result<AuthenticatedResult, WalletError> { self.0.is_authenticated(orig).await }
1090 async fn wait_for_authentication(&self, orig: Option<&str>) -> Result<AuthenticatedResult, WalletError> { self.0.wait_for_authentication(orig).await }
1091 async fn get_height(&self, orig: Option<&str>) -> Result<GetHeightResult, WalletError> { self.0.get_height(orig).await }
1092 async fn get_header_for_height(&self, args: GetHeaderArgs, orig: Option<&str>) -> Result<GetHeaderResult, WalletError> { self.0.get_header_for_height(args, orig).await }
1093 async fn get_network(&self, orig: Option<&str>) -> Result<GetNetworkResult, WalletError> { self.0.get_network(orig).await }
1094 async fn get_version(&self, orig: Option<&str>) -> Result<GetVersionResult, WalletError> { self.0.get_version(orig).await }
1095 }
1096
1097 #[test]
1103 fn test_send_message_request_format() {
1104 let req = SendMessageRequest {
1105 message: SendMessageParams {
1106 recipient: "03abc123".to_string(),
1107 message_box: "payment_inbox".to_string(),
1108 body: r#"{"encryptedMessage":"abc=="}"#.to_string(),
1109 message_id: "deadbeef01234567".to_string(),
1110 },
1111 payment: None,
1112 };
1113 let json = serde_json::to_string(&req).unwrap();
1114 assert!(json.starts_with(r#"{"message":"#), "must have message wrapper");
1116 assert!(json.contains("\"recipient\""), "camelCase recipient");
1117 assert!(json.contains("\"messageBox\""), "camelCase messageBox");
1118 assert!(json.contains("\"messageId\""), "camelCase messageId");
1119 assert!(json.contains("\"payment_inbox\""), "messageBox value preserved");
1120 assert!(!json.contains("message_box"), "no snake_case leakage");
1121 assert!(!json.contains("message_id"), "no snake_case leakage");
1122 }
1123
1124 #[test]
1126 fn test_acknowledge_request_format() {
1127 let params = AcknowledgeMessageParams {
1128 message_ids: vec!["id1".to_string(), "id2".to_string()],
1129 };
1130 let json = serde_json::to_string(¶ms).unwrap();
1131 assert_eq!(json, r#"{"messageIds":["id1","id2"]}"#);
1132 }
1133
1134 #[test]
1136 fn test_list_messages_response_parsing() {
1137 let raw = r#"{
1138 "status": "success",
1139 "messages": [
1140 {
1141 "messageId": "abc123",
1142 "body": "hello world",
1143 "sender": "03xyz",
1144 "created_at": "2024-01-01T00:00:00Z",
1145 "updated_at": "2024-01-01T00:01:00Z"
1146 }
1147 ]
1148 }"#;
1149 let resp: ListMessagesResponse = serde_json::from_str(raw).unwrap();
1150 assert_eq!(resp.status, "success");
1151 assert_eq!(resp.messages.len(), 1);
1152 assert_eq!(resp.messages[0].message_id, "abc123");
1153 assert_eq!(resp.messages[0].body, "hello world");
1154 assert_eq!(resp.messages[0].sender, "03xyz");
1155 }
1156
1157 #[tokio::test]
1163 async fn test_message_id_is_64_hex_chars() {
1164 let wallet = ArcWallet::new();
1165 let other = ArcWallet::new();
1167 let other_pk = other
1168 .get_public_key(
1169 GetPublicKeyArgs {
1170 identity_key: true,
1171 protocol_id: None,
1172 key_id: None,
1173 counterparty: None,
1174 privileged: false,
1175 privileged_reason: None,
1176 for_self: None,
1177 seek_permission: None,
1178 },
1179 None,
1180 )
1181 .await
1182 .expect("get_public_key")
1183 .public_key
1184 .to_der_hex();
1185
1186 let id = generate_message_id(&wallet, "test body", &other_pk, None)
1187 .await
1188 .expect("generate_message_id");
1189
1190 assert_eq!(id.len(), 64, "HMAC hex must be 64 chars (32 bytes)");
1191 assert!(
1192 id.chars().all(|c| c.is_ascii_hexdigit()),
1193 "all characters must be hex"
1194 );
1195 assert!(
1196 id.chars().all(|c| !c.is_uppercase()),
1197 "hex must be lowercase"
1198 );
1199 }
1200
1201 #[test]
1207 fn list_messages_parses_wrapped_body() {
1208 use super::WrappedMessageBody;
1209 let raw = r#"{"message": "hello world", "payment": {"tx": [1,2,3]}}"#;
1210 let wrapped: WrappedMessageBody = serde_json::from_str(raw).unwrap();
1211 assert!(wrapped.message.is_some(), "message sub-field must be present");
1212 assert!(wrapped.payment.is_some(), "payment sub-field must be present");
1213 let msg_val = wrapped.message.unwrap();
1215 assert_eq!(msg_val.as_str().unwrap(), "hello world");
1216 }
1217
1218 #[test]
1220 fn list_messages_plain_body_passthrough() {
1221 use super::WrappedMessageBody;
1222 let plain = "plain body text";
1224 let result = serde_json::from_str::<WrappedMessageBody>(plain);
1225 assert!(result.is_err(), "plain text must not parse as wrapped body");
1226 }
1227
1228 #[test]
1230 fn list_messages_missing_payment_no_crash() {
1231 use super::WrappedMessageBody;
1232 let raw = r#"{"message": "the content", "payment": null}"#;
1233 let wrapped: WrappedMessageBody = serde_json::from_str(raw).unwrap();
1234 assert!(wrapped.message.is_some(), "message present");
1235 assert!(wrapped.payment.is_none(), "payment is none when null");
1236 }
1237
1238 #[test]
1240 fn test_list_messages_dedup_by_id() {
1241 use super::dedup_messages;
1242 use bsv::remittance::types::PeerMessage;
1243
1244 let msg_a = PeerMessage {
1245 message_id: "id-1".to_string(),
1246 sender: "03sender".to_string(),
1247 recipient: "03me".to_string(),
1248 message_box: "inbox".to_string(),
1249 body: "first".to_string(),
1250 };
1251 let msg_a_dup = PeerMessage {
1252 message_id: "id-1".to_string(), sender: "03sender".to_string(),
1254 recipient: "03me".to_string(),
1255 message_box: "inbox".to_string(),
1256 body: "duplicate".to_string(), };
1258 let msg_b = PeerMessage {
1259 message_id: "id-2".to_string(),
1260 sender: "03sender".to_string(),
1261 recipient: "03me".to_string(),
1262 message_box: "inbox".to_string(),
1263 body: "second".to_string(),
1264 };
1265
1266 let results = vec![vec![msg_a.clone(), msg_b.clone()], vec![msg_a_dup]];
1268 let deduped = dedup_messages(results);
1269
1270 assert_eq!(deduped.len(), 2, "must deduplicate to 2 unique messages");
1271 let first = deduped.iter().find(|m| m.message_id == "id-1").unwrap();
1273 assert_eq!(first.body, "first", "first-seen must win on deduplication");
1274 }
1275
1276 #[tokio::test]
1278 async fn test_message_id_deterministic() {
1279 let wallet = ArcWallet::new();
1280 let other = ArcWallet::new();
1281 let other_pk = other
1282 .get_public_key(
1283 GetPublicKeyArgs {
1284 identity_key: true,
1285 protocol_id: None,
1286 key_id: None,
1287 counterparty: None,
1288 privileged: false,
1289 privileged_reason: None,
1290 for_self: None,
1291 seek_permission: None,
1292 },
1293 None,
1294 )
1295 .await
1296 .expect("get_public_key")
1297 .public_key
1298 .to_der_hex();
1299
1300 let id1 = generate_message_id(&wallet, "same body", &other_pk, None)
1301 .await
1302 .expect("first call");
1303 let id2 = generate_message_id(&wallet, "same body", &other_pk, None)
1304 .await
1305 .expect("second call");
1306
1307 assert_eq!(id1, id2, "same inputs must produce the same HMAC");
1308 }
1309}