iota_sdk_grpc_client/api/execution/simulate.rs
1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! High-level API for transaction simulation.
5
6use iota_grpc_types::{
7 read_mask_fields::{IntoReadMask, SimulateReadMask},
8 v1::transaction_execution_service::{
9 SimulateTransactionItem, SimulateTransactionsRequest, SimulatedTransaction,
10 simulate_transaction_item::TransactionCheckModes,
11 },
12};
13use iota_types::Transaction;
14
15use crate::{
16 Client,
17 api::{
18 Error, MetadataEnvelope, ProtocolError, Result, build_proto_transaction, into_item_results,
19 },
20};
21
22/// A single transaction with simulation options for use in batch simulation.
23pub struct SimulateTransactionInput {
24 /// The transaction to simulate.
25 pub transaction: Transaction,
26 /// Set to true for relaxed Move VM checks (useful for debugging and
27 /// development).
28 pub skip_checks: bool,
29}
30
31impl Client {
32 /// Simulate a transaction without executing it.
33 ///
34 /// This allows you to preview the effects of a transaction before
35 /// actually submitting it to the network.
36 ///
37 /// # Parameters
38 ///
39 /// - `transaction`: The transaction to simulate
40 /// - `skip_checks`: Set to true for relaxed Move VM checks (useful for
41 /// debugging and development)
42 ///
43 /// Returns [`SimulatedTransaction`] which contains:
44 /// - `executed_transaction()` - Access to the simulated ExecutedTransaction
45 /// - `command_results()` - Access to intermediate command execution results
46 ///
47 /// Use lazy conversion methods on the executed transaction to extract data:
48 /// - `result.executed_transaction()?.effects()` - Get simulated effects
49 /// - `result.executed_transaction()?.events()` - Get simulated events (if
50 /// available)
51 /// - `result.executed_transaction()?.input_objects()` - Get input objects
52 /// (if requested)
53 /// - `result.executed_transaction()?.output_objects()` - Get output objects
54 /// (if requested)
55 /// - `result.executed_transaction()?.balance_changes()` - Get balance
56 /// changes (if requested)
57 /// - `result.executed_transaction()?.object_changes()` - Get object changes
58 /// (if requested)
59 ///
60 /// # Example
61 ///
62 /// ```no_run
63 /// # use iota_sdk_grpc_client::Client;
64 /// # use iota_sdk_grpc_client::read_mask_fields::SimulateReadMask;
65 /// # use iota_types::Transaction;
66 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
67 /// let client = Client::new_localnet()?;
68 ///
69 /// let tx: Transaction = todo!();
70 /// let result = client
71 /// .simulate_transaction(tx, false, SimulateReadMask::default())
72 /// .await?;
73 ///
74 /// let executed_tx = result.body().executed_transaction()?;
75 /// let effects = executed_tx.effects()?.effects()?;
76 /// println!("Simulation status: {:?}", effects.as_v1().status);
77 ///
78 /// let output_objs = executed_tx.output_objects()?;
79 /// println!("Would create {} objects", output_objs.objects.len());
80 /// # Ok(())
81 /// # }
82 /// ```
83 ///
84 /// The `read_mask` controls which fields the server returns; use
85 /// `SimulateReadMask::default()` for the default mask. Pass a
86 /// [`SimulateField`](iota_grpc_types::read_mask_fields::SimulateField) or
87 /// any slice/array/vec of fields — conversion is automatic.
88 pub async fn simulate_transaction(
89 &self,
90 transaction: Transaction,
91 skip_checks: bool,
92 read_mask: impl IntoReadMask<SimulateReadMask>,
93 ) -> Result<MetadataEnvelope<SimulatedTransaction>> {
94 self.simulate_transactions(
95 vec![SimulateTransactionInput {
96 transaction,
97 skip_checks,
98 }],
99 read_mask,
100 )
101 .await?
102 .try_map(extract_single_simulation_result)
103 }
104
105 /// Simulate a batch of transactions without executing them.
106 ///
107 /// Transactions are simulated sequentially on the server. Each transaction
108 /// is independent — failure of one does not abort the rest.
109 ///
110 /// Returns a `Vec<Result<SimulatedTransaction>>` in the same order as the
111 /// input. Each element is either the successfully simulated transaction or
112 /// the per-item error returned by the server.
113 ///
114 /// The `read_mask` controls which fields the server returns for each
115 /// `SimulatedTransaction`; use `SimulateReadMask::default()` for the
116 /// default mask. Pass a
117 /// [`SimulateField`](iota_grpc_types::read_mask_fields::SimulateField) or
118 /// any slice/array/vec of fields — conversion is automatic.
119 ///
120 /// # Errors
121 ///
122 /// Returns [`Error::EmptyRequest`] if `transactions` is empty.
123 /// Returns a transport-level [`Error::Grpc`] if the entire RPC fails
124 /// (e.g. batch size exceeded).
125 pub async fn simulate_transactions(
126 &self,
127 transactions: Vec<SimulateTransactionInput>,
128 read_mask: impl IntoReadMask<SimulateReadMask>,
129 ) -> Result<MetadataEnvelope<Vec<Result<SimulatedTransaction>>>> {
130 let read_mask = read_mask.into_read_mask();
131 if transactions.is_empty() {
132 return Err(Error::EmptyRequest);
133 }
134
135 let items = transactions
136 .into_iter()
137 .map(|input| build_simulate_item(input.transaction, input.skip_checks))
138 .collect::<Result<Vec<_>>>()?;
139
140 let request = SimulateTransactionsRequest::default()
141 .with_transactions(items)
142 .with_read_mask(read_mask);
143
144 let response = self
145 .execution_service_client()
146 .simulate_transactions(request)
147 .await?;
148
149 Ok(MetadataEnvelope::from(response).map(|r| into_item_results(r.transaction_results)))
150 }
151}
152
153fn extract_single_simulation_result(
154 results: Vec<Result<SimulatedTransaction>>,
155) -> Result<SimulatedTransaction> {
156 results
157 .into_iter()
158 .next()
159 .ok_or_else(|| Error::Protocol(ProtocolError::EmptyResponseField("transaction_results")))?
160}
161
162/// Convert a transaction and options into a proto `SimulateTransactionItem`.
163fn build_simulate_item(
164 transaction: Transaction,
165 skip_checks: bool,
166) -> Result<SimulateTransactionItem> {
167 let proto_transaction = build_proto_transaction(&transaction, transaction.digest())?;
168
169 let tx_checks = if skip_checks {
170 vec![TransactionCheckModes::DisableVmChecks as i32]
171 } else {
172 vec![]
173 };
174
175 Ok(SimulateTransactionItem::default()
176 .with_transaction(proto_transaction)
177 .with_tx_checks(tx_checks))
178}