Skip to main content

sdkt_rpc/
invoke.rs

1//! End-to-end state-changing contract invocation: `sdkt invoke`.
2//!
3//! Drives the full lifecycle using the existing infrastructure:
4//! sequence fetch ([`get_next_sequence`]) → simulation
5//! ([`simulate_transaction`]) → final envelope build
6//! ([`sdkt_xdr::build_invoke_transaction_with_data`]) → signing
7//! ([`sdkt_xdr::sign_transaction`]) → submission + polling
8//! ([`submit_and_wait`]).
9
10use 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/// Final result of a state-changing contract invocation.
24#[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/// Result of preparing, but not submitting, an invocation envelope.
39#[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
74/// Simulate an invocation and adopt the footprint, auth entries and fee returned by RPC.
75pub 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, &params.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
162/// Build and sign an invocation without submitting it.
163pub 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
179/// Invoke a contract function, submitting and optionally polling for settlement.
180pub 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}