Skip to main content

host_product_view/
lib.rs

1//! ProductView delegate trait and supporting types for Polkadot product host embedding.
2//!
3//! This crate defines the contract between the host-api message processor and
4//! the platform-specific host layer (iOS, Android, desktop, WASM). It is
5//! intentionally free of host-api internals so it can be implemented across
6//! all target platforms without pulling in SCALE codecs or smoldot.
7//!
8//! # Usage
9//!
10//! 1. Implement [`ProductViewDelegate`] for your platform's view controller.
11//! 2. After calling `HostApi::handle_message`, map the `HostApiOutcome`
12//!    fields to the corresponding delegate method.
13//! 3. Use [`ProductLoadState`] to drive loading UI and [`ProductAssets`]
14//!    to serve the verified bundle into the WebView.
15
16pub mod delegate;
17pub mod error;
18pub mod grant_store;
19pub mod manifest;
20pub mod mime;
21pub mod permissions;
22pub mod preflight;
23pub mod security;
24pub mod serving;
25pub mod state;
26
27pub use delegate::{ChainRpcRequest, ProductViewDelegate};
28pub use error::ProductError;
29pub use grant_store::{InMemorySandboxGrantStore, SandboxGrantStore, SandboxGrantStoreError};
30pub use manifest::{
31    ConsentChainTarget, ConsentSandboxRequest, HostChainRequirements, HostChainTarget,
32    HostManifest, HostSandboxRequest, HostSandboxRequirements, ManifestError,
33    PreparedProductBundle, PresentationConsentSummary, ProductBundleError, ProductConsentSummary,
34    ProductManifest, ProductMetadata, ProductModality, ProductSource, PublisherMetadata,
35    SandboxRequestKind, VerifiedConsentSummary,
36};
37pub use permissions::{
38    EffectiveSandboxPermissions, GrantedSandboxRequest, SandboxGrant, SandboxPermissionResolution,
39};
40pub use preflight::{
41    ConsentDenyBehavior, ProductConsentDecision, ProductPreflightContext,
42    ProductPreflightDenyReason, ProductPreflightError, ProductPreflightOutcome,
43    ProductPreflightPrompt,
44};
45pub use security::{csp_for_scheme, inject_into_head};
46pub use serving::serve_asset;
47pub use state::{ProductAssets, ProductLoadState};
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    /// Mock delegate that records calls for verification.
54    struct MockDelegate {
55        last_product_id: std::cell::RefCell<String>,
56    }
57
58    impl MockDelegate {
59        fn new() -> Self {
60            Self {
61                last_product_id: std::cell::RefCell::new(String::new()),
62            }
63        }
64    }
65
66    impl ProductViewDelegate for MockDelegate {
67        fn send_response(&self, product_id: &str, _bytes: Vec<u8>) -> Result<(), ProductError> {
68            *self.last_product_id.borrow_mut() = product_id.to_string();
69            Ok(())
70        }
71
72        fn needs_sign(
73            &self,
74            product_id: &str,
75            _request_id: &str,
76            _request_tag: u8,
77            _public_key: Vec<u8>,
78            _payload: Vec<u8>,
79        ) -> Result<(), ProductError> {
80            *self.last_product_id.borrow_mut() = product_id.to_string();
81            Ok(())
82        }
83
84        fn needs_chain_query(
85            &self,
86            product_id: &str,
87            _request_id: &str,
88            _method: &str,
89            _params_json: &str,
90        ) -> Result<(), ProductError> {
91            *self.last_product_id.borrow_mut() = product_id.to_string();
92            Ok(())
93        }
94
95        fn needs_chain_subscription(
96            &self,
97            product_id: &str,
98            _request_id: &str,
99            _method: &str,
100            _params_json: &str,
101        ) -> Result<(), ProductError> {
102            *self.last_product_id.borrow_mut() = product_id.to_string();
103            Ok(())
104        }
105
106        fn needs_chain_follow(
107            &self,
108            product_id: &str,
109            _request_id: &str,
110            _genesis_hash: Vec<u8>,
111            _with_runtime: bool,
112        ) -> Result<(), ProductError> {
113            *self.last_product_id.borrow_mut() = product_id.to_string();
114            Ok(())
115        }
116
117        fn needs_chain_rpc(
118            &self,
119            product_id: &str,
120            _request_id: &str,
121            _req: ChainRpcRequest,
122        ) -> Result<(), ProductError> {
123            *self.last_product_id.borrow_mut() = product_id.to_string();
124            Ok(())
125        }
126
127        fn needs_navigate(
128            &self,
129            product_id: &str,
130            _request_id: &str,
131            _url: &str,
132        ) -> Result<(), ProductError> {
133            *self.last_product_id.borrow_mut() = product_id.to_string();
134            Ok(())
135        }
136    }
137
138    #[test]
139    fn test_mock_delegate_send_response() {
140        let d = MockDelegate::new();
141        d.send_response("app.dot", vec![1, 2, 3]).unwrap();
142        assert_eq!(*d.last_product_id.borrow(), "app.dot");
143    }
144
145    #[test]
146    fn test_mock_delegate_needs_sign() {
147        let d = MockDelegate::new();
148        d.needs_sign("app.dot", "req-1", 42, vec![0; 32], vec![0; 64])
149            .unwrap();
150        assert_eq!(*d.last_product_id.borrow(), "app.dot");
151    }
152
153    #[test]
154    fn test_mock_delegate_needs_chain_rpc() {
155        let d = MockDelegate::new();
156        let req = ChainRpcRequest {
157            request_tag: 1,
158            genesis_hash: vec![0; 32],
159            method: "chainHead_v1_header".into(),
160            params_json: "[]".into(),
161            follow_sub_id: Some("sub-1".into()),
162        };
163        d.needs_chain_rpc("app.dot", "req-2", req.clone()).unwrap();
164        assert_eq!(*d.last_product_id.borrow(), "app.dot");
165        // Verify Clone works
166        assert_eq!(req.method, "chainHead_v1_header");
167    }
168
169    #[test]
170    fn test_mock_delegate_needs_navigate() {
171        let d = MockDelegate::new();
172        d.needs_navigate("app.dot", "req-3", "https://polkadot.network")
173            .unwrap();
174        assert_eq!(*d.last_product_id.borrow(), "app.dot");
175    }
176
177    #[test]
178    fn test_delegate_returns_error() {
179        struct FailingDelegate;
180        impl ProductViewDelegate for FailingDelegate {
181            fn send_response(&self, _: &str, _: Vec<u8>) -> Result<(), ProductError> {
182                Err(ProductError::UnknownProduct("nope".into()))
183            }
184            fn needs_sign(
185                &self,
186                _: &str,
187                _: &str,
188                _: u8,
189                _: Vec<u8>,
190                _: Vec<u8>,
191            ) -> Result<(), ProductError> {
192                Err(ProductError::SignFailed("locked".into()))
193            }
194            fn needs_chain_query(
195                &self,
196                _: &str,
197                _: &str,
198                _: &str,
199                _: &str,
200            ) -> Result<(), ProductError> {
201                Err(ProductError::ChainRpcFailed("offline".into()))
202            }
203            fn needs_chain_subscription(
204                &self,
205                _: &str,
206                _: &str,
207                _: &str,
208                _: &str,
209            ) -> Result<(), ProductError> {
210                Err(ProductError::ChainSubscriptionFailed("no chain".into()))
211            }
212            fn needs_chain_follow(
213                &self,
214                _: &str,
215                _: &str,
216                _: Vec<u8>,
217                _: bool,
218            ) -> Result<(), ProductError> {
219                Err(ProductError::ChainFollowFailed("gone".into()))
220            }
221            fn needs_chain_rpc(
222                &self,
223                _: &str,
224                _: &str,
225                _: ChainRpcRequest,
226            ) -> Result<(), ProductError> {
227                Err(ProductError::ChainRpcFailed("timeout".into()))
228            }
229            fn needs_navigate(&self, _: &str, _: &str, _: &str) -> Result<(), ProductError> {
230                Err(ProductError::NavigateRejected("blocked".into()))
231            }
232        }
233
234        let d = FailingDelegate;
235        assert!(d.send_response("x", vec![]).is_err());
236        assert!(d.needs_sign("x", "r", 0, vec![], vec![]).is_err());
237        assert!(d.needs_chain_query("x", "r", "m", "[]").is_err());
238        assert!(d.needs_chain_subscription("x", "r", "m", "[]").is_err());
239        assert!(d.needs_chain_follow("x", "r", vec![], false).is_err());
240        assert!(d
241            .needs_chain_rpc(
242                "x",
243                "r",
244                ChainRpcRequest {
245                    request_tag: 0,
246                    genesis_hash: vec![],
247                    method: String::new(),
248                    params_json: String::new(),
249                    follow_sub_id: None,
250                }
251            )
252            .is_err());
253        assert!(d.needs_navigate("x", "r", "url").is_err());
254    }
255}