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