etopay_sdk/core/
transaction.rs

1use super::Sdk;
2use crate::backend::transactions::{
3    commit_transaction, create_new_transaction, get_transaction_details, get_transactions_list,
4};
5use crate::error::Result;
6use crate::tx_version::VersionedWalletTransaction;
7use crate::types::transactions::PurchaseDetails;
8use crate::types::{
9    newtypes::EncryptionPin,
10    transactions::{TxInfo, TxList},
11};
12use crate::wallet::error::WalletError;
13use api_types::api::networks::ApiProtocol;
14use api_types::api::transactions::{ApiApplicationMetadata, ApiTxStatus, PurchaseModel, Reason};
15use etopay_wallet::TransactionIntent;
16use etopay_wallet::types::CryptoAmount;
17use etopay_wallet::types::GasCostEstimation;
18use log::{debug, info};
19
20impl Sdk {
21    /// Create purchase request
22    ///
23    /// # Arguments
24    ///
25    /// * `receiver` - The receiver's username.
26    /// * `amount` - The amount of the purchase.
27    /// * `product_hash` - The hash of the product.
28    /// * `app_data` - The application data.
29    /// * `purchase_type` - The type of the purchase.
30    ///
31    /// # Returns
32    ///
33    /// The purchase ID. This is an internal index used to reference the transaction in etopay
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if the user or wallet is not initialized, or if there is an error creating the transaction.
38    pub async fn create_purchase_request(
39        &self,
40        receiver: &str,
41        amount: CryptoAmount,
42        product_hash: &str,
43        app_data: &str,
44        purchase_type: &str,
45    ) -> Result<String> {
46        info!("Creating a new purchase request");
47        let Some(_active_user) = &self.active_user else {
48            return Err(crate::Error::UserNotInitialized);
49        };
50
51        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
52        let access_token = self
53            .access_token
54            .as_ref()
55            .ok_or(crate::error::Error::MissingAccessToken)?;
56        let network = self.active_network.clone().ok_or(crate::Error::MissingNetwork)?;
57
58        let purchase_model = PurchaseModel::try_from(purchase_type.to_string()).map_err(crate::error::Error::Parse)?;
59
60        let reason = match purchase_model {
61            PurchaseModel::CLIK => Reason::LIKE,
62            PurchaseModel::CPIC => Reason::PURCHASE,
63        };
64
65        let metadata = ApiApplicationMetadata {
66            product_hash: product_hash.into(),
67            reason: reason.to_string(),
68            purchase_model: purchase_model.to_string(),
69            app_data: app_data.into(),
70        };
71        let response = create_new_transaction(config, access_token, receiver, network.key, amount, metadata).await?;
72        let purchase_id = response.index;
73        debug!("Created purchase request with id: {purchase_id}");
74        Ok(purchase_id)
75    }
76
77    /// Get purchase details
78    ///
79    /// # Arguments
80    ///
81    /// * `purchase_id` - The ID of the purchase.
82    ///
83    /// # Returns
84    ///
85    /// The purchase details.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if the user or wallet is not initialized, or if there is an error getting the transaction details.
90    pub async fn get_purchase_details(&self, purchase_id: &str) -> Result<PurchaseDetails> {
91        info!("Getting purchase details with id {purchase_id}");
92        let Some(_active_user) = &self.active_user else {
93            return Err(crate::Error::UserNotInitialized);
94        };
95
96        let access_token = self
97            .access_token
98            .as_ref()
99            .ok_or(crate::error::Error::MissingAccessToken)?;
100
101        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
102        let response = get_transaction_details(config, access_token, purchase_id).await?;
103
104        let details = PurchaseDetails {
105            system_address: response.system_address,
106            amount: response.amount.try_into()?,
107            status: response.status,
108            network: response.network,
109        };
110        Ok(details)
111    }
112
113    /// Confirm purchase request
114    ///
115    /// # Arguments
116    ///
117    /// * `pin` - The PIN of the user.
118    /// * `purchase_id` - The ID of the purchase request.
119    ///
120    /// # Returns
121    ///
122    /// Returns `Ok(())` if the purchase request is confirmed successfully.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the user or wallet is not initialized, if there is an error verifying the PIN,
127    /// if there is an error getting the transaction details, or if there is an error committing the transaction.
128    pub async fn confirm_purchase_request(&mut self, pin: &EncryptionPin, purchase_id: &str) -> Result<()> {
129        info!("Confirming purchase request with id {purchase_id}");
130        self.verify_pin(pin).await?;
131
132        let Some(repo) = &mut self.repo else {
133            return Err(crate::Error::UserRepoNotInitialized);
134        };
135        let Some(active_user) = &mut self.active_user else {
136            return Err(crate::Error::UserNotInitialized);
137        };
138
139        let config = self.config.as_mut().ok_or(crate::Error::MissingConfig)?;
140        let access_token = self
141            .access_token
142            .as_ref()
143            .ok_or(crate::error::Error::MissingAccessToken)?;
144        let tx_details = get_transaction_details(config, access_token, purchase_id).await?;
145
146        debug!("Tx details: {:?}", tx_details);
147
148        if tx_details.status != ApiTxStatus::Valid {
149            return Err(WalletError::InvalidTransaction(format!(
150                "Transaction is not valid, current status: {}.",
151                tx_details.status
152            )))?;
153        }
154
155        let current_network = self.active_network.as_ref().ok_or(crate::Error::MissingNetwork)?;
156
157        // for now we check that the correct network_key is configured, in the future we might just
158        // instantiate the correct wallet instead of throwing an error
159        let network = &tx_details.network;
160        if network.key != current_network.key {
161            return Err(WalletError::InvalidTransaction(format!(
162                "Transaction to commit is in network_key {:?}, but {:?} is the currently active current_network_key.",
163                network.key, current_network.key
164            )))?;
165        }
166
167        let wallet = active_user
168            .wallet_manager
169            .try_get(
170                config,
171                &self.access_token,
172                repo,
173                network,
174                pin,
175                &active_user.mnemonic_derivation_options,
176            )
177            .await?;
178
179        let amount = tx_details.amount.try_into()?;
180
181        let intent = TransactionIntent {
182            address_to: tx_details.system_address.clone(),
183            amount,
184            data: Some(purchase_id.to_string().into_bytes()),
185        };
186
187        let tx_id = wallet.send_amount(&intent).await?;
188
189        // Store tx details for the new transaction
190        let newly_created_transaction = wallet.get_wallet_tx(&tx_id).await?;
191        let mut user = repo.get(&active_user.username)?;
192        user.wallet_transactions_versioned
193            .push(VersionedWalletTransaction::V2(newly_created_transaction));
194        let _ = repo.set_wallet_transactions(&active_user.username, user.wallet_transactions_versioned);
195
196        debug!("Transaction id on network: {tx_id}");
197
198        commit_transaction(config, access_token, purchase_id, &tx_id).await?;
199
200        Ok(())
201    }
202
203    /// Send amount to receiver address
204    ///
205    /// # Arguments
206    ///
207    /// * `pin` - The PIN of the user.
208    /// * `address` - The receiver's address.
209    /// * `amount` - The amount to send.
210    /// * `data` - The associated data with the tag. Optional.
211    ///
212    /// # Returns
213    ///
214    /// Returns `Ok(String)` containing the transaction hash if the amount is sent successfully.
215    ///
216    /// # Errors
217    ///
218    /// Returns an error if the user or wallet is not initialized, if there is an error verifying the PIN,
219    /// or if there is an error sending the amount.
220    pub async fn send_amount(
221        &mut self,
222        pin: &EncryptionPin,
223        address: &str,
224        amount: CryptoAmount,
225        data: Option<Vec<u8>>,
226    ) -> Result<String> {
227        info!("Sending amount {amount:?} to receiver {address}");
228        self.verify_pin(pin).await?;
229
230        let Some(repo) = &mut self.repo else {
231            return Err(crate::Error::UserRepoNotInitialized);
232        };
233        let Some(active_user) = &mut self.active_user else {
234            return Err(crate::Error::UserNotInitialized);
235        };
236
237        let config = self.config.as_mut().ok_or(crate::Error::MissingConfig)?;
238        let network = self.active_network.as_ref().ok_or(crate::Error::MissingNetwork)?;
239
240        let wallet = active_user
241            .wallet_manager
242            .try_get(
243                config,
244                &self.access_token,
245                repo,
246                network,
247                pin,
248                &active_user.mnemonic_derivation_options,
249            )
250            .await?;
251
252        // create the transaction payload which holds a tag and associated data
253        let intent = TransactionIntent {
254            address_to: address.to_string(),
255            amount,
256            data,
257        };
258
259        let tx_id = match network.protocol {
260            ApiProtocol::EvmERC20 {
261                chain_id: _,
262                contract_address: _,
263            } => wallet.send_amount(&intent).await?,
264            ApiProtocol::Evm { chain_id: _ } | ApiProtocol::IotaRebased { .. } => {
265                let tx_id = wallet.send_amount(&intent).await?;
266
267                // store the created transaction in the repo
268                let newly_created_transaction = wallet.get_wallet_tx(&tx_id).await?;
269
270                let user = repo.get(&active_user.username)?;
271
272                let mut wallet_transactions = user.wallet_transactions_versioned;
273
274                wallet_transactions.push(VersionedWalletTransaction::V2(newly_created_transaction));
275
276                let _ = repo.set_wallet_transactions(&active_user.username, wallet_transactions);
277                tx_id
278            }
279        };
280
281        Ok(tx_id)
282    }
283
284    /// Estimate gas for sending amount to receiver
285    ///
286    /// # Arguments
287    ///
288    /// * `pin` - The PIN of the user.
289    /// * `address` - The receiver's address.
290    /// * `amount` - The amount to send.
291    /// * `data` - The associated data with the tag. Optional.
292    ///
293    /// # Returns
294    ///
295    /// Returns the gas estimation.
296    ///
297    /// # Errors
298    ///
299    /// Returns an error if the user or wallet is not initialized, if there is an error verifying the PIN,
300    /// or if there is an error estimating the gas.
301    pub async fn estimate_gas(
302        &mut self,
303        pin: &EncryptionPin,
304        address: &str,
305        amount: CryptoAmount,
306        data: Option<Vec<u8>>,
307    ) -> Result<GasCostEstimation> {
308        info!("Estimating gas for sending amount {amount:?} to receiver {address}");
309        self.verify_pin(pin).await?;
310
311        let Some(repo) = &mut self.repo else {
312            return Err(crate::Error::UserRepoNotInitialized);
313        };
314        let Some(active_user) = &mut self.active_user else {
315            return Err(crate::Error::UserNotInitialized);
316        };
317
318        let config = self.config.as_mut().ok_or(crate::Error::MissingConfig)?;
319        let network = self.active_network.as_ref().ok_or(crate::Error::MissingNetwork)?;
320
321        let wallet = active_user
322            .wallet_manager
323            .try_get(
324                config,
325                &self.access_token,
326                repo,
327                network,
328                pin,
329                &active_user.mnemonic_derivation_options,
330            )
331            .await?;
332
333        // create the transaction payload which holds a tag and associated data
334        let intent = TransactionIntent {
335            address_to: address.to_string(),
336            amount,
337            data,
338        };
339
340        let estimate = wallet.estimate_gas_cost(&intent).await?;
341        info!("Estimate: {estimate:?}");
342
343        Ok(estimate)
344    }
345    /// Get transaction list
346    ///
347    /// # Arguments
348    ///
349    /// * `start` - The starting page number.
350    /// * `limit` - The maximum number of transactions per page.
351    ///
352    /// # Returns
353    ///
354    /// Returns a `Result` containing a `TxList` if successful.
355    ///
356    /// # Errors
357    ///
358    /// Returns an error if there is a problem getting the list of transactions.
359    pub async fn get_tx_list(&self, start: u32, limit: u32) -> Result<TxList> {
360        info!("Getting list of transactions");
361        let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
362
363        let user = self.get_user().await?;
364
365        let access_token = self
366            .access_token
367            .as_ref()
368            .ok_or(crate::error::Error::MissingAccessToken)?;
369        let txs_list = get_transactions_list(config, access_token, start, limit).await?;
370        log::debug!("Txs list for user {}: {:?}", user.username, txs_list);
371
372        Ok(TxList {
373            txs: txs_list
374                .txs
375                .into_iter()
376                .map(|val| {
377                    Ok(TxInfo {
378                        date: Some(val.created_at),
379                        sender: val.incoming.username,
380                        receiver: val.outgoing.username,
381                        reference_id: val.index,
382                        amount: val.incoming.amount.0.try_into()?,
383                        currency: val.incoming.network.display_symbol,
384                        application_metadata: val.application_metadata,
385                        status: val.status,
386                        transaction_hash: val.incoming.transaction_id,
387                        course: val.incoming.exchange_rate.0.try_into()?,
388                    })
389                })
390                .collect::<Result<Vec<_>>>()?,
391        })
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::core::core_testing_utils::handle_error_test_cases;
399    use crate::testing_utils::{
400        AUTH_PROVIDER, ETH_NETWORK_KEY, HEADER_X_APP_NAME, IOTA_NETWORK_KEY, PURCHASE_ID, TOKEN, TX_INDEX, USERNAME,
401        example_api_network, example_api_networks, example_get_user, example_tx_details, example_tx_metadata,
402        example_versioned_wallet_transaction, example_wallet_borrow, set_config,
403    };
404    use crate::types::users::KycType;
405    use crate::{
406        core::Sdk,
407        user::MockUserRepo,
408        wallet_manager::{MockWalletManager, WalletBorrow},
409    };
410    use api_types::api::transactions::GetTxsDetailsResponse;
411    use api_types::api::transactions::{
412        ApiTransaction, ApiTransferDetails, CreateTransactionResponse, GetTransactionDetailsResponse,
413    };
414    use api_types::api::viviswap::detail::SwapPaymentDetailKey;
415    use chrono::Utc;
416    use etopay_wallet::MockWalletUser;
417    use etopay_wallet::types::{WalletTransaction, WalletTxStatus};
418    use mockito::Matcher;
419    use rstest::rstest;
420    use rust_decimal_macros::dec;
421
422    fn examples_wallet_tx_list() -> GetTxsDetailsResponse {
423        let main_address = "atoi1qzt0nhsf38nh6rs4p6zs5knqp6psgha9wsv74uajqgjmwc75ugupx3y7x0r".to_string();
424        let aux_address = "atoi1qpnrumvaex24dy0duulp4q07lpa00w20ze6jfd0xly422kdcjxzakzsz5kf".to_string();
425
426        GetTxsDetailsResponse {
427            txs: vec![ApiTransaction {
428                index: "1127f4ba-a0b8-4ecc-a928-bbebc401ac1a".to_string(),
429                status: ApiTxStatus::Completed,
430                created_at: "2022-12-09T09:30:33.52Z".to_string(),
431                updated_at: "2022-12-09T09:30:33.52Z".to_string(),
432                fee_rate: dec!(0.2).into(),
433                incoming: ApiTransferDetails {
434                    transaction_id: Some(
435                        "0x215322f8afdba4e22463a9d8a2e25d96ab0cb9ae6d56ee5ab13065068dae46c0".to_string(),
436                    ),
437                    block_id: Some("0x215322f8afdba4e22463a9d8a2e25d96ab0cb9ae6d56ee5ab13065068dae46c0".to_string()),
438                    username: "satoshi".into(),
439                    address: main_address.clone(),
440                    amount: dec!(920.89).into(),
441                    exchange_rate: dec!(0.06015).into(),
442                    network: example_api_network(IOTA_NETWORK_KEY.to_string()),
443                },
444                outgoing: ApiTransferDetails {
445                    transaction_id: Some(
446                        "0x215322f8afdba4e22463a9d8a2e25d96ab0cb9ae6d56ee5ab13065068dae46c0".to_string(),
447                    ),
448                    block_id: Some("0x215322f8afdba4e22463a9d8a2e25d96ab0cb9ae6d56ee5ab13065068dae46c0".to_string()),
449                    username: "hulk".into(),
450                    address: aux_address.clone(),
451                    amount: dec!(920.89).into(),
452                    exchange_rate: dec!(0.06015).into(),
453                    network: example_api_network(IOTA_NETWORK_KEY.to_string()),
454                },
455                application_metadata: Some(example_tx_metadata()),
456            }],
457        }
458    }
459
460    #[rstest]
461    #[case::success(Ok(CreateTransactionResponse { index: TX_INDEX.into() }))]
462    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
463    #[case::unauthorized(Err(crate::Error::MissingAccessToken))]
464    #[case::missing_config(Err(crate::Error::MissingConfig))]
465    #[tokio::test]
466    async fn test_create_purchase_request(#[case] expected: Result<CreateTransactionResponse>) {
467        // Arrange
468        let (mut srv, config, _cleanup) = set_config().await;
469        let mut sdk = Sdk::new(config).unwrap();
470        sdk.set_networks(example_api_networks());
471        sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
472        let mut mock_server = None;
473
474        match &expected {
475            Ok(_) => {
476                sdk.active_user = Some(crate::types::users::ActiveUser {
477                    username: USERNAME.into(),
478                    wallet_manager: Box::new(MockWalletManager::new()),
479                    mnemonic_derivation_options: Default::default(),
480                });
481                sdk.access_token = Some(TOKEN.clone());
482
483                let mock_response = CreateTransactionResponse { index: TX_INDEX.into() };
484                let body = serde_json::to_string(&mock_response).unwrap();
485
486                mock_server = Some(
487                    srv.mock("POST", "/api/transactions/create")
488                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
489                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
490                        .with_status(201)
491                        .with_header("content-type", "application/json")
492                        .with_body(body)
493                        .expect(1)
494                        .create(),
495                );
496            }
497            Err(error) => {
498                handle_error_test_cases(error, &mut sdk, 0, 0).await;
499            }
500        }
501
502        // Act
503        let amount = CryptoAmount::try_from(dec!(10.0)).unwrap();
504        let response = sdk
505            .create_purchase_request("receiver", amount, "hash", "app_data", "CLIK")
506            .await;
507
508        // Assert
509        match expected {
510            Ok(resp) => {
511                assert_eq!(response.unwrap(), resp.index);
512            }
513            Err(ref expected_err) => {
514                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
515            }
516        }
517        if let Some(m) = mock_server {
518            m.assert();
519        }
520    }
521
522    #[rstest]
523    #[case::success(Ok(()))]
524    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
525    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
526    #[case::unauthorized(Err(crate::Error::MissingAccessToken))]
527    #[case::missing_config(Err(crate::Error::MissingConfig))]
528    #[case::invalid_tx(Err(crate::Error::Wallet(WalletError::InvalidTransaction(format!(
529        "Transaction is not valid, current status: {}.",
530        ApiTxStatus::Invalid(vec!["ReceiverNotVerified".to_string()])
531    )))))]
532    #[tokio::test]
533    async fn test_commit_transaction(#[case] expected: Result<()>) {
534        // Arrange
535        let (mut srv, config, _cleanup) = set_config().await;
536        let mut sdk = Sdk::new(config).unwrap();
537        sdk.set_networks(example_api_networks());
538        sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
539        let mut mock_server_details = None;
540        let mut mock_server_commit = None;
541
542        match &expected {
543            Ok(_) => {
544                let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 2, KycType::Undefined);
545                mock_user_repo
546                    .expect_set_wallet_transactions()
547                    .once()
548                    .returning(|_, _| Ok(()));
549
550                sdk.repo = Some(Box::new(mock_user_repo));
551
552                let mut mock_wallet_manager = MockWalletManager::new();
553                mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
554                    let mut mock_wallet_user = MockWalletUser::new();
555                    mock_wallet_user
556                        .expect_send_amount()
557                        .once()
558                        .returning(|_| Ok("tx_id".to_string()));
559
560                    mock_wallet_user.expect_get_wallet_tx().once().returning(|_| {
561                        Ok(WalletTransaction {
562                            date: Utc::now(),
563                            block_number_hash: None,
564                            transaction_hash: "tx_hash".to_string(),
565                            sender: "sender".to_string(),
566                            receiver: "receiver".to_string(),
567                            amount: CryptoAmount::ZERO,
568                            network_key: "key".to_string(),
569                            status: WalletTxStatus::Pending,
570                            explorer_url: None,
571                            gas_fee: None,
572                            is_sender: false,
573                        })
574                    });
575
576                    Ok(WalletBorrow::from(mock_wallet_user))
577                });
578                sdk.active_user = Some(crate::types::users::ActiveUser {
579                    username: USERNAME.into(),
580                    wallet_manager: Box::new(mock_wallet_manager),
581                    mnemonic_derivation_options: Default::default(),
582                });
583
584                sdk.access_token = Some(TOKEN.clone());
585
586                let mock_tx_response = GetTransactionDetailsResponse {
587                    system_address: "".to_string(),
588                    amount: dec!(5.0).into(),
589                    status: ApiTxStatus::Valid,
590                    network: example_api_network(IOTA_NETWORK_KEY.to_string()),
591                };
592                let body = serde_json::to_string(&mock_tx_response).unwrap();
593
594                mock_server_details = Some(
595                    srv.mock("GET", "/api/transactions/details?index=123")
596                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
597                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
598                        .with_status(200)
599                        .with_body(&body)
600                        .with_header("content-type", "application/json")
601                        .create(),
602                );
603
604                mock_server_commit = Some(
605                    srv.mock("POST", "/api/transactions/commit")
606                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
607                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
608                        .with_status(202)
609                        .expect(1)
610                        .with_header("content-type", "application/json")
611                        .create(),
612                );
613            }
614            Err(crate::Error::Wallet(WalletError::InvalidTransaction(_))) => {
615                let mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
616                sdk.repo = Some(Box::new(mock_user_repo));
617
618                let mock_wallet_manager = example_wallet_borrow();
619                sdk.active_user = Some(crate::types::users::ActiveUser {
620                    username: USERNAME.into(),
621                    wallet_manager: Box::new(mock_wallet_manager),
622                    mnemonic_derivation_options: Default::default(),
623                });
624
625                sdk.access_token = Some(TOKEN.clone());
626
627                let mock_tx_response = GetTransactionDetailsResponse {
628                    system_address: "".to_string(),
629                    amount: dec!(5.0).into(),
630                    status: ApiTxStatus::Invalid(vec!["ReceiverNotVerified".to_string()]),
631                    network: example_api_network(IOTA_NETWORK_KEY.to_string()),
632                };
633                let body = serde_json::to_string(&mock_tx_response).unwrap();
634
635                mock_server_details = Some(
636                    srv.mock("GET", "/api/transactions/details?index=123")
637                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
638                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
639                        .with_status(200)
640                        .with_body(&body)
641                        .with_header("content-type", "application/json")
642                        .create(),
643                );
644            }
645            Err(error) => {
646                handle_error_test_cases(error, &mut sdk, 1, 1).await;
647            }
648        }
649
650        // Act
651        let pin = EncryptionPin::try_from_string("123456").unwrap();
652        let response = sdk.confirm_purchase_request(&pin, PURCHASE_ID).await;
653
654        // Assert
655        match expected {
656            Ok(_) => response.unwrap(),
657            Err(ref err) => {
658                assert_eq!(response.unwrap_err().to_string(), err.to_string());
659            }
660        }
661        if mock_server_details.is_some() & mock_server_commit.is_some() {
662            mock_server_details.unwrap().assert();
663            mock_server_commit.unwrap().assert();
664        }
665    }
666
667    #[rstest]
668    #[case::success(Ok(example_tx_details()))]
669    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
670    #[case::unauthorized(Err(crate::Error::MissingAccessToken))]
671    #[case::missing_config(Err(crate::Error::MissingConfig))]
672    #[tokio::test]
673    async fn test_get_purchase_details(#[case] expected: Result<GetTransactionDetailsResponse>) {
674        // Arrange
675        let (mut srv, config, _cleanup) = set_config().await;
676        let mut sdk = Sdk::new(config).unwrap();
677        let mut mock_server = None;
678
679        match &expected {
680            Ok(_) => {
681                sdk.repo = Some(Box::new(MockUserRepo::new()));
682                sdk.active_user = Some(crate::types::users::ActiveUser {
683                    username: USERNAME.into(),
684                    wallet_manager: Box::new(MockWalletManager::new()),
685                    mnemonic_derivation_options: Default::default(),
686                });
687                sdk.access_token = Some(TOKEN.clone());
688
689                let mock_response = example_tx_details();
690                let body = serde_json::to_string(&mock_response).unwrap();
691
692                mock_server = Some(
693                    srv.mock("GET", "/api/transactions/details?index=123")
694                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
695                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
696                        .with_status(200)
697                        .with_body(&body)
698                        .with_header("content-type", "application/json")
699                        .with_body(&body)
700                        .create(),
701                );
702            }
703            Err(error) => {
704                handle_error_test_cases(error, &mut sdk, 0, 0).await;
705            }
706        }
707
708        // Act
709        let response = sdk.get_purchase_details(PURCHASE_ID).await;
710
711        // Assert
712        match expected {
713            Ok(resp) => {
714                assert_eq!(
715                    GetTransactionDetailsResponse {
716                        system_address: response.as_ref().unwrap().system_address.clone(),
717                        amount: response.as_ref().unwrap().amount.into(),
718                        status: response.unwrap().status,
719                        network: example_api_network(IOTA_NETWORK_KEY.to_string()),
720                    },
721                    resp
722                );
723            }
724            Err(ref expected_err) => {
725                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
726            }
727        }
728        if let Some(m) = mock_server {
729            m.assert();
730        }
731    }
732
733    #[rstest]
734    #[case::success(Ok(()))]
735    #[case::repo_init_error(Err(crate::Error::UserRepoNotInitialized))]
736    #[case::user_init_error(Err(crate::Error::UserNotInitialized))]
737    #[case::missing_config(Err(crate::Error::MissingConfig))]
738    #[tokio::test]
739    async fn test_send_amount(#[case] expected: Result<()>) {
740        // Arrange
741        let (_srv, config, _cleanup) = set_config().await;
742        let mut sdk = Sdk::new(config).unwrap();
743        sdk.set_networks(example_api_networks());
744        sdk.set_network(IOTA_NETWORK_KEY.to_string()).await.unwrap();
745
746        match &expected {
747            Ok(_) => {
748                let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 2, KycType::Undefined);
749                mock_user_repo
750                    .expect_set_wallet_transactions()
751                    .once()
752                    .returning(|_, _| Ok(()));
753                sdk.repo = Some(Box::new(mock_user_repo));
754
755                let mut mock_wallet_manager = MockWalletManager::new();
756                mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
757                    let mut mock_wallet = MockWalletUser::new();
758                    mock_wallet
759                        .expect_send_amount()
760                        .times(1)
761                        .returning(move |_| Ok(String::from("transaction id")));
762                    mock_wallet
763                        .expect_get_wallet_tx()
764                        .once()
765                        .returning(|_| Ok(WalletTransaction::from(example_versioned_wallet_transaction())));
766                    Ok(WalletBorrow::from(mock_wallet))
767                });
768
769                sdk.active_user = Some(crate::types::users::ActiveUser {
770                    username: USERNAME.into(),
771                    wallet_manager: Box::new(mock_wallet_manager),
772                    mnemonic_derivation_options: Default::default(),
773                });
774            }
775            Err(error) => {
776                handle_error_test_cases(error, &mut sdk, 1, 0).await;
777            }
778        }
779
780        // Act
781        let amount = CryptoAmount::try_from(dec!(25.0)).unwrap();
782        let response = sdk
783            .send_amount(
784                &EncryptionPin::try_from_string("123456").unwrap(),
785                "smrq1...",
786                amount,
787                Some(String::from("test message").into_bytes()),
788            )
789            .await;
790
791        // Assert
792        match expected {
793            Ok(_) => {
794                response.unwrap();
795            }
796            Err(ref expected_err) => {
797                assert_eq!(response.err().unwrap().to_string(), expected_err.to_string());
798            }
799        }
800    }
801
802    #[tokio::test]
803    async fn test_send_amount_with_eth_should_trigger_a_call_to_set_wallet_transaction() {
804        // Arrange
805        let (_srv, config, _cleanup) = set_config().await;
806        let mut sdk = Sdk::new(config).unwrap();
807        sdk.set_networks(example_api_networks());
808        sdk.set_network(ETH_NETWORK_KEY.to_string()).await.unwrap();
809
810        let wallet_transaction = VersionedWalletTransaction::V2(WalletTransaction {
811            date: Utc::now(),
812            block_number_hash: Some((0, String::new())),
813            transaction_hash: String::from("tx_id"),
814            receiver: String::new(),
815            sender: String::new(),
816            // SAFETY: the value is non-negative
817            amount: unsafe { CryptoAmount::new_unchecked(dec!(5)) },
818            network_key: ETH_NETWORK_KEY.to_string(),
819            status: WalletTxStatus::Pending,
820            explorer_url: Some(String::new()),
821            gas_fee: None,
822            is_sender: false,
823        });
824
825        let wallet_transactions = vec![wallet_transaction.clone()].to_owned();
826
827        let mut mock_user_repo = example_get_user(SwapPaymentDetailKey::Eth, false, 2, KycType::Undefined);
828        mock_user_repo
829            .expect_set_wallet_transactions()
830            .times(1)
831            .returning(move |_, expected_wallet_transactions| {
832                assert_eq!(wallet_transactions, expected_wallet_transactions);
833                Ok(())
834            });
835        sdk.repo = Some(Box::new(mock_user_repo));
836
837        let mut mock_wallet_manager = MockWalletManager::new();
838        mock_wallet_manager.expect_try_get().returning(move |_, _, _, _, _, _| {
839            let mut mock_wallet = MockWalletUser::new();
840            mock_wallet
841                .expect_send_amount()
842                .times(1)
843                .returning(move |_| Ok(String::from("tx_id")));
844
845            let value = wallet_transaction.clone();
846            mock_wallet
847                .expect_get_wallet_tx()
848                .times(1)
849                .returning(move |_| Ok(WalletTransaction::from(value.clone())));
850
851            Ok(WalletBorrow::from(mock_wallet))
852        });
853
854        sdk.active_user = Some(crate::types::users::ActiveUser {
855            username: USERNAME.into(),
856            wallet_manager: Box::new(mock_wallet_manager),
857            mnemonic_derivation_options: Default::default(),
858        });
859
860        // Act
861        let amount = CryptoAmount::try_from(dec!(5.0)).unwrap();
862        let response = sdk
863            .send_amount(
864                &EncryptionPin::try_from_string("123456").unwrap(),
865                "0xb0b...",
866                amount,
867                Some(String::from("test message").into_bytes()),
868            )
869            .await;
870
871        // Assert
872        response.unwrap();
873    }
874
875    #[rstest]
876    #[case::success(Ok(examples_wallet_tx_list()))]
877    #[case::unauthorized(Err(crate::Error::MissingAccessToken))]
878    #[case::missing_config(Err(crate::Error::MissingConfig))]
879    #[tokio::test]
880    async fn test_get_tx_list(#[case] expected: Result<GetTxsDetailsResponse>) {
881        // Arrange
882        let (mut srv, config, _cleanup) = set_config().await;
883        let mut sdk = Sdk::new(config).unwrap();
884
885        let start = 1u32;
886        let limit = 5u32;
887
888        let mut mock_server = None;
889        match &expected {
890            Ok(_) => {
891                let mock_user_repo = example_get_user(SwapPaymentDetailKey::Iota, false, 1, KycType::Undefined);
892                sdk.repo = Some(Box::new(mock_user_repo));
893                sdk.active_user = Some(crate::types::users::ActiveUser {
894                    username: USERNAME.into(),
895                    wallet_manager: Box::new(MockWalletManager::new()),
896                    mnemonic_derivation_options: Default::default(),
897                });
898                sdk.access_token = Some(TOKEN.clone());
899
900                let txs_details_mock_response = examples_wallet_tx_list();
901                let body = serde_json::to_string(&txs_details_mock_response).unwrap();
902
903                mock_server = Some(
904                    srv.mock("GET", "/api/transactions/txs-details")
905                        .match_header(HEADER_X_APP_NAME, AUTH_PROVIDER)
906                        .match_header("authorization", format!("Bearer {}", TOKEN.as_str()).as_str())
907                        .match_query(Matcher::Exact(format!("is_sender=false&start={start}&limit={limit}")))
908                        .with_status(200)
909                        .with_body(&body)
910                        .expect(1)
911                        .with_header("content-type", "application/json")
912                        .with_body(&body)
913                        .create(),
914                );
915            }
916            Err(error) => {
917                handle_error_test_cases(error, &mut sdk, 0, 1).await;
918            }
919        }
920
921        // Act
922        let response = sdk.get_tx_list(start, limit).await;
923
924        // Assert
925        match expected {
926            Ok(_) => assert!(response.is_ok()),
927            Err(ref err) => {
928                assert_eq!(response.unwrap_err().to_string(), err.to_string());
929            }
930        }
931        if let Some(m) = mock_server {
932            m.assert();
933        }
934    }
935}