iota_sdk_graphql_client/api/
dry_run.rs1use base64ct::Encoding;
8use cynic::QueryBuilder;
9use iota_types::{Address, ObjectReference, Transaction, TransactionEffects, TransactionKind};
10
11use crate::{
12 DryRunEffect, DryRunResult, GraphQLClient,
13 api::define_query,
14 error::GraphQLResult,
15 query_types::{DryRunArgs, DryRunQueryFragment, ObjectRef, TransactionMetadata},
16};
17
18define_query! {
19 pub struct DryRunTransactionQuery {
22 client: GraphQLClient,
23 transaction: Transaction,
24 skip_checks: bool,
25 }
26 output: GraphQLResult<DryRunResult>;
27}
28
29impl DryRunTransactionQuery {
30 pub fn skip_checks(mut self, skip_checks: bool) -> Self {
35 self.skip_checks = skip_checks;
36 self
37 }
38
39 async fn send(self) -> GraphQLResult<DryRunResult> {
40 let Transaction::V1(v1) = &self.transaction else {
41 unimplemented!("a new Transaction enum variant was added and needs to be handled")
42 };
43 let mut query = self
44 .client
45 .dry_run_transaction_kind(&v1.kind)
46 .sender(v1.sender)
47 .gas_budget(v1.gas_payment.budget)
48 .gas_price(v1.gas_payment.price)
49 .gas_sponsor(v1.gas_payment.owner)
50 .skip_checks(self.skip_checks);
51 if !v1.gas_payment.objects.is_empty() {
52 query = query.gas_objects(v1.gas_payment.objects.clone());
53 }
54 query.await
55 }
56}
57
58define_query! {
59 pub struct DryRunTransactionKindQuery {
62 client: GraphQLClient,
63 transaction_kind: TransactionKind,
64 transaction_metadata: TransactionMetadata,
65 skip_checks: bool,
66 }
67 output: GraphQLResult<DryRunResult>;
68}
69
70impl DryRunTransactionKindQuery {
71 pub fn sender(mut self, sender: Address) -> Self {
73 self.transaction_metadata.sender = Some(sender);
74 self
75 }
76
77 pub fn gas_budget(mut self, gas_budget: u64) -> Self {
79 self.transaction_metadata.gas_budget = Some(gas_budget);
80 self
81 }
82
83 pub fn gas_price(mut self, gas_price: u64) -> Self {
85 self.transaction_metadata.gas_price = Some(gas_price);
86 self
87 }
88
89 pub fn gas_objects(mut self, gas_objects: Vec<ObjectReference>) -> Self {
91 self.transaction_metadata.gas_objects =
92 Some(gas_objects.into_iter().map(ObjectRef::from).collect());
93 self
94 }
95
96 pub fn gas_sponsor(mut self, gas_sponsor: Address) -> Self {
98 self.transaction_metadata.gas_sponsor = Some(gas_sponsor);
99 self
100 }
101
102 pub fn skip_checks(mut self, skip_checks: bool) -> Self {
107 self.skip_checks = skip_checks;
108 self
109 }
110
111 async fn send(self) -> GraphQLResult<DryRunResult> {
112 let tx_bytes = base64ct::Base64::encode_string(
113 &bcs::to_bytes(&self.transaction_kind).map_err(iota_types::BcsError::new)?,
114 );
115 self.client
116 .dry_run(tx_bytes, self.skip_checks, self.transaction_metadata)
117 .await
118 }
119}
120
121impl GraphQLClient {
122 pub fn dry_run_transaction(&self, transaction: &Transaction) -> DryRunTransactionQuery {
125 DryRunTransactionQuery {
126 client: self.clone(),
127 transaction: transaction.clone(),
128 skip_checks: false,
129 }
130 }
131
132 pub fn dry_run_transaction_kind(
135 &self,
136 transaction_kind: &TransactionKind,
137 ) -> DryRunTransactionKindQuery {
138 DryRunTransactionKindQuery {
139 client: self.clone(),
140 transaction_kind: transaction_kind.clone(),
141 transaction_metadata: TransactionMetadata::default(),
142 skip_checks: false,
143 }
144 }
145
146 pub(crate) async fn dry_run(
148 &self,
149 tx_bytes: String,
150 skip_checks: bool,
151 tx_meta: impl Into<Option<TransactionMetadata>>,
152 ) -> GraphQLResult<DryRunResult> {
153 let operation = DryRunQueryFragment::build(DryRunArgs {
154 tx_bytes,
155 skip_checks,
156 tx_meta: tx_meta.into(),
157 });
158 let response = self.run_query(&operation).await?;
159
160 let results = response
162 .dry_run_transaction_block
163 .results
164 .iter()
165 .flatten()
166 .map(DryRunEffect::try_from)
167 .collect::<GraphQLResult<Vec<_>>>()?;
168
169 let txn_block = &response.dry_run_transaction_block.transaction;
170
171 let effects = txn_block
172 .as_ref()
173 .and_then(|tx| tx.effects.as_ref())
174 .and_then(|tx| tx.bcs.as_ref())
175 .map(|bcs| crate::base64::decode(bcs.0.as_str()))
176 .transpose()?
177 .map(|bcs| {
178 bcs::from_bytes::<TransactionEffects>(&bcs).map_err(iota_types::BcsError::new)
179 })
180 .transpose()?;
181
182 let transaction = txn_block
184 .as_ref()
185 .and_then(|tx| tx.bcs_unsigned.as_ref())
186 .map(|bcs| crate::base64::decode(bcs.0.as_str()))
187 .transpose()?
188 .map(|bcs| bcs::from_bytes::<Transaction>(&bcs).map_err(iota_types::BcsError::new))
189 .transpose()?;
190
191 let suggested_gas_price = response
192 .dry_run_transaction_block
193 .suggested_gas_price
194 .map(u64::try_from)
195 .transpose()?;
196
197 Ok(DryRunResult {
198 error: response.dry_run_transaction_block.error,
199 results,
200 transaction,
201 effects,
202 suggested_gas_price,
203 })
204 }
205}
206
207#[cfg(all(test, not(target_arch = "wasm32")))]
208mod tests {
209 use base64ct::Encoding;
210 use iota_types::{Address, ObjectDigest, ObjectId, ObjectReference, Transaction, Version};
211
212 use crate::{
213 GraphQLClient,
214 test_utils::{sent_variables, test_transaction},
215 };
216
217 #[tokio::test]
218 async fn dry_run_transaction_sends_the_kind_metadata_and_skip_checks() {
219 let transaction = test_transaction();
220 let Transaction::V1(v1) = &transaction else {
221 unreachable!()
222 };
223 let expected_tx_bytes = base64ct::Base64::encode_string(&bcs::to_bytes(&v1.kind).unwrap());
224 let vars = sent_variables("DryRunQueryFragment", |client| async move {
225 let _ = client
226 .dry_run_transaction(&transaction)
227 .skip_checks(true)
228 .await;
229 })
230 .await;
231 assert_eq!(vars["txBytes"], expected_tx_bytes);
232 assert_eq!(vars["skipChecks"], true);
233 assert_eq!(vars["txMeta"]["sender"], Address::STD.to_string());
234 assert_eq!(vars["txMeta"]["gasBudget"], 5_000_000);
235 assert_eq!(vars["txMeta"]["gasPrice"], 1000);
236 assert_eq!(vars["txMeta"]["gasSponsor"], Address::FRAMEWORK.to_string());
237 let gas_object = &vars["txMeta"]["gasObjects"][0];
238 assert_eq!(gas_object["address"], ObjectId::SYSTEM_STATE.to_string());
239 assert_eq!(gas_object["version"], 3);
240 assert_eq!(gas_object["digest"], ObjectDigest::ZERO.to_base58());
241
242 let transaction = test_transaction();
243 let vars = sent_variables("DryRunQueryFragment", |client| async move {
244 let _ = client.dry_run_transaction(&transaction).await;
245 })
246 .await;
247 assert_eq!(vars["skipChecks"], false);
248 }
249
250 #[tokio::test]
251 async fn dry_run_transaction_kind_sends_the_kind_metadata_and_skip_checks() {
252 let Transaction::V1(v1) = test_transaction() else {
253 unreachable!()
254 };
255 let expected_tx_bytes = base64ct::Base64::encode_string(&bcs::to_bytes(&v1.kind).unwrap());
256 let gas_object = ObjectReference::new(
257 ObjectId::SYSTEM_STATE,
258 Version::from_u64(3),
259 ObjectDigest::ZERO,
260 );
261 let kind = v1.kind.clone();
262 let vars = sent_variables("DryRunQueryFragment", |client| async move {
263 let _ = client
264 .dry_run_transaction_kind(&kind)
265 .sender(Address::STD)
266 .gas_budget(5_000_000)
267 .gas_price(1000)
268 .gas_objects(vec![gas_object])
269 .gas_sponsor(Address::FRAMEWORK)
270 .skip_checks(true)
271 .await;
272 })
273 .await;
274 assert_eq!(vars["txBytes"], expected_tx_bytes);
275 assert_eq!(vars["skipChecks"], true);
276 assert_eq!(vars["txMeta"]["sender"], Address::STD.to_string());
277 assert_eq!(vars["txMeta"]["gasBudget"], 5_000_000);
278 assert_eq!(vars["txMeta"]["gasPrice"], 1000);
279 assert_eq!(vars["txMeta"]["gasSponsor"], Address::FRAMEWORK.to_string());
280 let gas_object = &vars["txMeta"]["gasObjects"][0];
281 assert_eq!(gas_object["address"], ObjectId::SYSTEM_STATE.to_string());
282 assert_eq!(gas_object["version"], 3);
283 assert_eq!(gas_object["digest"], ObjectDigest::ZERO.to_base58());
284
285 let vars = sent_variables("DryRunQueryFragment", |client| async move {
286 let _ = client.dry_run_transaction_kind(&v1.kind).await;
287 })
288 .await;
289 assert_eq!(vars["skipChecks"], false);
290 for field in [
291 "sender",
292 "gasBudget",
293 "gasPrice",
294 "gasObjects",
295 "gasSponsor",
296 ] {
297 assert!(vars["txMeta"][field].is_null(), "{field} is set by default");
298 }
299 }
300
301 #[tokio::test]
303 async fn test_dry_run() {
304 let client = GraphQLClient::new_testnet().unwrap();
305 let tx_bytes = "AAACAAgAypo7AAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgABAQAAAQEDAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACg9WqbvnpQmublI1+/dnonzEvhVPHnGEX++ianEHLIZmoiqRAAAAAAAgmrviNLnSJMjhRUZ8il2SFFjZ60cdJWv9v3M7pTsTQaA0FjZwX1JlYTftfc/+nF7J1QTfVacG+5wc2teKJoJHBDf/BgAAAAAAIOFdV7nQyvw+7AJpDmJFifAa4SqrI5qqXqAq1IKZsSxKVTI1Cd7yJVFzIqi4nnPX1ShmHEJWweFl5BId7OSkHXViNQ0AAAAAACA4U7t1jiQwTs87xenAvOkQWAAMWbElg0Exz1annhowtXPQJaMX5mcenWnm/aFAXhUM2rGsvqqa2zM2OOQyEKqbNP8GAAAAAAAg7pHVs4Z58mP71Y53cDuY3X/TbTgfmBHkDWe16J+kBOqhnfl+yRNiYZ3fpWvyc4rB2u+a2qjUGqcw7yFnlhJAj1w00w8AAAAAIDEjW30S0iN4lnDXpigCjEmOA0tUYKf339ZayYUU9PG6s1wmB/dndlMUdTZGe5MOz1baxXMESHbVd5L7XTObgECAQpEAAAAAACBCkCOAwD6Dl2DkdXj/eFRBTsNPWg3XYATTPxeThLuhzrTmcYf4XqT8ceMAoKbQBjtzyaTv+xb0K0MzHfvJR1NFgUKRAAAAAAAgxUVPvQUU/R1jcC2+AxZ7uC3ls+09G7xAk0xusdBSUkXPNNWDsV8xzw6ipjnf5pk9W3R9P0RD6iORRe+0JKaLtmE1DQAAAAAAIPhsUoriBlzhLc4SHds72JTbjeI37VhyjlFVtQurLY+26e+jqKb2TsdARpYEvxPl31WAelj2RMuUyK8S5NeluEWjKpEAAAAAACCR/0nc3l5UIXpl6I6SEpWABP/vJewHhZ5iMDpIDXdMqf0VCu+y2k/TZIpRFMDRiBO0oUW+L8+06uAi3pZkwpbFNf8GAAAAAAAgyIfExjdHxdt7+eiOLRh4N4/iSMZCrHf2t5iYI+Kl8ysAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOgDAAAAAAAA4G88AAAAAAAA";
306
307 client
308 .dry_run(tx_bytes.to_string(), false, None)
309 .await
310 .map_err(|e| {
311 format!(
312 "Dry run failed for {} network. Error: {e}",
313 client.rpc_server()
314 )
315 })
316 .unwrap();
317 }
318}