iota_rest_api/transactions/
execution.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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
59/// Execute Transaction REST endpoint.
60///
61/// Handles client transaction submission request by passing off the provided
62/// signed transaction to an internal QuorumDriver which drives execution of the
63/// transaction with the current validator set.
64///
65/// A client can signal, using the `Accept` header, the response format as
66/// either JSON or BCS.
67async 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/// Query parameters for the execute transaction endpoint
174#[derive(Debug, Default, serde::Serialize, serde::Deserialize, JsonSchema)]
175pub struct ExecuteTransactionQueryParameters {
176    // TODO once transaction finality support is more fully implemented up and down the stack, add
177    // back in this parameter, which will be mutually-exclusive with the other parameters. When
178    // `true` will submit the txn and return a `202 Accepted` response with no payload.
179    // effects: Option<bool>,
180    /// Request `TransactionEvents` be included in the Response.
181    #[serde(default)]
182    pub events: bool,
183    /// Request `BalanceChanges` be included in the Response.
184    #[serde(default)]
185    pub balance_changes: bool,
186    /// Request input `Object`s be included in the Response.
187    #[serde(default)]
188    pub input_objects: bool,
189    /// Request output `Object`s be included in the Response.
190    #[serde(default)]
191    pub output_objects: bool,
192}
193
194/// Response type for the execute transaction endpoint
195#[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        /// Validator aggregated signature
210        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        /// Validator aggregated signature
289        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        /// Validator aggregated signature
304        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    // 1. subtract all input coins
329    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    // 2. add all mutated coins
339    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    // TODO allow accepting JSON as well as BCS
401    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, &parameters, 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/// Response type for the transaction simulation endpoint
464#[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/// Query parameters for the simulate transaction endpoint
474#[derive(Debug, Default, serde::Serialize, serde::Deserialize, JsonSchema)]
475pub struct SimulateTransactionQueryParameters {
476    /// Request `BalanceChanges` be included in the Response.
477    #[serde(default)]
478    #[serde(with = "serde_with::As::<serde_with::DisplayFromStr>")]
479    #[schemars(with = "bool")]
480    pub balance_changes: bool,
481    /// Request input `Object`s be included in the Response.
482    #[serde(default)]
483    #[serde(with = "serde_with::As::<serde_with::DisplayFromStr>")]
484    #[schemars(with = "bool")]
485    pub input_objects: bool,
486    /// Request output `Object`s be included in the Response.
487    #[serde(default)]
488    #[serde(with = "serde_with::As::<serde_with::DisplayFromStr>")]
489    #[schemars(with = "bool")]
490    pub output_objects: bool,
491}