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