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}