1use crate::account::get_next_sequence;
11use crate::error::RpcError;
12use crate::simulate::simulate_transaction;
13use crate::submission::{submit_and_wait, PollConfig, TransactionStatus};
14use crate::SorobanRpcClient;
15use sdkt_xdr::sign::{Ed25519Signer, Network, SigningOptions};
16use sdkt_xdr::sign_transaction;
17use sdkt_xdr::InvokeTransactionParams;
18use stellar_xdr::{
19 LedgerFootprint, SorobanAuthorizationEntry, SorobanResources, SorobanTransactionData,
20 SorobanTransactionDataExt, VecM,
21};
22
23#[derive(Debug, Clone, PartialEq)]
25pub struct InvokeResult {
26 pub hash: String,
27 pub status: String,
28 pub contract_id: String,
29 pub function: String,
30 pub fee: u32,
31 pub result_xdr: Option<String>,
32 pub events: Vec<String>,
33 pub error_code: Option<String>,
34 pub error_result_xdr: Option<String>,
35 pub diagnostic_events: Vec<String>,
36}
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct InvokeBuildResult {
41 pub envelope_xdr: String,
42 pub fee: u32,
43 pub sequence: i64,
44 pub contract_id: String,
45 pub function: String,
46}
47
48struct PreparedInvoke {
49 signed_envelope: String,
50 fee: u32,
51 sequence: i64,
52}
53
54fn parse_min_resource_fee(raw: &str) -> Result<u32, RpcError> {
55 let parsed: u64 = raw.parse().map_err(|_| {
56 RpcError::Rpc(format!(
57 "simulation returned invalid min_resource_fee: {raw:?}"
58 ))
59 })?;
60 u32::try_from(parsed)
61 .map_err(|_| RpcError::Rpc(format!("simulation min_resource_fee overflowed u32: {raw}")))
62}
63
64pub const INCLUSION_FEE: u32 = 100;
65
66#[derive(Debug, Clone, PartialEq)]
67pub struct SimulatedInvoke {
68 pub soroban_data: SorobanTransactionData,
69 pub auth_entries: Vec<SorobanAuthorizationEntry>,
70 pub min_resource_fee: u32,
71 pub total_fee: u32,
72}
73
74pub async fn simulate_invoke(
76 client: &SorobanRpcClient,
77 params: &InvokeTransactionParams,
78) -> Result<SimulatedInvoke, RpcError> {
79 let sim_envelope =
80 sdkt_xdr::build_invoke_transaction_with_data(params, empty_soroban_data(), Vec::new())
81 .map_err(|e| RpcError::Rpc(format!("Failed to build invoke transaction: {e}")))?;
82 let simulation = simulate_transaction(client, &sim_envelope)
83 .await
84 .map_err(|e| RpcError::Rpc(format!("Invoke simulation failed: {e}")))?;
85
86 if let Some(err) = &simulation.error {
87 return Err(RpcError::Rpc(format!("Invoke simulation error: {err}")));
88 }
89 if simulation.transaction_data.is_empty() {
90 return Err(RpcError::Rpc(
91 "Simulation did not return SorobanTransactionData".into(),
92 ));
93 }
94
95 let soroban_data = sdkt_xdr::parse_soroban_transaction_data(&simulation.transaction_data)
96 .map_err(|e| RpcError::Rpc(format!("Failed to parse SorobanTransactionData: {e}")))?;
97 let min_resource_fee = parse_min_resource_fee(&simulation.min_resource_fee)?;
98 let auth_entries = if simulation.results.is_empty() {
99 Vec::new()
100 } else {
101 sdkt_xdr::builder::parse_soroban_authorization_entries(&simulation.results[0].auth)
102 .map_err(|e| RpcError::Rpc(format!("Failed to parse auth entries: {e}")))?
103 };
104
105 Ok(SimulatedInvoke {
106 soroban_data,
107 auth_entries,
108 min_resource_fee,
109 total_fee: INCLUSION_FEE.saturating_add(min_resource_fee),
110 })
111}
112
113fn empty_soroban_data() -> SorobanTransactionData {
114 SorobanTransactionData {
115 ext: SorobanTransactionDataExt::V0,
116 resources: SorobanResources {
117 footprint: LedgerFootprint {
118 read_only: VecM::default(),
119 read_write: VecM::default(),
120 },
121 instructions: 0,
122 disk_read_bytes: 0,
123 write_bytes: 0,
124 },
125 resource_fee: 0,
126 }
127}
128
129async fn prepare_invoke_envelope(
130 client: &SorobanRpcClient,
131 params: &InvokeTransactionParams,
132 signer: &Ed25519Signer,
133 network: Network,
134) -> Result<PreparedInvoke, RpcError> {
135 let sequence = get_next_sequence(client, ¶ms.source_account).await?;
136 let sim_params = InvokeTransactionParams {
137 sequence,
138 ..params.clone()
139 };
140 let simulated = simulate_invoke(client, &sim_params).await?;
141 let final_params = InvokeTransactionParams {
142 fee: simulated.total_fee,
143 ..sim_params
144 };
145 let final_envelope = sdkt_xdr::build_invoke_transaction_with_data(
146 &final_params,
147 simulated.soroban_data,
148 simulated.auth_entries,
149 )
150 .map_err(|e| RpcError::Rpc(format!("Failed to build final invoke transaction: {e}")))?;
151 let signing_opts = SigningOptions::with(network);
152 let signed_envelope = sign_transaction(&final_envelope, signer, &signing_opts)
153 .map_err(|e| RpcError::Rpc(format!("Failed to sign invoke transaction: {e}")))?;
154
155 Ok(PreparedInvoke {
156 signed_envelope,
157 fee: final_params.fee,
158 sequence,
159 })
160}
161
162pub async fn build_invoke_envelope(
164 client: &SorobanRpcClient,
165 params: &InvokeTransactionParams,
166 signer: &Ed25519Signer,
167 network: Network,
168) -> Result<InvokeBuildResult, RpcError> {
169 let prepared = prepare_invoke_envelope(client, params, signer, network).await?;
170 Ok(InvokeBuildResult {
171 envelope_xdr: prepared.signed_envelope,
172 fee: prepared.fee,
173 sequence: prepared.sequence,
174 contract_id: params.contract_id.clone(),
175 function: params.function.clone(),
176 })
177}
178
179pub async fn invoke_contract(
181 client: &SorobanRpcClient,
182 params: &InvokeTransactionParams,
183 signer: &Ed25519Signer,
184 network: Network,
185 poll: &PollConfig,
186 wait: bool,
187) -> Result<InvokeResult, RpcError> {
188 let prepared = prepare_invoke_envelope(client, params, signer, network).await?;
189 let submission = submit_and_wait(client, &prepared.signed_envelope, wait, poll).await?;
190 let status = match submission.status {
191 TransactionStatus::Success => "SUCCESS",
192 TransactionStatus::Failed => "FAILED",
193 _ => "PENDING",
194 };
195
196 Ok(InvokeResult {
197 hash: submission.hash,
198 status: status.to_string(),
199 contract_id: params.contract_id.clone(),
200 function: params.function.clone(),
201 fee: prepared.fee,
202 result_xdr: submission.result_xdr,
203 events: submission.events,
204 error_code: submission.error_code,
205 error_result_xdr: submission.error_result_xdr,
206 diagnostic_events: submission.diagnostic_events,
207 })
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn parse_min_resource_fee_accepts_valid() {
216 assert_eq!(parse_min_resource_fee("150").unwrap(), 150);
217 }
218
219 #[test]
220 fn parse_min_resource_fee_rejects_garbage() {
221 assert!(parse_min_resource_fee("abc").is_err());
222 }
223
224 #[test]
225 fn invoke_result_fields_roundtrip() {
226 let r = InvokeResult {
227 hash: "h".into(),
228 status: "SUCCESS".into(),
229 contract_id: "C".into(),
230 function: "f".into(),
231 fee: 250,
232 result_xdr: Some("xdr".into()),
233 events: Vec::new(),
234 error_code: None,
235 error_result_xdr: None,
236 diagnostic_events: Vec::new(),
237 };
238 assert_eq!(r.status, "SUCCESS");
239 assert_eq!(r.fee, 250);
240 }
241
242 #[test]
243 fn invoke_build_result_fields_roundtrip() {
244 let r = InvokeBuildResult {
245 envelope_xdr: "AAAAAgAAA...".into(),
246 fee: 250,
247 sequence: 42,
248 contract_id: "C...".into(),
249 function: "increment".into(),
250 };
251 assert_eq!(r.fee, 250);
252 assert_eq!(r.sequence, 42);
253 assert_eq!(r.function, "increment");
254 assert!(r.envelope_xdr.starts_with("AAAA"));
255 }
256}