iota_rest_api/transactions/
execution.rs1use 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
60async 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#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)]
176pub struct ExecuteTransactionQueryParameters {
177 #[serde(default)]
183 pub events: bool,
184 #[serde(default)]
186 pub balance_changes: bool,
187 #[serde(default)]
189 pub input_objects: bool,
190 #[serde(default)]
192 pub output_objects: bool,
193}
194
195#[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 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 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 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 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 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}