1use std::collections::HashMap;
17
18use bsv::script::locking_script::LockingScript;
19use bsv::script::op::Op;
20use bsv::script::script::Script;
21use bsv::script::script_chunk::ScriptChunk;
22use bsv::script::templates::push_drop::{decode as decode_push_drop, LockPosition, PushDrop};
23use bsv::services::overlay_tools::{
24 LookupResolver, LookupResolverConfig, TopicBroadcaster, TopicBroadcasterConfig,
25};
26use bsv::services::overlay_tools::{LookupAnswer, LookupQuestion};
27use bsv::transaction::Transaction;
28use bsv::wallet::interfaces::{
29 CreateActionArgs, CreateActionInput, CreateActionOptions, CreateActionOutput,
30 SignActionArgs, SignActionSpend, WalletInterface,
31};
32use bsv::wallet::types::{
33 BooleanDefaultTrue, Counterparty, CounterpartyType, Protocol,
34};
35
36use crate::client::MessageBoxClient;
37use crate::error::MessageBoxError;
38use crate::types::{AdvertisementToken, ListDevicesResponse, RegisterDeviceRequest, RegisterDeviceResponse, RegisteredDevice};
39
40fn make_data_push(data: &[u8]) -> ScriptChunk {
52 let len = data.len();
53 if len < 0x4c {
54 ScriptChunk::new_raw(len as u8, Some(data.to_vec()))
55 } else if len < 256 {
56 ScriptChunk::new_raw(Op::OpPushData1.to_byte(), Some(data.to_vec()))
57 } else if len < 65536 {
58 ScriptChunk::new_raw(Op::OpPushData2.to_byte(), Some(data.to_vec()))
59 } else {
60 ScriptChunk::new_raw(Op::OpPushData4.to_byte(), Some(data.to_vec()))
61 }
62}
63
64
65impl<W: WalletInterface + Clone + 'static + Send + Sync> MessageBoxClient<W> {
70 pub async fn query_advertisements(
83 &self,
84 identity_key: Option<&str>,
85 host: Option<&str>,
86 ) -> Result<Vec<AdvertisementToken>, MessageBoxError> {
87 match self.query_advertisements_inner(identity_key, host).await {
89 Ok(tokens) => Ok(tokens),
90 Err(_) => Ok(vec![]),
91 }
92 }
93
94 async fn query_advertisements_inner(
96 &self,
97 identity_key: Option<&str>,
98 host: Option<&str>,
99 ) -> Result<Vec<AdvertisementToken>, MessageBoxError> {
100 let ik = match identity_key {
101 Some(k) => k.to_string(),
102 None => self.get_identity_key().await?,
103 };
104
105 let mut query_obj = serde_json::json!({ "identityKey": ik });
106 if let Some(h) = host {
107 let trimmed = h.trim();
108 if !trimmed.is_empty() {
109 query_obj["host"] = serde_json::Value::String(trimmed.to_string());
110 }
111 }
112
113 let question = LookupQuestion {
114 service: "ls_messagebox".to_string(),
115 query: query_obj,
116 };
117
118 let mut host_overrides = std::collections::HashMap::new();
123 let tracker_urls = self.network.default_slap_trackers();
124 host_overrides.insert("ls_messagebox".to_string(), tracker_urls);
125
126 let resolver = LookupResolver::new(LookupResolverConfig {
127 network: self.network.clone(),
128 host_overrides,
129 ..Default::default()
130 });
131
132 let answer = resolver
133 .query(&question, None)
134 .await
135 .map_err(|e| MessageBoxError::Overlay(e.to_string()))?;
136
137 let mut tokens = Vec::new();
138
139 if let LookupAnswer::OutputList { outputs } = answer {
140 for output in outputs {
141 let beef_hex = hex::encode(&output.beef);
143 let tx = match Transaction::from_beef(&beef_hex) {
144 Ok(t) => t,
145 Err(_) => continue,
146 };
147
148 let idx = output.output_index as usize;
149 if idx >= tx.outputs.len() {
150 continue;
151 }
152
153 let script = &tx.outputs[idx].locking_script;
154 let pd = match decode_push_drop(script) {
155 Ok(t) => t,
156 Err(_) => continue,
157 };
158
159 if pd.fields.len() < 2 {
160 continue;
161 }
162
163 let host_url = match String::from_utf8(pd.fields[1].clone()) {
164 Ok(h) => h,
165 Err(_) => continue,
166 };
167
168 let txid = match tx.id() {
173 Ok(id) => id,
174 Err(_) => continue,
175 };
176
177 tokens.push(AdvertisementToken {
178 host: host_url,
179 txid,
180 output_index: output.output_index,
181 locking_script: script.to_hex(),
182 beef: output.beef,
183 });
184 }
185 }
186
187 Ok(tokens)
188 }
189
190 pub async fn resolve_host_for_recipient(
197 &self,
198 recipient: &str,
199 ) -> Result<String, MessageBoxError> {
200 let ads = self.query_advertisements(Some(recipient), None).await?;
201 if let Some(ad) = ads.into_iter().next() {
202 Ok(ad.host)
203 } else {
204 Ok(self.host().to_string())
205 }
206 }
207
208 pub async fn anoint_host(&self, host: &str) -> Result<String, MessageBoxError> {
217 let identity_key = self.get_identity_key().await?;
218
219
220 let id_key_bytes = hex::decode(&identity_key)
222 .map_err(|e| MessageBoxError::Overlay(format!("hex decode identity key: {e}")))?;
223 let host_bytes = host.as_bytes().to_vec();
224
225 let locking_script = PushDrop::new(self.wallet(), self.originator().map(String::from))
236 .lock(
237 vec![id_key_bytes, host_bytes],
238 Protocol {
239 security_level: 1,
240 protocol: "messagebox advertisement".to_string(),
241 },
242 "1",
243 Counterparty {
244 counterparty_type: CounterpartyType::Anyone,
245 public_key: None,
246 },
247 true, true, LockPosition::Before,
250 )
251 .await
252 .map_err(|e| MessageBoxError::Overlay(format!("PushDrop lock: {e}")))?;
253
254 let create_result = self
256 .wallet()
257 .create_action(
258 CreateActionArgs {
259 description: "Anoint host for overlay routing".to_string(),
260 input_beef: None,
261 inputs: vec![],
262 outputs: vec![CreateActionOutput {
263 locking_script: Some(locking_script.to_binary()),
264 satoshis: 1,
265 output_description: "Overlay advertisement output".to_string(),
266 basket: Some("overlay advertisements".to_string()),
267 custom_instructions: None,
268 tags: vec![],
269 }],
270 lock_time: None,
271 version: None,
272 labels: vec![],
273 options: Some(CreateActionOptions {
274 randomize_outputs: BooleanDefaultTrue(Some(false)),
276 accept_delayed_broadcast: BooleanDefaultTrue(Some(false)),
277 ..Default::default()
278 }),
279 reference: None,
280 },
281 self.originator(),
282 )
283 .await
284 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
285
286 let beef_bytes = create_result
290 .tx
291 .ok_or_else(|| MessageBoxError::Overlay("create_action returned no tx".into()))?;
292 let beef_hex = hex::encode(&beef_bytes);
293 let tx = Transaction::from_beef(&beef_hex)
294 .map_err(|e| MessageBoxError::Overlay(format!("parse BEEF: {e}")))?;
295 let txid = tx
296 .id()
297 .map_err(|e| MessageBoxError::Overlay(format!("tx.id(): {e}")))?;
298
299 let broadcaster = TopicBroadcaster::new(
304 vec!["tm_messagebox".to_string()],
305 TopicBroadcasterConfig {
306 network: self.network.clone(),
307 ..Default::default()
308 },
309 LookupResolver::new(LookupResolverConfig {
310 network: self.network.clone(),
311 ..Default::default()
312 }),
313 )
314 .map_err(|e| MessageBoxError::Overlay(format!("build broadcaster: {e}")))?;
315
316 broadcaster
317 .broadcast_beef(beef_bytes)
318 .await
319 .map_err(|e| MessageBoxError::Overlay(format!("broadcast failed: {}", e.description)))?;
320
321 Ok(txid)
322 }
323
324 pub async fn register_device(
331 &self,
332 fcm_token: &str,
333 device_id: Option<&str>,
334 platform: Option<&str>,
335 override_host: Option<&str>,
336 ) -> Result<RegisterDeviceResponse, MessageBoxError> {
337 self.assert_initialized().await?;
338
339 let base = override_host.unwrap_or_else(|| self.host());
340 let request = RegisterDeviceRequest {
341 fcm_token: fcm_token.to_string(),
342 device_id: device_id.map(String::from),
343 platform: platform.map(String::from),
344 };
345
346 let body_bytes = serde_json::to_vec(&request)
347 .map_err(|e| MessageBoxError::Overlay(format!("serialize RegisterDeviceRequest: {e}")))?;
348
349 let url = format!("{base}/registerDevice");
350 let response = self.post_json(&url, body_bytes).await?;
351
352 let resp: RegisterDeviceResponse = serde_json::from_slice(&response.body)
353 .map_err(|e| MessageBoxError::Overlay(format!("deserialize RegisterDeviceResponse: {e}")))?;
354
355 Ok(resp)
356 }
357
358 pub async fn list_registered_devices(
364 &self,
365 override_host: Option<&str>,
366 ) -> Result<Vec<RegisteredDevice>, MessageBoxError> {
367 self.assert_initialized().await?;
368
369 let base = override_host.unwrap_or_else(|| self.host());
370 let url = format!("{base}/devices");
371 let response = self.get_json(&url).await?;
372
373 let resp: ListDevicesResponse = serde_json::from_slice(&response.body)
374 .map_err(|e| MessageBoxError::Overlay(format!("deserialize ListDevicesResponse: {e}")))?;
375
376 Ok(resp.devices)
377 }
378
379 pub async fn revoke_host_advertisement(
391 &self,
392 token: &AdvertisementToken,
393 ) -> Result<String, MessageBoxError> {
394 let create_result = self
397 .wallet()
398 .create_action(
399 CreateActionArgs {
400 description: "Revoke MessageBox host advertisement".to_string(),
401 input_beef: Some(token.beef.clone()),
402 inputs: vec![CreateActionInput {
403 outpoint: format!("{}.{}", token.txid, token.output_index),
404 input_description: "Revoking host advertisement token".to_string(),
405 unlocking_script: None,
406 unlocking_script_length: Some(73),
407 sequence_number: None,
408 }],
409 outputs: vec![],
410 lock_time: None,
411 version: None,
412 labels: vec![],
413 options: Some(CreateActionOptions {
414 accept_delayed_broadcast: BooleanDefaultTrue(Some(false)),
415 ..Default::default()
416 }),
417 reference: None,
418 },
419 self.originator(),
420 )
421 .await
422 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
423
424 let signable = create_result.signable_transaction.ok_or_else(|| {
426 MessageBoxError::Overlay("create_action returned no signable_transaction".into())
427 })?;
428
429 let partial_tx = Transaction::from_beef(&hex::encode(&signable.tx))
431 .map_err(|e| MessageBoxError::Overlay(format!("parse signable tx: {e}")))?;
432
433 let lock_script = LockingScript::from_hex(&token.locking_script)
435 .map_err(|e| MessageBoxError::Overlay(format!("parse locking script hex: {e}")))?;
436
437 let sighash_type: u32 = 0x41;
439
440 let preimage = partial_tx
441 .sighash_preimage(0, sighash_type, 1, &lock_script)
442 .map_err(|e| MessageBoxError::Overlay(format!("sighash_preimage: {e}")))?;
443
444 let sig_result = self
447 .wallet()
448 .create_signature(
449 bsv::wallet::interfaces::CreateSignatureArgs {
450 protocol_id: Protocol {
451 security_level: 1,
452 protocol: "messagebox advertisement".to_string(),
453 },
454 key_id: "1".to_string(),
455 counterparty: Counterparty {
456 counterparty_type: CounterpartyType::Anyone,
457 public_key: None,
458 },
459 data: Some(preimage),
460 hash_to_directly_sign: None,
461 privileged: false,
462 privileged_reason: None,
463 seek_permission: None,
464 },
465 self.originator(),
466 )
467 .await
468 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
469
470 let mut sig_bytes = sig_result.signature;
472 sig_bytes.push(sighash_type as u8);
473 let unlock_chunks = vec![make_data_push(&sig_bytes)];
474 let unlock_script = Script::from_chunks(unlock_chunks);
475
476 let sign_result = self
478 .wallet()
479 .sign_action(
480 SignActionArgs {
481 reference: signable.reference,
482 spends: HashMap::from([(
483 0u32,
484 SignActionSpend {
485 unlocking_script: unlock_script.to_binary(),
486 sequence_number: None,
487 },
488 )]),
489 options: None,
490 },
491 self.originator(),
492 )
493 .await
494 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
495
496 let signed_bytes = sign_result
498 .tx
499 .ok_or_else(|| MessageBoxError::Overlay("sign_action returned no tx".into()))?;
500
501 let signed_tx = Transaction::from_beef(&hex::encode(&signed_bytes))
503 .map_err(|e| MessageBoxError::Overlay(format!("parse signed tx: {e}")))?;
504 let txid = signed_tx
505 .id()
506 .map_err(|e| MessageBoxError::Overlay(format!("signed_tx.id(): {e}")))?;
507
508 let broadcaster = TopicBroadcaster::new(
509 vec!["tm_messagebox".to_string()],
510 TopicBroadcasterConfig {
511 network: self.network.clone(),
512 ..Default::default()
513 },
514 LookupResolver::new(LookupResolverConfig {
515 network: self.network.clone(),
516 ..Default::default()
517 }),
518 )
519 .map_err(|e| MessageBoxError::Overlay(format!("build broadcaster: {e}")))?;
520
521 broadcaster
522 .broadcast_beef(signed_bytes)
523 .await
524 .map_err(|e| MessageBoxError::Overlay(format!("broadcast failed: {}", e.description)))?;
525
526 Ok(txid)
527 }
528}
529
530#[cfg(test)]
535mod tests {
536 use super::*;
537 use bsv::primitives::private_key::PrivateKey;
538 use bsv::services::overlay_tools::Network;
539 use bsv::wallet::error::WalletError;
540 use bsv::wallet::interfaces::*;
541 use bsv::wallet::proto_wallet::ProtoWallet;
542 use std::sync::Arc;
543
544 #[derive(Clone)]
546 struct ArcWallet(Arc<ProtoWallet>);
547
548 impl ArcWallet {
549 fn new() -> Self {
550 let key = PrivateKey::from_random().expect("random key");
551 ArcWallet(Arc::new(ProtoWallet::new(key)))
552 }
553 }
554
555 #[async_trait::async_trait]
556 impl WalletInterface for ArcWallet {
557 async fn create_action(&self, args: CreateActionArgs, orig: Option<&str>) -> Result<CreateActionResult, WalletError> { self.0.create_action(args, orig).await }
558 async fn sign_action(&self, args: SignActionArgs, orig: Option<&str>) -> Result<SignActionResult, WalletError> { self.0.sign_action(args, orig).await }
559 async fn abort_action(&self, args: AbortActionArgs, orig: Option<&str>) -> Result<AbortActionResult, WalletError> { self.0.abort_action(args, orig).await }
560 async fn list_actions(&self, args: ListActionsArgs, orig: Option<&str>) -> Result<ListActionsResult, WalletError> { self.0.list_actions(args, orig).await }
561 async fn internalize_action(&self, args: InternalizeActionArgs, orig: Option<&str>) -> Result<InternalizeActionResult, WalletError> { self.0.internalize_action(args, orig).await }
562 async fn list_outputs(&self, args: ListOutputsArgs, orig: Option<&str>) -> Result<ListOutputsResult, WalletError> { self.0.list_outputs(args, orig).await }
563 async fn relinquish_output(&self, args: RelinquishOutputArgs, orig: Option<&str>) -> Result<RelinquishOutputResult, WalletError> { self.0.relinquish_output(args, orig).await }
564 async fn get_public_key(&self, args: GetPublicKeyArgs, orig: Option<&str>) -> Result<GetPublicKeyResult, WalletError> { self.0.get_public_key(args, orig).await }
565 async fn reveal_counterparty_key_linkage(&self, args: RevealCounterpartyKeyLinkageArgs, orig: Option<&str>) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> { self.0.reveal_counterparty_key_linkage(args, orig).await }
566 async fn reveal_specific_key_linkage(&self, args: RevealSpecificKeyLinkageArgs, orig: Option<&str>) -> Result<RevealSpecificKeyLinkageResult, WalletError> { self.0.reveal_specific_key_linkage(args, orig).await }
567 async fn encrypt(&self, args: EncryptArgs, orig: Option<&str>) -> Result<EncryptResult, WalletError> { self.0.encrypt(args, orig).await }
568 async fn decrypt(&self, args: DecryptArgs, orig: Option<&str>) -> Result<DecryptResult, WalletError> { self.0.decrypt(args, orig).await }
569 async fn create_hmac(&self, args: CreateHmacArgs, orig: Option<&str>) -> Result<CreateHmacResult, WalletError> { self.0.create_hmac(args, orig).await }
570 async fn verify_hmac(&self, args: VerifyHmacArgs, orig: Option<&str>) -> Result<VerifyHmacResult, WalletError> { self.0.verify_hmac(args, orig).await }
571 async fn create_signature(&self, args: CreateSignatureArgs, orig: Option<&str>) -> Result<CreateSignatureResult, WalletError> { self.0.create_signature(args, orig).await }
572 async fn verify_signature(&self, args: VerifySignatureArgs, orig: Option<&str>) -> Result<VerifySignatureResult, WalletError> { self.0.verify_signature(args, orig).await }
573 async fn acquire_certificate(&self, args: AcquireCertificateArgs, orig: Option<&str>) -> Result<Certificate, WalletError> { self.0.acquire_certificate(args, orig).await }
574 async fn list_certificates(&self, args: ListCertificatesArgs, orig: Option<&str>) -> Result<ListCertificatesResult, WalletError> { self.0.list_certificates(args, orig).await }
575 async fn prove_certificate(&self, args: ProveCertificateArgs, orig: Option<&str>) -> Result<ProveCertificateResult, WalletError> { self.0.prove_certificate(args, orig).await }
576 async fn relinquish_certificate(&self, args: RelinquishCertificateArgs, orig: Option<&str>) -> Result<RelinquishCertificateResult, WalletError> { self.0.relinquish_certificate(args, orig).await }
577 async fn discover_by_identity_key(&self, args: DiscoverByIdentityKeyArgs, orig: Option<&str>) -> Result<DiscoverCertificatesResult, WalletError> { self.0.discover_by_identity_key(args, orig).await }
578 async fn discover_by_attributes(&self, args: DiscoverByAttributesArgs, orig: Option<&str>) -> Result<DiscoverCertificatesResult, WalletError> { self.0.discover_by_attributes(args, orig).await }
579 async fn is_authenticated(&self, orig: Option<&str>) -> Result<AuthenticatedResult, WalletError> { self.0.is_authenticated(orig).await }
580 async fn wait_for_authentication(&self, orig: Option<&str>) -> Result<AuthenticatedResult, WalletError> { self.0.wait_for_authentication(orig).await }
581 async fn get_height(&self, orig: Option<&str>) -> Result<GetHeightResult, WalletError> { self.0.get_height(orig).await }
582 async fn get_header_for_height(&self, args: GetHeaderArgs, orig: Option<&str>) -> Result<GetHeaderResult, WalletError> { self.0.get_header_for_height(args, orig).await }
583 async fn get_network(&self, orig: Option<&str>) -> Result<GetNetworkResult, WalletError> { self.0.get_network(orig).await }
584 async fn get_version(&self, orig: Option<&str>) -> Result<GetVersionResult, WalletError> { self.0.get_version(orig).await }
585 }
586
587 fn make_client() -> MessageBoxClient<ArcWallet> {
588 MessageBoxClient::new(
589 "https://example.com".to_string(),
590 ArcWallet::new(),
591 None,
592 Network::Mainnet,
593 )
594 }
595
596 #[tokio::test]
601 async fn test_resolve_host_falls_back_to_default() {
602 let client = make_client();
603 let host = client
605 .resolve_host_for_recipient("03deadbeef")
606 .await
607 .expect("should not error");
608 assert_eq!(host, "https://example.com", "must fall back to self.host");
609 }
610
611 #[test]
613 fn test_revoke_host_args_correct() {
614 let txid = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab";
615 let output_index: u32 = 0;
616 let outpoint = format!("{txid}.{output_index}");
617 assert_eq!(
618 outpoint,
619 "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab.0"
620 );
621 }
622
623 #[test]
629 fn test_register_device_request_serializes_camelcase() {
630 use crate::types::RegisterDeviceRequest;
631 let req = RegisterDeviceRequest {
632 fcm_token: "abc".to_string(),
633 device_id: Some("d1".to_string()),
634 platform: None,
635 };
636 let json = serde_json::to_string(&req).unwrap();
637 assert!(json.contains("\"fcmToken\":\"abc\""), "fcmToken must be camelCase: {json}");
638 assert!(json.contains("\"deviceId\":\"d1\""), "deviceId must be camelCase: {json}");
639 assert!(!json.contains("platform"), "platform absent when None: {json}");
640 assert!(!json.contains("fcm_token"), "no snake_case leakage: {json}");
641 assert!(!json.contains("device_id"), "no snake_case leakage: {json}");
642 }
643
644 #[test]
646 fn test_list_devices_response_deserializes() {
647 use crate::types::ListDevicesResponse;
648 let raw = r#"{
649 "status": "success",
650 "devices": [{
651 "id": 1,
652 "deviceId": "d1",
653 "fcmToken": "tok",
654 "platform": "ios",
655 "active": true,
656 "createdAt": "2026-01-01",
657 "updatedAt": "2026-01-01",
658 "lastUsed": "2026-01-01"
659 }]
660 }"#;
661 let resp: ListDevicesResponse = serde_json::from_str(raw).unwrap();
662 assert_eq!(resp.status, "success");
663 assert_eq!(resp.devices.len(), 1);
664 let dev = &resp.devices[0];
665 assert_eq!(dev.id, Some(1));
666 assert_eq!(dev.device_id.as_deref(), Some("d1"));
667 assert_eq!(dev.fcm_token, "tok");
668 assert_eq!(dev.platform.as_deref(), Some("ios"));
669 assert_eq!(dev.active, Some(true));
670 assert!(dev.created_at.is_some());
671 assert!(dev.updated_at.is_some());
672 assert!(dev.last_used.is_some());
673 }
674
675 #[test]
677 fn test_register_device_response_deserializes() {
678 use crate::types::RegisterDeviceResponse;
679 let raw = r#"{"status":"success","message":"registered","deviceId":42}"#;
680 let resp: RegisterDeviceResponse = serde_json::from_str(raw).unwrap();
681 assert_eq!(resp.status, "success");
682 assert_eq!(resp.message.as_deref(), Some("registered"));
683 assert_eq!(resp.device_id, Some(42));
684 }
685
686 #[allow(dead_code)]
690 fn register_device_compiles(client: &MessageBoxClient<ArcWallet>) {
691 let _fut = client.register_device("tok123", Some("dev1"), Some("ios"), None);
692 }
693
694 #[allow(dead_code)]
696 fn list_registered_devices_compiles(client: &MessageBoxClient<ArcWallet>) {
697 let _fut = client.list_registered_devices(None);
698 }
699}