1use bsv::wallet::interfaces::WalletInterface;
2use serde::Deserialize;
3
4use crate::client::{check_status_error, MessageBoxClient};
5use crate::error::MessageBoxError;
6use crate::types::{MessageBoxPermission, MessageBoxQuote, SetPermissionParams};
7
8#[derive(Deserialize)]
13struct GetPermissionResponse {
14 permission: Option<MessageBoxPermission>,
15}
16
17#[derive(Deserialize)]
18struct ListPermissionsResponse {
19 permissions: Vec<MessageBoxPermission>,
20}
21
22#[derive(Deserialize)]
23struct QuoteResponse {
24 quote: QuoteBody,
25}
26
27#[derive(Deserialize)]
28struct QuoteBody {
29 #[serde(rename = "recipientFee")]
30 recipient_fee: i64,
31 #[serde(rename = "deliveryFee")]
32 delivery_fee: i64,
33}
34
35impl<W: WalletInterface + Clone + 'static + Send + Sync> MessageBoxClient<W> {
40 pub async fn set_message_box_permission(
45 &self,
46 params: SetPermissionParams,
47 override_host: Option<&str>,
48 ) -> Result<(), MessageBoxError> {
49 self.assert_initialized().await?;
50
51 let base = override_host.unwrap_or_else(|| self.host());
52 let body_bytes = serde_json::to_vec(¶ms)?;
53 let url = format!("{base}/permissions/set");
54 let response = self.post_json(&url, body_bytes).await?;
55 check_status_error(&response.body)?;
56
57 Ok(())
58 }
59
60 pub async fn get_message_box_permission(
66 &self,
67 recipient: &str,
68 message_box: &str,
69 sender: Option<&str>,
70 override_host: Option<&str>,
71 ) -> Result<Option<MessageBoxPermission>, MessageBoxError> {
72 self.assert_initialized().await?;
73
74 let base = override_host.unwrap_or_else(|| self.host());
75 let mut url = format!(
77 "{base}/permissions/get?recipient={}&messageBox={}",
78 recipient,
79 message_box
80 );
81 if let Some(s) = sender {
82 url.push_str(&format!("&sender={s}"));
83 }
84
85 let response = self.get_json(&url).await?;
86 check_status_error(&response.body)?;
87
88 let parsed: GetPermissionResponse = serde_json::from_slice(&response.body)?;
89 Ok(parsed.permission)
90 }
91
92 pub async fn list_message_box_permissions(
98 &self,
99 message_box: Option<&str>,
100 limit: Option<u32>,
101 offset: Option<u32>,
102 override_host: Option<&str>,
103 ) -> Result<Vec<MessageBoxPermission>, MessageBoxError> {
104 self.assert_initialized().await?;
105
106 let base = override_host.unwrap_or_else(|| self.host());
107 let mut url = format!("{base}/permissions/list");
108 let mut params: Vec<String> = Vec::new();
109
110 if let Some(mb) = message_box {
112 params.push(format!("message_box={mb}"));
113 }
114 if let Some(l) = limit {
115 params.push(format!("limit={l}"));
116 }
117 if let Some(o) = offset {
118 params.push(format!("offset={o}"));
119 }
120
121 if !params.is_empty() {
122 url.push('?');
123 url.push_str(¶ms.join("&"));
124 }
125
126 let response = self.get_json(&url).await?;
127 check_status_error(&response.body)?;
128
129 let parsed: ListPermissionsResponse = serde_json::from_slice(&response.body)?;
130 Ok(parsed.permissions)
131 }
132
133 pub async fn get_message_box_quote(
139 &self,
140 recipient: &str,
141 message_box: &str,
142 override_host: Option<&str>,
143 ) -> Result<MessageBoxQuote, MessageBoxError> {
144 self.assert_initialized().await?;
145
146 let base = override_host.unwrap_or_else(|| self.host());
147 let url = format!(
149 "{base}/permissions/quote?recipient={}&messageBox={}",
150 recipient,
151 message_box
152 );
153
154 let response = self.get_json(&url).await?;
155 check_status_error(&response.body)?;
156
157 let delivery_agent_identity_key = response
160 .headers
161 .get("x-bsv-auth-identity-key")
162 .cloned()
163 .ok_or_else(|| {
164 MessageBoxError::MissingHeader("x-bsv-auth-identity-key".into())
165 })?;
166
167 let parsed: QuoteResponse = serde_json::from_slice(&response.body)?;
168
169 Ok(MessageBoxQuote {
170 delivery_fee: parsed.quote.delivery_fee,
171 recipient_fee: parsed.quote.recipient_fee,
172 delivery_agent_identity_key,
173 })
174 }
175
176 pub async fn allow_notifications_from_peer(
183 &self,
184 sender: &str,
185 recipient_fee: i64,
186 override_host: Option<&str>,
187 ) -> Result<(), MessageBoxError> {
188 self.set_message_box_permission(
189 SetPermissionParams {
190 message_box: "notifications".to_string(),
191 sender: Some(sender.to_string()),
192 recipient_fee,
193 },
194 override_host,
195 )
196 .await
197 }
198
199 pub async fn deny_notifications_from_peer(
202 &self,
203 sender: &str,
204 override_host: Option<&str>,
205 ) -> Result<(), MessageBoxError> {
206 self.set_message_box_permission(
207 SetPermissionParams {
208 message_box: "notifications".to_string(),
209 sender: Some(sender.to_string()),
210 recipient_fee: -1,
211 },
212 override_host,
213 )
214 .await
215 }
216
217 pub async fn check_peer_notification_status(
223 &self,
224 peer: &str,
225 override_host: Option<&str>,
226 ) -> Result<Option<MessageBoxPermission>, MessageBoxError> {
227 let recipient = self.get_identity_key().await?;
228 self.get_message_box_permission(&recipient, "notifications", Some(peer), override_host)
229 .await
230 }
231
232 pub async fn list_peer_notifications(
234 &self,
235 override_host: Option<&str>,
236 ) -> Result<Vec<MessageBoxPermission>, MessageBoxError> {
237 self.list_message_box_permissions(Some("notifications"), None, None, override_host)
238 .await
239 }
240
241 pub async fn send_notification(
247 &self,
248 recipient: &str,
249 body: &str,
250 override_host: Option<&str>,
251 ) -> Result<String, MessageBoxError> {
252 match override_host {
253 Some(host) => self.send_message_to_host(host, recipient, "notifications", body, false, true, None, None).await,
254 None => self.send_message(recipient, "notifications", body, false, true, None, None).await,
255 }
256 }
257
258 pub async fn get_message_box_quote_multi(
264 &self,
265 recipients: &[&str],
266 message_box: &str,
267 override_host: Option<&str>,
268 ) -> Result<crate::types::MessageBoxMultiQuote, MessageBoxError> {
269 use std::collections::HashMap;
270 use crate::types::{MessageBoxMultiQuote, RecipientQuote};
271
272 let mut quotes_by_recipient: Vec<RecipientQuote> = Vec::new();
273 let mut blocked_recipients: Vec<String> = Vec::new();
274 let mut delivery_agent_identity_key_by_host: HashMap<String, String> = HashMap::new();
275 let mut total_delivery_fee: i64 = 0;
276 let mut total_recipient_fee: i64 = 0;
277
278 for recipient in recipients {
279 let host = if let Some(h) = override_host {
281 h.to_string()
282 } else {
283 self.resolve_host_for_recipient(recipient).await.unwrap_or_else(|_| self.host().to_string())
284 };
285
286 let url = format!(
289 "{host}/permissions/quote?recipient={}&messageBox={}",
290 recipient,
291 message_box
292 );
293
294 let response = match self.get_json(&url).await {
295 Ok(r) => r,
296 Err(e) => {
297 blocked_recipients.push(recipient.to_string());
299 quotes_by_recipient.push(RecipientQuote {
300 recipient: recipient.to_string(),
301 message_box: message_box.to_string(),
302 delivery_fee: 0,
303 recipient_fee: 0,
304 status: format!("error: {e}"),
305 });
306 continue;
307 }
308 };
309
310 if let Some(key) = response.headers.get("x-bsv-auth-identity-key") {
312 delivery_agent_identity_key_by_host.insert(host.clone(), key.clone());
313 }
314
315 let parsed = match serde_json::from_slice::<QuoteResponse>(&response.body) {
316 Ok(p) => p,
317 Err(_) => {
318 blocked_recipients.push(recipient.to_string());
319 quotes_by_recipient.push(RecipientQuote {
320 recipient: recipient.to_string(),
321 message_box: message_box.to_string(),
322 delivery_fee: 0,
323 recipient_fee: 0,
324 status: "parse_error".to_string(),
325 });
326 continue;
327 }
328 };
329
330 let delivery_fee = parsed.quote.delivery_fee;
331 let recipient_fee = parsed.quote.recipient_fee;
332 let status = if recipient_fee < 0 {
333 "blocked".to_string()
334 } else if recipient_fee == 0 && delivery_fee == 0 {
335 "always_allow".to_string()
336 } else {
337 "payment_required".to_string()
338 };
339
340 if recipient_fee < 0 {
341 blocked_recipients.push(recipient.to_string());
342 } else {
343 total_delivery_fee += delivery_fee;
344 total_recipient_fee += recipient_fee;
345 }
346
347 quotes_by_recipient.push(RecipientQuote {
348 recipient: recipient.to_string(),
349 message_box: message_box.to_string(),
350 delivery_fee,
351 recipient_fee,
352 status,
353 });
354 }
355
356 Ok(MessageBoxMultiQuote {
357 quotes_by_recipient,
358 totals: Some(crate::types::SendListTotals {
359 delivery_fees: total_delivery_fee,
360 recipient_fees: total_recipient_fee,
361 total_for_payable_recipients: total_delivery_fee + total_recipient_fee,
362 }),
363 blocked_recipients,
364 delivery_agent_identity_key_by_host,
365 })
366 }
367
368 pub async fn send_notification_to_recipients(
373 &self,
374 recipients: &[&str],
375 body: &str,
376 override_host: Option<&str>,
377 ) -> Result<crate::types::SendListResult, MessageBoxError> {
378 use crate::types::SendListParams;
379
380 let params = SendListParams {
381 recipients: recipients.iter().map(|s| s.to_string()).collect(),
382 message_box: "notifications".to_string(),
383 body: body.to_string(),
384 skip_encryption: Some(false),
385 };
386
387 self.send_message_to_recipients(¶ms, override_host).await
388 }
389}
390
391#[cfg(test)]
396mod tests {
397 use super::*;
398 use crate::types::{MessageBoxPermission, MessageBoxQuote, SetPermissionParams};
399 use bsv::primitives::private_key::PrivateKey;
400 use bsv::wallet::error::WalletError;
401 use bsv::wallet::interfaces::*;
402 use bsv::wallet::proto_wallet::ProtoWallet;
403 use std::sync::Arc;
404
405 #[derive(Clone)]
410 struct ArcWallet(Arc<ProtoWallet>);
411
412 impl ArcWallet {
413 fn new() -> Self {
414 let key = PrivateKey::from_random().expect("random key");
415 ArcWallet(Arc::new(ProtoWallet::new(key)))
416 }
417 }
418
419 #[async_trait::async_trait]
420 impl WalletInterface for ArcWallet {
421 async fn create_action(&self, args: CreateActionArgs, orig: Option<&str>) -> Result<CreateActionResult, WalletError> { self.0.create_action(args, orig).await }
422 async fn sign_action(&self, args: SignActionArgs, orig: Option<&str>) -> Result<SignActionResult, WalletError> { self.0.sign_action(args, orig).await }
423 async fn abort_action(&self, args: AbortActionArgs, orig: Option<&str>) -> Result<AbortActionResult, WalletError> { self.0.abort_action(args, orig).await }
424 async fn list_actions(&self, args: ListActionsArgs, orig: Option<&str>) -> Result<ListActionsResult, WalletError> { self.0.list_actions(args, orig).await }
425 async fn internalize_action(&self, args: InternalizeActionArgs, orig: Option<&str>) -> Result<InternalizeActionResult, WalletError> { self.0.internalize_action(args, orig).await }
426 async fn list_outputs(&self, args: ListOutputsArgs, orig: Option<&str>) -> Result<ListOutputsResult, WalletError> { self.0.list_outputs(args, orig).await }
427 async fn relinquish_output(&self, args: RelinquishOutputArgs, orig: Option<&str>) -> Result<RelinquishOutputResult, WalletError> { self.0.relinquish_output(args, orig).await }
428 async fn get_public_key(&self, args: GetPublicKeyArgs, orig: Option<&str>) -> Result<GetPublicKeyResult, WalletError> { self.0.get_public_key(args, orig).await }
429 async fn reveal_counterparty_key_linkage(&self, args: RevealCounterpartyKeyLinkageArgs, orig: Option<&str>) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> { self.0.reveal_counterparty_key_linkage(args, orig).await }
430 async fn reveal_specific_key_linkage(&self, args: RevealSpecificKeyLinkageArgs, orig: Option<&str>) -> Result<RevealSpecificKeyLinkageResult, WalletError> { self.0.reveal_specific_key_linkage(args, orig).await }
431 async fn encrypt(&self, args: EncryptArgs, orig: Option<&str>) -> Result<EncryptResult, WalletError> { self.0.encrypt(args, orig).await }
432 async fn decrypt(&self, args: DecryptArgs, orig: Option<&str>) -> Result<DecryptResult, WalletError> { self.0.decrypt(args, orig).await }
433 async fn create_hmac(&self, args: CreateHmacArgs, orig: Option<&str>) -> Result<CreateHmacResult, WalletError> { self.0.create_hmac(args, orig).await }
434 async fn verify_hmac(&self, args: VerifyHmacArgs, orig: Option<&str>) -> Result<VerifyHmacResult, WalletError> { self.0.verify_hmac(args, orig).await }
435 async fn create_signature(&self, args: CreateSignatureArgs, orig: Option<&str>) -> Result<CreateSignatureResult, WalletError> { self.0.create_signature(args, orig).await }
436 async fn verify_signature(&self, args: VerifySignatureArgs, orig: Option<&str>) -> Result<VerifySignatureResult, WalletError> { self.0.verify_signature(args, orig).await }
437 async fn acquire_certificate(&self, args: AcquireCertificateArgs, orig: Option<&str>) -> Result<Certificate, WalletError> { self.0.acquire_certificate(args, orig).await }
438 async fn list_certificates(&self, args: ListCertificatesArgs, orig: Option<&str>) -> Result<ListCertificatesResult, WalletError> { self.0.list_certificates(args, orig).await }
439 async fn prove_certificate(&self, args: ProveCertificateArgs, orig: Option<&str>) -> Result<ProveCertificateResult, WalletError> { self.0.prove_certificate(args, orig).await }
440 async fn relinquish_certificate(&self, args: RelinquishCertificateArgs, orig: Option<&str>) -> Result<RelinquishCertificateResult, WalletError> { self.0.relinquish_certificate(args, orig).await }
441 async fn discover_by_identity_key(&self, args: DiscoverByIdentityKeyArgs, orig: Option<&str>) -> Result<DiscoverCertificatesResult, WalletError> { self.0.discover_by_identity_key(args, orig).await }
442 async fn discover_by_attributes(&self, args: DiscoverByAttributesArgs, orig: Option<&str>) -> Result<DiscoverCertificatesResult, WalletError> { self.0.discover_by_attributes(args, orig).await }
443 async fn is_authenticated(&self, orig: Option<&str>) -> Result<AuthenticatedResult, WalletError> { self.0.is_authenticated(orig).await }
444 async fn wait_for_authentication(&self, orig: Option<&str>) -> Result<AuthenticatedResult, WalletError> { self.0.wait_for_authentication(orig).await }
445 async fn get_height(&self, orig: Option<&str>) -> Result<GetHeightResult, WalletError> { self.0.get_height(orig).await }
446 async fn get_header_for_height(&self, args: GetHeaderArgs, orig: Option<&str>) -> Result<GetHeaderResult, WalletError> { self.0.get_header_for_height(args, orig).await }
447 async fn get_network(&self, orig: Option<&str>) -> Result<GetNetworkResult, WalletError> { self.0.get_network(orig).await }
448 async fn get_version(&self, orig: Option<&str>) -> Result<GetVersionResult, WalletError> { self.0.get_version(orig).await }
449 }
450
451 fn make_client(host: &str) -> MessageBoxClient<ArcWallet> {
452 MessageBoxClient::new(host.to_string(), ArcWallet::new(), None, bsv::services::overlay_tools::Network::Mainnet)
453 }
454
455 #[test]
463 fn set_permission_post_body_is_camel_case() {
464 let params = SetPermissionParams {
465 message_box: "payment_inbox".to_string(),
466 sender: Some("03abc".to_string()),
467 recipient_fee: 100,
468 };
469 let json = serde_json::to_string(¶ms).unwrap();
470 assert!(json.contains("\"messageBox\""), "messageBox must be camelCase");
471 assert!(json.contains("\"recipientFee\""), "recipientFee must be camelCase");
472 assert!(json.contains("\"sender\""), "sender must be present when Some");
473 assert!(!json.contains("message_box"), "no snake_case leakage");
474 assert!(!json.contains("recipient_fee"), "no snake_case leakage");
475 }
476
477 #[test]
481 fn get_permission_url_uses_camel_case_query_params() {
482 let host = "https://example.com";
484 let recipient = "03recipient";
485 let message_box = "inbox";
486 let sender = Some("03sender");
487
488 let mut url = format!(
489 "{}/permissions/get?recipient={}&messageBox={}",
490 host, recipient, message_box
491 );
492 if let Some(s) = sender {
493 url.push_str(&format!("&sender={s}"));
494 }
495
496 assert!(url.contains("messageBox=inbox"), "must use camelCase messageBox");
497 assert!(!url.contains("message_box"), "must not use snake_case");
498 assert!(url.contains("recipient=03recipient"), "recipient param present");
499 assert!(url.contains("sender=03sender"), "sender param present when Some");
500 }
501
502 #[test]
504 fn get_permission_url_omits_sender_when_none() {
505 let host = "https://example.com";
506 let recipient = "03recipient";
507 let message_box = "inbox";
508 let sender: Option<&str> = None;
509
510 let mut url = format!(
511 "{}/permissions/get?recipient={}&messageBox={}",
512 host, recipient, message_box
513 );
514 if let Some(s) = sender {
515 url.push_str(&format!("&sender={s}"));
516 }
517
518 assert!(!url.contains("sender"), "sender param absent when None");
519 assert!(url.contains("messageBox=inbox"), "messageBox present");
520 }
521
522 #[test]
527 fn list_permissions_url_uses_snake_case_message_box_param() {
528 let host = "https://example.com";
529 let message_box = Some("notifications");
530
531 let mut url = format!("{}/permissions/list", host);
532 let mut params: Vec<String> = Vec::new();
533
534 if let Some(mb) = message_box {
536 params.push(format!("message_box={mb}"));
537 }
538 if !params.is_empty() {
539 url.push('?');
540 url.push_str(¶ms.join("&"));
541 }
542
543 assert!(
544 url.contains("message_box=notifications"),
545 "must use snake_case message_box key: {}",
546 url
547 );
548 assert!(
549 !url.contains("messageBox"),
550 "must NOT use camelCase messageBox in list endpoint: {}",
551 url
552 );
553 }
554
555 #[test]
557 fn quote_url_uses_camel_case_message_box_param() {
558 let host = "https://example.com";
559 let url = format!(
560 "{}/permissions/quote?recipient=03r&messageBox=inbox",
561 host
562 );
563 assert!(url.contains("messageBox=inbox"), "quote endpoint uses camelCase");
564 assert!(!url.contains("message_box"), "not snake_case");
565 }
566
567 #[test]
575 fn missing_header_produces_missing_header_error() {
576 use std::collections::HashMap;
578 let headers: HashMap<String, String> = HashMap::new();
579
580 let result = headers
581 .get("x-bsv-auth-identity-key")
582 .cloned()
583 .ok_or_else(|| MessageBoxError::MissingHeader("x-bsv-auth-identity-key".into()));
584
585 assert!(result.is_err(), "must error when header absent");
586 assert!(
587 matches!(result.unwrap_err(), MessageBoxError::MissingHeader(_)),
588 "error must be MissingHeader variant"
589 );
590 }
591
592 #[test]
594 fn present_header_is_extracted_correctly() {
595 use std::collections::HashMap;
596 let mut headers: HashMap<String, String> = HashMap::new();
597 headers.insert(
598 "x-bsv-auth-identity-key".to_string(),
599 "03deadbeef".to_string(),
600 );
601
602 let result: Result<String, MessageBoxError> = headers
603 .get("x-bsv-auth-identity-key")
604 .cloned()
605 .ok_or_else(|| MessageBoxError::MissingHeader("x-bsv-auth-identity-key".into()));
606
607 assert!(result.is_ok());
608 assert_eq!(result.unwrap(), "03deadbeef");
609 }
610
611 #[test]
618 fn get_permission_response_parses_wrapped_body() {
619 let raw = r#"{"permission": {"messageBox": "inbox", "recipientFee": 0, "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z"}}"#;
620 let parsed: GetPermissionResponse = serde_json::from_str(raw).unwrap();
621 let perm = parsed.permission.unwrap();
622 assert_eq!(perm.message_box, "inbox");
623 assert_eq!(perm.recipient_fee, 0);
624 }
625
626 #[test]
628 fn get_permission_response_parses_null_as_none() {
629 let raw = r#"{"permission": null}"#;
630 let parsed: GetPermissionResponse = serde_json::from_str(raw).unwrap();
631 assert!(parsed.permission.is_none());
632 }
633
634 #[test]
636 fn list_permissions_response_parses_wrapped_body() {
637 let raw = r#"{"permissions": [{"message_box": "inbox", "recipient_fee": 100, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z"}]}"#;
639 let parsed: ListPermissionsResponse = serde_json::from_str(raw).unwrap();
640 assert_eq!(parsed.permissions.len(), 1);
641 assert_eq!(parsed.permissions[0].message_box, "inbox");
642 assert_eq!(parsed.permissions[0].recipient_fee, 100);
643 }
644
645 #[test]
647 fn quote_response_parses_wrapped_body() {
648 let raw = r#"{"quote": {"recipientFee": 50, "deliveryFee": 10}}"#;
649 let parsed: QuoteResponse = serde_json::from_str(raw).unwrap();
650 assert_eq!(parsed.quote.recipient_fee, 50);
651 assert_eq!(parsed.quote.delivery_fee, 10);
652 }
653
654 #[test]
660 fn allow_notifications_params_uses_notifications_box() {
661 let params = SetPermissionParams {
663 message_box: "notifications".to_string(),
664 sender: Some("03peer".to_string()),
665 recipient_fee: 0,
666 };
667 assert_eq!(params.message_box, "notifications");
668 assert_eq!(params.sender, Some("03peer".to_string()));
669 }
670
671 #[test]
673 fn deny_notifications_params_uses_negative_one_fee() {
674 let params = SetPermissionParams {
676 message_box: "notifications".to_string(),
677 sender: Some("03peer".to_string()),
678 recipient_fee: -1,
679 };
680 assert_eq!(params.recipient_fee, -1);
681 assert_eq!(params.message_box, "notifications");
682 }
683
684 #[test]
687 fn deny_fee_produces_blocked_status() {
688 let perm = MessageBoxPermission {
689 sender: Some("03peer".to_string()),
690 message_box: "notifications".to_string(),
691 recipient_fee: -1,
692 created_at: "2024-01-01".to_string(),
693 updated_at: "2024-01-01".to_string(),
694 };
695 assert_eq!(perm.status(), "blocked");
696 }
697
698 #[test]
700 fn quote_constructed_from_header_and_body() {
701 let quote = MessageBoxQuote {
702 delivery_fee: 10,
703 recipient_fee: 50,
704 delivery_agent_identity_key: "03agent".to_string(),
705 };
706 assert_eq!(quote.delivery_fee, 10);
707 assert_eq!(quote.recipient_fee, 50);
708 assert_eq!(quote.delivery_agent_identity_key, "03agent");
709 }
710
711 #[test]
713 fn client_with_permissions_compiles() {
714 let _client = make_client("https://example.com");
715 }
716}