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