etopay_sdk/core/
mod.rs

1//! Main SDK module.
2
3/// Config module.
4pub mod config;
5
6/// Postident module.
7#[cfg(feature = "postident")]
8pub mod postident;
9
10/// Transaction module.
11pub mod transaction;
12
13/// User module.
14pub mod user;
15/// Viviswap module.
16pub mod viviswap;
17/// Wallet module.
18pub mod wallet;
19
20/// Exchange module.
21pub mod exchange;
22
23/// Share module.
24pub mod share;
25
26/// Testing utils in sdk core
27#[cfg(test)]
28pub(crate) mod core_testing_utils;
29
30use crate::backend::dlt::get_networks;
31use crate::build;
32use crate::error::Result;
33use crate::types::newtypes::{AccessToken, EncryptionPin};
34use crate::types::users::ActiveUser;
35use crate::user::UserRepo;
36use crate::wallet_manager::WalletBorrow;
37use api_types::api::networks::ApiNetwork;
38pub use config::Config;
39use log::debug;
40
41pub(crate) type UserRepoT = Box<dyn UserRepo + Send + Sync + 'static>;
42
43/// Struct representing the SDK and its core components including configuration, user management, and storage options.
44pub struct Sdk {
45    /// Contains SDK configuration.
46    config: Option<Config>,
47    /// Contains the initialized active user.
48    active_user: Option<ActiveUser>,
49    /// Contains the access token for various SDK operations.
50    access_token: Option<AccessToken>,
51    /// Contains the user repository for storing and loading different users.
52    repo: Option<UserRepoT>,
53    /// The currently active network
54    active_network: Option<ApiNetwork>,
55    /// Available networks
56    networks: Vec<ApiNetwork>,
57}
58
59impl Drop for Sdk {
60    /// Drop implementation for SDK
61    fn drop(&mut self) {
62        debug!("Dropping SDK");
63    }
64}
65
66impl Default for Sdk {
67    /// Default implementation for SDK
68    fn default() -> Self {
69        Self {
70            config: None,
71            active_user: None,
72            access_token: None,
73            repo: None,
74            active_network: None,
75            networks: vec![],
76        }
77    }
78}
79
80impl Sdk {
81    /// Initialize an SDK instance from a config
82    #[allow(clippy::result_large_err)]
83    pub fn new(config: Config) -> Result<Self> {
84        debug!("Configuration: {:?}", config);
85        let mut s = Self::default();
86        s.set_config(config)?;
87        Ok(s)
88    }
89
90    /// Set network
91    pub async fn set_network(&mut self, network_key: String) -> Result<()> {
92        debug!("Selected network_key: {:?}", network_key.clone());
93
94        let Some(network) = self.networks.iter().find(|network| network.key == network_key) else {
95            return Err(crate::Error::NetworkUnavailable(network_key));
96        };
97
98        debug!("Selected Network: {:?}", network);
99        self.active_network = Some(network.clone());
100
101        Ok(())
102    }
103
104    /// Set networks
105    pub fn set_networks(&mut self, networks: Vec<ApiNetwork>) {
106        self.networks = networks;
107    }
108
109    /// Get networks
110    pub async fn get_networks(&mut self) -> Result<Vec<ApiNetwork>> {
111        if self.networks.is_empty() {
112            if self.access_token.is_none() {
113                return Err(crate::Error::MissingAccessToken);
114            }
115
116            let result = self.get_networks_backend().await;
117            match result {
118                Ok(n) => {
119                    self.networks = n.clone();
120                }
121                Err(e) => Err(e)?,
122            }
123        }
124
125        Ok(self.networks.clone())
126    }
127
128    /// Get supported networks from backend
129    async fn get_networks_backend(&self) -> Result<Vec<ApiNetwork>> {
130        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
131        let access_token = self
132            .access_token
133            .as_ref()
134            .ok_or(crate::error::Error::MissingAccessToken)?;
135        let backend_networks = get_networks(config, access_token).await?;
136
137        Ok(backend_networks)
138    }
139
140    /// Tries to get the wallet of the currently active user. Or returns an error if no user is
141    /// initialized, or if creating the wallet fails.
142    ///
143    /// Note: this will borrow `self` as mutable, and thus is not usable in cases when you want
144    /// to call functions that take `&mut self` as receiver while holding on to the
145    /// [`WalletBorrow`]
146    async fn try_get_active_user_wallet(&mut self, pin: &EncryptionPin) -> Result<WalletBorrow<'_>> {
147        let Some(repo) = &mut self.repo else {
148            return Err(crate::Error::UserRepoNotInitialized);
149        };
150        let Some(active_user) = &mut self.active_user else {
151            return Err(crate::Error::UserNotInitialized);
152        };
153        let network = self.active_network.as_ref().ok_or(crate::Error::MissingNetwork)?;
154        let config = self.config.as_mut().ok_or(crate::Error::MissingConfig)?;
155        let wallet = active_user
156            .wallet_manager
157            .try_get(
158                config,
159                &self.access_token,
160                repo,
161                network,
162                pin,
163                &active_user.mnemonic_derivation_options,
164            )
165            .await?;
166        Ok(wallet)
167    }
168
169    /// A function that returns a multi-line String containing:
170    /// * Branch name       (e.g. main)
171    /// * Commit hash       (e.g. 92cedead),
172    /// * Build time        (e.g. 2024-10-29 12:10:09 +00:00),
173    /// * Rust version      (e.g. 1.80.1 (3f5fd8dd4 2024-08-06))
174    /// * Toolchain channel (e.g. stable-x86_64-unknown-linux-gnu)
175    pub fn get_build_info() -> String {
176        build::CLAP_LONG_VERSION.to_string()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use crate::core::core_testing_utils::handle_error_test_cases;
183    use crate::testing_utils::IOTA_NETWORK_KEY;
184    use crate::{
185        core::Sdk,
186        error::Result,
187        testing_utils::{PIN, USERNAME, example_wallet_borrow, set_config},
188        user::MockUserRepo,
189        wallet_manager::WalletBorrow,
190    };
191    use api_types::api::dlt::ApiGetNetworksResponse;
192    use api_types::api::networks::ApiNetwork;
193    use etopay_wallet::MockWalletUser;
194    use rstest::rstest;
195
196    use crate::{
197        testing_utils::{AUTH_PROVIDER, HEADER_X_APP_NAME, TOKEN, example_api_networks},
198        wallet_manager::MockWalletManager,
199    };
200
201    #[rstest]
202    #[case::success(Ok(WalletBorrow::from(MockWalletUser::new())))]
203    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
204    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
205    #[case::missing_network(Err(crate::Error::MissingNetwork))]
206    #[case::missing_config(Err(crate::Error::MissingConfig))]
207    #[tokio::test]
208    async fn test_try_get_active_user_wallet(#[case] expected: Result<WalletBorrow<'_>>) {
209        // Arrange
210        let (_srv, config, _cleanup) = set_config().await;
211        let mut sdk = Sdk::new(config).unwrap();
212
213        match &expected {
214            Ok(_) => {
215                sdk.repo = Some(Box::new(MockUserRepo::new()));
216                let mock_wallet_manager = example_wallet_borrow();
217                sdk.active_user = Some(crate::types::users::ActiveUser {
218                    username: USERNAME.into(),
219                    wallet_manager: Box::new(mock_wallet_manager),
220                    mnemonic_derivation_options: Default::default(),
221                });
222                sdk.set_networks(example_api_networks());
223                sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
224            }
225            Err(error) => {
226                handle_error_test_cases(error, &mut sdk, 0, 0).await;
227            }
228        }
229
230        // Act
231        let response = Sdk::try_get_active_user_wallet(&mut sdk, &PIN).await;
232
233        // Assert
234        match expected {
235            Ok(_wallet_borrow) => {
236                response.unwrap();
237            }
238            Err(ref expected_err) => {
239                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
240            }
241        }
242    }
243
244    #[test]
245    fn test_get_build_info() {
246        let build_info = Sdk::get_build_info();
247        assert!(!build_info.is_empty());
248        println!("{build_info}");
249    }
250
251    #[rstest]
252    #[case::success(Ok(example_api_networks()))]
253    #[case::missing_config(Err(crate::Error::MissingConfig))]
254    #[case::unauthorized(Err(crate::Error::MissingAccessToken))]
255    #[tokio::test]
256    async fn test_get_networks_backend(#[case] expected: Result<Vec<ApiNetwork>>) {
257        // Arrange
258        let (mut srv, config, _cleanup) = set_config().await;
259        let mut sdk = Sdk::new(config).unwrap();
260        let mut mock_server = None;
261
262        match &expected {
263            Ok(_) => {
264                sdk.active_user = Some(crate::types::users::ActiveUser {
265                    username: USERNAME.into(),
266                    wallet_manager: Box::new(MockWalletManager::new()),
267                    mnemonic_derivation_options: Default::default(),
268                });
269                sdk.access_token = Some(TOKEN.clone());
270
271                let resp_body = ApiGetNetworksResponse {
272                    networks: example_api_networks(),
273                };
274                let mock_body_response = serde_json::to_string(&resp_body).unwrap();
275
276                mock_server = Some(
277                    srv.mock("GET", "/api/config/networks")
278                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
279                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
280                        .with_status(200)
281                        .with_header("content-type", "application/json")
282                        .with_body(&mock_body_response)
283                        .expect(1)
284                        .create(),
285                );
286            }
287            Err(error) => {
288                handle_error_test_cases(error, &mut sdk, 0, 0).await;
289            }
290        }
291
292        // Act
293        let response = Sdk::get_networks_backend(&sdk).await;
294
295        // Assert
296        match expected {
297            Ok(resp) => {
298                assert_eq!(response.unwrap(), resp);
299            }
300            Err(ref expected_err) => {
301                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
302            }
303        }
304        if let Some(m) = mock_server {
305            m.assert();
306        }
307    }
308}