1use std::collections::HashMap;
14
15use bsv::script::locking_script::LockingScript;
16use bsv::script::op::Op;
17use bsv::script::script::Script;
18use bsv::script::script_chunk::ScriptChunk;
19use bsv::script::templates::PushDrop;
20use bsv::services::overlay_tools::{
21 LookupResolver, LookupResolverConfig, TopicBroadcaster, TopicBroadcasterConfig,
22};
23use bsv::services::overlay_tools::{LookupAnswer, LookupQuestion};
24use bsv::transaction::Transaction;
25use bsv::wallet::interfaces::{
26 CreateActionArgs, CreateActionInput, CreateActionOptions, CreateActionOutput, GetPublicKeyArgs,
27 SignActionArgs, SignActionSpend, WalletInterface,
28};
29use bsv::wallet::types::{
30 BooleanDefaultTrue, Counterparty, CounterpartyType, Protocol,
31};
32
33use crate::client::MessageBoxClient;
34use crate::error::MessageBoxError;
35use crate::types::{AdvertisementToken, ListDevicesResponse, RegisterDeviceRequest, RegisterDeviceResponse, RegisteredDevice};
36
37fn make_data_push(data: &[u8]) -> ScriptChunk {
49 let len = data.len();
50 if len < 0x4c {
51 ScriptChunk::new_raw(len as u8, Some(data.to_vec()))
52 } else if len < 256 {
53 ScriptChunk::new_raw(Op::OpPushData1.to_byte(), Some(data.to_vec()))
54 } else if len < 65536 {
55 ScriptChunk::new_raw(Op::OpPushData2.to_byte(), Some(data.to_vec()))
56 } else {
57 ScriptChunk::new_raw(Op::OpPushData4.to_byte(), Some(data.to_vec()))
58 }
59}
60
61
62impl<W: WalletInterface + Clone + 'static + Send + Sync> MessageBoxClient<W> {
67 pub async fn query_advertisements(
80 &self,
81 identity_key: Option<&str>,
82 host: Option<&str>,
83 ) -> Result<Vec<AdvertisementToken>, MessageBoxError> {
84 match self.query_advertisements_inner(identity_key, host).await {
86 Ok(tokens) => Ok(tokens),
87 Err(_) => Ok(vec![]),
88 }
89 }
90
91 async fn query_advertisements_inner(
93 &self,
94 identity_key: Option<&str>,
95 host: Option<&str>,
96 ) -> Result<Vec<AdvertisementToken>, MessageBoxError> {
97 let ik = match identity_key {
98 Some(k) => k.to_string(),
99 None => self.get_identity_key().await?,
100 };
101
102 let mut query_obj = serde_json::json!({ "identityKey": ik });
103 if let Some(h) = host {
104 let trimmed = h.trim();
105 if !trimmed.is_empty() {
106 query_obj["host"] = serde_json::Value::String(trimmed.to_string());
107 }
108 }
109
110 let question = LookupQuestion {
111 service: "ls_messagebox".to_string(),
112 query: query_obj,
113 };
114
115 let mut host_overrides = std::collections::HashMap::new();
120 let tracker_urls = self.network.default_slap_trackers();
121 host_overrides.insert("ls_messagebox".to_string(), tracker_urls);
122
123 let resolver = LookupResolver::new(LookupResolverConfig {
124 network: self.network.clone(),
125 host_overrides,
126 ..Default::default()
127 });
128
129 let answer = resolver
130 .query(&question, None)
131 .await
132 .map_err(|e| MessageBoxError::Overlay(e.to_string()))?;
133
134 let mut tokens = Vec::new();
135
136 if let LookupAnswer::OutputList { outputs } = answer {
137 for output in outputs {
138 let beef_hex = hex::encode(&output.beef);
140 let tx = match Transaction::from_beef(&beef_hex) {
141 Ok(t) => t,
142 Err(_) => continue,
143 };
144
145 let idx = output.output_index as usize;
146 if idx >= tx.outputs.len() {
147 continue;
148 }
149
150 let script = &tx.outputs[idx].locking_script;
151 let pd = match PushDrop::decode(script) {
152 Ok(t) => t,
153 Err(_) => continue,
154 };
155
156 if pd.fields.len() < 2 {
157 continue;
158 }
159
160 let host_url = match String::from_utf8(pd.fields[1].clone()) {
161 Ok(h) => h,
162 Err(_) => continue,
163 };
164
165 let txid = match tx.id() {
170 Ok(id) => id,
171 Err(_) => continue,
172 };
173
174 tokens.push(AdvertisementToken {
175 host: host_url,
176 txid,
177 output_index: output.output_index,
178 locking_script: script.to_hex(),
179 beef: output.beef,
180 });
181 }
182 }
183
184 Ok(tokens)
185 }
186
187 pub async fn resolve_host_for_recipient(
194 &self,
195 recipient: &str,
196 ) -> Result<String, MessageBoxError> {
197 let ads = self.query_advertisements(Some(recipient), None).await?;
198 if let Some(ad) = ads.into_iter().next() {
199 Ok(ad.host)
200 } else {
201 Ok(self.host().to_string())
202 }
203 }
204
205 pub async fn anoint_host(&self, host: &str) -> Result<String, MessageBoxError> {
214 let identity_key = self.get_identity_key().await?;
215
216 let pk_result = self
220 .wallet()
221 .get_public_key(
222 GetPublicKeyArgs {
223 identity_key: false,
224 protocol_id: Some(Protocol {
225 security_level: 1,
226 protocol: "messagebox advertisement".to_string(),
227 }),
228 key_id: Some("1".to_string()),
229 counterparty: Some(Counterparty {
230 counterparty_type: CounterpartyType::Anyone,
231 public_key: None,
232 }),
233 privileged: false,
234 privileged_reason: None,
235 for_self: Some(true),
236 seek_permission: None,
237 },
238 self.originator(),
239 )
240 .await
241 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
242
243 let pubkey_bytes = pk_result.public_key.to_der();
244
245 let id_key_bytes = hex::decode(&identity_key)
247 .map_err(|e| MessageBoxError::Overlay(format!("hex decode identity key: {e}")))?;
248 let host_bytes = host.as_bytes().to_vec();
249
250 let data_to_sign: Vec<u8> = [id_key_bytes.as_slice(), host_bytes.as_slice()].concat();
252 let sig_result = self
253 .wallet()
254 .create_signature(
255 bsv::wallet::interfaces::CreateSignatureArgs {
256 data: Some(data_to_sign),
257 hash_to_directly_sign: None,
258 protocol_id: Protocol {
259 security_level: 1,
260 protocol: "messagebox advertisement".to_string(),
261 },
262 key_id: "1".to_string(),
263 counterparty: Counterparty {
264 counterparty_type: CounterpartyType::Anyone,
265 public_key: None,
266 },
267 privileged: false,
268 privileged_reason: None,
269 seek_permission: None,
270 },
271 self.originator(),
272 )
273 .await
274 .map_err(|e| MessageBoxError::Overlay(format!("sign fields: {e}")))?;
275
276 let fields = vec![id_key_bytes, host_bytes, sig_result.signature];
283
284 use bsv::script::templates::ScriptTemplateLock;
288 let locking_script = {
289 let mut dummy_buf = [0u8; 32];
291 dummy_buf[31] = 1;
292 let dummy_key = bsv::primitives::private_key::PrivateKey::from_bytes(&dummy_buf)
293 .map_err(|e| MessageBoxError::Overlay(format!("dummy key: {e}")))?;
294 let pd = PushDrop::new(fields, dummy_key);
295 let script = pd.lock()
296 .map_err(|e| MessageBoxError::Overlay(format!("PushDrop lock: {e}")))?;
297
298 let mut chunks = script.chunks().to_vec();
300 chunks[0] = ScriptChunk::new_raw(
301 pubkey_bytes.len() as u8,
302 Some(pubkey_bytes),
303 );
304 LockingScript::from_script(Script::from_chunks(chunks))
305 };
306
307 let create_result = self
309 .wallet()
310 .create_action(
311 CreateActionArgs {
312 description: "Anoint host for overlay routing".to_string(),
313 input_beef: None,
314 inputs: vec![],
315 outputs: vec![CreateActionOutput {
316 locking_script: Some(locking_script.to_binary()),
317 satoshis: 1,
318 output_description: "Overlay advertisement output".to_string(),
319 basket: Some("overlay advertisements".to_string()),
320 custom_instructions: None,
321 tags: vec![],
322 }],
323 lock_time: None,
324 version: None,
325 labels: vec![],
326 options: Some(CreateActionOptions {
327 randomize_outputs: BooleanDefaultTrue(Some(false)),
329 accept_delayed_broadcast: BooleanDefaultTrue(Some(false)),
330 ..Default::default()
331 }),
332 reference: None,
333 },
334 self.originator(),
335 )
336 .await
337 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
338
339 let beef_bytes = create_result
343 .tx
344 .ok_or_else(|| MessageBoxError::Overlay("create_action returned no tx".into()))?;
345 let beef_hex = hex::encode(&beef_bytes);
346 let tx = Transaction::from_beef(&beef_hex)
347 .map_err(|e| MessageBoxError::Overlay(format!("parse BEEF: {e}")))?;
348 let txid = tx
349 .id()
350 .map_err(|e| MessageBoxError::Overlay(format!("tx.id(): {e}")))?;
351
352 let broadcaster = TopicBroadcaster::new(
357 vec!["tm_messagebox".to_string()],
358 TopicBroadcasterConfig {
359 network: self.network.clone(),
360 ..Default::default()
361 },
362 LookupResolver::new(LookupResolverConfig {
363 network: self.network.clone(),
364 ..Default::default()
365 }),
366 )
367 .map_err(|e| MessageBoxError::Overlay(format!("build broadcaster: {e}")))?;
368
369 broadcaster
370 .broadcast_beef(beef_bytes)
371 .await
372 .map_err(|e| MessageBoxError::Overlay(format!("broadcast failed: {}", e.description)))?;
373
374 Ok(txid)
375 }
376
377 pub async fn register_device(
384 &self,
385 fcm_token: &str,
386 device_id: Option<&str>,
387 platform: Option<&str>,
388 override_host: Option<&str>,
389 ) -> Result<RegisterDeviceResponse, MessageBoxError> {
390 self.assert_initialized().await?;
391
392 let base = override_host.unwrap_or_else(|| self.host());
393 let request = RegisterDeviceRequest {
394 fcm_token: fcm_token.to_string(),
395 device_id: device_id.map(String::from),
396 platform: platform.map(String::from),
397 };
398
399 let body_bytes = serde_json::to_vec(&request)
400 .map_err(|e| MessageBoxError::Overlay(format!("serialize RegisterDeviceRequest: {e}")))?;
401
402 let url = format!("{base}/registerDevice");
403 let response = self.post_json(&url, body_bytes).await?;
404
405 let resp: RegisterDeviceResponse = serde_json::from_slice(&response.body)
406 .map_err(|e| MessageBoxError::Overlay(format!("deserialize RegisterDeviceResponse: {e}")))?;
407
408 Ok(resp)
409 }
410
411 pub async fn list_registered_devices(
417 &self,
418 override_host: Option<&str>,
419 ) -> Result<Vec<RegisteredDevice>, MessageBoxError> {
420 self.assert_initialized().await?;
421
422 let base = override_host.unwrap_or_else(|| self.host());
423 let url = format!("{base}/devices");
424 let response = self.get_json(&url).await?;
425
426 let resp: ListDevicesResponse = serde_json::from_slice(&response.body)
427 .map_err(|e| MessageBoxError::Overlay(format!("deserialize ListDevicesResponse: {e}")))?;
428
429 Ok(resp.devices)
430 }
431
432 pub async fn revoke_host_advertisement(
444 &self,
445 token: &AdvertisementToken,
446 ) -> Result<String, MessageBoxError> {
447 let create_result = self
450 .wallet()
451 .create_action(
452 CreateActionArgs {
453 description: "Revoke MessageBox host advertisement".to_string(),
454 input_beef: Some(token.beef.clone()),
455 inputs: vec![CreateActionInput {
456 outpoint: format!("{}.{}", token.txid, token.output_index),
457 input_description: "Revoking host advertisement token".to_string(),
458 unlocking_script: None,
459 unlocking_script_length: Some(73),
460 sequence_number: None,
461 }],
462 outputs: vec![],
463 lock_time: None,
464 version: None,
465 labels: vec![],
466 options: Some(CreateActionOptions {
467 accept_delayed_broadcast: BooleanDefaultTrue(Some(false)),
468 ..Default::default()
469 }),
470 reference: None,
471 },
472 self.originator(),
473 )
474 .await
475 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
476
477 let signable = create_result.signable_transaction.ok_or_else(|| {
479 MessageBoxError::Overlay("create_action returned no signable_transaction".into())
480 })?;
481
482 let partial_tx = Transaction::from_beef(&hex::encode(&signable.tx))
484 .map_err(|e| MessageBoxError::Overlay(format!("parse signable tx: {e}")))?;
485
486 let lock_script = LockingScript::from_hex(&token.locking_script)
488 .map_err(|e| MessageBoxError::Overlay(format!("parse locking script hex: {e}")))?;
489
490 let sighash_type: u32 = 0x41;
492
493 let preimage = partial_tx
494 .sighash_preimage(0, sighash_type, 1, &lock_script)
495 .map_err(|e| MessageBoxError::Overlay(format!("sighash_preimage: {e}")))?;
496
497 let sig_result = self
500 .wallet()
501 .create_signature(
502 bsv::wallet::interfaces::CreateSignatureArgs {
503 protocol_id: Protocol {
504 security_level: 1,
505 protocol: "messagebox advertisement".to_string(),
506 },
507 key_id: "1".to_string(),
508 counterparty: Counterparty {
509 counterparty_type: CounterpartyType::Anyone,
510 public_key: None,
511 },
512 data: Some(preimage),
513 hash_to_directly_sign: None,
514 privileged: false,
515 privileged_reason: None,
516 seek_permission: None,
517 },
518 self.originator(),
519 )
520 .await
521 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
522
523 let mut sig_bytes = sig_result.signature;
525 sig_bytes.push(sighash_type as u8);
526 let unlock_chunks = vec![make_data_push(&sig_bytes)];
527 let unlock_script = Script::from_chunks(unlock_chunks);
528
529 let sign_result = self
531 .wallet()
532 .sign_action(
533 SignActionArgs {
534 reference: signable.reference,
535 spends: HashMap::from([(
536 0u32,
537 SignActionSpend {
538 unlocking_script: unlock_script.to_binary(),
539 sequence_number: None,
540 },
541 )]),
542 options: None,
543 },
544 self.originator(),
545 )
546 .await
547 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
548
549 let signed_bytes = sign_result
551 .tx
552 .ok_or_else(|| MessageBoxError::Overlay("sign_action returned no tx".into()))?;
553
554 let signed_tx = Transaction::from_beef(&hex::encode(&signed_bytes))
556 .map_err(|e| MessageBoxError::Overlay(format!("parse signed tx: {e}")))?;
557 let txid = signed_tx
558 .id()
559 .map_err(|e| MessageBoxError::Overlay(format!("signed_tx.id(): {e}")))?;
560
561 let broadcaster = TopicBroadcaster::new(
562 vec!["tm_messagebox".to_string()],
563 TopicBroadcasterConfig {
564 network: self.network.clone(),
565 ..Default::default()
566 },
567 LookupResolver::new(LookupResolverConfig {
568 network: self.network.clone(),
569 ..Default::default()
570 }),
571 )
572 .map_err(|e| MessageBoxError::Overlay(format!("build broadcaster: {e}")))?;
573
574 broadcaster
575 .broadcast_beef(signed_bytes)
576 .await
577 .map_err(|e| MessageBoxError::Overlay(format!("broadcast failed: {}", e.description)))?;
578
579 Ok(txid)
580 }
581}
582
583#[cfg(test)]
588mod tests {
589 use super::*;
590 use bsv::primitives::private_key::PrivateKey;
591 use bsv::services::overlay_tools::Network;
592 use bsv::wallet::error::WalletError;
593 use bsv::wallet::interfaces::*;
594 use bsv::wallet::proto_wallet::ProtoWallet;
595 use std::sync::Arc;
596
597 #[derive(Clone)]
599 struct ArcWallet(Arc<ProtoWallet>);
600
601 impl ArcWallet {
602 fn new() -> Self {
603 let key = PrivateKey::from_random().expect("random key");
604 ArcWallet(Arc::new(ProtoWallet::new(key)))
605 }
606 }
607
608 #[async_trait::async_trait]
609 impl WalletInterface for ArcWallet {
610 async fn create_action(&self, args: CreateActionArgs, orig: Option<&str>) -> Result<CreateActionResult, WalletError> { self.0.create_action(args, orig).await }
611 async fn sign_action(&self, args: SignActionArgs, orig: Option<&str>) -> Result<SignActionResult, WalletError> { self.0.sign_action(args, orig).await }
612 async fn abort_action(&self, args: AbortActionArgs, orig: Option<&str>) -> Result<AbortActionResult, WalletError> { self.0.abort_action(args, orig).await }
613 async fn list_actions(&self, args: ListActionsArgs, orig: Option<&str>) -> Result<ListActionsResult, WalletError> { self.0.list_actions(args, orig).await }
614 async fn internalize_action(&self, args: InternalizeActionArgs, orig: Option<&str>) -> Result<InternalizeActionResult, WalletError> { self.0.internalize_action(args, orig).await }
615 async fn list_outputs(&self, args: ListOutputsArgs, orig: Option<&str>) -> Result<ListOutputsResult, WalletError> { self.0.list_outputs(args, orig).await }
616 async fn relinquish_output(&self, args: RelinquishOutputArgs, orig: Option<&str>) -> Result<RelinquishOutputResult, WalletError> { self.0.relinquish_output(args, orig).await }
617 async fn get_public_key(&self, args: GetPublicKeyArgs, orig: Option<&str>) -> Result<GetPublicKeyResult, WalletError> { self.0.get_public_key(args, orig).await }
618 async fn reveal_counterparty_key_linkage(&self, args: RevealCounterpartyKeyLinkageArgs, orig: Option<&str>) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> { self.0.reveal_counterparty_key_linkage(args, orig).await }
619 async fn reveal_specific_key_linkage(&self, args: RevealSpecificKeyLinkageArgs, orig: Option<&str>) -> Result<RevealSpecificKeyLinkageResult, WalletError> { self.0.reveal_specific_key_linkage(args, orig).await }
620 async fn encrypt(&self, args: EncryptArgs, orig: Option<&str>) -> Result<EncryptResult, WalletError> { self.0.encrypt(args, orig).await }
621 async fn decrypt(&self, args: DecryptArgs, orig: Option<&str>) -> Result<DecryptResult, WalletError> { self.0.decrypt(args, orig).await }
622 async fn create_hmac(&self, args: CreateHmacArgs, orig: Option<&str>) -> Result<CreateHmacResult, WalletError> { self.0.create_hmac(args, orig).await }
623 async fn verify_hmac(&self, args: VerifyHmacArgs, orig: Option<&str>) -> Result<VerifyHmacResult, WalletError> { self.0.verify_hmac(args, orig).await }
624 async fn create_signature(&self, args: CreateSignatureArgs, orig: Option<&str>) -> Result<CreateSignatureResult, WalletError> { self.0.create_signature(args, orig).await }
625 async fn verify_signature(&self, args: VerifySignatureArgs, orig: Option<&str>) -> Result<VerifySignatureResult, WalletError> { self.0.verify_signature(args, orig).await }
626 async fn acquire_certificate(&self, args: AcquireCertificateArgs, orig: Option<&str>) -> Result<Certificate, WalletError> { self.0.acquire_certificate(args, orig).await }
627 async fn list_certificates(&self, args: ListCertificatesArgs, orig: Option<&str>) -> Result<ListCertificatesResult, WalletError> { self.0.list_certificates(args, orig).await }
628 async fn prove_certificate(&self, args: ProveCertificateArgs, orig: Option<&str>) -> Result<ProveCertificateResult, WalletError> { self.0.prove_certificate(args, orig).await }
629 async fn relinquish_certificate(&self, args: RelinquishCertificateArgs, orig: Option<&str>) -> Result<RelinquishCertificateResult, WalletError> { self.0.relinquish_certificate(args, orig).await }
630 async fn discover_by_identity_key(&self, args: DiscoverByIdentityKeyArgs, orig: Option<&str>) -> Result<DiscoverCertificatesResult, WalletError> { self.0.discover_by_identity_key(args, orig).await }
631 async fn discover_by_attributes(&self, args: DiscoverByAttributesArgs, orig: Option<&str>) -> Result<DiscoverCertificatesResult, WalletError> { self.0.discover_by_attributes(args, orig).await }
632 async fn is_authenticated(&self, orig: Option<&str>) -> Result<AuthenticatedResult, WalletError> { self.0.is_authenticated(orig).await }
633 async fn wait_for_authentication(&self, orig: Option<&str>) -> Result<AuthenticatedResult, WalletError> { self.0.wait_for_authentication(orig).await }
634 async fn get_height(&self, orig: Option<&str>) -> Result<GetHeightResult, WalletError> { self.0.get_height(orig).await }
635 async fn get_header_for_height(&self, args: GetHeaderArgs, orig: Option<&str>) -> Result<GetHeaderResult, WalletError> { self.0.get_header_for_height(args, orig).await }
636 async fn get_network(&self, orig: Option<&str>) -> Result<GetNetworkResult, WalletError> { self.0.get_network(orig).await }
637 async fn get_version(&self, orig: Option<&str>) -> Result<GetVersionResult, WalletError> { self.0.get_version(orig).await }
638 }
639
640 fn make_client() -> MessageBoxClient<ArcWallet> {
641 MessageBoxClient::new(
642 "https://example.com".to_string(),
643 ArcWallet::new(),
644 None,
645 Network::Mainnet,
646 )
647 }
648
649 #[tokio::test]
654 async fn test_resolve_host_falls_back_to_default() {
655 let client = make_client();
656 let host = client
658 .resolve_host_for_recipient("03deadbeef")
659 .await
660 .expect("should not error");
661 assert_eq!(host, "https://example.com", "must fall back to self.host");
662 }
663
664 #[test]
666 fn test_revoke_host_args_correct() {
667 let txid = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab";
668 let output_index: u32 = 0;
669 let outpoint = format!("{txid}.{output_index}");
670 assert_eq!(
671 outpoint,
672 "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab.0"
673 );
674 }
675
676 #[test]
682 fn test_register_device_request_serializes_camelcase() {
683 use crate::types::RegisterDeviceRequest;
684 let req = RegisterDeviceRequest {
685 fcm_token: "abc".to_string(),
686 device_id: Some("d1".to_string()),
687 platform: None,
688 };
689 let json = serde_json::to_string(&req).unwrap();
690 assert!(json.contains("\"fcmToken\":\"abc\""), "fcmToken must be camelCase: {json}");
691 assert!(json.contains("\"deviceId\":\"d1\""), "deviceId must be camelCase: {json}");
692 assert!(!json.contains("platform"), "platform absent when None: {json}");
693 assert!(!json.contains("fcm_token"), "no snake_case leakage: {json}");
694 assert!(!json.contains("device_id"), "no snake_case leakage: {json}");
695 }
696
697 #[test]
699 fn test_list_devices_response_deserializes() {
700 use crate::types::ListDevicesResponse;
701 let raw = r#"{
702 "status": "success",
703 "devices": [{
704 "id": 1,
705 "deviceId": "d1",
706 "fcmToken": "tok",
707 "platform": "ios",
708 "active": true,
709 "createdAt": "2026-01-01",
710 "updatedAt": "2026-01-01",
711 "lastUsed": "2026-01-01"
712 }]
713 }"#;
714 let resp: ListDevicesResponse = serde_json::from_str(raw).unwrap();
715 assert_eq!(resp.status, "success");
716 assert_eq!(resp.devices.len(), 1);
717 let dev = &resp.devices[0];
718 assert_eq!(dev.id, Some(1));
719 assert_eq!(dev.device_id.as_deref(), Some("d1"));
720 assert_eq!(dev.fcm_token, "tok");
721 assert_eq!(dev.platform.as_deref(), Some("ios"));
722 assert_eq!(dev.active, Some(true));
723 assert!(dev.created_at.is_some());
724 assert!(dev.updated_at.is_some());
725 assert!(dev.last_used.is_some());
726 }
727
728 #[test]
730 fn test_register_device_response_deserializes() {
731 use crate::types::RegisterDeviceResponse;
732 let raw = r#"{"status":"success","message":"registered","deviceId":42}"#;
733 let resp: RegisterDeviceResponse = serde_json::from_str(raw).unwrap();
734 assert_eq!(resp.status, "success");
735 assert_eq!(resp.message.as_deref(), Some("registered"));
736 assert_eq!(resp.device_id, Some(42));
737 }
738
739 #[allow(dead_code)]
743 fn register_device_compiles(client: &MessageBoxClient<ArcWallet>) {
744 let _fut = client.register_device("tok123", Some("dev1"), Some("ios"), None);
745 }
746
747 #[allow(dead_code)]
749 fn list_registered_devices_compiles(client: &MessageBoxClient<ArcWallet>) {
750 let _fut = client.list_registered_devices(None);
751 }
752}