etopay_sdk/core/
wallet.rs

1//! This module provides methods for initializing, verifying, deleting, and creating wallets, as well as
2//! migrating wallets from mnemonic or backup, creating backups, and verifying PINs.
3//!
4//! It also includes various helper functions and imports required for the wallet functionality.
5
6use super::Sdk;
7use crate::{
8    backend::dlt::put_user_address,
9    error::Result,
10    types::newtypes::{EncryptionPin, EncryptionSalt, PlainPassword},
11    wallet::error::{ErrorKind, WalletError},
12};
13use etopay_wallet::{
14    MnemonicDerivationOption, sort_by_date,
15    types::{CryptoAmount, WalletTxInfo, WalletTxInfoList, WalletTxStatus},
16};
17
18use log::{debug, info, warn};
19
20impl Sdk {
21    /// Create and store a wallet from a new random mnemonic
22    ///
23    /// # Arguments
24    ///
25    /// * `pin` - The PIN for the wallet.
26    ///
27    /// # Returns
28    ///
29    /// The new random mnemonic.
30    ///
31    /// # Errors
32    ///
33    /// * [`crate::Error::MissingConfig`] - If the sdk config is missing.
34    /// * [`crate::Error::UserRepoNotInitialized`] - If there is an error initializing the repository.
35    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
36    pub async fn create_wallet_from_new_mnemonic(&mut self, pin: &EncryptionPin) -> Result<String> {
37        info!("Creating a new wallet from random mnemonic");
38
39        let Some(repo) = &mut self.repo else {
40            return Err(crate::Error::UserRepoNotInitialized);
41        };
42
43        let Some(active_user) = &mut self.active_user else {
44            return Err(crate::Error::UserNotInitialized);
45        };
46
47        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
48
49        let mnemonic = active_user
50            .wallet_manager
51            .create_wallet_from_new_mnemonic(config, &self.access_token, repo, pin)
52            .await?;
53        Ok(mnemonic)
54    }
55
56    /// Create and store a wallet from an existing mnemonic
57    ///
58    /// # Arguments
59    ///
60    /// * `pin` - The PIN for the wallet.
61    /// * `mnemonic` - The mnemonic to use for the wallet.
62    ///
63    /// # Errors
64    ///
65    /// * [`crate::Error::MissingConfig`] - If the sdk config is missing.
66    /// * [`crate::Error::UserRepoNotInitialized`] - If there is an error initializing the repository.
67    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
68    pub async fn create_wallet_from_existing_mnemonic(&mut self, pin: &EncryptionPin, mnemonic: &str) -> Result<()> {
69        info!("Creating a new wallet from existing mnemonic");
70
71        let Some(repo) = &mut self.repo else {
72            return Err(crate::Error::UserRepoNotInitialized);
73        };
74
75        let Some(active_user) = &mut self.active_user else {
76            return Err(crate::Error::UserNotInitialized);
77        };
78
79        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
80
81        active_user
82            .wallet_manager
83            .create_wallet_from_existing_mnemonic(config, &self.access_token, repo, pin, mnemonic)
84            .await?;
85        Ok(())
86    }
87
88    /// Create and store a wallet from an existing kdbx backup file
89    ///
90    /// # Arguments
91    ///
92    /// * `pin` - The PIN for the wallet.
93    /// * `backup` - The bytes representing the backup file.
94    /// * `backup_password` - The password used when creating the backup file.
95    ///
96    /// # Errors
97    ///
98    /// * [`crate::Error::MissingConfig`] - If the sdk config is missing.
99    /// * [`crate::Error::UserRepoNotInitialized`] - If there is an error initializing the repository.
100    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
101    pub async fn create_wallet_from_backup(
102        &mut self,
103        pin: &EncryptionPin,
104        backup: &[u8],
105        backup_password: &PlainPassword,
106    ) -> Result<()> {
107        info!("Creating a new wallet from backup");
108
109        let Some(repo) = &mut self.repo else {
110            return Err(crate::Error::UserRepoNotInitialized);
111        };
112
113        let Some(active_user) = &mut self.active_user else {
114            return Err(crate::Error::UserNotInitialized);
115        };
116
117        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
118
119        active_user
120            .wallet_manager
121            .create_wallet_from_backup(config, &self.access_token, repo, pin, backup, backup_password)
122            .await?;
123        Ok(())
124    }
125
126    /// Create a kdbx wallet backup from an existing wallet.
127    ///
128    /// # Arguments
129    ///
130    /// * `pin` - The PIN for the wallet.
131    /// * `backup_password` - The password to use when creating the backup file.
132    ///
133    /// # Returns
134    ///
135    /// The bytes of the kdbx backup file.
136    ///
137    /// # Errors
138    ///
139    /// * [`crate::Error::MissingConfig`] - If the sdk config is missing.
140    /// * [`crate::Error::UserRepoNotInitialized`] - If there is an error initializing the repository.
141    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
142    pub async fn create_wallet_backup(
143        &mut self,
144        pin: &EncryptionPin,
145        backup_password: &PlainPassword,
146    ) -> Result<Vec<u8>> {
147        info!("Creating wallet backup");
148
149        let Some(repo) = &mut self.repo else {
150            return Err(crate::Error::UserRepoNotInitialized);
151        };
152
153        let Some(active_user) = &mut self.active_user else {
154            return Err(crate::Error::UserNotInitialized);
155        };
156
157        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
158
159        let backup = active_user
160            .wallet_manager
161            .create_wallet_backup(config, &self.access_token, repo, pin, backup_password)
162            .await?;
163        Ok(backup)
164    }
165
166    /// Verify the mnemonic by checking if the mnemonic is the same as the one in the shares
167    ///
168    /// # Arguments
169    ///
170    /// * `pin` - The PIN for the wallet.
171    /// * `mnemonic` - The mnemonic to verify.
172    ///
173    /// # Returns
174    ///
175    /// Returns `Ok(true)` if the mnemonic is successfully verified, otherwise returns `Ok(false)`,
176    /// or an `Error`.
177    ///
178    /// # Errors
179    ///
180    /// * [`crate::Error::MissingConfig`] - If the sdk config is missing.
181    /// * [`crate::Error::UserRepoNotInitialized`] - If there is an error initializing the repository.
182    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
183    pub async fn verify_mnemonic(&mut self, pin: &EncryptionPin, mnemonic: &str) -> Result<bool> {
184        info!("Verifying mnemonic");
185
186        let Some(repo) = &mut self.repo else {
187            return Err(crate::Error::UserRepoNotInitialized);
188        };
189
190        let Some(active_user) = &mut self.active_user else {
191            return Err(crate::Error::UserNotInitialized);
192        };
193
194        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
195
196        let is_verified = active_user
197            .wallet_manager
198            .check_mnemonic(config, &self.access_token, repo, pin, mnemonic)
199            .await?;
200        Ok(is_verified)
201    }
202
203    /// Delete the currently active wallet
204    ///
205    /// Deletes the currently active wallet, potentially resulting in loss of funds if the mnemonic or wallet is not backed up.
206    ///
207    /// # Returns
208    ///
209    /// Returns `Ok(())` if the wallet is successfully deleted, otherwise returns an `Error`.
210    ///
211    /// # Errors
212    ///
213    /// * [`crate::Error::MissingConfig`] - If the sdk config is missing.
214    /// * [`crate::Error::UserRepoNotInitialized`] - If there is an error initializing the repository.
215    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
216    pub async fn delete_wallet(&mut self, pin: &EncryptionPin) -> Result<()> {
217        warn!("Deleting wallet for user. Potential loss of funds if mnemonic/wallet is not backed up!");
218
219        self.verify_pin(pin).await?;
220
221        let Some(repo) = &mut self.repo else {
222            return Err(crate::Error::UserRepoNotInitialized);
223        };
224        let Some(active_user) = &mut self.active_user else {
225            return Err(crate::Error::UserNotInitialized);
226        };
227
228        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
229
230        active_user
231            .wallet_manager
232            .delete_wallet(config, &self.access_token, repo)
233            .await?;
234
235        Ok(())
236    }
237
238    /// Verify pin
239    ///
240    /// Verifies the pin for the wallet.
241    ///
242    /// # Arguments
243    ///
244    /// * `pin` - The pin to verify.
245    ///
246    /// # Returns
247    ///
248    /// Returns `Ok(())` if the pin is verified successfully, otherwise returns an `Error`.
249    ///
250    /// # Errors
251    ///
252    /// * [`crate::Error::UserRepoNotInitialized`] - If there is an error initializing the repository.
253    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
254    /// * [`WalletError::WalletNotInitialized`] - If there is an error initializing the wallet.
255    /// * [`WalletError::WrongPinOrPassword`] - If the pin or password is incorrect.
256    pub async fn verify_pin(&self, pin: &EncryptionPin) -> Result<()> {
257        info!("Verifying wallet pin");
258        let Some(repo) = &self.repo else {
259            return Err(crate::Error::UserRepoNotInitialized);
260        };
261        let Some(active_user) = &self.active_user else {
262            return Err(crate::Error::UserNotInitialized);
263        };
264
265        let username = &active_user.username;
266        let user = repo.get(username)?;
267
268        // Ensure encrypted password exists in user
269        let Some(encrypted_password) = user.encrypted_password else {
270            return Err(WalletError::WalletNotInitialized(ErrorKind::MissingPassword))?;
271        };
272
273        // Decrypt the password using the provided PIN
274        if encrypted_password.decrypt(pin, &user.salt).is_err() {
275            return Err(WalletError::WrongPinOrPassword)?;
276        }
277        Ok(())
278    }
279
280    /// Reset pin
281    ///
282    /// Resets the pin for the wallet using the provided password and new pin.
283    ///
284    /// # Arguments
285    ///
286    /// * `old_pin` - The old wallet pin.
287    /// * `new_pin` - The new pin to set for the wallet.
288    ///
289    /// # Returns
290    ///
291    /// Returns `Ok(())` if the pin is changed successfully, otherwise returns an `Error`.
292    ///
293    /// # Errors
294    ///
295    /// * [`crate::Error::UserRepoNotInitialized`] - If there is an error initializing the repository.
296    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
297    /// * [`WalletError::WalletNotInitialized`] - If there is an error initializing the wallet.
298    /// * [`WalletError::WrongPinOrPassword`] - If the pin or password is incorrect.
299    pub async fn change_pin(&mut self, old_pin: &EncryptionPin, new_pin: &EncryptionPin) -> Result<()> {
300        info!("Resetting pin with password");
301        let Some(repo) = &mut self.repo else {
302            return Err(crate::Error::UserRepoNotInitialized);
303        };
304        let Some(active_user) = &mut self.active_user else {
305            return Err(crate::Error::UserNotInitialized);
306        };
307
308        let username = &active_user.username;
309        let mut user = repo.get(username)?;
310
311        let Some(encrypted_password) = user.encrypted_password else {
312            return Err(WalletError::WalletNotInitialized(ErrorKind::MissingPassword))?;
313        };
314
315        // decrypt the password
316        let password = encrypted_password.decrypt(old_pin, &user.salt)?;
317
318        // Set new pin and encrypted password
319        let salt = EncryptionSalt::generate();
320        let encrypted_password = password.encrypt(new_pin, &salt)?;
321
322        // Update user
323        user.salt = salt;
324        user.encrypted_password = Some(encrypted_password);
325        repo.update(&user)?;
326
327        Ok(())
328    }
329
330    /// Set the password to use for wallet operations. If the password was already set, this changes it.
331    ///
332    /// # Arguments
333    ///
334    /// * `pin` - The pin to encrypt the password with.
335    /// * `new_password` - The new password to set for the wallet.
336    ///
337    /// # Returns
338    ///
339    /// Returns `Ok(())` if the password is set successfully, otherwise returns an `Error`.
340    ///
341    /// # Errors
342    ///
343    /// * [`crate::Error::UserRepoNotInitialized`] - If there is an error initializing the repository.
344    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
345    pub async fn set_wallet_password(&mut self, pin: &EncryptionPin, new_password: &PlainPassword) -> Result<()> {
346        info!("Setting password");
347
348        let Some(repo) = &mut self.repo else {
349            return Err(crate::Error::UserRepoNotInitialized);
350        };
351        let Some(active_user) = &mut self.active_user else {
352            return Err(crate::Error::UserNotInitialized);
353        };
354
355        let mut user = repo.get(&active_user.username)?;
356
357        // if password already exists, return an error!
358        if let Some(encrypted_password) = user.encrypted_password {
359            info!("Password exists, changing password");
360
361            // verify that the pin is correct by decrypting the password using the provided PIN
362            if encrypted_password.decrypt(pin, &user.salt).is_err() {
363                return Err(WalletError::WrongPinOrPassword)?;
364            }
365
366            let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
367
368            active_user
369                .wallet_manager
370                .change_wallet_password(config, &self.access_token, repo, pin, new_password)
371                .await?;
372        } else {
373            // Set new pin and encrypted password
374            let salt = EncryptionSalt::generate();
375            let encrypted_password = new_password.encrypt(pin, &salt)?;
376
377            // Update user
378            user.salt = salt;
379            user.encrypted_password = Some(encrypted_password);
380            repo.update(&user)?;
381        }
382
383        Ok(())
384    }
385
386    /// Check if the password to use for wallet operations is set. If this returns `false`,
387    /// the password should be set with [`set_wallet_password`], otherwise you need to use
388    /// [`change_password`] to change it.
389    ///
390    /// # Returns
391    ///
392    /// Returns `Ok(true)` if the password is set successfully, otherwise returns `Ok(false)`.
393    ///
394    /// # Errors
395    ///
396    /// * [`crate::Error::UserRepoNotInitialized`] - If there is an error initializing the repository.
397    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
398    pub async fn is_wallet_password_set(&self) -> Result<bool> {
399        info!("Checking if password is set");
400
401        let Some(repo) = &self.repo else {
402            return Err(crate::Error::UserRepoNotInitialized);
403        };
404        let Some(active_user) = &self.active_user else {
405            return Err(crate::Error::UserNotInitialized);
406        };
407
408        let user = repo.get(&active_user.username)?;
409
410        Ok(user.encrypted_password.is_some())
411    }
412
413    /// Generates a new receiver address (based on selected currency in the config) for the wallet.
414    ///
415    /// # Returns
416    ///
417    /// Returns the generated address as a `String` if successful, otherwise returns an `Error`.
418    ///
419    /// # Errors
420    ///
421    /// * [`crate::Error::UserRepoNotInitialized`] - If there is an error initializing the repository.
422    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
423    /// * [`crate::Error::MissingConfig`] - If the sdk config is missing.
424    pub async fn generate_new_address(&mut self, pin: &EncryptionPin) -> Result<String> {
425        info!("Generating new wallet address");
426        self.verify_pin(pin).await?;
427        let Some(repo) = &mut self.repo else {
428            return Err(crate::Error::UserRepoNotInitialized);
429        };
430        let Some(active_user) = &mut self.active_user else {
431            return Err(crate::Error::UserNotInitialized);
432        };
433        let network = self.active_network.as_ref().ok_or(crate::Error::MissingNetwork)?;
434        let config = self.config.as_mut().ok_or(crate::Error::MissingConfig)?;
435        let wallet = active_user
436            .wallet_manager
437            .try_get(
438                config,
439                &self.access_token,
440                repo,
441                network,
442                pin,
443                &active_user.mnemonic_derivation_options,
444            )
445            .await?;
446
447        let address = wallet.get_address().await?;
448
449        // if there is an access token, push the generated address to the backend
450        if let Some(access_token) = self.access_token.as_ref() {
451            if network.can_do_purchases {
452                put_user_address(config, access_token, &network.key, &address).await?;
453            }
454        }
455        debug!("Generated address: {address}");
456        Ok(address)
457    }
458
459    /// Get the balance of the user
460    ///
461    /// Fetches the balance of the user from the wallet.
462    ///
463    /// # Returns
464    ///
465    /// Returns the balance as a `f64` if successful, otherwise returns an `Error`.
466    ///
467    /// # Errors
468    ///
469    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
470    /// * [`WalletError::WalletNotInitialized`] - If there is an error initializing the wallet.
471    pub async fn get_balance(&mut self, pin: &EncryptionPin) -> Result<CryptoAmount> {
472        info!("Fetching balance");
473        self.verify_pin(pin).await?;
474        let wallet = self.try_get_active_user_wallet(pin).await?;
475        let balance = wallet.get_balance().await?;
476        debug!("Balance: {balance:?}");
477        Ok(balance)
478    }
479
480    /// wallet transaction list
481    ///
482    /// Returns paginated list of wallet transaction list.
483    ///
484    /// # Arguments
485    ///
486    /// * `start` - The starting index of transactions to fetch.
487    /// * `limit` - The number of transactions per page.
488    ///
489    /// # Returns
490    ///
491    /// Returns a `WalletTxInfoList` containing paginated history of wallet transactions if the outputs are claimed successfully, otherwise returns an `Error`.
492    ///
493    /// # Errors
494    ///
495    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
496    /// * [`WalletError::WalletNotInitialized`] - If there is an error initializing the wallet.
497    pub async fn get_wallet_tx_list(
498        &mut self,
499        pin: &EncryptionPin,
500        start: usize,
501        limit: usize,
502    ) -> Result<WalletTxInfoList> {
503        info!("Wallet getting list of transactions");
504        self.verify_pin(pin).await?;
505
506        let Some(repo) = &mut self.repo else {
507            return Err(crate::Error::UserRepoNotInitialized);
508        };
509        let Some(active_user) = &mut self.active_user else {
510            return Err(crate::Error::UserNotInitialized);
511        };
512        let network = self.active_network.as_ref().ok_or(crate::Error::MissingNetwork)?;
513        let config = self.config.as_mut().ok_or(crate::Error::MissingConfig)?;
514        let wallet = active_user
515            .wallet_manager
516            .try_get(
517                config,
518                &self.access_token,
519                repo,
520                network,
521                pin,
522                &active_user.mnemonic_derivation_options,
523            )
524            .await?;
525
526        let user = repo.get(active_user.username.as_str())?;
527
528        // We retrieve the transaction list from the wallet,
529        // then synchronize selected transactions (by fetching their current status from the network),
530        // and finally, save the refreshed list back to the wallet
531        let mut wallet_transactions = user.wallet_transactions;
532
533        // We call get_wallet_tx_list to merge the responses with any existing transactions
534        // (if there are new ones, we need to get their details and insert into the local list)
535        match wallet.get_wallet_tx_list(start, limit).await {
536            Ok(transaction_hashes) => {
537                // go through and get the details for any new hashes
538
539                log::debug!("Digests: {:#?}", transaction_hashes);
540                for hash in transaction_hashes {
541                    // check if transaction is already in the list (not very efficient to do a linear search, but good enough for now)
542                    // check both the transaction hash and the network key, as hash collisions can occur across different blockchain networks
543                    if wallet_transactions
544                        .iter()
545                        .any(|t| t.transaction_hash == hash && t.network_key == network.key)
546                    {
547                        continue;
548                    }
549
550                    log::debug!("Getting details for new transaction with hash {hash}");
551
552                    // not included, we should add it!
553                    match wallet.get_wallet_tx(&hash).await {
554                        Err(e) => log::warn!("Could not get transaction details for {hash}: {e}"),
555                        Ok(details) => {
556                            // TODO: insert into sorted list based on date?
557                            wallet_transactions.push(details);
558                        }
559                    }
560                }
561            }
562            // do nothing if feature is not supported
563            Err(etopay_wallet::WalletError::WalletFeatureNotImplemented) => {}
564            Err(e) => return Err(e.into()),
565        }
566
567        // sort wallet transactions
568        sort_by_date(&mut wallet_transactions);
569
570        for transaction in wallet_transactions
571            .iter_mut()
572            .filter(|tx| tx.network_key == network.key)
573            .skip(start)
574            .take(limit)
575        {
576            // We don't need to query the network for the state of this transaction,
577            // because it has already been synchronized earlier (as indicated by `WalletTxStatus::Confirmed`).
578            if transaction.status == WalletTxStatus::Confirmed {
579                continue;
580            }
581
582            match wallet.get_wallet_tx(&transaction.transaction_hash).await {
583                Ok(stx) => *transaction = stx,
584                Err(e) => {
585                    // On error, return historical (cached) transaction data
586                    log::debug!(
587                        "[sync_transactions] could not retrieve data about transaction from the network, transaction: {:?}, error: {:?}",
588                        transaction,
589                        e
590                    );
591                }
592            }
593        }
594
595        let tx_list_filtered = wallet_transactions
596            .iter()
597            .filter(|tx| tx.network_key == network.key)
598            .skip(start)
599            .take(limit)
600            .cloned()
601            .collect();
602
603        // store updated transactions in user DB
604        let _ = repo.set_wallet_transactions(&user.username, wallet_transactions);
605
606        Ok(WalletTxInfoList {
607            transactions: tx_list_filtered,
608        })
609    }
610
611    /// wallet transaction
612    ///
613    /// Returns the wallet transaction details.
614    ///
615    /// # Arguments
616    ///
617    /// * `tx_id` - The transaction id of particular transaction.
618    ///
619    /// # Returns
620    ///
621    /// Returns `WalletTxInfo` detailed report of particular wallet transaction if the outputs are claimed successfully, otherwise returns an `Error`.
622    ///
623    /// # Errors
624    ///
625    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
626    /// * [`WalletError::WalletNotInitialized`] - If there is an error initializing the wallet.
627    pub async fn get_wallet_tx(&mut self, pin: &EncryptionPin, tx_id: &str) -> Result<WalletTxInfo> {
628        info!("Wallet getting details of particular transactions");
629        self.verify_pin(pin).await?;
630        let wallet = self.try_get_active_user_wallet(pin).await?;
631        let wallet_tx = wallet.get_wallet_tx(tx_id).await?;
632        Ok(wallet_tx)
633    }
634
635    /// Set wallet mnemonic derivation options
636    ///
637    /// # Arguments
638    ///
639    /// * `account` - The account to use.
640    /// * `index` - The index to use.
641    ///
642    /// # Errors
643    ///
644    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
645    pub async fn set_wallet_derivation_options(&mut self, account: u32, index: u32) -> Result<()> {
646        let options = MnemonicDerivationOption { account, index };
647
648        info!("Setting wallet mnemonic derivation options: {options:?}");
649
650        let Some(active_user) = &mut self.active_user else {
651            return Err(crate::Error::UserNotInitialized);
652        };
653
654        active_user.mnemonic_derivation_options = options;
655
656        Ok(())
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663    use crate::core::core_testing_utils::handle_error_test_cases;
664    use crate::testing_utils::{
665        ADDRESS, AUTH_PROVIDER, ENCRYPTED_WALLET_PASSWORD, ETH_NETWORK_KEY, HEADER_X_APP_NAME, IOTA_NETWORK_KEY,
666        MNEMONIC, PIN, SALT, TOKEN, TX_INDEX, USERNAME, WALLET_PASSWORD, example_api_networks, example_get_user,
667        example_wallet_tx_info, set_config,
668    };
669    use crate::types::users::UserEntity;
670    use crate::{
671        core::Sdk,
672        types::users::KycType,
673        user::MockUserRepo,
674        wallet_manager::{MockWalletManager, WalletBorrow},
675    };
676    use api_types::api::dlt::SetUserAddressRequest;
677    use api_types::api::viviswap::detail::SwapPaymentDetailKey;
678    use etopay_wallet::MockWalletUser;
679    use mockall::predicate::eq;
680    use mockito::Matcher;
681    use rstest::rstest;
682    use rust_decimal_macros::dec;
683    use std::sync::LazyLock;
684
685    const BACKUP: &[u8] = &[42, 77, 15, 203, 89, 123, 34, 56, 178, 90, 210, 33, 47, 192, 1, 17];
686
687    #[rstest]
688    #[case::success(Ok(MNEMONIC.to_string()))]
689    #[case::missing_config(Err(crate::Error::MissingConfig))]
690    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
691    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
692    #[tokio::test]
693    async fn test_create_wallet_from_new_mnemonic(#[case] expected: Result<String>) {
694        // Arrange
695        let (_srv, config, _cleanup) = set_config().await;
696        let mut sdk = Sdk::new(config).unwrap();
697
698        match &expected {
699            Ok(_) => {
700                sdk.repo = Some(Box::new(MockUserRepo::new()));
701                let mut mock_wallet_manager = MockWalletManager::new();
702                mock_wallet_manager
703                    .expect_create_wallet_from_new_mnemonic()
704                    .once()
705                    .returning(|_, _, _, _| Ok(MNEMONIC.to_string()));
706                sdk.active_user = Some(crate::types::users::ActiveUser {
707                    username: USERNAME.into(),
708                    wallet_manager: Box::new(mock_wallet_manager),
709                    mnemonic_derivation_options: Default::default(),
710                });
711            }
712            Err(error) => {
713                handle_error_test_cases(error, &mut sdk, 0, 0).await;
714            }
715        }
716
717        // Act
718        let response = sdk.create_wallet_from_new_mnemonic(&PIN).await;
719
720        // Assert
721        match expected {
722            Ok(resp) => {
723                assert_eq!(response.unwrap(), resp);
724            }
725            Err(ref expected_err) => {
726                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
727            }
728        }
729    }
730
731    #[rstest]
732    #[case::success(Ok(()))]
733    #[case::missing_config(Err(crate::Error::MissingConfig))]
734    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
735    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
736    #[tokio::test]
737    async fn test_create_wallet_from_existing_mnemonic(#[case] expected: Result<()>) {
738        // Arrange
739        let (_srv, config, _cleanup) = set_config().await;
740        let mut sdk = Sdk::new(config).unwrap();
741
742        match &expected {
743            Ok(_) => {
744                sdk.repo = Some(Box::new(MockUserRepo::new()));
745                let mut mock_wallet_manager = MockWalletManager::new();
746                mock_wallet_manager
747                    .expect_create_wallet_from_existing_mnemonic()
748                    .once()
749                    .returning(|_, _, _, _, _| Ok(()));
750                sdk.active_user = Some(crate::types::users::ActiveUser {
751                    username: USERNAME.into(),
752                    wallet_manager: Box::new(mock_wallet_manager),
753                    mnemonic_derivation_options: Default::default(),
754                });
755            }
756            Err(error) => {
757                handle_error_test_cases(error, &mut sdk, 0, 0).await;
758            }
759        }
760
761        // Act
762        let response = sdk.create_wallet_from_existing_mnemonic(&PIN, MNEMONIC).await;
763
764        // Assert
765        match expected {
766            Ok(()) => response.unwrap(),
767            Err(ref expected_err) => {
768                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
769            }
770        }
771    }
772
773    #[rstest]
774    #[case::success(Ok(BACKUP.to_vec()))]
775    #[case::missing_config(Err(crate::Error::MissingConfig))]
776    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
777    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
778    #[tokio::test]
779    async fn test_create_wallet_backup(#[case] expected: Result<Vec<u8>>) {
780        // Arrange
781        let (_srv, config, _cleanup) = set_config().await;
782        let mut sdk = Sdk::new(config).unwrap();
783
784        match &expected {
785            Ok(_) => {
786                sdk.repo = Some(Box::new(MockUserRepo::new()));
787                let mut mock_wallet_manager = MockWalletManager::new();
788                mock_wallet_manager
789                    .expect_create_wallet_backup()
790                    .once()
791                    .returning(|_, _, _, _, _| Ok(BACKUP.to_vec()));
792                sdk.active_user = Some(crate::types::users::ActiveUser {
793                    username: USERNAME.into(),
794                    wallet_manager: Box::new(mock_wallet_manager),
795                    mnemonic_derivation_options: Default::default(),
796                });
797            }
798            Err(error) => {
799                handle_error_test_cases(error, &mut sdk, 0, 0).await;
800            }
801        }
802
803        // Act
804        let response = sdk.create_wallet_backup(&PIN, &WALLET_PASSWORD).await;
805
806        // Assert
807        match expected {
808            Ok(resp) => {
809                assert_eq!(response.unwrap(), resp);
810            }
811            Err(ref expected_err) => {
812                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
813            }
814        }
815    }
816
817    #[rstest]
818    #[case::success(Ok(()))]
819    #[case::missing_config(Err(crate::Error::MissingConfig))]
820    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
821    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
822    #[tokio::test]
823    async fn test_create_wallet_from_backup(#[case] expected: Result<()>) {
824        // Arrange
825        let (_srv, config, _cleanup) = set_config().await;
826        let mut sdk = Sdk::new(config).unwrap();
827
828        match &expected {
829            Ok(_) => {
830                sdk.repo = Some(Box::new(MockUserRepo::new()));
831                let mut mock_wallet_manager = MockWalletManager::new();
832                mock_wallet_manager
833                    .expect_create_wallet_from_backup()
834                    .once()
835                    .returning(|_, _, _, _, _, _| Ok(()));
836                sdk.active_user = Some(crate::types::users::ActiveUser {
837                    username: USERNAME.into(),
838                    wallet_manager: Box::new(mock_wallet_manager),
839                    mnemonic_derivation_options: Default::default(),
840                });
841            }
842            Err(error) => {
843                handle_error_test_cases(error, &mut sdk, 0, 0).await;
844            }
845        }
846
847        // Act
848        let response = sdk.create_wallet_from_backup(&PIN, BACKUP, &WALLET_PASSWORD).await;
849
850        // Assert
851        match expected {
852            Ok(()) => response.unwrap(),
853            Err(ref expected_err) => {
854                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
855            }
856        }
857    }
858
859    #[rstest]
860    #[case::success(Ok(true))]
861    #[case::missing_config(Err(crate::Error::MissingConfig))]
862    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
863    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
864    #[tokio::test]
865    async fn test_verify_mnemonic(#[case] expected: Result<bool>) {
866        // Arrange
867        let (_srv, config, _cleanup) = set_config().await;
868        let mut sdk = Sdk::new(config).unwrap();
869
870        match &expected {
871            Ok(_) => {
872                sdk.repo = Some(Box::new(MockUserRepo::new()));
873                let mut mock_wallet_manager = MockWalletManager::new();
874                mock_wallet_manager
875                    .expect_check_mnemonic()
876                    .once()
877                    .returning(|_, _, _, _, _| Ok(true));
878                sdk.active_user = Some(crate::types::users::ActiveUser {
879                    username: USERNAME.into(),
880                    wallet_manager: Box::new(mock_wallet_manager),
881                    mnemonic_derivation_options: Default::default(),
882                });
883            }
884            Err(error) => {
885                handle_error_test_cases(error, &mut sdk, 0, 0).await;
886            }
887        }
888
889        // Act
890        let response = sdk.verify_mnemonic(&PIN, MNEMONIC).await;
891
892        // Assert
893        match expected {
894            Ok(resp) => {
895                assert_eq!(response.unwrap(), resp);
896            }
897            Err(ref expected_err) => {
898                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
899            }
900        }
901    }
902
903    #[rstest]
904    #[case::success(Ok(()))]
905    #[case::missing_config(Err(crate::Error::MissingConfig))]
906    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
907    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
908    #[tokio::test]
909    async fn test_delete_wallet(#[case] expected: Result<()>) {
910        // Arrange
911        let (_srv, config, _cleanup) = set_config().await;
912        let mut sdk = Sdk::new(config).unwrap();
913
914        let pin = EncryptionPin::try_from_string("123456").unwrap();
915
916        match &expected {
917            Ok(_) => {
918                let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 2, KycType::Undefined);
919                mock_user_repo.expect_update().once().returning(|_| Ok(()));
920
921                sdk.repo = Some(Box::new(mock_user_repo));
922
923                let mut mock_wallet_manager = MockWalletManager::new();
924                mock_wallet_manager
925                    .expect_delete_wallet()
926                    .once()
927                    .returning(|_, _, _| Ok(()));
928                sdk.active_user = Some(crate::types::users::ActiveUser {
929                    username: USERNAME.into(),
930                    wallet_manager: Box::new(mock_wallet_manager),
931                    mnemonic_derivation_options: Default::default(),
932                });
933
934                let new_pin = EncryptionPin::try_from_string("123456").unwrap();
935                sdk.change_pin(&pin, &new_pin).await.unwrap();
936            }
937            Err(error) => {
938                handle_error_test_cases(error, &mut sdk, 1, 0).await;
939            }
940        }
941
942        // Act
943        let response = sdk.delete_wallet(&PIN).await;
944
945        // Assert
946        match expected {
947            Ok(()) => response.unwrap(),
948            Err(ref expected_err) => {
949                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
950            }
951        }
952    }
953
954    #[rstest]
955    #[case::success(Ok(()))]
956    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
957    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
958    #[case::wallet_not_initialized(Err(crate::Error::Wallet(WalletError::WalletNotInitialized(
959        ErrorKind::MissingPassword
960    ))))]
961    #[tokio::test]
962    async fn test_verify_pin(#[case] expected: Result<()>) {
963        // Arrange
964        let (_srv, config, _cleanup) = set_config().await;
965        let mut sdk = Sdk::new(config).unwrap();
966
967        match &expected {
968            Ok(_) => {
969                let mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
970                sdk.repo = Some(Box::new(mock_user_repo));
971
972                sdk.active_user = Some(crate::types::users::ActiveUser {
973                    username: USERNAME.into(),
974                    wallet_manager: Box::new(MockWalletManager::new()),
975                    mnemonic_derivation_options: Default::default(),
976                });
977            }
978            Err(error) => {
979                handle_error_test_cases(error, &mut sdk, 1, 0).await;
980            }
981        }
982
983        // Act
984        let response = sdk.verify_pin(&PIN).await;
985
986        // Assert
987        match expected {
988            Ok(()) => response.unwrap(),
989            Err(ref expected_err) => {
990                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
991            }
992        }
993    }
994
995    #[rstest]
996    #[case::success(Ok(()))]
997    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
998    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
999    #[case::wallet_not_initialized(Err(crate::Error::Wallet(WalletError::WalletNotInitialized(
1000        ErrorKind::MissingPassword
1001    ))))]
1002    #[tokio::test]
1003    async fn test_change_pin(#[case] expected: Result<()>) {
1004        // Arrange
1005        let (_srv, config, _cleanup) = set_config().await;
1006        let mut sdk = Sdk::new(config).unwrap();
1007
1008        match &expected {
1009            Ok(_) => {
1010                let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
1011                mock_user_repo.expect_update().once().returning(|_| Ok(()));
1012                sdk.repo = Some(Box::new(mock_user_repo));
1013
1014                sdk.active_user = Some(crate::types::users::ActiveUser {
1015                    username: USERNAME.into(),
1016                    wallet_manager: Box::new(MockWalletManager::new()),
1017                    mnemonic_derivation_options: Default::default(),
1018                });
1019            }
1020            Err(error) => {
1021                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1022            }
1023        }
1024
1025        // Act
1026        let new_pin: LazyLock<EncryptionPin> = LazyLock::new(|| EncryptionPin::try_from_string("432154").unwrap());
1027        let response = sdk.change_pin(&PIN, &new_pin).await;
1028
1029        // Assert
1030        match expected {
1031            Ok(()) => response.unwrap(),
1032            Err(ref expected_err) => {
1033                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1034            }
1035        }
1036    }
1037
1038    #[rstest]
1039    #[case::success(Ok(()))]
1040    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
1041    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
1042    #[tokio::test]
1043    async fn test_set_wallet_password(#[case] expected: Result<()>) {
1044        // Arrange
1045        let (_srv, config, _cleanup) = set_config().await;
1046        let mut sdk = Sdk::new(config).unwrap();
1047
1048        match &expected {
1049            Ok(_) => {
1050                let mut mock_user_repo = MockUserRepo::new();
1051                mock_user_repo.expect_get().times(1).returning(move |r1| {
1052                    assert_eq!(r1, USERNAME);
1053                    Ok(UserEntity {
1054                        user_id: None,
1055                        username: USERNAME.into(),
1056                        encrypted_password: None,
1057                        salt: SALT.into(),
1058                        is_kyc_verified: false,
1059                        kyc_type: KycType::Undefined,
1060                        viviswap_state: Option::None,
1061                        local_share: None,
1062                        wallet_transactions: Vec::new(),
1063                    })
1064                });
1065                mock_user_repo.expect_update().once().returning(|_| Ok(()));
1066                sdk.repo = Some(Box::new(mock_user_repo));
1067
1068                sdk.active_user = Some(crate::types::users::ActiveUser {
1069                    username: USERNAME.into(),
1070                    wallet_manager: Box::new(MockWalletManager::new()),
1071                    mnemonic_derivation_options: Default::default(),
1072                });
1073            }
1074            Err(error) => {
1075                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1076            }
1077        }
1078
1079        // Act
1080        let response = sdk.set_wallet_password(&PIN, &WALLET_PASSWORD).await;
1081
1082        // Assert
1083        match expected {
1084            Ok(()) => response.unwrap(),
1085            Err(ref expected_err) => {
1086                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1087            }
1088        }
1089    }
1090
1091    #[rstest]
1092    #[case::success(Ok(ADDRESS.to_string()))]
1093    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
1094    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
1095    #[case::missing_config(Err(crate::Error::MissingConfig))]
1096    #[tokio::test]
1097    async fn test_generate_new_address(#[case] expected: Result<String>) {
1098        // Arrange
1099        let (mut srv, config, _cleanup) = set_config().await;
1100        let mut sdk = Sdk::new(config).unwrap();
1101        let mut mock_server = None;
1102
1103        match &expected {
1104            Ok(_) => {
1105                let mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
1106                sdk.repo = Some(Box::new(mock_user_repo));
1107
1108                let mut mock_wallet_manager = MockWalletManager::new();
1109                mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1110                    let mut mock_wallet_user = MockWalletUser::new();
1111                    mock_wallet_user
1112                        .expect_get_address()
1113                        .once()
1114                        .returning(|| Ok(ADDRESS.to_string()));
1115                    Ok(WalletBorrow::from(mock_wallet_user))
1116                });
1117                sdk.active_user = Some(crate::types::users::ActiveUser {
1118                    username: USERNAME.into(),
1119                    wallet_manager: Box::new(mock_wallet_manager),
1120                    mnemonic_derivation_options: Default::default(),
1121                });
1122                sdk.access_token = Some(TOKEN.clone());
1123                sdk.set_networks(example_api_networks());
1124                sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
1125
1126                let mock_request = SetUserAddressRequest {
1127                    address: ADDRESS.into(),
1128                };
1129                let body = serde_json::to_string(&mock_request).unwrap();
1130
1131                mock_server = Some(
1132                    srv.mock("PUT", "/api/user/address")
1133                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
1134                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
1135                        .match_header("content-type", "application/json")
1136                        .match_query(Matcher::Exact("network_key=IOTA".to_string()))
1137                        .match_body(Matcher::Exact(body))
1138                        .with_status(201)
1139                        .expect(1)
1140                        .with_header("content-type", "application/json")
1141                        .create(),
1142                );
1143            }
1144            Err(error) => {
1145                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1146            }
1147        }
1148
1149        // Act
1150        let response = sdk.generate_new_address(&PIN).await;
1151
1152        // Assert
1153        match expected {
1154            Ok(resp) => {
1155                assert_eq!(response.unwrap(), resp);
1156            }
1157            Err(ref expected_err) => {
1158                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1159            }
1160        }
1161        if let Some(m) = mock_server {
1162            m.assert();
1163        }
1164    }
1165
1166    #[rstest]
1167    // SAFETY: we know that this value is not negative
1168    #[case::success(Ok(unsafe { CryptoAmount::new_unchecked(dec!(25.0)) }))]
1169    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
1170    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
1171    #[case::missing_config(Err(crate::Error::MissingConfig))]
1172    #[tokio::test]
1173    async fn test_get_balance(#[case] expected: Result<CryptoAmount>) {
1174        // Arrange
1175        let (_srv, config, _cleanup) = set_config().await;
1176        let mut sdk = Sdk::new(config).unwrap();
1177
1178        match &expected {
1179            Ok(_) => {
1180                let mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
1181                sdk.repo = Some(Box::new(mock_user_repo));
1182
1183                let mut mock_wallet_manager = MockWalletManager::new();
1184                mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1185                    let mut mock_wallet_user = MockWalletUser::new();
1186                    mock_wallet_user
1187                        .expect_get_balance()
1188                        .once()
1189                        // SAFETY: we know that this value is not negative
1190                        .returning(|| Ok(unsafe { CryptoAmount::new_unchecked(dec!(25.0)) }));
1191                    Ok(WalletBorrow::from(mock_wallet_user))
1192                });
1193                sdk.active_user = Some(crate::types::users::ActiveUser {
1194                    username: USERNAME.into(),
1195                    wallet_manager: Box::new(mock_wallet_manager),
1196                    mnemonic_derivation_options: Default::default(),
1197                });
1198                sdk.set_networks(example_api_networks());
1199                sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
1200            }
1201            Err(error) => {
1202                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1203            }
1204        }
1205
1206        // Act
1207        let response = sdk.get_balance(&PIN).await;
1208
1209        // Assert
1210        match expected {
1211            Ok(resp) => {
1212                assert_eq!(response.unwrap(), resp);
1213            }
1214            Err(ref expected_err) => {
1215                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1216            }
1217        }
1218    }
1219
1220    #[rstest]
1221    #[case::success(Ok(example_wallet_tx_info()))]
1222    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
1223    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
1224    #[case::missing_config(Err(crate::Error::MissingConfig))]
1225    #[tokio::test]
1226    async fn test_get_wallet_tx(#[case] expected: Result<WalletTxInfo>) {
1227        // Arrange
1228        let (_srv, config, _cleanup) = set_config().await;
1229        let mut sdk = Sdk::new(config).unwrap();
1230
1231        match &expected {
1232            Ok(_) => {
1233                let mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
1234                sdk.repo = Some(Box::new(mock_user_repo));
1235
1236                let mut mock_wallet_manager = MockWalletManager::new();
1237                mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1238                    let mut mock_wallet_user = MockWalletUser::new();
1239                    mock_wallet_user
1240                        .expect_get_wallet_tx()
1241                        .once()
1242                        .returning(|_| Ok(example_wallet_tx_info()));
1243                    Ok(WalletBorrow::from(mock_wallet_user))
1244                });
1245                sdk.active_user = Some(crate::types::users::ActiveUser {
1246                    username: USERNAME.into(),
1247                    wallet_manager: Box::new(mock_wallet_manager),
1248                    mnemonic_derivation_options: Default::default(),
1249                });
1250                sdk.set_networks(example_api_networks());
1251                sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
1252            }
1253            Err(error) => {
1254                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1255            }
1256        }
1257
1258        // Act
1259        let response = sdk.get_wallet_tx(&PIN, TX_INDEX).await;
1260
1261        // Assert
1262        match expected {
1263            Ok(resp) => {
1264                assert_eq!(response.unwrap(), resp);
1265            }
1266            Err(ref expected_err) => {
1267                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1268            }
1269        }
1270    }
1271
1272    #[rstest]
1273    #[case::success(Ok(WalletTxInfoList { transactions: vec![]}))]
1274    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
1275    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
1276    #[case::missing_config(Err(crate::Error::MissingConfig))]
1277    #[tokio::test]
1278    async fn test_get_wallet_tx_list(#[case] expected: Result<WalletTxInfoList>) {
1279        // Arrange
1280        let (_srv, config, _cleanup) = set_config().await;
1281        let mut sdk = Sdk::new(config).unwrap();
1282
1283        match &expected {
1284            Ok(_) => {
1285                let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 2, KycType::Undefined);
1286                mock_user_repo
1287                    .expect_set_wallet_transactions()
1288                    .once()
1289                    .returning(|_, _| Ok(()));
1290                sdk.repo = Some(Box::new(mock_user_repo));
1291
1292                let mut mock_wallet_manager = MockWalletManager::new();
1293                mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1294                    let mut mock_wallet_user = MockWalletUser::new();
1295                    mock_wallet_user
1296                        .expect_get_wallet_tx_list()
1297                        .once()
1298                        .returning(|_, _| Ok(vec![]));
1299                    Ok(WalletBorrow::from(mock_wallet_user))
1300                });
1301                sdk.active_user = Some(crate::types::users::ActiveUser {
1302                    username: USERNAME.into(),
1303                    wallet_manager: Box::new(mock_wallet_manager),
1304                    mnemonic_derivation_options: Default::default(),
1305                });
1306                sdk.set_networks(example_api_networks());
1307                sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
1308            }
1309            Err(error) => {
1310                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1311            }
1312        }
1313
1314        // Act
1315        let response = sdk.get_wallet_tx_list(&PIN, 0, 10).await;
1316
1317        // Assert
1318        match expected {
1319            Ok(resp) => {
1320                assert_eq!(response.unwrap(), resp);
1321            }
1322            Err(ref expected_err) => {
1323                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1324            }
1325        }
1326    }
1327
1328    #[tokio::test]
1329    async fn test_get_wallet_tx_list_filters_transactions_correctly() {
1330        // Arrange
1331        let (_srv, config, _cleanup) = set_config().await;
1332        let mut sdk = Sdk::new(config).unwrap();
1333
1334        // During the test, we expect the status of WalletTxInfo with transaction_id = 2
1335        // to transition from 'Pending' to 'Confirmed' after synchronization
1336        let mixed_wallet_transactions = vec![
1337            WalletTxInfo {
1338                date: "some date".to_string(),
1339                block_number_hash: None,
1340                transaction_hash: "some tx id".to_string(),
1341                receiver: String::new(),
1342                sender: String::new(),
1343                amount: unsafe { CryptoAmount::new_unchecked(dec!(20.0)) },
1344                network_key: "IOTA".to_string(),
1345                status: WalletTxStatus::Confirmed,
1346                explorer_url: None,
1347            },
1348            WalletTxInfo {
1349                date: "some date".to_string(),
1350                block_number_hash: None,
1351                transaction_hash: "1".to_string(),
1352                receiver: String::new(),
1353                sender: String::new(),
1354                amount: unsafe { CryptoAmount::new_unchecked(dec!(1.0)) },
1355                network_key: "ETH".to_string(),
1356                status: WalletTxStatus::Pending,
1357                explorer_url: None,
1358            },
1359            WalletTxInfo {
1360                date: "some date".to_string(),
1361                block_number_hash: None,
1362                transaction_hash: "2".to_string(),
1363                receiver: String::new(),
1364                sender: String::new(),
1365                amount: unsafe { CryptoAmount::new_unchecked(dec!(2.0)) },
1366                network_key: "ETH".to_string(),
1367                status: WalletTxStatus::Pending, // this one
1368                explorer_url: None,
1369            },
1370            WalletTxInfo {
1371                date: "some date".to_string(),
1372                block_number_hash: None,
1373                transaction_hash: "3".to_string(),
1374                receiver: String::new(),
1375                sender: String::new(),
1376                amount: unsafe { CryptoAmount::new_unchecked(dec!(3.0)) },
1377                network_key: "ETH".to_string(),
1378                status: WalletTxStatus::Pending,
1379                explorer_url: None,
1380            },
1381        ];
1382
1383        let mut mock_user_repo = MockUserRepo::new();
1384        mock_user_repo.expect_get().returning(move |_| {
1385            Ok(UserEntity {
1386                user_id: None,
1387                username: USERNAME.to_string(),
1388                encrypted_password: Some(ENCRYPTED_WALLET_PASSWORD.clone()),
1389                salt: SALT.into(),
1390                is_kyc_verified: false,
1391                kyc_type: KycType::Undefined,
1392                viviswap_state: None,
1393                local_share: None,
1394                wallet_transactions: mixed_wallet_transactions.clone(),
1395            })
1396        });
1397
1398        let mixed_wallet_transactions_after_synchronization = vec![
1399            WalletTxInfo {
1400                date: "some date".to_string(),
1401                block_number_hash: None,
1402                transaction_hash: "some tx id".to_string(),
1403                receiver: String::new(),
1404                sender: String::new(),
1405                amount: unsafe { CryptoAmount::new_unchecked(dec!(20.0)) },
1406                network_key: "IOTA".to_string(),
1407                status: WalletTxStatus::Confirmed,
1408                explorer_url: None,
1409            },
1410            WalletTxInfo {
1411                date: "some date".to_string(),
1412                block_number_hash: None,
1413                transaction_hash: "1".to_string(),
1414                receiver: String::new(),
1415                sender: String::new(),
1416                amount: unsafe { CryptoAmount::new_unchecked(dec!(1.0)) },
1417                network_key: "ETH".to_string(),
1418                status: WalletTxStatus::Pending,
1419                explorer_url: None,
1420            },
1421            WalletTxInfo {
1422                date: "some date".to_string(),
1423                block_number_hash: None,
1424                transaction_hash: "2".to_string(),
1425                receiver: String::new(),
1426                sender: String::new(),
1427                amount: unsafe { CryptoAmount::new_unchecked(dec!(2.0)) },
1428                network_key: "ETH".to_string(),
1429                status: WalletTxStatus::Confirmed,
1430                explorer_url: None,
1431            },
1432            WalletTxInfo {
1433                date: "some date".to_string(),
1434                block_number_hash: None,
1435                transaction_hash: "3".to_string(),
1436                receiver: String::new(),
1437                sender: String::new(),
1438                amount: unsafe { CryptoAmount::new_unchecked(dec!(3.0)) },
1439                network_key: "ETH".to_string(),
1440                status: WalletTxStatus::Pending,
1441                explorer_url: None,
1442            },
1443        ];
1444
1445        mock_user_repo
1446            .expect_set_wallet_transactions()
1447            .once()
1448            .with(
1449                eq(USERNAME.to_string()),
1450                eq(mixed_wallet_transactions_after_synchronization.clone()),
1451            )
1452            .returning(|_, _| Ok(()));
1453
1454        sdk.repo = Some(Box::new(mock_user_repo));
1455
1456        let mut mock_wallet_manager = MockWalletManager::new();
1457        mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1458            let mut mock_wallet_user = MockWalletUser::new();
1459            mock_wallet_user
1460                .expect_get_wallet_tx_list()
1461                .once()
1462                .returning(|_, _| Ok(vec![]));
1463            mock_wallet_user
1464                .expect_get_wallet_tx()
1465                .once()
1466                .with(eq(String::from("2"))) // WalletTxInfo.transaction_id = 2
1467                .returning(move |_| {
1468                    Ok(WalletTxInfo {
1469                        date: "some date".to_string(),
1470                        block_number_hash: None,
1471                        transaction_hash: "2".to_string(),
1472                        receiver: String::new(),
1473                        sender: String::new(),
1474                        amount: unsafe { CryptoAmount::new_unchecked(dec!(2.0)) },
1475                        network_key: "ETH".to_string(),
1476                        status: WalletTxStatus::Confirmed, // Pending -> Confirmed
1477                        explorer_url: None,
1478                    })
1479                });
1480            Ok(WalletBorrow::from(mock_wallet_user))
1481        });
1482
1483        sdk.active_user = Some(crate::types::users::ActiveUser {
1484            username: USERNAME.into(),
1485            wallet_manager: Box::new(mock_wallet_manager),
1486            mnemonic_derivation_options: Default::default(),
1487        });
1488
1489        sdk.set_networks(example_api_networks());
1490        sdk.set_network(ETH_NETWORK_KEY.to_string()).await.unwrap();
1491
1492        // Act
1493
1494        // We request a single WalletTxInfo using get_wallet_tx_list(start = 1, limit = 1)
1495        // We have stored transactions: [1 IOTA, 3 ETH]
1496        // The network key is ETH, so we search through the 3 ETH transactions
1497        // We select this one:
1498        // [WalletTxInfo{ ... }, -> WalletTxInfo{ transaction_id = 2 }, WalletTxInfo{ ... }]
1499        let response = sdk.get_wallet_tx_list(&PIN, 1, 1).await;
1500
1501        // Assert
1502        assert_eq!(
1503            response.unwrap(),
1504            WalletTxInfoList {
1505                transactions: vec![WalletTxInfo {
1506                    date: "some date".to_string(),
1507                    block_number_hash: None,
1508                    transaction_hash: "2".to_string(),
1509                    receiver: String::new(),
1510                    sender: String::new(),
1511                    amount: unsafe { CryptoAmount::new_unchecked(dec!(2.0)) },
1512                    network_key: "ETH".to_string(),
1513                    status: WalletTxStatus::Confirmed,
1514                    explorer_url: None,
1515                }]
1516            }
1517        );
1518    }
1519
1520    #[tokio::test]
1521    async fn test_get_wallet_tx_list_does_not_query_network_for_transaction_state() {
1522        // Arrange
1523        let (_srv, config, _cleanup) = set_config().await;
1524        let mut sdk = Sdk::new(config).unwrap();
1525
1526        let wallet_transactions = vec![WalletTxInfo {
1527            date: "some date".to_string(),
1528            block_number_hash: None,
1529            transaction_hash: "1".to_string(),
1530            receiver: String::new(),
1531            sender: String::new(),
1532            amount: unsafe { CryptoAmount::new_unchecked(dec!(1.0)) },
1533            network_key: "ETH".to_string(),
1534            status: WalletTxStatus::Confirmed,
1535            explorer_url: None,
1536        }];
1537
1538        let mut mock_user_repo = MockUserRepo::new();
1539        mock_user_repo.expect_get().returning(move |_| {
1540            Ok(UserEntity {
1541                user_id: None,
1542                username: USERNAME.to_string(),
1543                encrypted_password: Some(ENCRYPTED_WALLET_PASSWORD.clone()),
1544                salt: SALT.into(),
1545                is_kyc_verified: false,
1546                kyc_type: KycType::Undefined,
1547                viviswap_state: None,
1548                local_share: None,
1549                wallet_transactions: wallet_transactions.clone(),
1550            })
1551        });
1552
1553        mock_user_repo
1554            .expect_set_wallet_transactions()
1555            .once()
1556            .returning(|_, _| Ok(()));
1557
1558        sdk.repo = Some(Box::new(mock_user_repo));
1559
1560        let mut mock_wallet_manager = MockWalletManager::new();
1561        mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1562            let mut mock_wallet_user = MockWalletUser::new();
1563            mock_wallet_user
1564                .expect_get_wallet_tx_list()
1565                .once()
1566                .returning(|_, _| Ok(vec![]));
1567            mock_wallet_user.expect_get_wallet_tx().never();
1568            Ok(WalletBorrow::from(mock_wallet_user))
1569        });
1570
1571        sdk.active_user = Some(crate::types::users::ActiveUser {
1572            username: USERNAME.into(),
1573            wallet_manager: Box::new(mock_wallet_manager),
1574            mnemonic_derivation_options: Default::default(),
1575        });
1576
1577        sdk.set_networks(example_api_networks());
1578        sdk.set_network(ETH_NETWORK_KEY.to_string()).await.unwrap();
1579
1580        // Act
1581        let response = sdk.get_wallet_tx_list(&PIN, 0, 1).await;
1582
1583        // Assert
1584        assert!(response.is_ok())
1585    }
1586
1587    #[tokio::test]
1588    async fn test_get_wallet_tx_list_should_sort_wallet_transactions() {
1589        // Arrange
1590        let (_srv, config, _cleanup) = set_config().await;
1591        let mut sdk = Sdk::new(config).unwrap();
1592
1593        let tx_3 = WalletTxInfo {
1594            date: "2025-05-29T08:37:15.183+00:00".to_string(),
1595            block_number_hash: None,
1596            transaction_hash: "3".to_string(),
1597            receiver: String::new(),
1598            sender: String::new(),
1599            amount: CryptoAmount::from(1),
1600            network_key: "ETH".to_string(),
1601            status: WalletTxStatus::Confirmed,
1602            explorer_url: None,
1603        };
1604        let tx_1 = WalletTxInfo {
1605            date: "2025-05-29T08:37:13.183+00:00".to_string(),
1606            block_number_hash: None,
1607            transaction_hash: "1".to_string(),
1608            receiver: String::new(),
1609            sender: String::new(),
1610            amount: CryptoAmount::from(1),
1611            network_key: "ETH".to_string(),
1612            status: WalletTxStatus::Confirmed,
1613            explorer_url: None,
1614        };
1615
1616        let tx_2 = WalletTxInfo {
1617            date: "2025-05-29T08:37:14.183+00:00".to_string(),
1618            block_number_hash: None,
1619            transaction_hash: "2".to_string(),
1620            receiver: String::new(),
1621            sender: String::new(),
1622            amount: CryptoAmount::from(1),
1623            network_key: "ETH".to_string(),
1624            status: WalletTxStatus::Confirmed,
1625            explorer_url: None,
1626        };
1627
1628        let wallet_transactions = vec![tx_3.clone(), tx_1.clone(), tx_2.clone()];
1629        let expected = vec![tx_3, tx_2, tx_1];
1630
1631        let mut mock_user_repo = MockUserRepo::new();
1632        mock_user_repo.expect_get().returning(move |_| {
1633            Ok(UserEntity {
1634                user_id: None,
1635                username: USERNAME.to_string(),
1636                encrypted_password: Some(ENCRYPTED_WALLET_PASSWORD.clone()),
1637                salt: SALT.into(),
1638                is_kyc_verified: false,
1639                kyc_type: KycType::Undefined,
1640                viviswap_state: None,
1641                local_share: None,
1642                wallet_transactions: wallet_transactions.clone(),
1643            })
1644        });
1645
1646        mock_user_repo
1647            .expect_set_wallet_transactions()
1648            .once()
1649            .returning(|_, _| Ok(()));
1650
1651        sdk.repo = Some(Box::new(mock_user_repo));
1652
1653        let mut mock_wallet_manager = MockWalletManager::new();
1654        mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1655            let mut mock_wallet_user = MockWalletUser::new();
1656            mock_wallet_user
1657                .expect_get_wallet_tx_list()
1658                .once()
1659                .returning(|_, _| Ok(vec![]));
1660            mock_wallet_user.expect_get_wallet_tx().never();
1661            Ok(WalletBorrow::from(mock_wallet_user))
1662        });
1663
1664        sdk.active_user = Some(crate::types::users::ActiveUser {
1665            username: USERNAME.into(),
1666            wallet_manager: Box::new(mock_wallet_manager),
1667            mnemonic_derivation_options: Default::default(),
1668        });
1669
1670        sdk.set_networks(example_api_networks());
1671        sdk.set_network(ETH_NETWORK_KEY.to_string()).await.unwrap();
1672
1673        // Act
1674        let response = sdk.get_wallet_tx_list(&PIN, 0, 5).await;
1675
1676        // Assert
1677        assert_eq!(expected, response.unwrap().transactions)
1678    }
1679}