1use std::collections::HashMap;
17
18use bsv::primitives::hash::sha256;
19use bsv::script::locking_script::LockingScript;
20use bsv::script::op::Op;
21use bsv::script::script::Script;
22use bsv::script::script_chunk::ScriptChunk;
23use bsv::script::templates::push_drop::{decode as decode_push_drop, LockPosition, PushDrop};
24use bsv::services::overlay_tools::{LookupAnswer, LookupQuestion};
25use bsv::services::overlay_tools::{
26 LookupResolver, LookupResolverConfig, TopicBroadcaster, TopicBroadcasterConfig,
27};
28use bsv::transaction::Transaction;
29use bsv::wallet::interfaces::{
30 CreateActionArgs, CreateActionInput, CreateActionOptions, CreateActionOutput, SignActionArgs,
31 SignActionSpend, WalletInterface,
32};
33use bsv::wallet::types::{BooleanDefaultTrue, Counterparty, CounterpartyType, Protocol};
34
35use crate::client::MessageBoxClient;
36use crate::error::MessageBoxError;
37use crate::types::{
38 AdvertisementToken, ListDevicesResponse, RegisterDeviceRequest, RegisterDeviceResponse,
39 RegisteredDevice,
40};
41
42fn make_data_push(data: &[u8]) -> ScriptChunk {
54 let len = data.len();
55 if len < 0x4c {
56 ScriptChunk::new_raw(len as u8, Some(data.to_vec()))
57 } else if len < 256 {
58 ScriptChunk::new_raw(Op::OpPushData1.to_byte(), Some(data.to_vec()))
59 } else if len < 65536 {
60 ScriptChunk::new_raw(Op::OpPushData2.to_byte(), Some(data.to_vec()))
61 } else {
62 ScriptChunk::new_raw(Op::OpPushData4.to_byte(), Some(data.to_vec()))
63 }
64}
65
66async fn build_advertisement_unlock_script<W: WalletInterface + ?Sized>(
76 wallet: &W,
77 originator: Option<&str>,
78 partial_tx: &Transaction,
79 input_index: usize,
80 sighash_type: u32,
81 source_satoshis: u64,
82 lock_script: &LockingScript,
83) -> Result<Script, MessageBoxError> {
84 let preimage = partial_tx
85 .sighash_preimage(input_index, sighash_type, source_satoshis, lock_script)
86 .map_err(|e| MessageBoxError::Overlay(format!("sighash_preimage: {e}")))?;
87
88 let sig_result = wallet
89 .create_signature(
90 bsv::wallet::interfaces::CreateSignatureArgs {
91 protocol_id: Protocol {
92 security_level: 1,
93 protocol: "messagebox advertisement".to_string(),
94 },
95 key_id: "1".to_string(),
96 counterparty: Counterparty {
97 counterparty_type: CounterpartyType::Anyone,
98 public_key: None,
99 },
100 data: Some(sha256(&preimage).to_vec()),
101 hash_to_directly_sign: None,
102 privileged: false,
103 privileged_reason: None,
104 seek_permission: None,
105 },
106 originator,
107 )
108 .await
109 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
110
111 let mut sig_bytes = sig_result.signature;
113 sig_bytes.push(sighash_type as u8);
114 Ok(Script::from_chunks(vec![make_data_push(&sig_bytes)]))
115}
116
117impl<W: WalletInterface + Clone + 'static + Send + Sync> MessageBoxClient<W> {
122 pub async fn query_advertisements(
135 &self,
136 identity_key: Option<&str>,
137 host: Option<&str>,
138 ) -> Result<Vec<AdvertisementToken>, MessageBoxError> {
139 match self.query_advertisements_inner(identity_key, host).await {
141 Ok(tokens) => Ok(tokens),
142 Err(_) => Ok(vec![]),
143 }
144 }
145
146 async fn query_advertisements_inner(
148 &self,
149 identity_key: Option<&str>,
150 host: Option<&str>,
151 ) -> Result<Vec<AdvertisementToken>, MessageBoxError> {
152 let ik = match identity_key {
153 Some(k) => k.to_string(),
154 None => self.get_identity_key().await?,
155 };
156
157 let mut query_obj = serde_json::json!({ "identityKey": ik });
158 if let Some(h) = host {
159 let trimmed = h.trim();
160 if !trimmed.is_empty() {
161 query_obj["host"] = serde_json::Value::String(trimmed.to_string());
162 }
163 }
164
165 let question = LookupQuestion {
166 service: "ls_messagebox".to_string(),
167 query: query_obj,
168 };
169
170 let mut host_overrides = std::collections::HashMap::new();
175 let tracker_urls = self.network.default_slap_trackers();
176 host_overrides.insert("ls_messagebox".to_string(), tracker_urls);
177
178 let resolver = LookupResolver::new(LookupResolverConfig {
179 network: self.network.clone(),
180 host_overrides,
181 ..Default::default()
182 });
183
184 let answer = resolver
185 .query(&question, None)
186 .await
187 .map_err(|e| MessageBoxError::Overlay(e.to_string()))?;
188
189 let mut tokens = Vec::new();
190
191 if let LookupAnswer::OutputList { outputs } = answer {
192 for output in outputs {
193 let beef_hex = hex::encode(&output.beef);
195 let tx = match Transaction::from_beef(&beef_hex) {
196 Ok(t) => t,
197 Err(_) => continue,
198 };
199
200 let idx = output.output_index as usize;
201 if idx >= tx.outputs.len() {
202 continue;
203 }
204
205 let script = &tx.outputs[idx].locking_script;
206 let pd = match decode_push_drop(script) {
207 Ok(t) => t,
208 Err(_) => continue,
209 };
210
211 if pd.fields.len() < 2 {
212 continue;
213 }
214
215 let host_url = match String::from_utf8(pd.fields[1].clone()) {
216 Ok(h) => h,
217 Err(_) => continue,
218 };
219
220 let txid = match tx.id() {
225 Ok(id) => id,
226 Err(_) => continue,
227 };
228
229 tokens.push(AdvertisementToken {
230 host: host_url,
231 txid,
232 output_index: output.output_index,
233 locking_script: script.to_hex(),
234 beef: output.beef,
235 });
236 }
237 }
238
239 Ok(tokens)
240 }
241
242 pub async fn resolve_host_for_recipient(
249 &self,
250 recipient: &str,
251 ) -> Result<String, MessageBoxError> {
252 let ads = self.query_advertisements(Some(recipient), None).await?;
253 if let Some(ad) = ads.into_iter().next() {
254 Ok(ad.host)
255 } else {
256 Ok(self.host().to_string())
257 }
258 }
259
260 pub async fn anoint_host(&self, host: &str) -> Result<String, MessageBoxError> {
269 let identity_key = self.get_identity_key().await?;
270
271 let id_key_bytes = hex::decode(&identity_key)
273 .map_err(|e| MessageBoxError::Overlay(format!("hex decode identity key: {e}")))?;
274 let host_bytes = host.as_bytes().to_vec();
275
276 let locking_script = PushDrop::new(self.wallet(), self.originator().map(String::from))
287 .lock(
288 vec![id_key_bytes, host_bytes],
289 Protocol {
290 security_level: 1,
291 protocol: "messagebox advertisement".to_string(),
292 },
293 "1",
294 Counterparty {
295 counterparty_type: CounterpartyType::Anyone,
296 public_key: None,
297 },
298 true, true, LockPosition::Before,
301 )
302 .await
303 .map_err(|e| MessageBoxError::Overlay(format!("PushDrop lock: {e}")))?;
304
305 let create_result = self
307 .wallet()
308 .create_action(
309 CreateActionArgs {
310 description: "Anoint host for overlay routing".to_string(),
311 input_beef: None,
312 inputs: None,
313 outputs: Some(vec![CreateActionOutput {
314 locking_script: Some(locking_script.to_binary()),
315 satoshis: 1,
316 output_description: "Overlay advertisement output".to_string(),
317 basket: Some("overlay advertisements".to_string()),
318 custom_instructions: None,
319 tags: None,
320 }]),
321 lock_time: None,
322 version: None,
323 labels: None,
324 options: Some(CreateActionOptions {
325 randomize_outputs: BooleanDefaultTrue(Some(false)),
327 accept_delayed_broadcast: BooleanDefaultTrue(Some(false)),
328 ..Default::default()
329 }),
330 reference: None,
331 },
332 self.originator(),
333 )
334 .await
335 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
336
337 let beef_bytes = create_result
341 .tx
342 .ok_or_else(|| MessageBoxError::Overlay("create_action returned no tx".into()))?;
343 let beef_hex = hex::encode(&beef_bytes);
344 let tx = Transaction::from_beef(&beef_hex)
345 .map_err(|e| MessageBoxError::Overlay(format!("parse BEEF: {e}")))?;
346 let txid = tx
347 .id()
348 .map_err(|e| MessageBoxError::Overlay(format!("tx.id(): {e}")))?;
349
350 let broadcaster = TopicBroadcaster::new(
355 vec!["tm_messagebox".to_string()],
356 TopicBroadcasterConfig {
357 network: self.network.clone(),
358 ..Default::default()
359 },
360 LookupResolver::new(LookupResolverConfig {
361 network: self.network.clone(),
362 ..Default::default()
363 }),
364 )
365 .map_err(|e| MessageBoxError::Overlay(format!("build broadcaster: {e}")))?;
366
367 broadcaster.broadcast_beef(beef_bytes).await.map_err(|e| {
368 MessageBoxError::Overlay(format!("broadcast failed: {}", e.description))
369 })?;
370
371 Ok(txid)
372 }
373
374 pub async fn register_device(
381 &self,
382 fcm_token: &str,
383 device_id: Option<&str>,
384 platform: Option<&str>,
385 override_host: Option<&str>,
386 ) -> Result<RegisterDeviceResponse, MessageBoxError> {
387 self.assert_initialized().await?;
388
389 let base = override_host.unwrap_or_else(|| self.host());
390 let request = RegisterDeviceRequest {
391 fcm_token: fcm_token.to_string(),
392 device_id: device_id.map(String::from),
393 platform: platform.map(String::from),
394 };
395
396 let body_bytes = serde_json::to_vec(&request).map_err(|e| {
397 MessageBoxError::Overlay(format!("serialize RegisterDeviceRequest: {e}"))
398 })?;
399
400 let url = format!("{base}/registerDevice");
401 let response = self.post_json(&url, body_bytes).await?;
402
403 let resp: RegisterDeviceResponse = serde_json::from_slice(&response.body).map_err(|e| {
404 MessageBoxError::Overlay(format!("deserialize RegisterDeviceResponse: {e}"))
405 })?;
406
407 Ok(resp)
408 }
409
410 pub async fn list_registered_devices(
416 &self,
417 override_host: Option<&str>,
418 ) -> Result<Vec<RegisteredDevice>, MessageBoxError> {
419 self.assert_initialized().await?;
420
421 let base = override_host.unwrap_or_else(|| self.host());
422 let url = format!("{base}/devices");
423 let response = self.get_json(&url).await?;
424
425 let resp: ListDevicesResponse = serde_json::from_slice(&response.body).map_err(|e| {
426 MessageBoxError::Overlay(format!("deserialize ListDevicesResponse: {e}"))
427 })?;
428
429 Ok(resp.devices)
430 }
431
432 pub async fn revoke_host_advertisement(
445 &self,
446 token: &AdvertisementToken,
447 ) -> Result<String, MessageBoxError> {
448 let create_result = self
451 .wallet()
452 .create_action(
453 CreateActionArgs {
454 description: "Revoke MessageBox host advertisement".to_string(),
455 input_beef: Some(token.beef.clone()),
456 inputs: Some(vec![CreateActionInput {
457 outpoint: format!("{}.{}", token.txid, token.output_index),
458 input_description: "Revoking host advertisement token".to_string(),
459 unlocking_script: None,
460 unlocking_script_length: Some(73),
461 sequence_number: None,
462 }]),
463 outputs: None,
464 lock_time: None,
465 version: None,
466 labels: None,
467 options: Some(CreateActionOptions {
468 accept_delayed_broadcast: BooleanDefaultTrue(Some(false)),
469 ..Default::default()
470 }),
471 reference: None,
472 },
473 self.originator(),
474 )
475 .await
476 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
477
478 let signable = create_result.signable_transaction.ok_or_else(|| {
480 MessageBoxError::Overlay("create_action returned no signable_transaction".into())
481 })?;
482
483 let partial_tx = Transaction::from_beef(&hex::encode(&signable.tx))
485 .map_err(|e| MessageBoxError::Overlay(format!("parse signable tx: {e}")))?;
486
487 let lock_script = LockingScript::from_hex(&token.locking_script)
489 .map_err(|e| MessageBoxError::Overlay(format!("parse locking script hex: {e}")))?;
490
491 let sighash_type: u32 = 0x41;
493
494 let unlock_script = build_advertisement_unlock_script(
498 self.wallet(),
499 self.originator(),
500 &partial_tx,
501 0,
502 sighash_type,
503 1,
504 &lock_script,
505 )
506 .await?;
507
508 let sign_result = self
510 .wallet()
511 .sign_action(
512 SignActionArgs {
513 reference: signable.reference,
514 spends: HashMap::from([(
515 0u32,
516 SignActionSpend {
517 unlocking_script: unlock_script.to_binary(),
518 sequence_number: None,
519 },
520 )]),
521 options: None,
522 },
523 self.originator(),
524 )
525 .await
526 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
527
528 let signed_bytes = sign_result
530 .tx
531 .ok_or_else(|| MessageBoxError::Overlay("sign_action returned no tx".into()))?;
532
533 let signed_tx = Transaction::from_beef(&hex::encode(&signed_bytes))
535 .map_err(|e| MessageBoxError::Overlay(format!("parse signed tx: {e}")))?;
536 let txid = signed_tx
537 .id()
538 .map_err(|e| MessageBoxError::Overlay(format!("signed_tx.id(): {e}")))?;
539
540 let broadcaster = TopicBroadcaster::new(
541 vec!["tm_messagebox".to_string()],
542 TopicBroadcasterConfig {
543 network: self.network.clone(),
544 ..Default::default()
545 },
546 LookupResolver::new(LookupResolverConfig {
547 network: self.network.clone(),
548 ..Default::default()
549 }),
550 )
551 .map_err(|e| MessageBoxError::Overlay(format!("build broadcaster: {e}")))?;
552
553 broadcaster
554 .broadcast_beef(signed_bytes)
555 .await
556 .map_err(|e| {
557 MessageBoxError::Overlay(format!("broadcast failed: {}", e.description))
558 })?;
559
560 Ok(txid)
561 }
562}
563
564#[cfg(test)]
569mod tests {
570 use super::*;
571 use bsv::primitives::private_key::PrivateKey;
572 use bsv::services::overlay_tools::Network;
573 use bsv::wallet::error::WalletError;
574 use bsv::wallet::interfaces::*;
575 use bsv::wallet::proto_wallet::ProtoWallet;
576 use std::sync::Arc;
577
578 #[derive(Clone)]
580 struct ArcWallet(Arc<ProtoWallet>);
581
582 impl ArcWallet {
583 fn new() -> Self {
584 let key = PrivateKey::from_random().expect("random key");
585 ArcWallet(Arc::new(ProtoWallet::new(key)))
586 }
587
588 fn deterministic() -> Self {
590 let key = PrivateKey::from_bytes(&[0x42u8; 32]).expect("fixed key");
591 ArcWallet(Arc::new(ProtoWallet::new(key)))
592 }
593 }
594
595 #[async_trait::async_trait]
596 impl WalletInterface for ArcWallet {
597 async fn create_action(
598 &self,
599 args: CreateActionArgs,
600 orig: Option<&str>,
601 ) -> Result<CreateActionResult, WalletError> {
602 self.0.create_action(args, orig).await
603 }
604 async fn sign_action(
605 &self,
606 args: SignActionArgs,
607 orig: Option<&str>,
608 ) -> Result<SignActionResult, WalletError> {
609 self.0.sign_action(args, orig).await
610 }
611 async fn abort_action(
612 &self,
613 args: AbortActionArgs,
614 orig: Option<&str>,
615 ) -> Result<AbortActionResult, WalletError> {
616 self.0.abort_action(args, orig).await
617 }
618 async fn list_actions(
619 &self,
620 args: ListActionsArgs,
621 orig: Option<&str>,
622 ) -> Result<ListActionsResult, WalletError> {
623 self.0.list_actions(args, orig).await
624 }
625 async fn internalize_action(
626 &self,
627 args: InternalizeActionArgs,
628 orig: Option<&str>,
629 ) -> Result<InternalizeActionResult, WalletError> {
630 self.0.internalize_action(args, orig).await
631 }
632 async fn list_outputs(
633 &self,
634 args: ListOutputsArgs,
635 orig: Option<&str>,
636 ) -> Result<ListOutputsResult, WalletError> {
637 self.0.list_outputs(args, orig).await
638 }
639 async fn relinquish_output(
640 &self,
641 args: RelinquishOutputArgs,
642 orig: Option<&str>,
643 ) -> Result<RelinquishOutputResult, WalletError> {
644 self.0.relinquish_output(args, orig).await
645 }
646 async fn get_public_key(
647 &self,
648 args: GetPublicKeyArgs,
649 orig: Option<&str>,
650 ) -> Result<GetPublicKeyResult, WalletError> {
651 self.0.get_public_key(args, orig).await
652 }
653 async fn reveal_counterparty_key_linkage(
654 &self,
655 args: RevealCounterpartyKeyLinkageArgs,
656 orig: Option<&str>,
657 ) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> {
658 self.0.reveal_counterparty_key_linkage(args, orig).await
659 }
660 async fn reveal_specific_key_linkage(
661 &self,
662 args: RevealSpecificKeyLinkageArgs,
663 orig: Option<&str>,
664 ) -> Result<RevealSpecificKeyLinkageResult, WalletError> {
665 self.0.reveal_specific_key_linkage(args, orig).await
666 }
667 async fn encrypt(
668 &self,
669 args: EncryptArgs,
670 orig: Option<&str>,
671 ) -> Result<EncryptResult, WalletError> {
672 self.0.encrypt(args, orig).await
673 }
674 async fn decrypt(
675 &self,
676 args: DecryptArgs,
677 orig: Option<&str>,
678 ) -> Result<DecryptResult, WalletError> {
679 self.0.decrypt(args, orig).await
680 }
681 async fn create_hmac(
682 &self,
683 args: CreateHmacArgs,
684 orig: Option<&str>,
685 ) -> Result<CreateHmacResult, WalletError> {
686 self.0.create_hmac(args, orig).await
687 }
688 async fn verify_hmac(
689 &self,
690 args: VerifyHmacArgs,
691 orig: Option<&str>,
692 ) -> Result<VerifyHmacResult, WalletError> {
693 self.0.verify_hmac(args, orig).await
694 }
695 async fn create_signature(
696 &self,
697 args: CreateSignatureArgs,
698 orig: Option<&str>,
699 ) -> Result<CreateSignatureResult, WalletError> {
700 self.0.create_signature(args, orig).await
701 }
702 async fn verify_signature(
703 &self,
704 args: VerifySignatureArgs,
705 orig: Option<&str>,
706 ) -> Result<VerifySignatureResult, WalletError> {
707 self.0.verify_signature(args, orig).await
708 }
709 async fn acquire_certificate(
710 &self,
711 args: AcquireCertificateArgs,
712 orig: Option<&str>,
713 ) -> Result<Certificate, WalletError> {
714 self.0.acquire_certificate(args, orig).await
715 }
716 async fn list_certificates(
717 &self,
718 args: ListCertificatesArgs,
719 orig: Option<&str>,
720 ) -> Result<ListCertificatesResult, WalletError> {
721 self.0.list_certificates(args, orig).await
722 }
723 async fn prove_certificate(
724 &self,
725 args: ProveCertificateArgs,
726 orig: Option<&str>,
727 ) -> Result<ProveCertificateResult, WalletError> {
728 self.0.prove_certificate(args, orig).await
729 }
730 async fn relinquish_certificate(
731 &self,
732 args: RelinquishCertificateArgs,
733 orig: Option<&str>,
734 ) -> Result<RelinquishCertificateResult, WalletError> {
735 self.0.relinquish_certificate(args, orig).await
736 }
737 async fn discover_by_identity_key(
738 &self,
739 args: DiscoverByIdentityKeyArgs,
740 orig: Option<&str>,
741 ) -> Result<DiscoverCertificatesResult, WalletError> {
742 self.0.discover_by_identity_key(args, orig).await
743 }
744 async fn discover_by_attributes(
745 &self,
746 args: DiscoverByAttributesArgs,
747 orig: Option<&str>,
748 ) -> Result<DiscoverCertificatesResult, WalletError> {
749 self.0.discover_by_attributes(args, orig).await
750 }
751 async fn is_authenticated(
752 &self,
753 orig: Option<&str>,
754 ) -> Result<AuthenticatedResult, WalletError> {
755 self.0.is_authenticated(orig).await
756 }
757 async fn wait_for_authentication(
758 &self,
759 orig: Option<&str>,
760 ) -> Result<AuthenticatedResult, WalletError> {
761 self.0.wait_for_authentication(orig).await
762 }
763 async fn get_height(&self, orig: Option<&str>) -> Result<GetHeightResult, WalletError> {
764 self.0.get_height(orig).await
765 }
766 async fn get_header_for_height(
767 &self,
768 args: GetHeaderArgs,
769 orig: Option<&str>,
770 ) -> Result<GetHeaderResult, WalletError> {
771 self.0.get_header_for_height(args, orig).await
772 }
773 async fn get_network(&self, orig: Option<&str>) -> Result<GetNetworkResult, WalletError> {
774 self.0.get_network(orig).await
775 }
776 async fn get_version(&self, orig: Option<&str>) -> Result<GetVersionResult, WalletError> {
777 self.0.get_version(orig).await
778 }
779 }
780
781 fn make_client() -> MessageBoxClient<ArcWallet> {
782 MessageBoxClient::new(
783 "https://example.com".to_string(),
784 ArcWallet::new(),
785 None,
786 Network::Mainnet,
787 )
788 }
789
790 #[tokio::test]
795 async fn test_resolve_host_falls_back_to_default() {
796 let client = make_client();
797 let host = client
799 .resolve_host_for_recipient("03deadbeef")
800 .await
801 .expect("should not error");
802 assert_eq!(host, "https://example.com", "must fall back to self.host");
803 }
804
805 #[test]
807 fn test_revoke_host_args_correct() {
808 let txid = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab";
809 let output_index: u32 = 0;
810 let outpoint = format!("{txid}.{output_index}");
811 assert_eq!(
812 outpoint,
813 "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab.0"
814 );
815 }
816
817 #[tokio::test]
827 async fn revocation_unlock_script_validates_against_the_advertisement_lock() {
828 use bsv::script::spend::{Spend, SpendParams};
829 use bsv::script::unlocking_script::UnlockingScript;
830 use bsv::transaction::{TransactionInput, TransactionOutput};
831
832 let wallet = ArcWallet::deterministic();
833 let sighash_type: u32 = 0x41; let locking_script = PushDrop::new(&wallet, None)
839 .lock(
840 vec![vec![0x02u8; 33], b"https://example.com".to_vec()],
841 Protocol {
842 security_level: 1,
843 protocol: "messagebox advertisement".to_string(),
844 },
845 "1",
846 Counterparty {
847 counterparty_type: CounterpartyType::Anyone,
848 public_key: None,
849 },
850 true, true, LockPosition::Before,
853 )
854 .await
855 .expect("PushDrop lock");
856
857 let mut source = Transaction::new();
858 source.outputs.push(TransactionOutput {
859 satoshis: Some(1),
860 locking_script: locking_script.clone(),
861 change: false,
862 });
863 let source_txid = source.id().expect("source txid");
864
865 let mut partial_tx = Transaction::new();
867 partial_tx.inputs.push(TransactionInput {
868 source_transaction: Some(Box::new(source)),
869 source_txid: Some(source_txid.clone()),
870 source_output_index: 0,
871 unlocking_script: None,
872 sequence: 0xFFFF_FFFF,
873 });
874
875 let unlock_script = build_advertisement_unlock_script(
877 &wallet,
878 None,
879 &partial_tx,
880 0,
881 sighash_type,
882 1,
883 &locking_script,
884 )
885 .await
886 .expect("build unlock script");
887
888 let mut spend = Spend::new(SpendParams {
889 locking_script: locking_script.clone(),
890 unlocking_script: UnlockingScript::from_binary(&unlock_script.to_binary()),
891 source_txid,
892 source_output_index: 0,
893 source_satoshis: 1,
894 transaction_version: partial_tx.version,
895 transaction_lock_time: partial_tx.lock_time,
896 transaction_sequence: partial_tx.inputs[0].sequence,
897 other_inputs: vec![],
898 other_outputs: partial_tx.outputs.clone(),
899 input_index: 0,
900 });
901
902 let valid = spend
903 .validate()
904 .unwrap_or_else(|e| panic!("script engine errored on the revocation spend: {e:?}"));
905 assert!(
906 valid,
907 "the revocation unlocking script must satisfy the advertisement locking \
908 script; a signature over sha256(preimage) instead of sha256d(preimage) \
909 fails here"
910 );
911 }
912
913 #[test]
919 fn test_register_device_request_serializes_camelcase() {
920 use crate::types::RegisterDeviceRequest;
921 let req = RegisterDeviceRequest {
922 fcm_token: "abc".to_string(),
923 device_id: Some("d1".to_string()),
924 platform: None,
925 };
926 let json = serde_json::to_string(&req).unwrap();
927 assert!(
928 json.contains("\"fcmToken\":\"abc\""),
929 "fcmToken must be camelCase: {json}"
930 );
931 assert!(
932 json.contains("\"deviceId\":\"d1\""),
933 "deviceId must be camelCase: {json}"
934 );
935 assert!(
936 !json.contains("platform"),
937 "platform absent when None: {json}"
938 );
939 assert!(!json.contains("fcm_token"), "no snake_case leakage: {json}");
940 assert!(!json.contains("device_id"), "no snake_case leakage: {json}");
941 }
942
943 #[test]
945 fn test_list_devices_response_deserializes() {
946 use crate::types::ListDevicesResponse;
947 let raw = r#"{
948 "status": "success",
949 "devices": [{
950 "id": 1,
951 "deviceId": "d1",
952 "fcmToken": "tok",
953 "platform": "ios",
954 "active": true,
955 "createdAt": "2026-01-01",
956 "updatedAt": "2026-01-01",
957 "lastUsed": "2026-01-01"
958 }]
959 }"#;
960 let resp: ListDevicesResponse = serde_json::from_str(raw).unwrap();
961 assert_eq!(resp.status, "success");
962 assert_eq!(resp.devices.len(), 1);
963 let dev = &resp.devices[0];
964 assert_eq!(dev.id, Some(1));
965 assert_eq!(dev.device_id.as_deref(), Some("d1"));
966 assert_eq!(dev.fcm_token, "tok");
967 assert_eq!(dev.platform.as_deref(), Some("ios"));
968 assert_eq!(dev.active, Some(true));
969 assert!(dev.created_at.is_some());
970 assert!(dev.updated_at.is_some());
971 assert!(dev.last_used.is_some());
972 }
973
974 #[test]
976 fn test_register_device_response_deserializes() {
977 use crate::types::RegisterDeviceResponse;
978 let raw = r#"{"status":"success","message":"registered","deviceId":42}"#;
979 let resp: RegisterDeviceResponse = serde_json::from_str(raw).unwrap();
980 assert_eq!(resp.status, "success");
981 assert_eq!(resp.message.as_deref(), Some("registered"));
982 assert_eq!(resp.device_id, Some(42));
983 }
984
985 #[allow(dead_code)]
989 fn register_device_compiles(client: &MessageBoxClient<ArcWallet>) {
990 let _fut = client.register_device("tok123", Some("dev1"), Some("ios"), None);
991 }
992
993 #[allow(dead_code)]
995 fn list_registered_devices_compiles(client: &MessageBoxClient<ArcWallet>) {
996 let _fut = client.list_registered_devices(None);
997 }
998}