1use std::collections::{BTreeMap, HashMap};
6
7use axum::{
8 Json,
9 extract::{Query, State},
10};
11use iota_protocol_config::ProtocolConfig;
12use iota_sdk2::types::{
13 Argument, Command, ObjectId, Transaction, UnresolvedInputArgument, UnresolvedObjectReference,
14 UnresolvedProgrammableTransaction, UnresolvedTransaction,
15};
16use iota_types::{
17 base_types::{IotaAddress, ObjectID, ObjectRef},
18 effects::TransactionEffectsAPI,
19 gas::GasCostSummary,
20 gas_coin::GasCoin,
21 move_package::MovePackage,
22 transaction::{
23 CallArg, GasData, ObjectArg, ProgrammableTransaction, TransactionData, TransactionDataAPI,
24 },
25 transaction_executor::VmChecks,
26};
27use itertools::Itertools;
28use move_binary_format::normalized;
29use schemars::JsonSchema;
30use tap::Pipe;
31
32use super::{TransactionSimulationResponse, execution::SimulateTransactionQueryParameters};
33use crate::{
34 RestError, RestService, Result,
35 accept::AcceptFormat,
36 objects::ObjectNotFoundError,
37 openapi::{ApiEndpoint, OperationBuilder, RequestBodyBuilder, ResponseBuilder, RouteHandler},
38 reader::StateReader,
39 response::ResponseContent,
40};
41
42pub struct ResolveTransaction;
47
48impl ApiEndpoint<RestService> for ResolveTransaction {
49 fn method(&self) -> axum::http::Method {
50 axum::http::Method::POST
51 }
52
53 fn path(&self) -> &'static str {
54 "/transactions/resolve"
55 }
56
57 fn operation(
58 &self,
59 generator: &mut schemars::gen::SchemaGenerator,
60 ) -> openapiv3::v3_1::Operation {
61 OperationBuilder::new()
62 .tag("Transactions")
63 .operation_id("ResolveTransaction")
64 .query_parameters::<ResolveTransactionQueryParameters>(generator)
65 .request_body(
66 RequestBodyBuilder::new()
67 .build(),
69 )
70 .response(
71 200,
72 ResponseBuilder::new()
73 .json_content::<ResolveTransactionResponse>(generator)
74 .bcs_content()
75 .build(),
76 )
77 .build()
78 }
79
80 fn handler(&self) -> RouteHandler<RestService> {
81 RouteHandler::new(self.method(), resolve_transaction)
82 }
83}
84
85async fn resolve_transaction(
86 State(state): State<RestService>,
87 Query(parameters): Query<ResolveTransactionQueryParameters>,
88 accept: AcceptFormat,
89 Json(unresolved_transaction): Json<UnresolvedTransaction>,
90) -> Result<ResponseContent<ResolveTransactionResponse>> {
91 let executor = state
92 .executor
93 .as_ref()
94 .ok_or_else(|| anyhow::anyhow!("No Transaction Executor"))?;
95 let (reference_gas_price, protocol_config) = {
96 let system_state = state.reader.get_system_state_summary()?;
97
98 let current_protocol_version = state.reader.get_system_state_summary()?.protocol_version;
99
100 let protocol_config = ProtocolConfig::get_for_version_if_supported(
101 current_protocol_version.into(),
102 state.reader.inner().get_chain_identifier()?.chain(),
103 )
104 .ok_or_else(|| {
105 RestError::new(
106 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
107 "unable to get current protocol config",
108 )
109 })?;
110
111 (system_state.reference_gas_price, protocol_config)
112 };
113 let mut called_packages =
114 called_packages(&state.reader, &protocol_config, &unresolved_transaction)?;
115 let user_provided_budget = unresolved_transaction
116 .gas_payment
117 .as_ref()
118 .and_then(|payment| payment.budget);
119 let mut resolved_transaction = resolve_unresolved_transaction(
120 &state.reader,
121 &mut called_packages,
122 reference_gas_price,
123 protocol_config.max_tx_gas(),
124 unresolved_transaction,
125 )?;
126
127 let budget = if let Some(user_provided_budget) = user_provided_budget {
130 user_provided_budget
131 } else {
132 let dry_run_checks = VmChecks::Enabled;
134 let simulation_result = executor
135 .simulate_transaction(resolved_transaction.clone(), dry_run_checks)
136 .map_err(anyhow::Error::from)?;
137
138 let estimate = estimate_gas_budget_from_gas_cost(
139 simulation_result.effects.gas_cost_summary(),
140 reference_gas_price,
141 );
142 resolved_transaction.gas_data_mut().budget = estimate;
143 estimate
144 };
145
146 if resolved_transaction.gas_data().payment.is_empty() {
148 let input_objects = resolved_transaction
149 .input_objects()
150 .map_err(anyhow::Error::from)?
151 .iter()
152 .flat_map(|obj| match obj {
153 iota_types::transaction::InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => {
154 Some(*id)
155 }
156 _ => None,
157 })
158 .collect_vec();
159 let gas_coins = select_gas(
160 &state.reader,
161 resolved_transaction.gas_data().owner,
162 budget,
163 protocol_config.max_gas_payment_objects(),
164 &input_objects,
165 )?;
166 resolved_transaction.gas_data_mut().payment = gas_coins;
167 }
168
169 let simulation = if parameters.simulate {
170 super::execution::simulate_transaction_impl(
171 executor,
172 ¶meters.simulate_transaction_parameters,
173 resolved_transaction.clone().try_into()?,
174 )?
175 .pipe(Some)
176 } else {
177 None
178 };
179
180 ResolveTransactionResponse {
181 transaction: resolved_transaction.try_into()?,
182 simulation,
183 }
184 .pipe(|response| match accept {
185 AcceptFormat::Json => ResponseContent::Json(response),
186 AcceptFormat::Bcs => ResponseContent::Bcs(response),
187 })
188 .pipe(Ok)
189}
190
191#[derive(Debug, Default, serde::Serialize, serde::Deserialize, JsonSchema)]
193pub struct ResolveTransactionQueryParameters {
194 #[serde(default)]
197 pub simulate: bool,
198 #[serde(flatten)]
199 pub simulate_transaction_parameters: SimulateTransactionQueryParameters,
200}
201
202struct NormalizedPackages {
203 packages: HashMap<ObjectId, NormalizedPackage>,
204}
205
206struct NormalizedPackage {
207 #[allow(unused)]
208 package: MovePackage,
209 normalized_modules: BTreeMap<String, normalized::Module<normalized::RcIdentifier>>,
210}
211
212fn called_packages(
213 reader: &StateReader,
214 protocol_config: &ProtocolConfig,
215 unresolved_transaction: &UnresolvedTransaction,
216) -> Result<NormalizedPackages> {
217 let binary_config = iota_types::execution_config_utils::to_binary_config(protocol_config);
218 let mut pool = normalized::RcPool::new();
219 let mut packages = HashMap::new();
220
221 for move_call in unresolved_transaction
222 .ptb
223 .commands
224 .iter()
225 .filter_map(|command| {
226 if let Command::MoveCall(move_call) = command {
227 Some(move_call)
228 } else {
229 None
230 }
231 })
232 {
233 let package = reader
234 .inner()
235 .try_get_object(&(move_call.package.into()))?
236 .ok_or_else(|| ObjectNotFoundError::new(move_call.package))?
237 .data
238 .try_as_package()
239 .ok_or_else(|| {
240 RestError::new(
241 axum::http::StatusCode::BAD_REQUEST,
242 format!("object {} is not a package", move_call.package),
243 )
244 })?
245 .to_owned();
246
247 let normalized_modules = package
256 .normalize(&mut pool, &binary_config, true)
257 .map_err(|e| {
258 RestError::new(
259 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
260 format!("unable to normalize package {}: {e}", move_call.package),
261 )
262 })?;
263 let package = NormalizedPackage {
264 package,
265 normalized_modules,
266 };
267
268 packages.insert(move_call.package, package);
269 }
270
271 Ok(NormalizedPackages { packages })
272}
273
274fn resolve_unresolved_transaction(
275 reader: &StateReader,
276 called_packages: &mut NormalizedPackages,
277 reference_gas_price: u64,
278 max_gas_budget: u64,
279 unresolved_transaction: UnresolvedTransaction,
280) -> Result<TransactionData> {
281 let sender = unresolved_transaction.sender.into();
282 let gas_data = if let Some(unresolved_gas_payment) = unresolved_transaction.gas_payment {
283 let payment = unresolved_gas_payment
284 .objects
285 .into_iter()
286 .map(|unresolved| resolve_object_reference(reader, unresolved))
287 .collect::<Result<Vec<_>>>()?;
288 GasData {
289 payment,
290 owner: unresolved_gas_payment.owner.into(),
291 price: unresolved_gas_payment.price.unwrap_or(reference_gas_price),
292 budget: unresolved_gas_payment.budget.unwrap_or(max_gas_budget),
293 }
294 } else {
295 GasData {
296 payment: vec![],
297 owner: sender,
298 price: reference_gas_price,
299 budget: max_gas_budget,
300 }
301 };
302 let expiration = unresolved_transaction.expiration.into();
303 let ptb = resolve_ptb(reader, called_packages, unresolved_transaction.ptb)?;
304 Ok(TransactionData::V1(
305 iota_types::transaction::TransactionDataV1 {
306 kind: iota_types::transaction::TransactionKind::ProgrammableTransaction(ptb),
307 sender,
308 gas_data,
309 expiration,
310 },
311 ))
312}
313
314#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)]
316pub struct ResolveTransactionResponse {
317 pub transaction: Transaction,
318 pub simulation: Option<TransactionSimulationResponse>,
319}
320
321fn resolve_object_reference(
322 reader: &StateReader,
323 unresolved_object_reference: UnresolvedObjectReference,
324) -> Result<ObjectRef> {
325 let UnresolvedObjectReference {
326 object_id,
327 version,
328 digest,
329 } = unresolved_object_reference;
330
331 let id = object_id.into();
332 let (v, d) = if let Some(version) = version {
333 let object = reader
334 .inner()
335 .try_get_object_by_key(&id, version.into())?
336 .ok_or_else(|| ObjectNotFoundError::new_with_version(object_id, version))?;
337 (object.version(), object.digest())
338 } else {
339 let object = reader
340 .inner()
341 .try_get_object(&id)?
342 .ok_or_else(|| ObjectNotFoundError::new(object_id))?;
343 (object.version(), object.digest())
344 };
345
346 if digest.is_some_and(|digest| digest.inner() != d.inner()) {
347 return Err(RestError::new(
348 axum::http::StatusCode::BAD_REQUEST,
349 format!("provided digest doesn't match, provided: {digest:?} actual: {d}"),
350 ));
351 }
352
353 Ok((id, v, d))
354}
355
356fn resolve_ptb(
357 reader: &StateReader,
358 called_packages: &mut NormalizedPackages,
359 unresolved_ptb: UnresolvedProgrammableTransaction,
360) -> Result<ProgrammableTransaction> {
361 let inputs = unresolved_ptb
362 .inputs
363 .into_iter()
364 .enumerate()
365 .map(|(arg_idx, arg)| {
366 resolve_arg(
367 reader,
368 called_packages,
369 &unresolved_ptb.commands,
370 arg,
371 arg_idx,
372 )
373 })
374 .collect::<Result<_>>()?;
375
376 ProgrammableTransaction {
377 inputs,
378 commands: unresolved_ptb
379 .commands
380 .into_iter()
381 .map(TryInto::try_into)
382 .collect::<Result<_, _>>()?,
383 }
384 .pipe(Ok)
385}
386
387fn resolve_arg(
388 reader: &StateReader,
389 called_packages: &mut NormalizedPackages,
390 commands: &[Command],
391 arg: UnresolvedInputArgument,
392 arg_idx: usize,
393) -> Result<CallArg> {
394 match arg {
395 UnresolvedInputArgument::Pure { value } => CallArg::Pure(value),
396 UnresolvedInputArgument::ImmutableOrOwned(obj_ref) => CallArg::Object(
397 ObjectArg::ImmOrOwnedObject(resolve_object_reference(reader, obj_ref)?),
398 ),
399 UnresolvedInputArgument::Shared {
400 object_id,
401 initial_shared_version: _,
402 mutable: _,
403 } => {
404 let id = object_id.into();
405 let object = reader
406 .inner()
407 .try_get_object(&id)?
408 .ok_or_else(|| ObjectNotFoundError::new(object_id))?;
409
410 let initial_shared_version = if let iota_types::object::Owner::Shared {
411 initial_shared_version,
412 } = object.owner()
413 {
414 *initial_shared_version
415 } else {
416 return Err(RestError::new(
417 axum::http::StatusCode::BAD_REQUEST,
418 format!("object {object_id} is not a shared object"),
419 ));
420 };
421
422 let mut mutable = false;
423
424 for (command, idx) in find_arg_uses(arg_idx, commands) {
425 match (command, idx) {
426 (Command::MoveCall(move_call), Some(idx)) => {
427 let function = called_packages
428 .packages
429 .get(&move_call.package)
431 .and_then(|package| {
433 package.normalized_modules.get(move_call.module.as_str())
434 })
435 .and_then(|module| module.functions.get(move_call.function.as_str()))
437 .ok_or_else(|| {
438 RestError::new(
439 axum::http::StatusCode::BAD_REQUEST,
440 format!(
441 "unable to find function {package}::{module}::{function}",
442 package = move_call.package,
443 module = move_call.module,
444 function = move_call.function
445 ),
446 )
447 })?;
448
449 let arg_type = function.parameters.get(idx).ok_or_else(|| {
450 RestError::new(
451 axum::http::StatusCode::BAD_REQUEST,
452 "invalid input parameter",
453 )
454 })?;
455
456 if matches!(
457 &**arg_type,
458 normalized::Type::Reference(true, _)
459 | normalized::Type::Datatype(_)
460 ) {
461 mutable = true;
462 }
463 }
464
465 (
466 Command::SplitCoins(_)
467 | Command::MergeCoins(_)
468 | Command::MakeMoveVector(_),
469 _,
470 ) => {
471 mutable = true;
472 }
473
474 _ => {}
475 }
476
477 if mutable {
480 break;
481 }
482 }
483
484 CallArg::Object(ObjectArg::SharedObject {
485 id,
486 initial_shared_version,
487 mutable,
488 })
489 }
490 UnresolvedInputArgument::Receiving(obj_ref) => CallArg::Object(ObjectArg::Receiving(
491 resolve_object_reference(reader, obj_ref)?,
492 )),
493 }
494 .pipe(Ok)
495}
496
497fn find_arg_uses(
502 arg_idx: usize,
503 commands: &[Command],
504) -> impl Iterator<Item = (&Command, Option<usize>)> {
505 commands.iter().filter_map(move |command| {
506 match command {
507 Command::MoveCall(move_call) => move_call
508 .arguments
509 .iter()
510 .position(|elem| matches_input_arg(*elem, arg_idx))
511 .map(Some),
512 Command::TransferObjects(transfer_objects) => transfer_objects
513 .objects
514 .iter()
515 .position(|elem| matches_input_arg(*elem, arg_idx))
516 .map(Some),
517 Command::SplitCoins(split_coins) => {
518 matches_input_arg(split_coins.coin, arg_idx).then_some(None)
519 }
520 Command::MergeCoins(merge_coins) => {
521 if matches_input_arg(merge_coins.coin, arg_idx) {
522 Some(None)
523 } else {
524 merge_coins
525 .coins_to_merge
526 .iter()
527 .position(|elem| matches_input_arg(*elem, arg_idx))
528 .map(Some)
529 }
530 }
531 Command::Publish(_) => None,
532 Command::MakeMoveVector(make_move_vector) => make_move_vector
533 .elements
534 .iter()
535 .position(|elem| matches_input_arg(*elem, arg_idx))
536 .map(Some),
537 Command::Upgrade(upgrade) => matches_input_arg(upgrade.ticket, arg_idx).then_some(None),
538 }
539 .map(|x| (command, x))
540 })
541}
542
543fn matches_input_arg(arg: Argument, arg_idx: usize) -> bool {
544 matches!(arg, Argument::Input(idx) if idx as usize == arg_idx)
545}
546
547fn estimate_gas_budget_from_gas_cost(
556 gas_cost_summary: &GasCostSummary,
557 reference_gas_price: u64,
558) -> u64 {
559 const GAS_SAFE_OVERHEAD: u64 = 1000;
560
561 let safe_overhead = GAS_SAFE_OVERHEAD * reference_gas_price;
562 let computation_cost_with_overhead = gas_cost_summary.computation_cost + safe_overhead;
563
564 let gas_usage = gas_cost_summary.net_gas_usage() + safe_overhead as i64;
565 computation_cost_with_overhead.max(if gas_usage < 0 { 0 } else { gas_usage as u64 })
566}
567
568fn select_gas(
569 reader: &StateReader,
570 owner: IotaAddress,
571 budget: u64,
572 max_gas_payment_objects: u32,
573 input_objects: &[ObjectID],
574) -> Result<Vec<ObjectRef>> {
575 let gas_coins = reader
576 .inner()
577 .indexes()
578 .ok_or_else(RestError::not_found)?
579 .account_owned_objects_info_iter(owner, None)?
580 .filter(|info| info.type_.is_gas_coin())
581 .filter(|info| !input_objects.contains(&info.object_id))
582 .filter_map(|info| {
583 reader
584 .inner()
585 .try_get_object(&info.object_id)
586 .ok()
587 .flatten()
588 })
589 .filter_map(|object| {
590 GasCoin::try_from(&object)
591 .ok()
592 .map(|coin| (object.compute_object_reference(), coin.value()))
593 })
594 .sorted_by(|object1, object2| object2.1.cmp(&object1.1))
595 .take(max_gas_payment_objects as usize);
596
597 let mut selected_gas = vec![];
598 let mut selected_gas_value = 0;
599
600 for (object_ref, value) in gas_coins {
601 selected_gas.push(object_ref);
602 selected_gas_value += value;
603 if selected_gas_value >= budget {
604 return Ok(selected_gas);
605 }
606 }
607
608 Err(RestError::new(
609 axum::http::StatusCode::BAD_REQUEST,
610 format!(
611 "unable to select sufficient gas coins from account {owner} \
612 to satisfy required budget {budget}"
613 ),
614 ))
615}