1use std::{net::SocketAddr, sync::Arc};
6
7use axum::extract::{Query, State, rejection::ExtensionRejection};
8use iota_sdk2::types::{
9 Address, BalanceChange, CheckpointSequenceNumber, Object, Owner, SignedTransaction,
10 Transaction, TransactionEffects, TransactionEvents, ValidatorAggregatedSignature,
11 framework::Coin,
12};
13use iota_types::transaction_executor::{SimulateTransactionResult, TransactionExecutor};
14use schemars::JsonSchema;
15use tap::Pipe;
16
17use crate::{
18 RestError, RestService, Result,
19 accept::AcceptFormat,
20 openapi::{ApiEndpoint, OperationBuilder, RequestBodyBuilder, ResponseBuilder, RouteHandler},
21 response::{Bcs, ResponseContent},
22};
23
24pub struct ExecuteTransaction;
25
26impl ApiEndpoint<RestService> for ExecuteTransaction {
27 fn method(&self) -> axum::http::Method {
28 axum::http::Method::POST
29 }
30
31 fn path(&self) -> &'static str {
32 "/transactions"
33 }
34
35 fn operation(
36 &self,
37 generator: &mut schemars::gen::SchemaGenerator,
38 ) -> openapiv3::v3_1::Operation {
39 OperationBuilder::new()
40 .tag("Transactions")
41 .operation_id("ExecuteTransaction")
42 .query_parameters::<ExecuteTransactionQueryParameters>(generator)
43 .request_body(RequestBodyBuilder::new().bcs_content().build())
44 .response(
45 200,
46 ResponseBuilder::new()
47 .json_content::<TransactionExecutionResponse>(generator)
48 .bcs_content()
49 .build(),
50 )
51 .build()
52 }
53
54 fn handler(&self) -> RouteHandler<RestService> {
55 RouteHandler::new(self.method(), execute_transaction)
56 }
57}
58
59async fn execute_transaction(
68 State(state): State<Option<Arc<dyn TransactionExecutor>>>,
69 Query(parameters): Query<ExecuteTransactionQueryParameters>,
70 client_address: Result<axum::extract::ConnectInfo<SocketAddr>, ExtensionRejection>,
71 accept: AcceptFormat,
72 Bcs(transaction): Bcs<SignedTransaction>,
73) -> Result<ResponseContent<TransactionExecutionResponse>> {
74 let executor = state.ok_or_else(|| anyhow::anyhow!("No Transaction Executor"))?;
75 let request = iota_types::quorum_driver_types::ExecuteTransactionRequestV1 {
76 transaction: transaction.try_into()?,
77 include_events: parameters.events,
78 include_input_objects: parameters.input_objects || parameters.balance_changes,
79 include_output_objects: parameters.output_objects || parameters.balance_changes,
80 include_auxiliary_data: false,
81 };
82
83 let iota_types::quorum_driver_types::ExecuteTransactionResponseV1 {
84 effects,
85 events,
86 input_objects,
87 output_objects,
88 auxiliary_data: _,
89 } = executor
90 .execute_transaction(request, client_address.ok().map(|a| a.0))
91 .await?;
92
93 let (effects, finality) = {
94 let iota_types::quorum_driver_types::FinalizedEffects {
95 effects,
96 finality_info,
97 } = effects;
98 let finality = match finality_info {
99 iota_types::quorum_driver_types::EffectsFinalityInfo::Certified(sig) => {
100 EffectsFinality::Certified {
101 signature: sig.into(),
102 }
103 }
104 iota_types::quorum_driver_types::EffectsFinalityInfo::Checkpointed(
105 _epoch,
106 checkpoint,
107 ) => EffectsFinality::Checkpointed { checkpoint },
108 };
109
110 (effects.try_into()?, finality)
111 };
112
113 let events = if parameters.events {
114 events.map(TryInto::try_into).transpose()?
115 } else {
116 None
117 };
118
119 let input_objects = input_objects
120 .map(|objects| {
121 objects
122 .into_iter()
123 .map(TryInto::try_into)
124 .collect::<Result<Vec<_>, _>>()
125 })
126 .transpose()?;
127 let output_objects = output_objects
128 .map(|objects| {
129 objects
130 .into_iter()
131 .map(TryInto::try_into)
132 .collect::<Result<Vec<_>, _>>()
133 })
134 .transpose()?;
135
136 let balance_changes = match (parameters.balance_changes, &input_objects, &output_objects) {
137 (true, Some(input_objects), Some(output_objects)) => Some(derive_balance_changes(
138 &effects,
139 input_objects,
140 output_objects,
141 )),
142 _ => None,
143 };
144
145 let input_objects = if parameters.input_objects {
146 input_objects
147 } else {
148 None
149 };
150
151 let output_objects = if parameters.output_objects {
152 output_objects
153 } else {
154 None
155 };
156
157 let response = TransactionExecutionResponse {
158 effects,
159 finality,
160 events,
161 balance_changes,
162 input_objects,
163 output_objects,
164 };
165
166 match accept {
167 AcceptFormat::Json => ResponseContent::Json(response),
168 AcceptFormat::Bcs => ResponseContent::Bcs(response),
169 }
170 .pipe(Ok)
171}
172
173#[derive(Debug, Default, serde::Serialize, serde::Deserialize, JsonSchema)]
175pub struct ExecuteTransactionQueryParameters {
176 #[serde(default)]
182 pub events: bool,
183 #[serde(default)]
185 pub balance_changes: bool,
186 #[serde(default)]
188 pub input_objects: bool,
189 #[serde(default)]
191 pub output_objects: bool,
192}
193
194#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)]
196pub struct TransactionExecutionResponse {
197 effects: TransactionEffects,
198
199 finality: EffectsFinality,
200 events: Option<TransactionEvents>,
201 balance_changes: Option<Vec<BalanceChange>>,
202 input_objects: Option<Vec<Object>>,
203 output_objects: Option<Vec<Object>>,
204}
205
206#[derive(Clone, Debug)]
207pub enum EffectsFinality {
208 Certified {
209 signature: ValidatorAggregatedSignature,
211 },
212 Checkpointed {
213 checkpoint: CheckpointSequenceNumber,
214 },
215}
216
217impl serde::Serialize for EffectsFinality {
218 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
219 where
220 S: serde::Serializer,
221 {
222 if serializer.is_human_readable() {
223 let readable = match self.clone() {
224 EffectsFinality::Certified { signature } => {
225 ReadableEffectsFinality::Certified { signature }
226 }
227 EffectsFinality::Checkpointed { checkpoint } => {
228 ReadableEffectsFinality::Checkpointed { checkpoint }
229 }
230 };
231 readable.serialize(serializer)
232 } else {
233 let binary = match self.clone() {
234 EffectsFinality::Certified { signature } => {
235 BinaryEffectsFinality::Certified { signature }
236 }
237 EffectsFinality::Checkpointed { checkpoint } => {
238 BinaryEffectsFinality::Checkpointed { checkpoint }
239 }
240 };
241 binary.serialize(serializer)
242 }
243 }
244}
245
246impl<'de> serde::Deserialize<'de> for EffectsFinality {
247 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
248 where
249 D: serde::Deserializer<'de>,
250 {
251 if deserializer.is_human_readable() {
252 ReadableEffectsFinality::deserialize(deserializer).map(|readable| match readable {
253 ReadableEffectsFinality::Certified { signature } => {
254 EffectsFinality::Certified { signature }
255 }
256 ReadableEffectsFinality::Checkpointed { checkpoint } => {
257 EffectsFinality::Checkpointed { checkpoint }
258 }
259 })
260 } else {
261 BinaryEffectsFinality::deserialize(deserializer).map(|binary| match binary {
262 BinaryEffectsFinality::Certified { signature } => {
263 EffectsFinality::Certified { signature }
264 }
265 BinaryEffectsFinality::Checkpointed { checkpoint } => {
266 EffectsFinality::Checkpointed { checkpoint }
267 }
268 })
269 }
270 }
271}
272
273impl JsonSchema for EffectsFinality {
274 fn schema_name() -> String {
275 ReadableEffectsFinality::schema_name()
276 }
277
278 fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
279 ReadableEffectsFinality::json_schema(gen)
280 }
281}
282
283#[serde_with::serde_as]
284#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
285#[serde(rename = "EffectsFinality", untagged)]
286enum ReadableEffectsFinality {
287 Certified {
288 signature: ValidatorAggregatedSignature,
290 },
291 Checkpointed {
292 #[serde_as(
293 as = "iota_types::iota_serde::Readable<iota_types::iota_serde::BigInt<u64>, _>"
294 )]
295 #[schemars(with = "crate::_schemars::U64")]
296 checkpoint: CheckpointSequenceNumber,
297 },
298}
299
300#[derive(serde::Serialize, serde::Deserialize)]
301enum BinaryEffectsFinality {
302 Certified {
303 signature: ValidatorAggregatedSignature,
305 },
306 Checkpointed {
307 checkpoint: CheckpointSequenceNumber,
308 },
309}
310
311fn coins(objects: &[Object]) -> impl Iterator<Item = (&Address, Coin<'_>)> + '_ {
312 objects.iter().filter_map(|object| {
313 let address = match object.owner() {
314 Owner::Address(address) => address,
315 Owner::Object(object_id) => object_id.as_address(),
316 Owner::Shared { .. } | Owner::Immutable => return None,
317 };
318 let coin = Coin::try_from_object(object)?;
319 Some((address, coin))
320 })
321}
322
323fn derive_balance_changes(
324 _effects: &TransactionEffects,
325 input_objects: &[Object],
326 output_objects: &[Object],
327) -> Vec<BalanceChange> {
328 let balances = coins(input_objects).fold(
330 std::collections::BTreeMap::<_, i128>::new(),
331 |mut acc, (address, coin)| {
332 *acc.entry((address, coin.coin_type().to_owned()))
333 .or_default() -= coin.balance() as i128;
334 acc
335 },
336 );
337
338 let balances = coins(output_objects).fold(balances, |mut acc, (address, coin)| {
340 *acc.entry((address, coin.coin_type().to_owned()))
341 .or_default() += coin.balance() as i128;
342 acc
343 });
344
345 balances
346 .into_iter()
347 .filter_map(|((address, coin_type), amount)| {
348 if amount == 0 {
349 return None;
350 }
351
352 Some(BalanceChange {
353 address: *address,
354 coin_type,
355 amount,
356 })
357 })
358 .collect()
359}
360
361pub struct SimulateTransaction;
362
363impl ApiEndpoint<RestService> for SimulateTransaction {
364 fn method(&self) -> axum::http::Method {
365 axum::http::Method::POST
366 }
367
368 fn path(&self) -> &'static str {
369 "/transactions/simulate"
370 }
371
372 fn operation(
373 &self,
374 generator: &mut schemars::gen::SchemaGenerator,
375 ) -> openapiv3::v3_1::Operation {
376 OperationBuilder::new()
377 .tag("Transactions")
378 .operation_id("SimulateTransaction")
379 .query_parameters::<SimulateTransactionQueryParameters>(generator)
380 .request_body(RequestBodyBuilder::new().bcs_content().build())
381 .response(
382 200,
383 ResponseBuilder::new()
384 .json_content::<TransactionSimulationResponse>(generator)
385 .bcs_content()
386 .build(),
387 )
388 .build()
389 }
390
391 fn handler(&self) -> RouteHandler<RestService> {
392 RouteHandler::new(self.method(), simulate_transaction)
393 }
394}
395
396async fn simulate_transaction(
397 State(state): State<Option<Arc<dyn TransactionExecutor>>>,
398 Query(parameters): Query<SimulateTransactionQueryParameters>,
399 accept: AcceptFormat,
400 Bcs(transaction): Bcs<Transaction>,
402) -> Result<ResponseContent<TransactionSimulationResponse>> {
403 let executor = state.ok_or_else(|| anyhow::anyhow!("No Transaction Executor"))?;
404
405 simulate_transaction_impl(&executor, ¶meters, transaction).map(|response| match accept {
406 AcceptFormat::Json => ResponseContent::Json(response),
407 AcceptFormat::Bcs => ResponseContent::Bcs(response),
408 })
409}
410
411pub(super) fn simulate_transaction_impl(
412 executor: &Arc<dyn TransactionExecutor>,
413 parameters: &SimulateTransactionQueryParameters,
414 transaction: Transaction,
415) -> Result<TransactionSimulationResponse> {
416 if transaction.gas_payment.objects.is_empty() {
417 return Err(RestError::new(
418 axum::http::StatusCode::BAD_REQUEST,
419 "no gas payment provided",
420 ));
421 }
422
423 let SimulateTransactionResult {
424 input_objects,
425 output_objects,
426 events,
427 effects,
428 mock_gas_id,
429 } = executor
430 .simulate_transaction(transaction.try_into()?)
431 .map_err(anyhow::Error::from)?;
432
433 if mock_gas_id.is_some() {
434 return Err(RestError::new(
435 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
436 "simulate unexpectedly used a mock gas payment",
437 ));
438 }
439
440 let events = events.map(TryInto::try_into).transpose()?;
441 let effects = effects.try_into()?;
442
443 let input_objects = input_objects
444 .into_values()
445 .map(TryInto::try_into)
446 .collect::<Result<Vec<_>, _>>()?;
447 let output_objects = output_objects
448 .into_values()
449 .map(TryInto::try_into)
450 .collect::<Result<Vec<_>, _>>()?;
451 let balance_changes = derive_balance_changes(&effects, &input_objects, &output_objects);
452
453 TransactionSimulationResponse {
454 events,
455 effects,
456 balance_changes: parameters.balance_changes.then_some(balance_changes),
457 input_objects: parameters.input_objects.then_some(input_objects),
458 output_objects: parameters.output_objects.then_some(output_objects),
459 }
460 .pipe(Ok)
461}
462
463#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)]
465pub struct TransactionSimulationResponse {
466 pub effects: TransactionEffects,
467 pub events: Option<TransactionEvents>,
468 pub balance_changes: Option<Vec<BalanceChange>>,
469 pub input_objects: Option<Vec<Object>>,
470 pub output_objects: Option<Vec<Object>>,
471}
472
473#[derive(Debug, Default, serde::Serialize, serde::Deserialize, JsonSchema)]
475pub struct SimulateTransactionQueryParameters {
476 #[serde(default)]
478 #[serde(with = "serde_with::As::<serde_with::DisplayFromStr>")]
479 #[schemars(with = "bool")]
480 pub balance_changes: bool,
481 #[serde(default)]
483 #[serde(with = "serde_with::As::<serde_with::DisplayFromStr>")]
484 #[schemars(with = "bool")]
485 pub input_objects: bool,
486 #[serde(default)]
488 #[serde(with = "serde_with::As::<serde_with::DisplayFromStr>")]
489 #[schemars(with = "bool")]
490 pub output_objects: bool,
491}