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