Skip to main content

bsv_messagebox_client/
adapter.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use bsv::remittance::error::RemittanceError;
5use bsv::remittance::types::PeerMessage;
6use bsv::remittance::CommsLayer;
7use bsv::wallet::interfaces::WalletInterface;
8
9use crate::client::MessageBoxClient;
10
11/// Bridge between `MessageBoxClient<W>` and the `CommsLayer` trait.
12///
13/// `RemittanceAdapter<W>` is the primary integration point for downstream
14/// consumers (e.g. `metawatt-edge-rs`) that interact with the messagebox
15/// protocol through the SDK's `CommsLayer` trait boundary.
16///
17/// Uses composition (`Arc<MessageBoxClient<W>>`) rather than inheritance —
18/// this matches the pre-phase architectural decision in STATE.md and keeps
19/// the adapter lightweight.
20pub struct RemittanceAdapter<W: WalletInterface + Clone + 'static> {
21    inner: Arc<MessageBoxClient<W>>,
22}
23
24impl<W: WalletInterface + Clone + 'static> RemittanceAdapter<W> {
25    /// Construct a new `RemittanceAdapter` wrapping `client`.
26    pub fn new(client: Arc<MessageBoxClient<W>>) -> Self {
27        Self { inner: client }
28    }
29}
30
31#[async_trait]
32impl<W: WalletInterface + Clone + 'static + Send + Sync> CommsLayer for RemittanceAdapter<W> {
33    /// Delegate to `MessageBoxClient`, passing `host_override` through.
34    ///
35    /// When `host_override` is `Some(host)`, calls `send_message_to_host` directly
36    /// bypassing overlay resolution. When `None`, calls `send_message` which resolves
37    /// the recipient's host via overlay (TS parity: `overrideHost ?? resolveHostForRecipient`).
38    async fn send_message(
39        &self,
40        recipient: &str,
41        message_box: &str,
42        body: &str,
43        host_override: Option<&str>,
44    ) -> Result<String, RemittanceError> {
45        match host_override {
46            Some(host) => self
47                .inner
48                .send_message_to_host(host, recipient, message_box, body, false, false, None, None)
49                .await
50                .map_err(|e| RemittanceError::Protocol(e.to_string())),
51            None => self
52                .inner
53                .send_message(recipient, message_box, body, false, false, None, None)
54                .await
55                .map_err(|e| RemittanceError::Protocol(e.to_string())),
56        }
57    }
58
59    /// Retrieve messages and map them to `Vec<PeerMessage>`.
60    ///
61    /// CRITICAL: `PeerMessage.recipient` is populated from `get_identity_key()`
62    /// — NOT from `ServerPeerMessage`, which does not carry a recipient field.
63    /// `PeerMessage.message_box` comes from the parameter, not the server response.
64    async fn list_messages(
65        &self,
66        message_box: &str,
67        _host: Option<&str>,
68    ) -> Result<Vec<PeerMessage>, RemittanceError> {
69        // Fetch identity key once before the mapping loop (cached by OnceCell).
70        let identity_key = self
71            .inner
72            .get_identity_key()
73            .await
74            .map_err(|e| RemittanceError::Protocol(e.to_string()))?;
75
76        let server_msgs = self
77            .inner
78            .list_messages_lite(message_box, _host)
79            .await
80            .map_err(|e| RemittanceError::Protocol(e.to_string()))?;
81
82        Ok(server_msgs
83            .into_iter()
84            .map(|m| PeerMessage {
85                message_id: m.message_id,
86                sender: m.sender,
87                recipient: identity_key.clone(), // Pitfall 3: not from ServerPeerMessage
88                message_box: message_box.to_string(), // from parameter, not server response
89                body: m.body,
90            })
91            .collect())
92    }
93
94    /// Delegate to `MessageBoxClient::acknowledge_message`.
95    ///
96    /// Converts `&[String]` to `Vec<String>` to match the inner method signature (Pitfall 4).
97    async fn acknowledge_message(&self, message_ids: &[String]) -> Result<(), RemittanceError> {
98        self.inner
99            .acknowledge_message(message_ids.to_vec(), None)
100            .await
101            .map_err(|e| RemittanceError::Protocol(e.to_string()))
102    }
103
104    /// Delegate to `MessageBoxClient::send_live_message`.
105    ///
106    /// Passes `host_override` through to `MessageBoxClient::send_live_message`
107    /// which applies it on the HTTP fallback path.
108    ///
109    /// The `CommsLayer` trait requires `Result<String, RemittanceError>`.
110    /// `MessageBoxClient::send_live_message` now returns `Result<DeliveryMode>`;
111    /// we extract the message ID via `.message_id()`. Callers that need to
112    /// distinguish live vs persisted delivery should use `MessageBoxClient`
113    /// directly rather than going through this adapter.
114    async fn send_live_message(
115        &self,
116        recipient: &str,
117        message_box: &str,
118        body: &str,
119        host_override: Option<&str>,
120    ) -> Result<String, RemittanceError> {
121        self.inner
122            .send_live_message(
123                recipient,
124                message_box,
125                body,
126                false,
127                false,
128                None,
129                host_override,
130            )
131            .await
132            .map(|d| d.message_id().to_string())
133            .map_err(|e| RemittanceError::Protocol(e.to_string()))
134    }
135
136    /// Delegate to `MessageBoxClient::listen_for_live_messages`.
137    ///
138    /// Passes `override_host` through (currently deferred in WS path).
139    async fn listen_for_live_messages(
140        &self,
141        message_box: &str,
142        override_host: Option<&str>,
143        on_message: Arc<dyn Fn(PeerMessage) + Send + Sync>,
144    ) -> Result<(), RemittanceError> {
145        self.inner
146            .listen_for_live_messages(message_box, on_message, override_host)
147            .await
148            .map_err(|e| RemittanceError::Protocol(e.to_string()))
149    }
150}
151
152// ---------------------------------------------------------------------------
153// Tests
154// ---------------------------------------------------------------------------
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::types::ServerPeerMessage;
160    use bsv::primitives::private_key::PrivateKey;
161    use bsv::wallet::error::WalletError;
162    use bsv::wallet::interfaces::*;
163    use bsv::wallet::proto_wallet::ProtoWallet;
164
165    // Reuse the same ArcWallet test helper pattern as client::tests / http_ops::tests.
166    #[derive(Clone)]
167    struct ArcWallet(Arc<ProtoWallet>);
168
169    impl ArcWallet {
170        fn new() -> Self {
171            let key = PrivateKey::from_random().expect("random key");
172            ArcWallet(Arc::new(ProtoWallet::new(key)))
173        }
174    }
175
176    #[async_trait::async_trait]
177    impl WalletInterface for ArcWallet {
178        async fn create_action(
179            &self,
180            args: CreateActionArgs,
181            orig: Option<&str>,
182        ) -> Result<CreateActionResult, WalletError> {
183            self.0.create_action(args, orig).await
184        }
185        async fn sign_action(
186            &self,
187            args: SignActionArgs,
188            orig: Option<&str>,
189        ) -> Result<SignActionResult, WalletError> {
190            self.0.sign_action(args, orig).await
191        }
192        async fn abort_action(
193            &self,
194            args: AbortActionArgs,
195            orig: Option<&str>,
196        ) -> Result<AbortActionResult, WalletError> {
197            self.0.abort_action(args, orig).await
198        }
199        async fn list_actions(
200            &self,
201            args: ListActionsArgs,
202            orig: Option<&str>,
203        ) -> Result<ListActionsResult, WalletError> {
204            self.0.list_actions(args, orig).await
205        }
206        async fn internalize_action(
207            &self,
208            args: InternalizeActionArgs,
209            orig: Option<&str>,
210        ) -> Result<InternalizeActionResult, WalletError> {
211            self.0.internalize_action(args, orig).await
212        }
213        async fn list_outputs(
214            &self,
215            args: ListOutputsArgs,
216            orig: Option<&str>,
217        ) -> Result<ListOutputsResult, WalletError> {
218            self.0.list_outputs(args, orig).await
219        }
220        async fn relinquish_output(
221            &self,
222            args: RelinquishOutputArgs,
223            orig: Option<&str>,
224        ) -> Result<RelinquishOutputResult, WalletError> {
225            self.0.relinquish_output(args, orig).await
226        }
227        async fn get_public_key(
228            &self,
229            args: GetPublicKeyArgs,
230            orig: Option<&str>,
231        ) -> Result<GetPublicKeyResult, WalletError> {
232            self.0.get_public_key(args, orig).await
233        }
234        async fn reveal_counterparty_key_linkage(
235            &self,
236            args: RevealCounterpartyKeyLinkageArgs,
237            orig: Option<&str>,
238        ) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> {
239            self.0.reveal_counterparty_key_linkage(args, orig).await
240        }
241        async fn reveal_specific_key_linkage(
242            &self,
243            args: RevealSpecificKeyLinkageArgs,
244            orig: Option<&str>,
245        ) -> Result<RevealSpecificKeyLinkageResult, WalletError> {
246            self.0.reveal_specific_key_linkage(args, orig).await
247        }
248        async fn encrypt(
249            &self,
250            args: EncryptArgs,
251            orig: Option<&str>,
252        ) -> Result<EncryptResult, WalletError> {
253            self.0.encrypt(args, orig).await
254        }
255        async fn decrypt(
256            &self,
257            args: DecryptArgs,
258            orig: Option<&str>,
259        ) -> Result<DecryptResult, WalletError> {
260            self.0.decrypt(args, orig).await
261        }
262        async fn create_hmac(
263            &self,
264            args: CreateHmacArgs,
265            orig: Option<&str>,
266        ) -> Result<CreateHmacResult, WalletError> {
267            self.0.create_hmac(args, orig).await
268        }
269        async fn verify_hmac(
270            &self,
271            args: VerifyHmacArgs,
272            orig: Option<&str>,
273        ) -> Result<VerifyHmacResult, WalletError> {
274            self.0.verify_hmac(args, orig).await
275        }
276        async fn create_signature(
277            &self,
278            args: CreateSignatureArgs,
279            orig: Option<&str>,
280        ) -> Result<CreateSignatureResult, WalletError> {
281            self.0.create_signature(args, orig).await
282        }
283        async fn verify_signature(
284            &self,
285            args: VerifySignatureArgs,
286            orig: Option<&str>,
287        ) -> Result<VerifySignatureResult, WalletError> {
288            self.0.verify_signature(args, orig).await
289        }
290        async fn acquire_certificate(
291            &self,
292            args: AcquireCertificateArgs,
293            orig: Option<&str>,
294        ) -> Result<Certificate, WalletError> {
295            self.0.acquire_certificate(args, orig).await
296        }
297        async fn list_certificates(
298            &self,
299            args: ListCertificatesArgs,
300            orig: Option<&str>,
301        ) -> Result<ListCertificatesResult, WalletError> {
302            self.0.list_certificates(args, orig).await
303        }
304        async fn prove_certificate(
305            &self,
306            args: ProveCertificateArgs,
307            orig: Option<&str>,
308        ) -> Result<ProveCertificateResult, WalletError> {
309            self.0.prove_certificate(args, orig).await
310        }
311        async fn relinquish_certificate(
312            &self,
313            args: RelinquishCertificateArgs,
314            orig: Option<&str>,
315        ) -> Result<RelinquishCertificateResult, WalletError> {
316            self.0.relinquish_certificate(args, orig).await
317        }
318        async fn discover_by_identity_key(
319            &self,
320            args: DiscoverByIdentityKeyArgs,
321            orig: Option<&str>,
322        ) -> Result<DiscoverCertificatesResult, WalletError> {
323            self.0.discover_by_identity_key(args, orig).await
324        }
325        async fn discover_by_attributes(
326            &self,
327            args: DiscoverByAttributesArgs,
328            orig: Option<&str>,
329        ) -> Result<DiscoverCertificatesResult, WalletError> {
330            self.0.discover_by_attributes(args, orig).await
331        }
332        async fn is_authenticated(
333            &self,
334            orig: Option<&str>,
335        ) -> Result<AuthenticatedResult, WalletError> {
336            self.0.is_authenticated(orig).await
337        }
338        async fn wait_for_authentication(
339            &self,
340            orig: Option<&str>,
341        ) -> Result<AuthenticatedResult, WalletError> {
342            self.0.wait_for_authentication(orig).await
343        }
344        async fn get_height(&self, orig: Option<&str>) -> Result<GetHeightResult, WalletError> {
345            self.0.get_height(orig).await
346        }
347        async fn get_header_for_height(
348            &self,
349            args: GetHeaderArgs,
350            orig: Option<&str>,
351        ) -> Result<GetHeaderResult, WalletError> {
352            self.0.get_header_for_height(args, orig).await
353        }
354        async fn get_network(&self, orig: Option<&str>) -> Result<GetNetworkResult, WalletError> {
355            self.0.get_network(orig).await
356        }
357        async fn get_version(&self, orig: Option<&str>) -> Result<GetVersionResult, WalletError> {
358            self.0.get_version(orig).await
359        }
360    }
361
362    fn make_client() -> Arc<MessageBoxClient<ArcWallet>> {
363        Arc::new(MessageBoxClient::new(
364            "https://example.com".to_string(),
365            ArcWallet::new(),
366            None,
367            bsv::services::overlay_tools::Network::Mainnet,
368        ))
369    }
370
371    /// `RemittanceAdapter::new` constructs successfully from an Arc<MessageBoxClient<W>>.
372    #[test]
373    fn adapter_can_be_constructed() {
374        let client = make_client();
375        let _adapter = RemittanceAdapter::new(client);
376    }
377
378    /// `RemittanceAdapter` satisfies `Arc<dyn CommsLayer + Send + Sync>` — compile check.
379    ///
380    /// If `RemittanceAdapter` does not implement `CommsLayer` correctly this
381    /// type coercion will fail to compile.
382    #[test]
383    fn adapter_is_comms_layer() {
384        let client = make_client();
385        let adapter = Arc::new(RemittanceAdapter::new(client));
386        let _: Arc<dyn CommsLayer + Send + Sync> = adapter;
387    }
388
389    /// `ServerPeerMessage` maps to `PeerMessage` with all 5 fields correct.
390    ///
391    /// This exercises the mapping logic from `list_messages` directly,
392    /// without a live HTTP call.
393    #[test]
394    fn map_server_message_all_five_fields() {
395        let server_msg = ServerPeerMessage {
396            message_id: "msg-001".to_string(),
397            body: "hello body".to_string(),
398            sender: "03senderkey".to_string(),
399            created_at: "2024-01-01T00:00:00Z".to_string(),
400            updated_at: "2024-01-01T00:01:00Z".to_string(),
401            acknowledged: None,
402            authenticated_decrypt: false,
403        };
404        let identity_key = "03myidentitykey".to_string();
405        let message_box = "payment_inbox";
406
407        // Apply the same mapping logic as list_messages.
408        let peer_msg = PeerMessage {
409            message_id: server_msg.message_id.clone(),
410            sender: server_msg.sender.clone(),
411            recipient: identity_key.clone(),
412            message_box: message_box.to_string(),
413            body: server_msg.body.clone(),
414        };
415
416        assert_eq!(peer_msg.message_id, "msg-001");
417        assert_eq!(peer_msg.sender, "03senderkey");
418        assert_eq!(
419            peer_msg.recipient, "03myidentitykey",
420            "recipient from identity key"
421        );
422        assert_eq!(
423            peer_msg.message_box, "payment_inbox",
424            "message_box from parameter"
425        );
426        assert_eq!(peer_msg.body, "hello body");
427    }
428
429    /// `PeerMessage.recipient` is the identity key — NOT an empty string.
430    ///
431    /// This test guards against the common mistake of leaving recipient empty
432    /// when ServerPeerMessage has no recipient field.
433    #[tokio::test]
434    async fn recipient_from_identity_key() {
435        let client = make_client();
436        let identity_key = client.get_identity_key().await.expect("get_identity_key");
437
438        assert!(!identity_key.is_empty(), "identity key must not be empty");
439
440        // The mapping assigns this key to PeerMessage.recipient.
441        let peer_msg = PeerMessage {
442            message_id: "x".to_string(),
443            sender: "03other".to_string(),
444            recipient: identity_key.clone(),
445            message_box: "inbox".to_string(),
446            body: "body".to_string(),
447        };
448
449        assert_eq!(peer_msg.recipient, identity_key);
450        assert_ne!(peer_msg.recipient, "", "recipient must not be empty string");
451    }
452
453    /// `acknowledge_message` converts `&[String]` to `Vec<String>` — compile check.
454    ///
455    /// Verifies the `.to_vec()` conversion compiles correctly with the adapter impl.
456    #[test]
457    fn acknowledge_message_accepts_slice() {
458        // Verifies that &[String] (the CommsLayer signature) is accepted.
459        // This is a compile-time check — if &[String] -> Vec<String> conversion
460        // is missing in the adapter, this function fails to compile.
461        let ids: &[String] = &["id1".to_string(), "id2".to_string()];
462        let converted: Vec<String> = ids.to_vec();
463        assert_eq!(converted, vec!["id1", "id2"]);
464    }
465
466    /// `send_live_message` is overridden — compile check via method resolution.
467    ///
468    /// If this resolves to the override (not the default trait impl), the method
469    /// is wired to `MessageBoxClient::send_live_message`.
470    #[allow(dead_code)]
471    fn send_live_message_compiles(adapter: &RemittanceAdapter<ArcWallet>) {
472        let _fut = adapter.send_live_message("03abc", "inbox", "hello", None);
473    }
474
475    /// `listen_for_live_messages` is overridden — compile check via method resolution.
476    ///
477    /// Constructs a dummy callback to verify the method resolves and compiles.
478    #[allow(dead_code)]
479    fn listen_for_live_messages_compiles(adapter: &RemittanceAdapter<ArcWallet>) {
480        let cb: Arc<dyn Fn(PeerMessage) + Send + Sync> = Arc::new(|_msg| {});
481        let _fut = adapter.listen_for_live_messages("inbox", None, cb);
482    }
483
484    /// `send_message` with a host_override compiles — compile check.
485    ///
486    /// Verifies `send_message_to_host` is called when host_override is Some.
487    #[allow(dead_code)]
488    fn test_adapter_send_message_with_host_override_compiles(
489        adapter: &RemittanceAdapter<ArcWallet>,
490    ) {
491        let _fut = adapter.send_message("03recipient", "inbox", "body", Some("https://other.host"));
492    }
493
494    /// `send_message` with no host_override compiles — compile check.
495    #[allow(dead_code)]
496    fn test_adapter_send_message_without_override_compiles(adapter: &RemittanceAdapter<ArcWallet>) {
497        let _fut = adapter.send_message("03recipient", "inbox", "body", None);
498    }
499}