soroban_rs/
env.rs

1//! # Soroban Environment
2//!
3//! This module provides environment configuration and network interaction for Soroban operations.
4//! The environment manages RPC connections, network configuration, and transaction handling.
5//!
6//! ## Features
7//!
8//! - RPC client configuration and management
9//! - Network identification and parameters
10//! - Account information retrieval
11//! - Transaction simulation and submission
12//!
13//! ## Example
14//!
15//! ```rust
16//! use soroban_rs::{Env, EnvConfigs};
17//! use stellar_xdr::curr::TransactionEnvelope;
18//!
19//! async fn example() {
20//!     // Create a new environment for the Stellar testnet
21//!     let env = Env::new(EnvConfigs {
22//!         rpc_url: "https://soroban-testnet.stellar.org".to_string(),
23//!         network_passphrase: "Test SDF Network ; September 2015".to_string(),
24//!     }).unwrap();
25//!
26//!     // Retrieve account information
27//!     let account = env.get_account("G..........").await.unwrap();
28//!     println!("Account: {:?}", account);
29//! }
30//! ```
31use crate::{
32    error::SorobanHelperError,
33    rpc::{ExternalRpcClient, RpcClient},
34};
35use sha2::{Digest, Sha256};
36use std::sync::Arc;
37use stellar_rpc_client::{GetTransactionResponse, SimulateTransactionResponse};
38use stellar_xdr::curr::{AccountEntry, Hash, TransactionEnvelope};
39
40/// Configuration for a Soroban environment.
41///
42/// Contains the necessary parameters to connect to a Soroban RPC server
43/// and identify the target network.
44#[derive(Clone)]
45pub struct EnvConfigs {
46    /// URL of the Soroban RPC server
47    pub rpc_url: String,
48    /// Network passphrase that identifies the Stellar network
49    pub network_passphrase: String,
50}
51
52/// The environment for Soroban operations.
53///
54/// Provides access to network functionality such as retrieving account information,
55/// simulating transactions, and submitting transactions to the network.
56#[derive(Clone)]
57pub struct Env {
58    /// RPC client for interacting with the Soroban network
59    pub(crate) rpc_client: Arc<dyn RpcClient + Send + Sync>,
60    /// Configuration for this environment
61    pub(crate) configs: EnvConfigs,
62}
63
64impl Env {
65    /// Creates a new environment with the specified configuration.
66    ///
67    /// # Parameters
68    ///
69    /// * `configs` - The environment configuration including RPC URL and network passphrase
70    ///
71    /// # Returns
72    ///
73    /// A new `Env` instance or an error if the RPC client could not be created
74    ///
75    /// # Errors
76    ///
77    /// Returns `SorobanHelperError` if the RPC client initialization fails
78    pub fn new(configs: EnvConfigs) -> Result<Self, SorobanHelperError> {
79        let client = ExternalRpcClient::new(&configs.rpc_url)?;
80        Ok(Self {
81            rpc_client: Arc::new(client),
82            configs,
83        })
84    }
85
86    /// Returns the network passphrase for this environment.
87    ///
88    /// The network passphrase is a string that uniquely identifies a Stellar network,
89    /// such as "Public Global Stellar Network ; September 2015" for the public network
90    /// or "Test SDF Network ; September 2015" for the testnet.
91    ///
92    /// # Returns
93    ///
94    /// The network passphrase as a string slice
95    pub fn network_passphrase(&self) -> &str {
96        &self.configs.network_passphrase
97    }
98
99    /// Calculates the network ID hash from the network passphrase.
100    ///
101    /// The network ID is the SHA-256 hash of the network passphrase and is used
102    /// in various cryptographic operations, including transaction signing.
103    ///
104    /// # Returns
105    ///
106    /// The SHA-256 hash of the network passphrase
107    pub fn network_id(&self) -> Hash {
108        let network_pass_bytes = self.configs.network_passphrase.as_bytes();
109        Hash(Sha256::digest(network_pass_bytes).into())
110    }
111
112    /// Retrieves account information from the network.
113    ///
114    /// # Parameters
115    ///
116    /// * `account_id` - The Stellar account ID to retrieve
117    ///
118    /// # Returns
119    ///
120    /// The account entry information or an error if the account could not be retrieved
121    ///
122    /// # Errors
123    ///
124    /// Returns `SorobanHelperError::NetworkRequestFailed` if the RPC request fails
125    pub async fn get_account(&self, account_id: &str) -> Result<AccountEntry, SorobanHelperError> {
126        self.rpc_client.get_account(account_id).await.map_err(|e| {
127            SorobanHelperError::NetworkRequestFailed(format!(
128                "Failed to get account {}: {}",
129                account_id, e
130            ))
131        })
132    }
133
134    /// Simulates a transaction without submitting it to the network.
135    ///
136    /// This is useful for estimating transaction costs, validating transactions,
137    /// and retrieving the expected results of contract invocations.
138    ///
139    /// # Parameters
140    ///
141    /// * `tx_envelope` - The transaction envelope to simulate
142    ///
143    /// # Returns
144    ///
145    /// The simulation response or an error if the simulation failed
146    ///
147    /// # Errors
148    ///
149    /// Returns `SorobanHelperError::NetworkRequestFailed` if the RPC request fails
150    pub async fn simulate_transaction(
151        &self,
152        tx_envelope: &TransactionEnvelope,
153    ) -> Result<SimulateTransactionResponse, SorobanHelperError> {
154        self.rpc_client
155            .simulate_transaction_envelope(tx_envelope)
156            .await
157            .map_err(|e| {
158                SorobanHelperError::NetworkRequestFailed(format!(
159                    "Failed to simulate transaction: {}",
160                    e
161                ))
162            })
163    }
164
165    /// Submits a transaction to the network and waits for the result.
166    ///
167    /// # Parameters
168    ///
169    /// * `tx_envelope` - The signed transaction envelope to submit
170    ///
171    /// # Returns
172    ///
173    /// The transaction response or an error if the transaction failed
174    ///
175    /// # Errors
176    ///
177    /// Returns:
178    /// - `SorobanHelperError::ContractCodeAlreadyExists` if the transaction failed because the contract code already exists
179    /// - `SorobanHelperError::NetworkRequestFailed` for other transaction failures
180    pub async fn send_transaction(
181        &self,
182        tx_envelope: &TransactionEnvelope,
183    ) -> Result<GetTransactionResponse, SorobanHelperError> {
184        self.rpc_client
185            .send_transaction_polling(tx_envelope)
186            .await
187            .map_err(|e| {
188                // Check if this is a "contract code already exists" error
189                let error_string = e.to_string();
190                if error_string.contains(&SorobanHelperError::ContractCodeAlreadyExists.to_string())
191                {
192                    return SorobanHelperError::ContractCodeAlreadyExists;
193                }
194                // Otherwise, it's a general transaction failure
195                SorobanHelperError::NetworkRequestFailed(format!(
196                    "Failed to send transaction: {}",
197                    e
198                ))
199            })
200    }
201}
202
203#[cfg(test)]
204pub mod test {
205    use crate::mock::{mock_env, mock_signer3, mock_transaction_envelope};
206
207    use super::*;
208
209    #[test]
210    fn test_new() {
211        let env = Env::new(EnvConfigs {
212            rpc_url: "https://soroban-testnet.stellar.org".to_string(),
213            network_passphrase: "Test SDF Network ; September 2015".to_string(),
214        })
215        .unwrap();
216
217        assert_eq!(env.configs.rpc_url, "https://soroban-testnet.stellar.org");
218        assert_eq!(
219            env.configs.network_passphrase,
220            "Test SDF Network ; September 2015"
221        );
222    }
223
224    #[test]
225    fn test_network_id() {
226        let env = Env::new(EnvConfigs {
227            rpc_url: "https://test.com".to_string(),
228            network_passphrase: "test".to_string(),
229        })
230        .unwrap();
231
232        assert_eq!(
233            env.network_id().0,
234            [
235                159, 134, 208, 129, 136, 76, 125, 101, 154, 47, 234, 160, 197, 90, 208, 21, 163,
236                191, 79, 27, 43, 11, 130, 44, 209, 93, 108, 21, 176, 240, 10, 8
237            ]
238        );
239    }
240
241    #[tokio::test]
242    async fn test_code_already_exists_error() {
243        let send_transaction_polling_result = Err(SorobanHelperError::ContractCodeAlreadyExists);
244        let env = mock_env(None, None, Some(send_transaction_polling_result));
245        let account_id = mock_signer3().account_id();
246        let result = env
247            .send_transaction(&mock_transaction_envelope(account_id))
248            .await;
249        assert!(matches!(
250            result,
251            Err(SorobanHelperError::ContractCodeAlreadyExists)
252        ));
253    }
254
255    #[tokio::test]
256    async fn test_send_transaction_error() {
257        let send_transaction_polling_result = Err(SorobanHelperError::NetworkRequestFailed(
258            "OtherError".to_string(),
259        ));
260        let env = mock_env(None, None, Some(send_transaction_polling_result));
261        let account_id = mock_signer3().account_id();
262        let result = env
263            .send_transaction(&mock_transaction_envelope(account_id))
264            .await;
265        assert!(matches!(
266            result,
267            Err(SorobanHelperError::NetworkRequestFailed(_))
268        ));
269    }
270}