iota_sdk/wallet/core/
builder.rs

1// Copyright 2021 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::sync::{
5    atomic::{AtomicU32, AtomicUsize},
6    Arc,
7};
8#[cfg(feature = "storage")]
9use std::{collections::HashSet, sync::atomic::Ordering};
10
11use futures::{future::try_join_all, FutureExt};
12use serde::Serialize;
13use tokio::sync::RwLock;
14
15use super::operations::storage::SaveLoadWallet;
16#[cfg(feature = "events")]
17use crate::wallet::events::EventEmitter;
18#[cfg(all(feature = "storage", not(feature = "rocksdb")))]
19use crate::wallet::storage::adapter::memory::Memory;
20#[cfg(feature = "storage")]
21use crate::wallet::{
22    account::AccountDetails,
23    storage::{StorageManager, StorageOptions},
24};
25use crate::{
26    client::secret::{SecretManage, SecretManager},
27    wallet::{core::WalletInner, Account, ClientOptions, Wallet},
28};
29
30/// Builder for the wallet.
31#[derive(Debug, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct WalletBuilder<S: SecretManage = SecretManager> {
34    pub(crate) client_options: Option<ClientOptions>,
35    pub(crate) coin_type: Option<u32>,
36    #[cfg(feature = "storage")]
37    pub(crate) storage_options: Option<StorageOptions>,
38    #[serde(skip)]
39    pub(crate) secret_manager: Option<Arc<RwLock<S>>>,
40}
41
42impl<S: SecretManage> Default for WalletBuilder<S> {
43    fn default() -> Self {
44        Self {
45            client_options: Default::default(),
46            coin_type: Default::default(),
47            #[cfg(feature = "storage")]
48            storage_options: Default::default(),
49            secret_manager: Default::default(),
50        }
51    }
52}
53
54impl<S: 'static + SecretManage> WalletBuilder<S>
55where
56    crate::wallet::Error: From<S::Error>,
57{
58    /// Initialises a new instance of the wallet builder with the default storage adapter.
59    pub fn new() -> Self {
60        Self {
61            secret_manager: None,
62            ..Default::default()
63        }
64    }
65
66    /// Set the client options for the core nodes.
67    pub fn with_client_options(mut self, client_options: impl Into<Option<ClientOptions>>) -> Self {
68        self.client_options = client_options.into();
69        self
70    }
71
72    /// Set the coin type for the wallet. Registered coin types can be found at <https://github.com/satoshilabs/slips/blob/master/slip-0044.md>.
73    pub fn with_coin_type(mut self, coin_type: impl Into<Option<u32>>) -> Self {
74        self.coin_type = coin_type.into();
75        self
76    }
77
78    /// Set the storage options to be used.
79    #[cfg(feature = "storage")]
80    #[cfg_attr(docsrs, doc(cfg(feature = "storage")))]
81    pub fn with_storage_options(mut self, storage_options: impl Into<Option<StorageOptions>>) -> Self {
82        self.storage_options = storage_options.into();
83        self
84    }
85
86    /// Set the secret_manager to be used.
87    pub fn with_secret_manager(mut self, secret_manager: impl Into<Option<S>>) -> Self {
88        self.secret_manager = secret_manager.into().map(|sm| Arc::new(RwLock::new(sm)));
89        self
90    }
91
92    /// Set the secret_manager to be used wrapped in an Arc<RwLock<>> so it can be cloned and mutated also outside of
93    /// the Wallet.
94    pub fn with_secret_manager_arc(mut self, secret_manager: impl Into<Option<Arc<RwLock<S>>>>) -> Self {
95        self.secret_manager = secret_manager.into();
96        self
97    }
98
99    /// Set the storage path to be used.
100    #[cfg(feature = "storage")]
101    #[cfg_attr(docsrs, doc(cfg(feature = "storage")))]
102    pub fn with_storage_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
103        self.storage_options = Some(StorageOptions {
104            path: path.into(),
105            ..Default::default()
106        });
107        self
108    }
109}
110
111impl<S: 'static + SecretManage> WalletBuilder<S>
112where
113    crate::wallet::Error: From<S::Error>,
114    Self: SaveLoadWallet,
115{
116    /// Builds the wallet
117    pub async fn finish(mut self) -> crate::wallet::Result<Wallet<S>> {
118        log::debug!("[WalletBuilder]");
119
120        #[cfg(feature = "storage")]
121        let storage_options = self.storage_options.clone().unwrap_or_default();
122        // Check if the db exists and if not, return an error if one parameter is missing, because otherwise the db
123        // would be created with an empty parameter which just leads to errors later
124        #[cfg(feature = "storage")]
125        if !storage_options.path.is_dir() {
126            if self.client_options.is_none() {
127                return Err(crate::wallet::Error::MissingParameter("client_options"));
128            }
129            if self.coin_type.is_none() {
130                return Err(crate::wallet::Error::MissingParameter("coin_type"));
131            }
132            if self.secret_manager.is_none() {
133                return Err(crate::wallet::Error::MissingParameter("secret_manager"));
134            }
135        }
136
137        #[cfg(all(feature = "rocksdb", feature = "storage"))]
138        let storage =
139            crate::wallet::storage::adapter::rocksdb::RocksdbStorageAdapter::new(storage_options.path.clone())?;
140        #[cfg(all(not(feature = "rocksdb"), feature = "storage"))]
141        let storage = Memory::default();
142
143        #[cfg(feature = "storage")]
144        let mut storage_manager = StorageManager::new(storage, storage_options.encryption_key.clone()).await?;
145
146        #[cfg(feature = "storage")]
147        let read_manager_builder = Self::load(&storage_manager).await?;
148        #[cfg(not(feature = "storage"))]
149        let read_manager_builder: Option<Self> = None;
150
151        // Prioritize provided client_options and secret_manager over stored ones
152        let new_provided_client_options = if self.client_options.is_none() {
153            let loaded_client_options = read_manager_builder
154                .as_ref()
155                .and_then(|data| data.client_options.clone())
156                .ok_or(crate::wallet::Error::MissingParameter("client_options"))?;
157
158            // Update self so it gets used and stored again
159            self.client_options.replace(loaded_client_options);
160            false
161        } else {
162            true
163        };
164
165        if self.secret_manager.is_none() {
166            let secret_manager = read_manager_builder
167                .as_ref()
168                .and_then(|data| data.secret_manager.clone())
169                .ok_or(crate::wallet::Error::MissingParameter("secret_manager"))?;
170
171            // Update self so it gets used and stored again
172            self.secret_manager.replace(secret_manager);
173        }
174
175        if self.coin_type.is_none() {
176            self.coin_type = read_manager_builder.and_then(|builder| builder.coin_type);
177        }
178        let coin_type = self.coin_type.ok_or(crate::wallet::Error::MissingParameter(
179            "coin_type (IOTA: 4218, Shimmer: 4219)",
180        ))?;
181
182        #[cfg(feature = "storage")]
183        let mut accounts = storage_manager.get_accounts().await?;
184
185        // Check against potential account coin type before saving the wallet data
186        #[cfg(feature = "storage")]
187        if let Some(account) = accounts.first() {
188            if *account.coin_type() != coin_type {
189                return Err(crate::wallet::Error::InvalidCoinType {
190                    new_coin_type: coin_type,
191                    existing_coin_type: *account.coin_type(),
192                });
193            }
194        }
195
196        // Store wallet data in storage
197        #[cfg(feature = "storage")]
198        self.save(&storage_manager).await?;
199
200        #[cfg(feature = "events")]
201        let event_emitter = tokio::sync::RwLock::new(EventEmitter::new());
202
203        // It happened that inputs got locked, the transaction failed, but they weren't unlocked again, so we do this
204        // here
205        #[cfg(feature = "storage")]
206        unlock_unused_inputs(&mut accounts)?;
207        #[cfg(not(feature = "storage"))]
208        let accounts = Vec::new();
209        let wallet_inner = Arc::new(WalletInner {
210            background_syncing_status: AtomicUsize::new(0),
211            client: self
212                .client_options
213                .clone()
214                .ok_or(crate::wallet::Error::MissingParameter("client_options"))?
215                .finish()
216                .await?,
217            coin_type: AtomicU32::new(coin_type),
218            secret_manager: self
219                .secret_manager
220                .ok_or(crate::wallet::Error::MissingParameter("secret_manager"))?,
221            #[cfg(feature = "events")]
222            event_emitter,
223            #[cfg(feature = "storage")]
224            storage_options,
225            #[cfg(feature = "storage")]
226            storage_manager: tokio::sync::RwLock::new(storage_manager),
227        });
228
229        let mut accounts: Vec<Account<S>> = try_join_all(
230            accounts
231                .into_iter()
232                .map(|a| Account::new(a, wallet_inner.clone()).boxed()),
233        )
234        .await?;
235
236        // If the wallet builder is not set, it means the user provided it and we need to update the addresses.
237        // In the other case it was loaded from the database and addresses are up to date.
238        if new_provided_client_options {
239            for account in accounts.iter_mut() {
240                // Safe to unwrap because we create the client if accounts aren't empty
241                account.update_account_bech32_hrp().await?;
242            }
243        }
244
245        Ok(Wallet {
246            inner: wallet_inner,
247            accounts: Arc::new(RwLock::new(accounts)),
248        })
249    }
250
251    #[cfg(feature = "storage")]
252    pub(crate) async fn from_wallet(wallet: &Wallet<S>) -> Self {
253        Self {
254            client_options: Some(wallet.client_options().await),
255            coin_type: Some(wallet.coin_type.load(Ordering::Relaxed)),
256            storage_options: Some(wallet.storage_options.clone()),
257            secret_manager: Some(wallet.secret_manager.clone()),
258        }
259    }
260}
261
262// Check if any of the locked inputs is not used in a transaction and unlock them, so they get available for new
263// transactions
264#[cfg(feature = "storage")]
265fn unlock_unused_inputs(accounts: &mut [AccountDetails]) -> crate::wallet::Result<()> {
266    log::debug!("[unlock_unused_inputs]");
267    for account in accounts.iter_mut() {
268        let mut used_inputs = HashSet::new();
269        for transaction_id in account.pending_transactions() {
270            if let Some(tx) = account.transactions().get(transaction_id) {
271                for input in &tx.inputs {
272                    used_inputs.insert(*input.metadata.output_id());
273                }
274            }
275        }
276        account.locked_outputs.retain(|input| {
277            let used = used_inputs.contains(input);
278            if !used {
279                log::debug!("unlocking unused input {input}");
280            }
281            used
282        })
283    }
284    Ok(())
285}
286
287#[cfg(feature = "serde")]
288pub(crate) mod dto {
289    use serde::Deserialize;
290
291    use super::*;
292    #[cfg(feature = "storage")]
293    use crate::{client::secret::SecretManage, wallet::storage::StorageOptions};
294
295    #[derive(Debug, Deserialize)]
296    #[serde(rename_all = "camelCase")]
297    pub struct WalletBuilderDto {
298        #[serde(default, skip_serializing_if = "Option::is_none")]
299        pub(crate) client_options: Option<ClientOptions>,
300        #[serde(default, skip_serializing_if = "Option::is_none")]
301        pub(crate) coin_type: Option<u32>,
302        #[cfg(feature = "storage")]
303        #[serde(default, skip_serializing_if = "Option::is_none")]
304        pub(crate) storage_options: Option<StorageOptions>,
305    }
306
307    impl<S: SecretManage> From<WalletBuilderDto> for WalletBuilder<S> {
308        fn from(value: WalletBuilderDto) -> Self {
309            Self {
310                client_options: value.client_options,
311                coin_type: value.coin_type,
312                #[cfg(feature = "storage")]
313                storage_options: value.storage_options,
314                secret_manager: None,
315            }
316        }
317    }
318
319    impl<'de, S: SecretManage> Deserialize<'de> for WalletBuilder<S> {
320        fn deserialize<D>(d: D) -> Result<Self, D::Error>
321        where
322            D: serde::Deserializer<'de>,
323        {
324            WalletBuilderDto::deserialize(d).map(Into::into)
325        }
326    }
327}