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.
5use super::Sdk;
6use crate::{
7    backend::dlt::put_user_address,
8    error::Result,
9    tx_version::VersionedWalletTransaction,
10    types::newtypes::{EncryptionPin, EncryptionSalt, PlainPassword},
11    wallet::error::{ErrorKind, WalletError},
12};
13use etopay_wallet::{
14    MnemonicDerivationOption,
15    types::{CryptoAmount, WalletTransaction, 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        // Retrieve the transaction list from the wallet,
529        // 1) fetch and add new (untracked) transactions from the network,
530        // 2) migrate transactions to the latest version if necessary,
531        // 3) confirm pending transactions,
532        // 4) and save the updated list back to the wallet.
533        let mut wallet_transactions = user.wallet_transactions_versioned;
534
535        // 1) fetch and add new (untracked) transactions from the network
536        match wallet.get_wallet_tx_list(start, limit).await {
537            Ok(transaction_hashes) => {
538                // go through and get the details for any new hashes
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) => wallet_transactions.push(VersionedWalletTransaction::from(details)),
556                    }
557                }
558            }
559            // do nothing if feature is not supported
560            Err(etopay_wallet::WalletError::WalletFeatureNotImplemented) => {}
561            Err(e) => return Err(e.into()),
562        };
563
564        wallet_transactions.sort_by_key(|b| std::cmp::Reverse(b.date()));
565        let mut wallet_tx_list = Vec::new();
566
567        for t in wallet_transactions
568            .iter_mut()
569            .filter(|tx| tx.network_key() == network.key)
570            .skip(start)
571            .take(limit)
572        {
573            // 2) migrate transactions to the latest version if necessary
574            if let VersionedWalletTransaction::V1(v1) = t {
575                if let Ok(details) = wallet.get_wallet_tx(&v1.transaction_hash).await {
576                    *t = VersionedWalletTransaction::from(details);
577                    wallet_tx_list.push(WalletTransaction::from(t.clone()));
578                    continue;
579                }
580            }
581
582            // 3) confirm pending transactions
583            if t.status() == WalletTxStatus::Pending {
584                if let Ok(details) = wallet.get_wallet_tx(t.transaction_hash()).await {
585                    *t = VersionedWalletTransaction::V2(details);
586                    wallet_tx_list.push(WalletTransaction::from(t.clone()));
587                    continue;
588                }
589            }
590
591            wallet_tx_list.push(WalletTransaction::from(t.clone()));
592        }
593
594        // 4) and save the updated list back to the wallet.
595        let _ = repo.set_wallet_transactions(&user.username, wallet_transactions);
596
597        Ok(WalletTxInfoList {
598            transactions: wallet_tx_list,
599        })
600    }
601
602    /// wallet transaction
603    ///
604    /// Returns the wallet transaction details.
605    ///
606    /// # Arguments
607    ///
608    /// * `tx_id` - The transaction id of particular transaction.
609    ///
610    /// # Returns
611    ///
612    /// Returns `WalletTransaction` detailed report of particular wallet transaction if the outputs are claimed successfully, otherwise returns an `Error`.
613    ///
614    /// # Errors
615    ///
616    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
617    /// * [`WalletError::WalletNotInitialized`] - If there is an error initializing the wallet.
618    pub async fn get_wallet_tx(&mut self, pin: &EncryptionPin, tx_id: &str) -> Result<WalletTransaction> {
619        info!("Wallet getting details of particular transactions");
620        self.verify_pin(pin).await?;
621        let wallet = self.try_get_active_user_wallet(pin).await?;
622        let wallet_tx = wallet.get_wallet_tx(tx_id).await?;
623        Ok(wallet_tx)
624    }
625
626    /// Set wallet mnemonic derivation options
627    ///
628    /// # Arguments
629    ///
630    /// * `account` - The account to use.
631    /// * `index` - The index to use.
632    ///
633    /// # Errors
634    ///
635    /// * [`crate::Error::UserNotInitialized`] - If there is an error initializing the user.
636    pub async fn set_wallet_derivation_options(&mut self, account: u32, index: u32) -> Result<()> {
637        let options = MnemonicDerivationOption { account, index };
638
639        info!("Setting wallet mnemonic derivation options: {options:?}");
640
641        let Some(active_user) = &mut self.active_user else {
642            return Err(crate::Error::UserNotInitialized);
643        };
644
645        active_user.mnemonic_derivation_options = options;
646
647        Ok(())
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654    use crate::core::core_testing_utils::handle_error_test_cases;
655    use crate::testing_utils::{
656        ADDRESS, AUTH_PROVIDER, ENCRYPTED_WALLET_PASSWORD, ETH_NETWORK_KEY, HEADER_X_APP_NAME, IOTA_NETWORK_KEY,
657        MNEMONIC, PIN, SALT, TOKEN, TX_INDEX, USERNAME, WALLET_PASSWORD, example_api_networks, example_get_user,
658        example_versioned_wallet_transaction, set_config,
659    };
660    use crate::types::users::UserEntity;
661    use crate::{
662        core::Sdk,
663        types::users::KycType,
664        user::MockUserRepo,
665        wallet_manager::{MockWalletManager, WalletBorrow},
666    };
667    use api_types::api::dlt::SetUserAddressRequest;
668    use api_types::api::viviswap::detail::SwapPaymentDetailKey;
669    use chrono::{DateTime, TimeZone, Utc};
670    use etopay_wallet::MockWalletUser;
671    use etopay_wallet::types::{WalletTransaction, WalletTxStatus};
672    use mockall::predicate::eq;
673    use mockito::Matcher;
674    use rstest::rstest;
675    use rust_decimal_macros::dec;
676    use std::sync::LazyLock;
677
678    const BACKUP: &[u8] = &[42, 77, 15, 203, 89, 123, 34, 56, 178, 90, 210, 33, 47, 192, 1, 17];
679
680    #[rstest]
681    #[case::success(Ok(MNEMONIC.to_string()))]
682    #[case::missing_config(Err(crate::Error::MissingConfig))]
683    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
684    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
685    #[tokio::test]
686    async fn test_create_wallet_from_new_mnemonic(#[case] expected: Result<String>) {
687        // Arrange
688        let (_srv, config, _cleanup) = set_config().await;
689        let mut sdk = Sdk::new(config).unwrap();
690
691        match &expected {
692            Ok(_) => {
693                sdk.repo = Some(Box::new(MockUserRepo::new()));
694                let mut mock_wallet_manager = MockWalletManager::new();
695                mock_wallet_manager
696                    .expect_create_wallet_from_new_mnemonic()
697                    .once()
698                    .returning(|_, _, _, _| Ok(MNEMONIC.to_string()));
699                sdk.active_user = Some(crate::types::users::ActiveUser {
700                    username: USERNAME.into(),
701                    wallet_manager: Box::new(mock_wallet_manager),
702                    mnemonic_derivation_options: Default::default(),
703                });
704            }
705            Err(error) => {
706                handle_error_test_cases(error, &mut sdk, 0, 0).await;
707            }
708        }
709
710        // Act
711        let response = sdk.create_wallet_from_new_mnemonic(&PIN).await;
712
713        // Assert
714        match expected {
715            Ok(resp) => {
716                assert_eq!(response.unwrap(), resp);
717            }
718            Err(ref expected_err) => {
719                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
720            }
721        }
722    }
723
724    #[rstest]
725    #[case::success(Ok(()))]
726    #[case::missing_config(Err(crate::Error::MissingConfig))]
727    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
728    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
729    #[tokio::test]
730    async fn test_create_wallet_from_existing_mnemonic(#[case] expected: Result<()>) {
731        // Arrange
732        let (_srv, config, _cleanup) = set_config().await;
733        let mut sdk = Sdk::new(config).unwrap();
734
735        match &expected {
736            Ok(_) => {
737                sdk.repo = Some(Box::new(MockUserRepo::new()));
738                let mut mock_wallet_manager = MockWalletManager::new();
739                mock_wallet_manager
740                    .expect_create_wallet_from_existing_mnemonic()
741                    .once()
742                    .returning(|_, _, _, _, _| Ok(()));
743                sdk.active_user = Some(crate::types::users::ActiveUser {
744                    username: USERNAME.into(),
745                    wallet_manager: Box::new(mock_wallet_manager),
746                    mnemonic_derivation_options: Default::default(),
747                });
748            }
749            Err(error) => {
750                handle_error_test_cases(error, &mut sdk, 0, 0).await;
751            }
752        }
753
754        // Act
755        let response = sdk.create_wallet_from_existing_mnemonic(&PIN, MNEMONIC).await;
756
757        // Assert
758        match expected {
759            Ok(()) => response.unwrap(),
760            Err(ref expected_err) => {
761                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
762            }
763        }
764    }
765
766    #[rstest]
767    #[case::success(Ok(BACKUP.to_vec()))]
768    #[case::missing_config(Err(crate::Error::MissingConfig))]
769    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
770    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
771    #[tokio::test]
772    async fn test_create_wallet_backup(#[case] expected: Result<Vec<u8>>) {
773        // Arrange
774        let (_srv, config, _cleanup) = set_config().await;
775        let mut sdk = Sdk::new(config).unwrap();
776
777        match &expected {
778            Ok(_) => {
779                sdk.repo = Some(Box::new(MockUserRepo::new()));
780                let mut mock_wallet_manager = MockWalletManager::new();
781                mock_wallet_manager
782                    .expect_create_wallet_backup()
783                    .once()
784                    .returning(|_, _, _, _, _| Ok(BACKUP.to_vec()));
785                sdk.active_user = Some(crate::types::users::ActiveUser {
786                    username: USERNAME.into(),
787                    wallet_manager: Box::new(mock_wallet_manager),
788                    mnemonic_derivation_options: Default::default(),
789                });
790            }
791            Err(error) => {
792                handle_error_test_cases(error, &mut sdk, 0, 0).await;
793            }
794        }
795
796        // Act
797        let response = sdk.create_wallet_backup(&PIN, &WALLET_PASSWORD).await;
798
799        // Assert
800        match expected {
801            Ok(resp) => {
802                assert_eq!(response.unwrap(), resp);
803            }
804            Err(ref expected_err) => {
805                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
806            }
807        }
808    }
809
810    #[rstest]
811    #[case::success(Ok(()))]
812    #[case::missing_config(Err(crate::Error::MissingConfig))]
813    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
814    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
815    #[tokio::test]
816    async fn test_create_wallet_from_backup(#[case] expected: Result<()>) {
817        // Arrange
818        let (_srv, config, _cleanup) = set_config().await;
819        let mut sdk = Sdk::new(config).unwrap();
820
821        match &expected {
822            Ok(_) => {
823                sdk.repo = Some(Box::new(MockUserRepo::new()));
824                let mut mock_wallet_manager = MockWalletManager::new();
825                mock_wallet_manager
826                    .expect_create_wallet_from_backup()
827                    .once()
828                    .returning(|_, _, _, _, _, _| Ok(()));
829                sdk.active_user = Some(crate::types::users::ActiveUser {
830                    username: USERNAME.into(),
831                    wallet_manager: Box::new(mock_wallet_manager),
832                    mnemonic_derivation_options: Default::default(),
833                });
834            }
835            Err(error) => {
836                handle_error_test_cases(error, &mut sdk, 0, 0).await;
837            }
838        }
839
840        // Act
841        let response = sdk.create_wallet_from_backup(&PIN, BACKUP, &WALLET_PASSWORD).await;
842
843        // Assert
844        match expected {
845            Ok(()) => response.unwrap(),
846            Err(ref expected_err) => {
847                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
848            }
849        }
850    }
851
852    #[rstest]
853    #[case::success(Ok(true))]
854    #[case::missing_config(Err(crate::Error::MissingConfig))]
855    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
856    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
857    #[tokio::test]
858    async fn test_verify_mnemonic(#[case] expected: Result<bool>) {
859        // Arrange
860        let (_srv, config, _cleanup) = set_config().await;
861        let mut sdk = Sdk::new(config).unwrap();
862
863        match &expected {
864            Ok(_) => {
865                sdk.repo = Some(Box::new(MockUserRepo::new()));
866                let mut mock_wallet_manager = MockWalletManager::new();
867                mock_wallet_manager
868                    .expect_check_mnemonic()
869                    .once()
870                    .returning(|_, _, _, _, _| Ok(true));
871                sdk.active_user = Some(crate::types::users::ActiveUser {
872                    username: USERNAME.into(),
873                    wallet_manager: Box::new(mock_wallet_manager),
874                    mnemonic_derivation_options: Default::default(),
875                });
876            }
877            Err(error) => {
878                handle_error_test_cases(error, &mut sdk, 0, 0).await;
879            }
880        }
881
882        // Act
883        let response = sdk.verify_mnemonic(&PIN, MNEMONIC).await;
884
885        // Assert
886        match expected {
887            Ok(resp) => {
888                assert_eq!(response.unwrap(), resp);
889            }
890            Err(ref expected_err) => {
891                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
892            }
893        }
894    }
895
896    #[rstest]
897    #[case::success(Ok(()))]
898    #[case::missing_config(Err(crate::Error::MissingConfig))]
899    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
900    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
901    #[tokio::test]
902    async fn test_delete_wallet(#[case] expected: Result<()>) {
903        // Arrange
904        let (_srv, config, _cleanup) = set_config().await;
905        let mut sdk = Sdk::new(config).unwrap();
906
907        let pin = EncryptionPin::try_from_string("123456").unwrap();
908
909        match &expected {
910            Ok(_) => {
911                let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 2, KycType::Undefined);
912                mock_user_repo.expect_update().once().returning(|_| Ok(()));
913
914                sdk.repo = Some(Box::new(mock_user_repo));
915
916                let mut mock_wallet_manager = MockWalletManager::new();
917                mock_wallet_manager
918                    .expect_delete_wallet()
919                    .once()
920                    .returning(|_, _, _| Ok(()));
921                sdk.active_user = Some(crate::types::users::ActiveUser {
922                    username: USERNAME.into(),
923                    wallet_manager: Box::new(mock_wallet_manager),
924                    mnemonic_derivation_options: Default::default(),
925                });
926
927                let new_pin = EncryptionPin::try_from_string("123456").unwrap();
928                sdk.change_pin(&pin, &new_pin).await.unwrap();
929            }
930            Err(error) => {
931                handle_error_test_cases(error, &mut sdk, 1, 0).await;
932            }
933        }
934
935        // Act
936        let response = sdk.delete_wallet(&PIN).await;
937
938        // Assert
939        match expected {
940            Ok(()) => response.unwrap(),
941            Err(ref expected_err) => {
942                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
943            }
944        }
945    }
946
947    #[rstest]
948    #[case::success(Ok(()))]
949    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
950    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
951    #[case::wallet_not_initialized(Err(crate::Error::Wallet(WalletError::WalletNotInitialized(
952        ErrorKind::MissingPassword
953    ))))]
954    #[tokio::test]
955    async fn test_verify_pin(#[case] expected: Result<()>) {
956        // Arrange
957        let (_srv, config, _cleanup) = set_config().await;
958        let mut sdk = Sdk::new(config).unwrap();
959
960        match &expected {
961            Ok(_) => {
962                let mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
963                sdk.repo = Some(Box::new(mock_user_repo));
964
965                sdk.active_user = Some(crate::types::users::ActiveUser {
966                    username: USERNAME.into(),
967                    wallet_manager: Box::new(MockWalletManager::new()),
968                    mnemonic_derivation_options: Default::default(),
969                });
970            }
971            Err(error) => {
972                handle_error_test_cases(error, &mut sdk, 1, 0).await;
973            }
974        }
975
976        // Act
977        let response = sdk.verify_pin(&PIN).await;
978
979        // Assert
980        match expected {
981            Ok(()) => response.unwrap(),
982            Err(ref expected_err) => {
983                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
984            }
985        }
986    }
987
988    #[rstest]
989    #[case::success(Ok(()))]
990    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
991    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
992    #[case::wallet_not_initialized(Err(crate::Error::Wallet(WalletError::WalletNotInitialized(
993        ErrorKind::MissingPassword
994    ))))]
995    #[tokio::test]
996    async fn test_change_pin(#[case] expected: Result<()>) {
997        // Arrange
998        let (_srv, config, _cleanup) = set_config().await;
999        let mut sdk = Sdk::new(config).unwrap();
1000
1001        match &expected {
1002            Ok(_) => {
1003                let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
1004                mock_user_repo.expect_update().once().returning(|_| Ok(()));
1005                sdk.repo = Some(Box::new(mock_user_repo));
1006
1007                sdk.active_user = Some(crate::types::users::ActiveUser {
1008                    username: USERNAME.into(),
1009                    wallet_manager: Box::new(MockWalletManager::new()),
1010                    mnemonic_derivation_options: Default::default(),
1011                });
1012            }
1013            Err(error) => {
1014                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1015            }
1016        }
1017
1018        // Act
1019        let new_pin: LazyLock<EncryptionPin> = LazyLock::new(|| EncryptionPin::try_from_string("432154").unwrap());
1020        let response = sdk.change_pin(&PIN, &new_pin).await;
1021
1022        // Assert
1023        match expected {
1024            Ok(()) => response.unwrap(),
1025            Err(ref expected_err) => {
1026                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1027            }
1028        }
1029    }
1030
1031    #[rstest]
1032    #[case::success(Ok(()))]
1033    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
1034    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
1035    #[tokio::test]
1036    async fn test_set_wallet_password(#[case] expected: Result<()>) {
1037        // Arrange
1038        let (_srv, config, _cleanup) = set_config().await;
1039        let mut sdk = Sdk::new(config).unwrap();
1040
1041        match &expected {
1042            Ok(_) => {
1043                let mut mock_user_repo = MockUserRepo::new();
1044                mock_user_repo.expect_get().times(1).returning(move |r1| {
1045                    assert_eq!(r1, USERNAME);
1046                    Ok(UserEntity {
1047                        user_id: None,
1048                        username: USERNAME.into(),
1049                        encrypted_password: None,
1050                        salt: SALT.into(),
1051                        is_kyc_verified: false,
1052                        kyc_type: KycType::Undefined,
1053                        viviswap_state: Option::None,
1054                        local_share: None,
1055                        wallet_transactions: Vec::new(),
1056                        wallet_transactions_versioned: Vec::new(),
1057                    })
1058                });
1059                mock_user_repo.expect_update().once().returning(|_| Ok(()));
1060                sdk.repo = Some(Box::new(mock_user_repo));
1061
1062                sdk.active_user = Some(crate::types::users::ActiveUser {
1063                    username: USERNAME.into(),
1064                    wallet_manager: Box::new(MockWalletManager::new()),
1065                    mnemonic_derivation_options: Default::default(),
1066                });
1067            }
1068            Err(error) => {
1069                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1070            }
1071        }
1072
1073        // Act
1074        let response = sdk.set_wallet_password(&PIN, &WALLET_PASSWORD).await;
1075
1076        // Assert
1077        match expected {
1078            Ok(()) => response.unwrap(),
1079            Err(ref expected_err) => {
1080                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1081            }
1082        }
1083    }
1084
1085    #[rstest]
1086    #[case::success(Ok(ADDRESS.to_string()))]
1087    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
1088    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
1089    #[case::missing_config(Err(crate::Error::MissingConfig))]
1090    #[tokio::test]
1091    async fn test_generate_new_address(#[case] expected: Result<String>) {
1092        // Arrange
1093        let (mut srv, config, _cleanup) = set_config().await;
1094        let mut sdk = Sdk::new(config).unwrap();
1095        let mut mock_server = None;
1096
1097        match &expected {
1098            Ok(_) => {
1099                let mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
1100                sdk.repo = Some(Box::new(mock_user_repo));
1101
1102                let mut mock_wallet_manager = MockWalletManager::new();
1103                mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1104                    let mut mock_wallet_user = MockWalletUser::new();
1105                    mock_wallet_user
1106                        .expect_get_address()
1107                        .once()
1108                        .returning(|| Ok(ADDRESS.to_string()));
1109                    Ok(WalletBorrow::from(mock_wallet_user))
1110                });
1111                sdk.active_user = Some(crate::types::users::ActiveUser {
1112                    username: USERNAME.into(),
1113                    wallet_manager: Box::new(mock_wallet_manager),
1114                    mnemonic_derivation_options: Default::default(),
1115                });
1116                sdk.access_token = Some(TOKEN.clone());
1117                sdk.set_networks(example_api_networks());
1118                sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
1119
1120                let mock_request = SetUserAddressRequest {
1121                    address: ADDRESS.into(),
1122                };
1123                let body = serde_json::to_string(&mock_request).unwrap();
1124
1125                mock_server = Some(
1126                    srv.mock("PUT", "/api/user/address")
1127                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
1128                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
1129                        .match_header("content-type", "application/json")
1130                        .match_query(Matcher::Exact("network_key=IOTA".to_string()))
1131                        .match_body(Matcher::Exact(body))
1132                        .with_status(201)
1133                        .expect(1)
1134                        .with_header("content-type", "application/json")
1135                        .create(),
1136                );
1137            }
1138            Err(error) => {
1139                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1140            }
1141        }
1142
1143        // Act
1144        let response = sdk.generate_new_address(&PIN).await;
1145
1146        // Assert
1147        match expected {
1148            Ok(resp) => {
1149                assert_eq!(response.unwrap(), resp);
1150            }
1151            Err(ref expected_err) => {
1152                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1153            }
1154        }
1155        if let Some(m) = mock_server {
1156            m.assert();
1157        }
1158    }
1159
1160    #[rstest]
1161    // SAFETY: we know that this value is not negative
1162    #[case::success(Ok(unsafe { CryptoAmount::new_unchecked(dec!(25.0)) }))]
1163    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
1164    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
1165    #[case::missing_config(Err(crate::Error::MissingConfig))]
1166    #[tokio::test]
1167    async fn test_get_balance(#[case] expected: Result<CryptoAmount>) {
1168        // Arrange
1169        let (_srv, config, _cleanup) = set_config().await;
1170        let mut sdk = Sdk::new(config).unwrap();
1171
1172        match &expected {
1173            Ok(_) => {
1174                let mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
1175                sdk.repo = Some(Box::new(mock_user_repo));
1176
1177                let mut mock_wallet_manager = MockWalletManager::new();
1178                mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1179                    let mut mock_wallet_user = MockWalletUser::new();
1180                    mock_wallet_user
1181                        .expect_get_balance()
1182                        .once()
1183                        // SAFETY: we know that this value is not negative
1184                        .returning(|| Ok(unsafe { CryptoAmount::new_unchecked(dec!(25.0)) }));
1185                    Ok(WalletBorrow::from(mock_wallet_user))
1186                });
1187                sdk.active_user = Some(crate::types::users::ActiveUser {
1188                    username: USERNAME.into(),
1189                    wallet_manager: Box::new(mock_wallet_manager),
1190                    mnemonic_derivation_options: Default::default(),
1191                });
1192                sdk.set_networks(example_api_networks());
1193                sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
1194            }
1195            Err(error) => {
1196                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1197            }
1198        }
1199
1200        // Act
1201        let response = sdk.get_balance(&PIN).await;
1202
1203        // Assert
1204        match expected {
1205            Ok(resp) => {
1206                assert_eq!(response.unwrap(), resp);
1207            }
1208            Err(ref expected_err) => {
1209                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1210            }
1211        }
1212    }
1213
1214    #[rstest]
1215    #[case::success(Ok(WalletTransaction::from(example_versioned_wallet_transaction())))]
1216    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
1217    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
1218    #[case::missing_config(Err(crate::Error::MissingConfig))]
1219    #[tokio::test]
1220    async fn test_get_wallet_tx(#[case] expected: Result<WalletTransaction>) {
1221        // Arrange
1222        let (_srv, config, _cleanup) = set_config().await;
1223        let mut sdk = Sdk::new(config).unwrap();
1224
1225        match &expected {
1226            Ok(_) => {
1227                let mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
1228                sdk.repo = Some(Box::new(mock_user_repo));
1229
1230                let mut mock_wallet_manager = MockWalletManager::new();
1231                mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1232                    let mut mock_wallet_user = MockWalletUser::new();
1233                    mock_wallet_user
1234                        .expect_get_wallet_tx()
1235                        .once()
1236                        .returning(|_| Ok(WalletTransaction::from(example_versioned_wallet_transaction())));
1237                    Ok(WalletBorrow::from(mock_wallet_user))
1238                });
1239                sdk.active_user = Some(crate::types::users::ActiveUser {
1240                    username: USERNAME.into(),
1241                    wallet_manager: Box::new(mock_wallet_manager),
1242                    mnemonic_derivation_options: Default::default(),
1243                });
1244                sdk.set_networks(example_api_networks());
1245                sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
1246            }
1247            Err(error) => {
1248                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1249            }
1250        }
1251
1252        // Act
1253        let response = sdk.get_wallet_tx(&PIN, TX_INDEX).await;
1254
1255        // Assert
1256        match expected {
1257            Ok(resp) => {
1258                assert_eq!(response.unwrap(), resp);
1259            }
1260            Err(ref expected_err) => {
1261                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1262            }
1263        }
1264    }
1265
1266    #[rstest]
1267    #[case::success(Ok(WalletTxInfoList { transactions: vec![]}))]
1268    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
1269    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
1270    #[case::missing_config(Err(crate::Error::MissingConfig))]
1271    #[tokio::test]
1272    async fn test_get_wallet_tx_list(#[case] expected: Result<WalletTxInfoList>) {
1273        // Arrange
1274        let (_srv, config, _cleanup) = set_config().await;
1275        let mut sdk = Sdk::new(config).unwrap();
1276
1277        match &expected {
1278            Ok(_) => {
1279                let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 2, KycType::Undefined);
1280                mock_user_repo
1281                    .expect_set_wallet_transactions()
1282                    .once()
1283                    .returning(|_, _| Ok(()));
1284                sdk.repo = Some(Box::new(mock_user_repo));
1285
1286                let mut mock_wallet_manager = MockWalletManager::new();
1287                mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1288                    let mut mock_wallet_user = MockWalletUser::new();
1289                    mock_wallet_user
1290                        .expect_get_wallet_tx_list()
1291                        .once()
1292                        .returning(|_, _| Ok(vec![]));
1293                    Ok(WalletBorrow::from(mock_wallet_user))
1294                });
1295                sdk.active_user = Some(crate::types::users::ActiveUser {
1296                    username: USERNAME.into(),
1297                    wallet_manager: Box::new(mock_wallet_manager),
1298                    mnemonic_derivation_options: Default::default(),
1299                });
1300                sdk.set_networks(example_api_networks());
1301                sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
1302            }
1303            Err(error) => {
1304                handle_error_test_cases(error, &mut sdk, 1, 0).await;
1305            }
1306        }
1307
1308        // Act
1309        let response = sdk.get_wallet_tx_list(&PIN, 0, 10).await;
1310
1311        // Assert
1312        match expected {
1313            Ok(resp) => {
1314                assert_eq!(response.unwrap(), resp);
1315            }
1316            Err(ref expected_err) => {
1317                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
1318            }
1319        }
1320    }
1321
1322    fn mock_wallet_transaction(
1323        hash: String,
1324        status: WalletTxStatus,
1325        network_key: String,
1326        date: DateTime<Utc>,
1327    ) -> WalletTransaction {
1328        WalletTransaction {
1329            date,
1330            block_number_hash: None,
1331            transaction_hash: hash,
1332            receiver: String::new(),
1333            sender: String::new(),
1334            amount: unsafe { CryptoAmount::new_unchecked(dec!(20.0)) },
1335            network_key,
1336            status,
1337            explorer_url: None,
1338            gas_fee: None,
1339            is_sender: true,
1340        }
1341    }
1342
1343    #[tokio::test]
1344    async fn test_get_wallet_tx_list_filters_transactions_correctly() {
1345        // Arrange
1346        let (_srv, config, _cleanup) = set_config().await;
1347        let mut sdk = Sdk::new(config).unwrap();
1348        let mock_date = Utc::now();
1349
1350        // During the test, we expect the status of WalletTransaction
1351        // where transaction_hash = "2" and network_key = "ETH"
1352        // to transition from 'Pending' to 'Confirmed' after synchronization
1353        let wallet_transactions_versioned = vec![
1354            VersionedWalletTransaction::from(mock_wallet_transaction(
1355                String::from("some tx id"),
1356                WalletTxStatus::Confirmed,
1357                String::from("IOTA"),
1358                mock_date,
1359            )),
1360            VersionedWalletTransaction::from(mock_wallet_transaction(
1361                String::from("1"),
1362                WalletTxStatus::Pending,
1363                String::from("ETH"),
1364                mock_date,
1365            )),
1366            VersionedWalletTransaction::from(mock_wallet_transaction(
1367                String::from("2"),
1368                WalletTxStatus::Pending, // this one
1369                String::from("ETH"),
1370                mock_date,
1371            )),
1372            VersionedWalletTransaction::from(mock_wallet_transaction(
1373                String::from("3"),
1374                WalletTxStatus::Pending,
1375                String::from("ETH"),
1376                mock_date,
1377            )),
1378        ];
1379
1380        let mut mock_user_repo = MockUserRepo::new();
1381        mock_user_repo.expect_get().returning(move |_| {
1382            Ok(UserEntity {
1383                user_id: None,
1384                username: USERNAME.to_string(),
1385                encrypted_password: Some(ENCRYPTED_WALLET_PASSWORD.clone()),
1386                salt: SALT.into(),
1387                is_kyc_verified: false,
1388                kyc_type: KycType::Undefined,
1389                viviswap_state: None,
1390                local_share: None,
1391                wallet_transactions: Vec::new(),
1392                wallet_transactions_versioned: wallet_transactions_versioned.clone(),
1393            })
1394        });
1395
1396        let expected = vec![
1397            VersionedWalletTransaction::from(mock_wallet_transaction(
1398                String::from("some tx id"),
1399                WalletTxStatus::Confirmed,
1400                String::from("IOTA"),
1401                mock_date,
1402            )),
1403            VersionedWalletTransaction::from(mock_wallet_transaction(
1404                String::from("1"),
1405                WalletTxStatus::Pending,
1406                String::from("ETH"),
1407                mock_date,
1408            )),
1409            VersionedWalletTransaction::from(mock_wallet_transaction(
1410                String::from("2"),
1411                WalletTxStatus::Confirmed, // this one
1412                String::from("ETH"),
1413                mock_date,
1414            )),
1415            VersionedWalletTransaction::from(mock_wallet_transaction(
1416                String::from("3"),
1417                WalletTxStatus::Pending,
1418                String::from("ETH"),
1419                mock_date,
1420            )),
1421        ];
1422
1423        mock_user_repo
1424            .expect_set_wallet_transactions()
1425            .once()
1426            .with(eq(USERNAME.to_string()), eq(expected.clone()))
1427            .returning(|_, _| Ok(()));
1428
1429        sdk.repo = Some(Box::new(mock_user_repo));
1430
1431        let mut mock_wallet_manager = MockWalletManager::new();
1432        mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1433            let mut mock_wallet_user = MockWalletUser::new();
1434            mock_wallet_user
1435                .expect_get_wallet_tx_list()
1436                .once()
1437                .returning(|_, _| Ok(vec![]));
1438            mock_wallet_user
1439                .expect_get_wallet_tx()
1440                .once()
1441                .with(eq(String::from("2"))) // WalletTransaction { transaction_hash = "2" }
1442                .returning(move |_| {
1443                    Ok(mock_wallet_transaction(
1444                        String::from("2"),
1445                        WalletTxStatus::Confirmed, // Pending -> Confirmed
1446                        String::from("ETH"),
1447                        mock_date,
1448                    ))
1449                });
1450            Ok(WalletBorrow::from(mock_wallet_user))
1451        });
1452
1453        sdk.active_user = Some(crate::types::users::ActiveUser {
1454            username: USERNAME.into(),
1455            wallet_manager: Box::new(mock_wallet_manager),
1456            mnemonic_derivation_options: Default::default(),
1457        });
1458
1459        sdk.set_networks(example_api_networks());
1460        sdk.set_network(ETH_NETWORK_KEY.to_string()).await.unwrap();
1461
1462        // Act
1463
1464        // We request a single WalletTransaction using get_wallet_tx_list(start = 1, limit = 1)
1465        // We have stored transactions: [1 IOTA, 3 ETH]
1466        // The network key is ETH, so we search through the 3 ETH transactions
1467        // We select this one:
1468        // [WalletTransaction{ ... }, -> WalletTransaction{ transaction_hash = "2" }, WalletTransaction{ ... }]
1469        let response = sdk.get_wallet_tx_list(&PIN, 1, 1).await.unwrap();
1470
1471        // Assert
1472        assert_eq!(
1473            response,
1474            WalletTxInfoList {
1475                transactions: vec![mock_wallet_transaction(
1476                    String::from("2"),
1477                    WalletTxStatus::Confirmed, // Pending -> Confirmed
1478                    String::from("ETH"),
1479                    mock_date,
1480                )]
1481            }
1482        );
1483    }
1484
1485    #[tokio::test]
1486    async fn test_get_wallet_tx_list_does_not_query_network_for_transaction_state() {
1487        // Arrange
1488        let (_srv, config, _cleanup) = set_config().await;
1489        let mut sdk = Sdk::new(config).unwrap();
1490
1491        let wallet_transactions = vec![VersionedWalletTransaction::from(mock_wallet_transaction(
1492            String::from("1"),
1493            WalletTxStatus::Confirmed,
1494            String::from("ETH"),
1495            Utc::now(),
1496        ))];
1497
1498        let mut mock_user_repo = MockUserRepo::new();
1499        mock_user_repo.expect_get().returning(move |_| {
1500            Ok(UserEntity {
1501                user_id: None,
1502                username: USERNAME.to_string(),
1503                encrypted_password: Some(ENCRYPTED_WALLET_PASSWORD.clone()),
1504                salt: SALT.into(),
1505                is_kyc_verified: false,
1506                kyc_type: KycType::Undefined,
1507                viviswap_state: None,
1508                local_share: None,
1509                wallet_transactions: Vec::new(),
1510                wallet_transactions_versioned: wallet_transactions.clone(),
1511            })
1512        });
1513
1514        mock_user_repo
1515            .expect_set_wallet_transactions()
1516            .once()
1517            .returning(|_, _| Ok(()));
1518
1519        sdk.repo = Some(Box::new(mock_user_repo));
1520
1521        let mut mock_wallet_manager = MockWalletManager::new();
1522        mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1523            let mut mock_wallet_user = MockWalletUser::new();
1524            mock_wallet_user
1525                .expect_get_wallet_tx_list()
1526                .once()
1527                .returning(|_, _| Ok(vec![]));
1528            mock_wallet_user.expect_get_wallet_tx().never();
1529            Ok(WalletBorrow::from(mock_wallet_user))
1530        });
1531
1532        sdk.active_user = Some(crate::types::users::ActiveUser {
1533            username: USERNAME.into(),
1534            wallet_manager: Box::new(mock_wallet_manager),
1535            mnemonic_derivation_options: Default::default(),
1536        });
1537
1538        sdk.set_networks(example_api_networks());
1539        sdk.set_network(ETH_NETWORK_KEY.to_string()).await.unwrap();
1540
1541        // Act
1542        let response = sdk.get_wallet_tx_list(&PIN, 0, 1).await;
1543
1544        // Assert
1545        assert!(response.is_ok())
1546    }
1547
1548    #[tokio::test]
1549    async fn test_get_wallet_tx_list_should_sort_wallet_transactions() {
1550        // Arrange
1551        let (_srv, config, _cleanup) = set_config().await;
1552        let mut sdk = Sdk::new(config).unwrap();
1553
1554        let tx_3 = VersionedWalletTransaction::from(mock_wallet_transaction(
1555            String::from("3"),
1556            WalletTxStatus::Confirmed,
1557            String::from("ETH"),
1558            Utc.with_ymd_and_hms(2025, 5, 29, 8, 37, 15).unwrap(),
1559        ));
1560
1561        let tx_1 = VersionedWalletTransaction::from(mock_wallet_transaction(
1562            String::from("1"),
1563            WalletTxStatus::Confirmed,
1564            String::from("ETH"),
1565            Utc.with_ymd_and_hms(2025, 5, 29, 8, 37, 13).unwrap(),
1566        ));
1567
1568        let tx_2 = VersionedWalletTransaction::from(mock_wallet_transaction(
1569            String::from("2"),
1570            WalletTxStatus::Confirmed,
1571            String::from("ETH"),
1572            Utc.with_ymd_and_hms(2025, 5, 29, 8, 37, 14).unwrap(),
1573        ));
1574
1575        let wallet_transactions = vec![tx_3.clone(), tx_1.clone(), tx_2.clone()];
1576        let expected = vec![
1577            WalletTransaction::from(tx_3),
1578            WalletTransaction::from(tx_2),
1579            WalletTransaction::from(tx_1),
1580        ];
1581
1582        let mut mock_user_repo = MockUserRepo::new();
1583        mock_user_repo.expect_get().returning(move |_| {
1584            Ok(UserEntity {
1585                user_id: None,
1586                username: USERNAME.to_string(),
1587                encrypted_password: Some(ENCRYPTED_WALLET_PASSWORD.clone()),
1588                salt: SALT.into(),
1589                is_kyc_verified: false,
1590                kyc_type: KycType::Undefined,
1591                viviswap_state: None,
1592                local_share: None,
1593                wallet_transactions: Vec::new(),
1594                wallet_transactions_versioned: wallet_transactions.clone(),
1595            })
1596        });
1597
1598        mock_user_repo
1599            .expect_set_wallet_transactions()
1600            .once()
1601            .returning(|_, _| Ok(()));
1602
1603        sdk.repo = Some(Box::new(mock_user_repo));
1604
1605        let mut mock_wallet_manager = MockWalletManager::new();
1606        mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
1607            let mut mock_wallet_user = MockWalletUser::new();
1608            mock_wallet_user
1609                .expect_get_wallet_tx_list()
1610                .once()
1611                .returning(|_, _| Ok(vec![]));
1612            mock_wallet_user.expect_get_wallet_tx().never();
1613            Ok(WalletBorrow::from(mock_wallet_user))
1614        });
1615
1616        sdk.active_user = Some(crate::types::users::ActiveUser {
1617            username: USERNAME.into(),
1618            wallet_manager: Box::new(mock_wallet_manager),
1619            mnemonic_derivation_options: Default::default(),
1620        });
1621
1622        sdk.set_networks(example_api_networks());
1623        sdk.set_network(ETH_NETWORK_KEY.to_string()).await.unwrap();
1624
1625        // Act
1626        let response = sdk.get_wallet_tx_list(&PIN, 0, 5).await;
1627
1628        // Assert
1629        assert_eq!(expected, response.unwrap().transactions)
1630    }
1631}