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};
8use iota_sdk2::types::{
9    Address, BalanceChange, CheckpointSequenceNumber, Object, Owner, SignedTransaction,
10    TransactionEffects, TransactionEvents, ValidatorAggregatedSignature, framework::Coin,
11};
12use iota_types::transaction_executor::TransactionExecutor;
13use schemars::JsonSchema;
14use tap::Pipe;
15
16use crate::{
17    RestService, Result,
18    accept::AcceptFormat,
19    openapi::{ApiEndpoint, OperationBuilder, RequestBodyBuilder, ResponseBuilder, RouteHandler},
20    response::{Bcs, ResponseContent},
21};
22
23pub struct ExecuteTransaction;
24
25impl ApiEndpoint<RestService> for ExecuteTransaction {
26    fn method(&self) -> axum::http::Method {
27        axum::http::Method::POST
28    }
29
30    fn path(&self) -> &'static str {
31        "/transactions"
32    }
33
34    fn operation(
35        &self,
36        generator: &mut schemars::gen::SchemaGenerator,
37    ) -> openapiv3::v3_1::Operation {
38        generator.subschema_for::<SignedTransaction>();
39
40        OperationBuilder::new()
41            .tag("Transactions")
42            .operation_id("ExecuteTransaction")
43            .query_parameters::<ExecuteTransactionQueryParameters>(generator)
44            .request_body(RequestBodyBuilder::new().bcs_content().build())
45            .response(
46                200,
47                ResponseBuilder::new()
48                    .json_content::<TransactionExecutionResponse>(generator)
49                    .bcs_content()
50                    .build(),
51            )
52            .build()
53    }
54
55    fn handler(&self) -> RouteHandler<RestService> {
56        RouteHandler::new(self.method(), execute_transaction)
57    }
58}
59
60/// Execute Transaction REST endpoint.
61///
62/// Handles client transaction submission request by passing off the provided
63/// signed transaction to an internal QuorumDriver which drives execution of the
64/// transaction with the current validator set.
65///
66/// A client can signal, using the `Accept` header, the response format as
67/// either JSON or BCS.
68async fn execute_transaction(
69    State(state): State<Option<Arc<dyn TransactionExecutor>>>,
70    Query(parameters): Query<ExecuteTransactionQueryParameters>,
71    client_address: Option<axum::extract::ConnectInfo<SocketAddr>>,
72    accept: AcceptFormat,
73    Bcs(transaction): Bcs<SignedTransaction>,
74) -> Result<ResponseContent<TransactionExecutionResponse>> {
75    let executor = state.ok_or_else(|| anyhow::anyhow!("No Transaction Executor"))?;
76    let request = iota_types::quorum_driver_types::ExecuteTransactionRequestV1 {
77        transaction: transaction.try_into()?,
78        include_events: parameters.events,
79        include_input_objects: parameters.input_objects || parameters.balance_changes,
80        include_output_objects: parameters.output_objects || parameters.balance_changes,
81        include_auxiliary_data: false,
82    };
83
84    let iota_types::quorum_driver_types::ExecuteTransactionResponseV1 {
85        effects,
86        events,
87        input_objects,
88        output_objects,
89        auxiliary_data: _,
90    } = executor
91        .execute_transaction(request, client_address.map(|a| a.0))
92        .await?;
93
94    let (effects, finality) = {
95        let iota_types::quorum_driver_types::FinalizedEffects {
96            effects,
97            finality_info,
98        } = effects;
99        let finality = match finality_info {
100            iota_types::quorum_driver_types::EffectsFinalityInfo::Certified(sig) => {
101                EffectsFinality::Certified {
102                    signature: sig.into(),
103                }
104            }
105            iota_types::quorum_driver_types::EffectsFinalityInfo::Checkpointed(
106                _epoch,
107                checkpoint,
108            ) => EffectsFinality::Checkpointed { checkpoint },
109        };
110
111        (effects.try_into()?, finality)
112    };
113
114    let events = if parameters.events {
115        events.map(TryInto::try_into).transpose()?
116    } else {
117        None
118    };
119
120    let input_objects = input_objects
121        .map(|objects| {
122            objects
123                .into_iter()
124                .map(TryInto::try_into)
125                .collect::<Result<Vec<_>, _>>()
126        })
127        .transpose()?;
128    let output_objects = output_objects
129        .map(|objects| {
130            objects
131                .into_iter()
132                .map(TryInto::try_into)
133                .collect::<Result<Vec<_>, _>>()
134        })
135        .transpose()?;
136
137    let balance_changes = match (parameters.balance_changes, &input_objects, &output_objects) {
138        (true, Some(input_objects), Some(output_objects)) => Some(derive_balance_changes(
139            &effects,
140            input_objects,
141            output_objects,
142        )),
143        _ => None,
144    };
145
146    let input_objects = if parameters.input_objects {
147        input_objects
148    } else {
149        None
150    };
151
152    let output_objects = if parameters.output_objects {
153        output_objects
154    } else {
155        None
156    };
157
158    let response = TransactionExecutionResponse {
159        effects,
160        finality,
161        events,
162        balance_changes,
163        input_objects,
164        output_objects,
165    };
166
167    match accept {
168        AcceptFormat::Json => ResponseContent::Json(response),
169        AcceptFormat::Bcs => ResponseContent::Bcs(response),
170    }
171    .pipe(Ok)
172}
173
174/// Query parameters for the execute transaction endpoint
175#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)]
176pub struct ExecuteTransactionQueryParameters {
177    // TODO once transaction finality support is more fully implemented up and down the stack, add
178    // back in this parameter, which will be mutually-exclusive with the other parameters. When
179    // `true` will submit the txn and return a `202 Accepted` response with no payload.
180    // effects: Option<bool>,
181    /// Request `TransactionEvents` be included in the Response.
182    #[serde(default)]
183    pub events: bool,
184    /// Request `BalanceChanges` be included in the Response.
185    #[serde(default)]
186    pub balance_changes: bool,
187    /// Request input `Object`s be included in the Response.
188    #[serde(default)]
189    pub input_objects: bool,
190    /// Request output `Object`s be included in the Response.
191    #[serde(default)]
192    pub output_objects: bool,
193}
194
195/// Response type for the execute transaction endpoint
196#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)]
197pub struct TransactionExecutionResponse {
198    effects: TransactionEffects,
199
200    finality: EffectsFinality,
201    events: Option<TransactionEvents>,
202    balance_changes: Option<Vec<BalanceChange>>,
203    input_objects: Option<Vec<Object>>,
204    output_objects: Option<Vec<Object>>,
205}
206
207#[derive(Clone, Debug)]
208pub enum EffectsFinality {
209    Certified {
210        /// Validator aggregated signature
211        signature: ValidatorAggregatedSignature,
212    },
213    Checkpointed {
214        checkpoint: CheckpointSequenceNumber,
215    },
216}
217
218impl serde::Serialize for EffectsFinality {
219    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
220    where
221        S: serde::Serializer,
222    {
223        if serializer.is_human_readable() {
224            let readable = match self.clone() {
225                EffectsFinality::Certified { signature } => {
226                    ReadableEffectsFinality::Certified { signature }
227                }
228                EffectsFinality::Checkpointed { checkpoint } => {
229                    ReadableEffectsFinality::Checkpointed { checkpoint }
230                }
231            };
232            readable.serialize(serializer)
233        } else {
234            let binary = match self.clone() {
235                EffectsFinality::Certified { signature } => {
236                    BinaryEffectsFinality::Certified { signature }
237                }
238                EffectsFinality::Checkpointed { checkpoint } => {
239                    BinaryEffectsFinality::Checkpointed { checkpoint }
240                }
241            };
242            binary.serialize(serializer)
243        }
244    }
245}
246
247impl<'de> serde::Deserialize<'de> for EffectsFinality {
248    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
249    where
250        D: serde::Deserializer<'de>,
251    {
252        if deserializer.is_human_readable() {
253            ReadableEffectsFinality::deserialize(deserializer).map(|readable| match readable {
254                ReadableEffectsFinality::Certified { signature } => {
255                    EffectsFinality::Certified { signature }
256                }
257                ReadableEffectsFinality::Checkpointed { checkpoint } => {
258                    EffectsFinality::Checkpointed { checkpoint }
259                }
260            })
261        } else {
262            BinaryEffectsFinality::deserialize(deserializer).map(|binary| match binary {
263                BinaryEffectsFinality::Certified { signature } => {
264                    EffectsFinality::Certified { signature }
265                }
266                BinaryEffectsFinality::Checkpointed { checkpoint } => {
267                    EffectsFinality::Checkpointed { checkpoint }
268                }
269            })
270        }
271    }
272}
273
274impl JsonSchema for EffectsFinality {
275    fn schema_name() -> String {
276        ReadableEffectsFinality::schema_name()
277    }
278
279    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
280        ReadableEffectsFinality::json_schema(gen)
281    }
282}
283
284#[serde_with::serde_as]
285#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
286#[serde(rename = "EffectsFinality", untagged)]
287enum ReadableEffectsFinality {
288    Certified {
289        /// Validator aggregated signature
290        signature: ValidatorAggregatedSignature,
291    },
292    Checkpointed {
293        #[serde_as(
294            as = "iota_types::iota_serde::Readable<iota_types::iota_serde::BigInt<u64>, _>"
295        )]
296        #[schemars(with = "crate::_schemars::U64")]
297        checkpoint: CheckpointSequenceNumber,
298    },
299}
300
301#[derive(serde::Serialize, serde::Deserialize)]
302enum BinaryEffectsFinality {
303    Certified {
304        /// Validator aggregated signature
305        signature: ValidatorAggregatedSignature,
306    },
307    Checkpointed {
308        checkpoint: CheckpointSequenceNumber,
309    },
310}
311
312fn coins(objects: &[Object]) -> impl Iterator<Item = (&Address, Coin<'_>)> + '_ {
313    objects.iter().filter_map(|object| {
314        let address = match object.owner() {
315            Owner::Address(address) => address,
316            Owner::Object(object_id) => object_id.as_address(),
317            Owner::Shared { .. } | Owner::Immutable => return None,
318        };
319        let coin = Coin::try_from_object(object)?;
320        Some((address, coin))
321    })
322}
323
324fn derive_balance_changes(
325    _effects: &TransactionEffects,
326    input_objects: &[Object],
327    output_objects: &[Object],
328) -> Vec<BalanceChange> {
329    // 1. subtract all input coins
330    let balances = coins(input_objects).fold(
331        std::collections::BTreeMap::<_, i128>::new(),
332        |mut acc, (address, coin)| {
333            *acc.entry((address, coin.coin_type())).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())).or_default() += coin.balance() as i128;
341        acc
342    });
343
344    balances
345        .into_iter()
346        .filter_map(|((address, coin_type), amount)| {
347            if amount == 0 {
348                return None;
349            }
350
351            Some(BalanceChange {
352                address: *address,
353                coin_type: coin_type.to_owned(),
354                amount,
355            })
356        })
357        .collect()
358}