diff --git a/Cargo.lock b/Cargo.lock index e38be86a60a41..c51c0ec2d040e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2124,9 +2124,9 @@ dependencies = [ [[package]] name = "bnum" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" +checksum = "50202def95bf36cb7d1d7a7962cea1c36a3f8ad42425e5d2b71d7acb8041b5b8" [[package]] name = "brotli" @@ -11626,9 +11626,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.208" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] @@ -11675,9 +11675,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.208" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2 1.0.87", "quote 1.0.35", @@ -11697,12 +11697,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "indexmap 2.2.6", "itoa", + "memchr", "ryu", "serde", ] @@ -14505,6 +14506,7 @@ dependencies = [ "itertools 0.13.0", "mime", "move-binary-format", + "move-core-types", "mysten-network", "openapiv3", "prometheus", @@ -14636,8 +14638,7 @@ dependencies = [ [[package]] name = "sui-sdk-types" version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f0c33e0ed6354cc3c4a4646e89485af49e1525654672773db69abfd0bcc9bf" +source = "git+https://github.com/MystenLabs/sui-rust-sdk.git?rev=31bd9da32a8057edc45da8f5e6a8f25b83919b93#31bd9da32a8057edc45da8f5e6a8f25b83919b93" dependencies = [ "base64ct", "bcs", @@ -14651,7 +14652,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_with 3.9.0", - "winnow 0.6.18", + "winnow 0.6.20", ] [[package]] @@ -16227,7 +16228,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime 0.6.8", - "winnow 0.6.18", + "winnow 0.6.20", ] [[package]] @@ -17496,9 +17497,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 9a642f285f463..a03184519f89e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -593,7 +593,8 @@ anemo-cli = { git = "https://github.com/mystenlabs/anemo.git", rev = "e609f7697e anemo-tower = { git = "https://github.com/mystenlabs/anemo.git", rev = "e609f7697ed6169bf0760882a0b6c032a57e4f3b" } # core-types with json format for REST api -sui-sdk-types = { version = "0.0.1", features = ["hash", "serde", "schemars"] } +# sui-sdk-types = { version = "0.0.1", features = ["hash", "serde", "schemars"] } +sui-sdk-types = { git = "https://github.com/MystenLabs/sui-rust-sdk.git", rev = "31bd9da32a8057edc45da8f5e6a8f25b83919b93", features = ["hash", "serde", "schemars"] } ### Workspace Members ### anemo-benchmark = { path = "crates/anemo-benchmark" } diff --git a/crates/sui-e2e-tests/tests/rest.rs b/crates/sui-e2e-tests/tests/rest.rs index e1e35fe5588b0..629801f4f36c5 100644 --- a/crates/sui-e2e-tests/tests/rest.rs +++ b/crates/sui-e2e-tests/tests/rest.rs @@ -14,9 +14,9 @@ use sui_sdk_types::types::Command; use sui_sdk_types::types::TransactionExpiration; use sui_sdk_types::types::UnresolvedGasPayment; use sui_sdk_types::types::UnresolvedInputArgument; -use sui_sdk_types::types::UnresolvedObjectReference; use sui_sdk_types::types::UnresolvedProgrammableTransaction; use sui_sdk_types::types::UnresolvedTransaction; +use sui_sdk_types::types::UnresolvedValue; use sui_test_transaction_builder::make_transfer_sui_transaction; use sui_types::base_types::SuiAddress; use sui_types::effects::TransactionEffectsAPI; @@ -80,13 +80,13 @@ async fn resolve_transaction_simple_transfer() { let unresolved_transaction = UnresolvedTransaction { ptb: UnresolvedProgrammableTransaction { inputs: vec![ - UnresolvedInputArgument::ImmutableOrOwned(UnresolvedObjectReference { - object_id: obj_to_send.into(), - version: None, - digest: None, - }), - UnresolvedInputArgument::Pure { - value: bcs::to_bytes(&recipient).unwrap(), + UnresolvedInputArgument { + object_id: Some(obj_to_send.into()), + ..Default::default() + }, + UnresolvedInputArgument { + value: Some(UnresolvedValue::String(recipient.to_string())), + ..Default::default() }, ], commands: vec![Command::TransferObjects( @@ -147,13 +147,13 @@ async fn resolve_transaction_transfer_with_sponsor() { let unresolved_transaction = UnresolvedTransaction { ptb: UnresolvedProgrammableTransaction { inputs: vec![ - UnresolvedInputArgument::ImmutableOrOwned(UnresolvedObjectReference { - object_id: obj_to_send.into(), - version: None, - digest: None, - }), - UnresolvedInputArgument::Pure { - value: bcs::to_bytes(&recipient).unwrap(), + UnresolvedInputArgument { + object_id: Some(obj_to_send.into()), + ..Default::default() + }, + UnresolvedInputArgument { + value: Some(UnresolvedValue::String(recipient.to_string())), + ..Default::default() }, ], commands: vec![Command::TransferObjects( @@ -230,10 +230,9 @@ async fn resolve_transaction_borrowed_shared_object() { let unresolved_transaction = UnresolvedTransaction { ptb: UnresolvedProgrammableTransaction { - inputs: vec![UnresolvedInputArgument::Shared { - object_id: "0x6".parse().unwrap(), - initial_shared_version: None, - mutable: None, + inputs: vec![UnresolvedInputArgument { + object_id: Some("0x6".parse().unwrap()), + ..Default::default() }], commands: vec![Command::MoveCall(sui_sdk_types::types::MoveCall { package: "0x2".parse().unwrap(), @@ -299,18 +298,17 @@ async fn resolve_transaction_mutable_shared_object() { let unresolved_transaction = UnresolvedTransaction { ptb: UnresolvedProgrammableTransaction { inputs: vec![ - UnresolvedInputArgument::Shared { - object_id: "0x5".parse().unwrap(), - initial_shared_version: None, - mutable: None, + UnresolvedInputArgument { + object_id: Some("0x5".parse().unwrap()), + ..Default::default() + }, + UnresolvedInputArgument { + object_id: Some(obj_to_stake.into()), + ..Default::default() }, - UnresolvedInputArgument::ImmutableOrOwned(UnresolvedObjectReference { - object_id: obj_to_stake.into(), - version: None, - digest: None, - }), - UnresolvedInputArgument::Pure { - value: bcs::to_bytes(&validator_address).unwrap(), + UnresolvedInputArgument { + value: Some(UnresolvedValue::String(validator_address.to_string())), + ..Default::default() }, ], commands: vec![Command::MoveCall(sui_sdk_types::types::MoveCall { @@ -366,10 +364,9 @@ async fn resolve_transaction_insufficient_gas() { // Test the case where we don't have enough coins/gas for the required budget let unresolved_transaction = UnresolvedTransaction { ptb: UnresolvedProgrammableTransaction { - inputs: vec![UnresolvedInputArgument::Shared { - object_id: "0x6".parse().unwrap(), - initial_shared_version: None, - mutable: None, + inputs: vec![UnresolvedInputArgument { + object_id: Some("0x6".parse().unwrap()), + ..Default::default() }], commands: vec![Command::MoveCall(sui_sdk_types::types::MoveCall { package: "0x2".parse().unwrap(), @@ -402,3 +399,85 @@ fn assert_contains(haystack: &str, needle: &str) { panic!("{haystack:?} does not contain {needle:?}"); } } + +#[sim_test] +async fn resolve_transaction_with_raw_json() { + let test_cluster = TestClusterBuilder::new().build().await; + + let client = Client::new(test_cluster.rpc_url()); + let recipient = SuiAddress::random_for_testing_only(); + + let (sender, mut gas) = test_cluster.wallet.get_one_account().await.unwrap(); + gas.sort_by_key(|object_ref| object_ref.0); + let obj_to_send = gas.first().unwrap().0; + + let unresolved_transaction = serde_json::json!({ + "inputs": [ + { + "object_id": obj_to_send + }, + { + "value": 1 + }, + { + "value": recipient + } + ], + + "commands": [ + { + "command": "split_coins", + "coin": { "input": 0 }, + "amounts": [ + { + "input": 1, + }, + { + "input": 1, + } + ] + }, + { + "command": "transfer_objects", + "objects": [ + { "result": [0, 1] }, + { "result": [0, 0] } + ], + "address": { "input": 2 } + } + ], + + "sender": sender + }); + + let resolved = client + .inner() + .resolve_transaction_with_parameters( + &serde_json::from_value(unresolved_transaction).unwrap(), + &ResolveTransactionQueryParameters { + simulate: true, + ..Default::default() + }, + ) + .await + .unwrap() + .into_inner(); + + let signed_transaction = test_cluster + .wallet + .sign_transaction(&resolved.transaction.try_into().unwrap()); + let effects = client + .execute_transaction( + &ExecuteTransactionQueryParameters::default(), + &signed_transaction, + ) + .await + .unwrap() + .effects; + + assert!(effects.status().is_ok(), "{:?}", effects.status()); + assert_eq!( + resolved.simulation.unwrap().effects, + effects.try_into().unwrap() + ); +} diff --git a/crates/sui-rest-api/Cargo.toml b/crates/sui-rest-api/Cargo.toml index 01d163356eec1..3f6412fa11af6 100644 --- a/crates/sui-rest-api/Cargo.toml +++ b/crates/sui-rest-api/Cargo.toml @@ -33,6 +33,7 @@ sui-types.workspace = true mysten-network.workspace = true sui-protocol-config.workspace = true move-binary-format.workspace = true +move-core-types.workspace = true [dev-dependencies] diffy = "0.3" diff --git a/crates/sui-rest-api/openapi/openapi.json b/crates/sui-rest-api/openapi/openapi.json index 58a4d5841a7e9..1a41ebad1d62b 100644 --- a/crates/sui-rest-api/openapi/openapi.json +++ b/crates/sui-rest-api/openapi/openapi.json @@ -999,7 +999,15 @@ "style": "form" } ], - "requestBody": {}, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnresolvedTransaction" + } + } + } + }, "responses": { "200": { "description": "", @@ -5502,6 +5510,145 @@ } } }, + "UnresolvedGasPayment": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "budget": { + "description": "Radix-10 encoded 64-bit unsigned integer", + "type": "string", + "format": "u64" + }, + "objects": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnresolvedObjectReference" + } + }, + "owner": { + "$ref": "#/components/schemas/Address" + }, + "price": { + "description": "Radix-10 encoded 64-bit unsigned integer", + "type": "string", + "format": "u64" + } + } + }, + "UnresolvedInputArgument": { + "type": "object", + "properties": { + "digest": { + "$ref": "#/components/schemas/ObjectDigest" + }, + "kind": { + "$ref": "#/components/schemas/UnresolvedInputArgumentKind" + }, + "mutable": { + "type": "boolean" + }, + "object_id": { + "$ref": "#/components/schemas/ObjectId" + }, + "value": { + "$ref": "#/components/schemas/UnresolvedValue" + }, + "version": { + "description": "Either the `initial_shared_version` if object is a shared object, or the `version` if this is an owned object", + "type": "string", + "format": "u64" + } + } + }, + "UnresolvedInputArgumentKind": { + "type": "string", + "enum": [ + "pure", + "shared", + "receiving", + "immutable_or_owned", + "immutable", + "owned", + "literal" + ] + }, + "UnresolvedObjectReference": { + "type": "object", + "required": [ + "object_id" + ], + "properties": { + "digest": { + "$ref": "#/components/schemas/ObjectDigest" + }, + "object_id": { + "$ref": "#/components/schemas/ObjectId" + }, + "version": { + "description": "Radix-10 encoded 64-bit unsigned integer", + "type": "string", + "format": "u64" + } + } + }, + "UnresolvedTransaction": { + "type": "object", + "required": [ + "commands", + "expiration", + "inputs", + "sender" + ], + "properties": { + "commands": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Command" + } + }, + "expiration": { + "$ref": "#/components/schemas/TransactionExpiration" + }, + "gas_payment": { + "$ref": "#/components/schemas/UnresolvedGasPayment" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnresolvedInputArgument" + } + }, + "sender": { + "$ref": "#/components/schemas/Address" + } + } + }, + "UnresolvedValue": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnresolvedValue" + } + } + ] + }, "UpgradeInfo": { "description": "Upgraded package info for the linkage table", "type": "object", @@ -5629,7 +5776,7 @@ ] }, "signatures": { - "description": "The plain signature encoded with signature scheme.", + "description": "The plain signature encoded with signature scheme.\n\nThe signatures must be in the same order as they are listed in the committee.", "type": "array", "items": { "$ref": "#/components/schemas/MultisigMemberSignature" diff --git a/crates/sui-rest-api/src/error.rs b/crates/sui-rest-api/src/error.rs index bb43841817411..f33cf4332d8af 100644 --- a/crates/sui-rest-api/src/error.rs +++ b/crates/sui-rest-api/src/error.rs @@ -5,6 +5,7 @@ use axum::http::StatusCode; pub type Result = std::result::Result; +#[derive(Debug)] pub struct RestError { status: StatusCode, message: Option, diff --git a/crates/sui-rest-api/src/transactions/resolve/literal.rs b/crates/sui-rest-api/src/transactions/resolve/literal.rs new file mode 100644 index 0000000000000..6b25013faae3a --- /dev/null +++ b/crates/sui-rest-api/src/transactions/resolve/literal.rs @@ -0,0 +1,787 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; + +use super::NormalizedPackage; +use crate::RestError; +use crate::Result; +use move_binary_format::normalized::Type; +use sui_sdk_types::types::Command; +use sui_sdk_types::types::ObjectId; +use sui_sdk_types::types::UnresolvedValue; +use sui_types::base_types::ObjectID; +use sui_types::base_types::STD_ASCII_MODULE_NAME; +use sui_types::base_types::STD_ASCII_STRUCT_NAME; +use sui_types::base_types::STD_OPTION_MODULE_NAME; +use sui_types::base_types::STD_OPTION_STRUCT_NAME; +use sui_types::base_types::STD_UTF8_MODULE_NAME; +use sui_types::base_types::STD_UTF8_STRUCT_NAME; +use sui_types::MOVE_STDLIB_ADDRESS; + +pub(super) fn resolve_literal( + called_packages: &HashMap, + commands: &[Command], + arg_idx: usize, + value: UnresolvedValue, +) -> Result> { + let literal_type = determine_literal_type(called_packages, commands, arg_idx)?; + + let mut buf = Vec::new(); + + resolve_literal_to_type(&mut buf, &literal_type, &value)?; + + Ok(buf) +} + +fn determine_literal_type( + called_packages: &HashMap, + commands: &[Command], + arg_idx: usize, +) -> Result { + fn set_type(maybe_type: &mut Option, ty: Type) -> Result<()> { + match maybe_type { + Some(literal_type) if literal_type == &ty => {} + Some(_) => { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + "unable to resolve literal as it is used as multiple different types across commands", + )) + } + None => { + *maybe_type = Some(ty); + } + } + + Ok(()) + } + let mut literal_type = None; + + for (command, idx) in super::find_arg_uses(arg_idx, commands) { + match (command, idx) { + (Command::MoveCall(move_call), Some(idx)) => { + let arg_type = super::arg_type_of_move_call_input(called_packages, move_call, idx)?; + set_type(&mut literal_type, arg_type.to_owned())?; + } + (Command::TransferObjects(_), None) => { + set_type(&mut literal_type, Type::Address)?; + } + + (Command::SplitCoins(_), Some(_)) => { + set_type(&mut literal_type, Type::U64)?; + } + (Command::MakeMoveVector(make_move_vector), Some(_)) => { + if let Some(ty) = &make_move_vector.type_ { + let ty = + sui_types::sui_sdk_types_conversions::type_tag_sdk_to_core(ty.clone())?; + set_type(&mut literal_type, ty.into())?; + } else { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + "unable to resolve literal as an unknown type", + )); + } + } + + // Invalid uses of Literal Arguments + + // Pure arg can't be used as an object to transfer + (Command::TransferObjects(_), Some(_)) + | (Command::Upgrade(_), _) + | (Command::MergeCoins(_), _) + | (Command::SplitCoins(_), None) => { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + "invalid use of literal", + )); + } + + // bug in find_arg_uses + (Command::MakeMoveVector(_), None) + | (Command::Publish(_), _) + | (Command::MoveCall(_), None) => { + return Err(RestError::new( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + "error determining type of literal", + )); + } + } + } + + literal_type.ok_or_else(|| { + RestError::new( + axum::http::StatusCode::BAD_REQUEST, + "unable to determine type of literal", + ) + }) +} + +fn resolve_literal_to_type(buf: &mut Vec, type_: &Type, value: &UnresolvedValue) -> Result<()> { + match type_ { + Type::Bool => resolve_as_bool(buf, value), + Type::U8 => resolve_as_number::(buf, value), + Type::U16 => resolve_as_number::(buf, value), + Type::U32 => resolve_as_number::(buf, value), + Type::U64 => resolve_as_number::(buf, value), + Type::U128 => resolve_as_number::(buf, value), + Type::U256 => resolve_as_number::(buf, value), + Type::Address => resolve_as_address(buf, value), + + // 0x1::ascii::String and 0x1::string::String + Type::Struct { + address, + module, + name, + type_arguments, + } if address == &MOVE_STDLIB_ADDRESS + // 0x1::ascii::String + && ((module.as_ref() == STD_ASCII_MODULE_NAME + && name.as_ref() == STD_ASCII_STRUCT_NAME) + // 0x1::string::String + || (module.as_ref() == STD_UTF8_MODULE_NAME + && name.as_ref() == STD_UTF8_STRUCT_NAME)) + && type_arguments.is_empty() => + { + resolve_as_string(buf, value) + } + + // Option + Type::Struct { + address, + module, + name, + type_arguments, + } if address == &MOVE_STDLIB_ADDRESS + && module.as_ref() == STD_OPTION_MODULE_NAME + && name.as_ref() == STD_OPTION_STRUCT_NAME + && type_arguments.len() == 1 => + { + let ty = type_arguments + .first() + .expect("length of type_arguments is 1"); + + resolve_as_option(buf, ty, value) + } + + // Vec + Type::Vector(ty) => resolve_as_vector(buf, ty, value), + + Type::Signer + | Type::Struct { .. } + | Type::TypeParameter(_) + | Type::Reference(_) + | Type::MutableReference(_) => Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!("literal cannot be resolved into type {type_}"), + )), + } +} + +fn resolve_as_bool(buf: &mut Vec, value: &UnresolvedValue) -> Result<()> { + let b: bool = match value { + UnresolvedValue::Bool(b) => *b, + UnresolvedValue::String(s) => s.parse().map_err(|e| { + RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!("literal cannot be resolved as bool: {e}"), + ) + })?, + UnresolvedValue::Null | UnresolvedValue::Number(_) | UnresolvedValue::Array(_) => { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + "literal cannot be resolved into type bool", + )) + } + }; + + bcs::serialize_into(buf, &b)?; + + Ok(()) +} + +fn resolve_as_number(buf: &mut Vec, value: &UnresolvedValue) -> Result<()> +where + T: std::str::FromStr + TryFrom + serde::Serialize, + ::Err: std::fmt::Display, + >::Error: std::fmt::Display, +{ + let n: T = match value { + UnresolvedValue::Number(n) => T::try_from(*n).map_err(|e| { + RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!( + "literal cannot be resolved as {}: {e}", + std::any::type_name::() + ), + ) + })?, + + UnresolvedValue::String(s) => s.parse().map_err(|e| { + RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!( + "literal cannot be resolved as {}: {e}", + std::any::type_name::() + ), + ) + })?, + + UnresolvedValue::Null | UnresolvedValue::Bool(_) | UnresolvedValue::Array(_) => { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!( + "literal cannot be resolved into type {}", + std::any::type_name::() + ), + )) + } + }; + + bcs::serialize_into(buf, &n)?; + + Ok(()) +} + +fn resolve_as_address(buf: &mut Vec, value: &UnresolvedValue) -> Result<()> { + let address = match value { + // parse as ObjectID to handle the case where 0x is present or missing + UnresolvedValue::String(s) => s.parse::().map_err(|e| { + RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!("literal cannot be resolved as bool: {e}"), + ) + })?, + UnresolvedValue::Null + | UnresolvedValue::Bool(_) + | UnresolvedValue::Number(_) + | UnresolvedValue::Array(_) => { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + "literal cannot be resolved into type address", + )) + } + }; + + bcs::serialize_into(buf, &address)?; + + Ok(()) +} + +fn resolve_as_string(buf: &mut Vec, value: &UnresolvedValue) -> Result<()> { + match value { + UnresolvedValue::String(s) => { + bcs::serialize_into(buf, s)?; + } + UnresolvedValue::Bool(_) + | UnresolvedValue::Null + | UnresolvedValue::Number(_) + | UnresolvedValue::Array(_) => { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + "literal cannot be resolved into string", + )) + } + }; + + Ok(()) +} + +fn resolve_as_option(buf: &mut Vec, type_: &Type, value: &UnresolvedValue) -> Result<()> { + match value { + UnresolvedValue::Null => { + buf.push(0); + } + UnresolvedValue::Bool(_) + | UnresolvedValue::Number(_) + | UnresolvedValue::String(_) + | UnresolvedValue::Array(_) => { + buf.push(1); + resolve_literal_to_type(buf, type_, value)?; + } + } + + Ok(()) +} + +fn resolve_as_vector(buf: &mut Vec, type_: &Type, value: &UnresolvedValue) -> Result<()> { + fn write_u32_as_uleb128(buf: &mut Vec, mut value: u32) { + while value >= 0x80 { + // Write 7 (lowest) bits of data and set the 8th bit to 1. + let byte = (value & 0x7f) as u8; + buf.push(byte | 0x80); + value >>= 7; + } + // Write the remaining bits of data and set the highest bit to 0. + buf.push(value as u8); + } + + match value { + UnresolvedValue::Array(array) => { + write_u32_as_uleb128(buf, array.len() as u32); + for value in array { + resolve_literal_to_type(buf, type_, value)?; + } + } + UnresolvedValue::Bool(_) + | UnresolvedValue::Number(_) + | UnresolvedValue::String(_) + | UnresolvedValue::Null => { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!("literal cannot be resolved into type Vector<{type_}>"), + )); + } + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use move_binary_format::normalized::Type; + use move_core_types::{account_address::AccountAddress, u256::U256}; + + fn test_resolve_literal(ty: Type, value: UnresolvedValue, expected: Option>) { + let mut buf = Vec::new(); + match (resolve_literal_to_type(&mut buf, &ty, &value), expected) { + (Ok(_), None) => { + panic!("resolving literal succeeded but failure was expected: {ty} {value:?}") + } + (Ok(()), Some(expected)) => assert_eq!(buf, expected), + (Err(_), None) => {} + (Err(_), Some(_)) => { + panic!("failed to resolve literal {value:?} as {ty}"); + } + } + } + + #[test] + fn resolve_bool() { + let test_cases = [ + (Type::Bool, UnresolvedValue::Bool(true), Some(vec![1])), + (Type::Bool, UnresolvedValue::Bool(false), Some(vec![0])), + ( + Type::Bool, + UnresolvedValue::String("true".into()), + Some(vec![1]), + ), + ( + Type::Bool, + UnresolvedValue::String("false".into()), + Some(vec![0]), + ), + (Type::Bool, UnresolvedValue::Null, None), + (Type::Bool, UnresolvedValue::Number(0), None), + (Type::Bool, UnresolvedValue::Array(vec![]), None), + (Type::Bool, UnresolvedValue::String("foo".into()), None), + ]; + + for (ty, value, expected) in test_cases { + test_resolve_literal(ty, value, expected); + } + } + + #[test] + fn resolve_number() { + let test_cases = [ + // U8 Successful cases + ( + Type::U8, + UnresolvedValue::Number(u8::MAX.into()), + Some(bcs::to_bytes(&u8::MAX).unwrap()), + ), + ( + Type::U8, + UnresolvedValue::Number(u8::MIN.into()), + Some(bcs::to_bytes(&u8::MIN).unwrap()), + ), + ( + Type::U8, + UnresolvedValue::String(u8::MAX.to_string()), + Some(bcs::to_bytes(&u8::MAX).unwrap()), + ), + ( + Type::U8, + UnresolvedValue::String(u8::MIN.to_string()), + Some(bcs::to_bytes(&u8::MIN).unwrap()), + ), + // U8 failure cases + (Type::U8, UnresolvedValue::Bool(true), None), + (Type::U8, UnresolvedValue::Array(vec![]), None), + (Type::U8, UnresolvedValue::Null, None), + (Type::U8, UnresolvedValue::String("foo".into()), None), + ( + Type::U8, + UnresolvedValue::String(u64::MAX.to_string()), + None, + ), + (Type::U8, UnresolvedValue::Number(u64::MAX), None), + // U16 Successful cases + ( + Type::U16, + UnresolvedValue::Number(u16::MAX.into()), + Some(bcs::to_bytes(&u16::MAX).unwrap()), + ), + ( + Type::U16, + UnresolvedValue::Number(u16::MIN.into()), + Some(bcs::to_bytes(&u16::MIN).unwrap()), + ), + ( + Type::U16, + UnresolvedValue::String(u16::MAX.to_string()), + Some(bcs::to_bytes(&u16::MAX).unwrap()), + ), + ( + Type::U16, + UnresolvedValue::String(u16::MIN.to_string()), + Some(bcs::to_bytes(&u16::MIN).unwrap()), + ), + // U16 failure cases + (Type::U16, UnresolvedValue::Bool(true), None), + (Type::U16, UnresolvedValue::Array(vec![]), None), + (Type::U16, UnresolvedValue::Null, None), + (Type::U16, UnresolvedValue::String("foo".into()), None), + ( + Type::U16, + UnresolvedValue::String(u64::MAX.to_string()), + None, + ), + (Type::U16, UnresolvedValue::Number(u64::MAX), None), + // U32 Successful cases + ( + Type::U32, + UnresolvedValue::Number(u32::MAX.into()), + Some(bcs::to_bytes(&u32::MAX).unwrap()), + ), + ( + Type::U32, + UnresolvedValue::Number(u32::MIN.into()), + Some(bcs::to_bytes(&u32::MIN).unwrap()), + ), + ( + Type::U32, + UnresolvedValue::String(u32::MAX.to_string()), + Some(bcs::to_bytes(&u32::MAX).unwrap()), + ), + ( + Type::U32, + UnresolvedValue::String(u32::MIN.to_string()), + Some(bcs::to_bytes(&u32::MIN).unwrap()), + ), + // U32 failure cases + (Type::U32, UnresolvedValue::Bool(true), None), + (Type::U32, UnresolvedValue::Array(vec![]), None), + (Type::U32, UnresolvedValue::Null, None), + (Type::U32, UnresolvedValue::String("foo".into()), None), + ( + Type::U32, + UnresolvedValue::String(u64::MAX.to_string()), + None, + ), + (Type::U32, UnresolvedValue::Number(u64::MAX), None), + // U64 Successful cases + ( + Type::U64, + UnresolvedValue::Number(u64::MAX), + Some(bcs::to_bytes(&u64::MAX).unwrap()), + ), + ( + Type::U64, + UnresolvedValue::Number(u64::MIN), + Some(bcs::to_bytes(&u64::MIN).unwrap()), + ), + ( + Type::U64, + UnresolvedValue::String(u64::MAX.to_string()), + Some(bcs::to_bytes(&u64::MAX).unwrap()), + ), + ( + Type::U64, + UnresolvedValue::String(u64::MIN.to_string()), + Some(bcs::to_bytes(&u64::MIN).unwrap()), + ), + // U64 failure cases + (Type::U64, UnresolvedValue::Bool(true), None), + (Type::U64, UnresolvedValue::Array(vec![]), None), + (Type::U64, UnresolvedValue::Null, None), + (Type::U64, UnresolvedValue::String("foo".into()), None), + ( + Type::U64, + UnresolvedValue::String(u128::MAX.to_string()), + None, + ), + // U128 Successful cases + ( + Type::U128, + UnresolvedValue::Number(u64::MAX), + Some(bcs::to_bytes(&u128::from(u64::MAX)).unwrap()), + ), + ( + Type::U128, + UnresolvedValue::Number(u64::MIN), + Some(bcs::to_bytes(&u128::MIN).unwrap()), + ), + ( + Type::U128, + UnresolvedValue::String(u128::MAX.to_string()), + Some(bcs::to_bytes(&u128::MAX).unwrap()), + ), + ( + Type::U128, + UnresolvedValue::String(u128::MIN.to_string()), + Some(bcs::to_bytes(&u128::MIN).unwrap()), + ), + // U128 failure cases + (Type::U128, UnresolvedValue::Bool(true), None), + (Type::U128, UnresolvedValue::Array(vec![]), None), + (Type::U128, UnresolvedValue::Null, None), + (Type::U128, UnresolvedValue::String("foo".into()), None), + ( + Type::U128, + UnresolvedValue::String(U256::max_value().to_string()), + None, + ), + // U256 Successful cases + ( + Type::U256, + UnresolvedValue::Number(u64::MAX), + Some(bcs::to_bytes(&U256::from(u64::MAX)).unwrap()), + ), + ( + Type::U256, + UnresolvedValue::Number(u64::MIN), + Some(bcs::to_bytes(&U256::zero()).unwrap()), + ), + ( + Type::U256, + UnresolvedValue::String(U256::max_value().to_string()), + Some(bcs::to_bytes(&U256::max_value()).unwrap()), + ), + ( + Type::U256, + UnresolvedValue::String(U256::zero().to_string()), + Some(bcs::to_bytes(&U256::zero()).unwrap()), + ), + // U256 failure cases + (Type::U256, UnresolvedValue::Bool(true), None), + (Type::U256, UnresolvedValue::Array(vec![]), None), + (Type::U256, UnresolvedValue::Null, None), + (Type::U256, UnresolvedValue::String("foo".into()), None), + ]; + + for (ty, value, expected) in test_cases { + test_resolve_literal(ty, value, expected); + } + } + + #[test] + fn resolve_address() { + let test_cases = [ + // Address Successful cases + ( + Type::Address, + // with 0x prefix + UnresolvedValue::String(AccountAddress::TWO.to_canonical_string(true)), + Some(bcs::to_bytes(&AccountAddress::TWO).unwrap()), + ), + ( + Type::Address, + // without 0x prefix + UnresolvedValue::String(AccountAddress::TWO.to_canonical_string(false)), + Some(bcs::to_bytes(&AccountAddress::TWO).unwrap()), + ), + ( + Type::Address, + // with 0x prefix and trimmed 0s + UnresolvedValue::String(AccountAddress::TWO.to_hex_literal()), + Some(bcs::to_bytes(&AccountAddress::TWO).unwrap()), + ), + // Address failure cases + (Type::Address, UnresolvedValue::Bool(true), None), + (Type::Address, UnresolvedValue::Array(vec![]), None), + (Type::Address, UnresolvedValue::Null, None), + (Type::Address, UnresolvedValue::String("foo".into()), None), + (Type::Address, UnresolvedValue::Number(0), None), + ( + Type::Address, + // without 0x prefix and with trimmed 0s + UnresolvedValue::String(AccountAddress::TWO.short_str_lossless()), + None, + ), + ]; + + for (ty, value, expected) in test_cases { + test_resolve_literal(ty, value, expected); + } + } + + #[test] + fn resolve_string() { + fn utf8() -> Type { + Type::Struct { + address: MOVE_STDLIB_ADDRESS, + module: STD_UTF8_MODULE_NAME.to_owned(), + name: STD_UTF8_STRUCT_NAME.to_owned(), + type_arguments: vec![], + } + } + fn ascii() -> Type { + Type::Struct { + address: MOVE_STDLIB_ADDRESS, + module: STD_ASCII_MODULE_NAME.to_owned(), + name: STD_ASCII_STRUCT_NAME.to_owned(), + type_arguments: vec![], + } + } + + let test_cases = [ + // string Successful cases + ( + utf8(), + UnresolvedValue::String("foo".into()), + Some(bcs::to_bytes(&"foo").unwrap()), + ), + ( + ascii(), + UnresolvedValue::String("foo".into()), + Some(bcs::to_bytes(&"foo").unwrap()), + ), + ( + utf8(), + UnresolvedValue::String("".into()), + Some(bcs::to_bytes(&"").unwrap()), + ), + ( + ascii(), + UnresolvedValue::String("".into()), + Some(bcs::to_bytes(&"").unwrap()), + ), + // String failure cases + (utf8(), UnresolvedValue::Bool(true), None), + (utf8(), UnresolvedValue::Array(vec![]), None), + (utf8(), UnresolvedValue::Null, None), + (utf8(), UnresolvedValue::Number(0), None), + (ascii(), UnresolvedValue::Bool(true), None), + (ascii(), UnresolvedValue::Array(vec![]), None), + (ascii(), UnresolvedValue::Null, None), + (ascii(), UnresolvedValue::Number(0), None), + ]; + + for (ty, value, expected) in test_cases { + test_resolve_literal(ty, value, expected); + } + } + + #[test] + fn resolve_option() { + fn option_type(t: Type) -> Type { + Type::Struct { + address: MOVE_STDLIB_ADDRESS, + module: STD_OPTION_MODULE_NAME.to_owned(), + name: STD_OPTION_STRUCT_NAME.to_owned(), + type_arguments: vec![t], + } + } + + let test_cases = [ + // Option Successful cases + ( + option_type(Type::Address), + UnresolvedValue::String(AccountAddress::TWO.to_canonical_string(true)), + Some(bcs::to_bytes(&Some(AccountAddress::TWO)).unwrap()), + ), + ( + option_type(Type::Address), + UnresolvedValue::Null, + Some(vec![0]), + ), + ( + option_type(Type::U64), + UnresolvedValue::Number(u64::MIN), + Some(bcs::to_bytes(&Some(u64::MIN)).unwrap()), + ), + ( + option_type(Type::U64), + UnresolvedValue::String(u64::MAX.to_string()), + Some(bcs::to_bytes(&Some(u64::MAX)).unwrap()), + ), + ( + option_type(Type::Bool), + UnresolvedValue::Bool(true), + Some(bcs::to_bytes(&Some(true)).unwrap()), + ), + ( + option_type(Type::Bool), + UnresolvedValue::Null, + Some(vec![0]), + ), + // Option failure cases + (option_type(Type::Bool), UnresolvedValue::Number(0), None), + ]; + + for (ty, value, expected) in test_cases { + test_resolve_literal(ty, value, expected); + } + } + + #[test] + fn resolve_vector() { + fn vector_type(t: Type) -> Type { + Type::Vector(Box::new(t)) + } + + let test_cases = [ + // Vector Successful cases + ( + vector_type(Type::Address), + UnresolvedValue::Array(vec![ + UnresolvedValue::String(AccountAddress::TWO.to_canonical_string(true)), + UnresolvedValue::String(AccountAddress::ONE.to_canonical_string(true)), + ]), + Some(bcs::to_bytes(&vec![AccountAddress::TWO, AccountAddress::ONE]).unwrap()), + ), + ( + vector_type(Type::U8), + UnresolvedValue::Array(vec![UnresolvedValue::Number(9)]), + Some(vec![1, 9]), + ), + ( + vector_type(Type::U8), + UnresolvedValue::Array(vec![]), + Some(vec![0]), + ), + ( + vector_type(vector_type(Type::U8)), + UnresolvedValue::Array(vec![UnresolvedValue::Array(vec![ + UnresolvedValue::Number(9), + ])]), + Some(bcs::to_bytes(&vec![vec![9u8]]).unwrap()), + ), + ( + vector_type(Type::Bool), + // verify we handle uleb128 encoding of length properly + UnresolvedValue::Array(vec![UnresolvedValue::Bool(true); 256]), + Some(bcs::to_bytes(&vec![true; 256]).unwrap()), + ), + // Vector failure cases + (vector_type(Type::U64), UnresolvedValue::Bool(true), None), + (vector_type(Type::U64), UnresolvedValue::Number(0), None), + (vector_type(Type::U64), UnresolvedValue::Null, None), + (vector_type(Type::U64), UnresolvedValue::Number(0), None), + ( + vector_type(Type::Address), + UnresolvedValue::Array(vec![ + UnresolvedValue::String(AccountAddress::TWO.to_canonical_string(true)), + UnresolvedValue::Number(5), + ]), + None, + ), + ]; + + for (ty, value, expected) in test_cases { + test_resolve_literal(ty, value, expected); + } + } +} diff --git a/crates/sui-rest-api/src/transactions/resolve.rs b/crates/sui-rest-api/src/transactions/resolve/mod.rs similarity index 57% rename from crates/sui-rest-api/src/transactions/resolve.rs rename to crates/sui-rest-api/src/transactions/resolve/mod.rs index b1e69cbb5ee43..dcb905d8f9104 100644 --- a/crates/sui-rest-api/src/transactions/resolve.rs +++ b/crates/sui-rest-api/src/transactions/resolve/mod.rs @@ -33,6 +33,7 @@ use sui_sdk_types::types::UnresolvedInputArgument; use sui_sdk_types::types::UnresolvedObjectReference; use sui_sdk_types::types::UnresolvedProgrammableTransaction; use sui_sdk_types::types::UnresolvedTransaction; +use sui_sdk_types::types::UnresolvedValue; use sui_types::base_types::ObjectID; use sui_types::base_types::ObjectRef; use sui_types::base_types::SuiAddress; @@ -48,9 +49,8 @@ use sui_types::transaction::TransactionData; use sui_types::transaction::TransactionDataAPI; use tap::Pipe; -// TODO -// - Updating the UnresolvedTransaction format to provide less information about inputs -// - handle basic type inference and BCS serialization of pure args +mod literal; + pub struct ResolveTransaction; impl ApiEndpoint for ResolveTransaction { @@ -72,7 +72,7 @@ impl ApiEndpoint for ResolveTransaction { .query_parameters::(generator) .request_body( RequestBodyBuilder::new() - // .json_content::(generator) + .json_content::(generator) .build(), ) .response( @@ -318,6 +318,22 @@ pub struct ResolveTransactionResponse { fn resolve_object_reference( reader: &StateReader, unresolved_object_reference: UnresolvedObjectReference, +) -> Result { + let object_id = unresolved_object_reference.object_id; + let object = reader + .inner() + .get_object(&object_id.into())? + .ok_or_else(|| ObjectNotFoundError::new(object_id))?; + resolve_object_reference_with_object(&object, unresolved_object_reference) +} + +// Resolve an object reference against the provided object. +// +// Callers should check that the object_id matches the id in the `unresolved_object_reference` +// before calling. +fn resolve_object_reference_with_object( + object: &sui_types::object::Object, + unresolved_object_reference: UnresolvedObjectReference, ) -> Result { let UnresolvedObjectReference { object_id, @@ -325,20 +341,34 @@ fn resolve_object_reference( digest, } = unresolved_object_reference; - let id = object_id.into(); - let (v, d) = if let Some(version) = version { - let object = reader - .inner() - .get_object_by_key(&id, version.into())? - .ok_or_else(|| ObjectNotFoundError::new_with_version(object_id, version))?; - (object.version(), object.digest()) - } else { - let object = reader - .inner() - .get_object(&id)? - .ok_or_else(|| ObjectNotFoundError::new(object_id))?; - (object.version(), object.digest()) - }; + match object.owner() { + sui_types::object::Owner::AddressOwner(_) | sui_types::object::Owner::Immutable => {} + _ => { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!("object {object_id} is not Immutable or AddressOwned"), + )) + } + } + + let id = object.id(); + let v = object.version(); + let d = object.digest(); + + // This really should be an assert + if object_id.inner() != &id.into_bytes() { + return Err(RestError::new( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + "provided object and object_id should match", + )); + } + + if version.is_some_and(|version| version != v.value()) { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!("provided version doesn't match, provided: {version:?} actual: {v}"), + )); + } if digest.is_some_and(|digest| digest.inner() != d.inner()) { return Err(RestError::new( @@ -388,116 +418,309 @@ fn resolve_arg( arg: UnresolvedInputArgument, arg_idx: usize, ) -> Result { - match arg { - UnresolvedInputArgument::Pure { value } => CallArg::Pure(value), - UnresolvedInputArgument::ImmutableOrOwned(obj_ref) => CallArg::Object( - ObjectArg::ImmOrOwnedObject(resolve_object_reference(reader, obj_ref)?), + use fastcrypto::encoding::Base64; + use fastcrypto::encoding::Encoding; + use sui_sdk_types::types::UnresolvedInputArgumentKind::*; + + let UnresolvedInputArgument { + kind, + value, + object_id, + version, + digest, + mutable, + } = arg; + + match (kind, value, object_id, version, digest, mutable) { + // pre serialized BCS input encoded as a base64 string + (Some(Pure), Some(UnresolvedValue::String(v)), None, None, None, None) => { + let value = Base64::decode(&v).map_err(|e| { + RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!("argument is an invalid pure arguement: {e}"), + ) + })?; + CallArg::Pure(value) + } + // pre serialized BCS input encoded as a a JSON array of u8s + (Some(Pure), Some(array @ UnresolvedValue::Array(_)), None, None, None, None) => { + let value = serde_json::from_value(serde_json::Value::from(array)).map_err(|e| { + RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!("argument is an invalid pure arguement: {e}"), + ) + })?; + CallArg::Pure(value) + } + + // Literal, unresolved pure argument + (Some(Literal), Some(value), None, None, None, None) + | (None, Some(value), None, None, None, None) => CallArg::Pure(literal::resolve_literal( + called_packages, + commands, + arg_idx, + value, + )?), + + // Immutable or owned + ( + Some(ImmutableOrOwned | Immutable | Owned), + None, + Some(object_id), + version, + digest, + None, + ) => CallArg::Object(ObjectArg::ImmOrOwnedObject(resolve_object_reference( + reader, + UnresolvedObjectReference { + object_id, + version, + digest, + }, + )?)), + + // Shared object + (Some(Shared), None, Some(object_id), _version, None, _mutable) => CallArg::Object( + resolve_shared_input(reader, called_packages, commands, arg_idx, object_id)?, ), - UnresolvedInputArgument::Shared { + + // Receiving + (Some(Receiving), None, Some(object_id), version, digest, None) => { + CallArg::Object(ObjectArg::Receiving(resolve_object_reference( + reader, + UnresolvedObjectReference { + object_id, + version, + digest, + }, + )?)) + } + + // Object, could be Immutable, Owned, Shared, or Receiving + (None, None, Some(object_id), version, digest, mutable) => CallArg::Object(resolve_object( + reader, + called_packages, + commands, + arg_idx, object_id, - initial_shared_version: _, - mutable: _, - } => { - let id = object_id.into(); - let object = reader - .inner() - .get_object(&id)? - .ok_or_else(|| ObjectNotFoundError::new(object_id))?; - - let initial_shared_version = if let sui_types::object::Owner::Shared { - initial_shared_version, - } = object.owner() - { - *initial_shared_version + version, + digest, + mutable, + )?), + + _ => { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + "invalid unresolved input argument", + )) + } + } + .pipe(Ok) +} + +fn resolve_object( + reader: &StateReader, + called_packages: &HashMap, + commands: &[Command], + arg_idx: usize, + object_id: ObjectId, + version: Option, + digest: Option, + _mutable: Option, +) -> Result { + let id = object_id.into(); + let object = reader + .inner() + .get_object(&id)? + .ok_or_else(|| ObjectNotFoundError::new(object_id))?; + + match object.owner() { + sui_types::object::Owner::Immutable => resolve_object_reference_with_object( + &object, + UnresolvedObjectReference { + object_id, + version, + digest, + }, + ) + .map(ObjectArg::ImmOrOwnedObject), + + sui_types::object::Owner::AddressOwner(_) => { + let object_ref = resolve_object_reference_with_object( + &object, + UnresolvedObjectReference { + object_id, + version, + digest, + }, + )?; + + if is_input_argument_receiving(called_packages, commands, arg_idx)? { + ObjectArg::Receiving(object_ref) } else { - return Err(RestError::new( - axum::http::StatusCode::BAD_REQUEST, - format!("object {object_id} is not a shared object"), - )); - }; - - let mut mutable = false; - - for (command, idx) in find_arg_uses(arg_idx, commands) { - match (command, idx) { - (Command::MoveCall(move_call), Some(idx)) => { - let function = called_packages - // Find the package - .get(&move_call.package) - // Find the module - .and_then(|package| { - package.normalized_modules.get(move_call.module.as_str()) - }) - // Find the function - .and_then(|module| module.functions.get(move_call.function.as_str())) - .ok_or_else(|| { - RestError::new( - axum::http::StatusCode::BAD_REQUEST, - format!( - "unable to find function {package}::{module}::{function}", - package = move_call.package, - module = move_call.module, - function = move_call.function - ), - ) - })?; - - let arg_type = function.parameters.get(idx).ok_or_else(|| { - RestError::new( - axum::http::StatusCode::BAD_REQUEST, - "invalid input parameter", - ) - })?; - - if matches!( - arg_type, - move_binary_format::normalized::Type::MutableReference(_) - | move_binary_format::normalized::Type::Struct { .. } - ) { - mutable = true; - } - } - - ( - Command::SplitCoins(_) - | Command::MergeCoins(_) - | Command::MakeMoveVector(_), - _, - ) => { - mutable = true; - } - - _ => {} - } + ObjectArg::ImmOrOwnedObject(object_ref) + } + .pipe(Ok) + } + sui_types::object::Owner::Shared { .. } => { + resolve_shared_input_with_object(called_packages, commands, arg_idx, object) + } + sui_types::object::Owner::ObjectOwner(_) => Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!("object {object_id} is object owned and cannot be used as an input"), + )), + } +} - // Early break out of the loop if we've already determined that the shared object - // is needed to be mutable - if mutable { - break; +fn resolve_shared_input( + reader: &StateReader, + called_packages: &HashMap, + commands: &[Command], + arg_idx: usize, + object_id: ObjectId, +) -> Result { + let id = object_id.into(); + let object = reader + .inner() + .get_object(&id)? + .ok_or_else(|| ObjectNotFoundError::new(object_id))?; + resolve_shared_input_with_object(called_packages, commands, arg_idx, object) +} + +// Checks if the provided input argument is used as a recieving object +fn is_input_argument_receiving( + called_packages: &HashMap, + commands: &[Command], + arg_idx: usize, +) -> Result { + let (receiving_package, receiving_module, receiving_struct) = + sui_types::transfer::RESOLVED_RECEIVING_STRUCT; + + let mut receiving = false; + for (command, idx) in find_arg_uses(arg_idx, commands) { + if let (Command::MoveCall(move_call), Some(idx)) = (command, idx) { + let arg_type = arg_type_of_move_call_input(called_packages, move_call, idx)?; + + if let move_binary_format::normalized::Type::Struct { + address, + module, + name, + .. + } = arg_type + { + if receiving_package == address + && receiving_module == module.as_ref() + && receiving_struct == name.as_ref() + { + receiving = true; } } + } - CallArg::Object(ObjectArg::SharedObject { - id, - initial_shared_version, - mutable, - }) + //XXX do we want to ensure its only used once as receiving? + if receiving { + break; } - UnresolvedInputArgument::Receiving(obj_ref) => CallArg::Object(ObjectArg::Receiving( - resolve_object_reference(reader, obj_ref)?, - )), } - .pipe(Ok) + + Ok(receiving) +} + +// TODO still need to handle the case where a function parameter is a generic parameter and the +// real type needs to be lookedup from the provided type args in the MoveCall itself +fn arg_type_of_move_call_input<'a>( + called_packages: &'a HashMap, + move_call: &sui_sdk_types::types::MoveCall, + idx: usize, +) -> Result<&'a move_binary_format::normalized::Type> { + let function = called_packages + // Find the package + .get(&move_call.package) + // Find the module + .and_then(|package| package.normalized_modules.get(move_call.module.as_str())) + // Find the function + .and_then(|module| module.functions.get(move_call.function.as_str())) + .ok_or_else(|| { + RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!( + "unable to find function {package}::{module}::{function}", + package = move_call.package, + module = move_call.module, + function = move_call.function + ), + ) + })?; + function.parameters.get(idx).ok_or_else(|| { + RestError::new( + axum::http::StatusCode::BAD_REQUEST, + "invalid input parameter", + ) + }) +} + +fn resolve_shared_input_with_object( + called_packages: &HashMap, + commands: &[Command], + arg_idx: usize, + object: sui_types::object::Object, +) -> Result { + let object_id = object.id(); + let initial_shared_version = if let sui_types::object::Owner::Shared { + initial_shared_version, + } = object.owner() + { + *initial_shared_version + } else { + return Err(RestError::new( + axum::http::StatusCode::BAD_REQUEST, + format!("object {object_id} is not a shared object"), + )); + }; + let mut mutable = false; + for (command, idx) in find_arg_uses(arg_idx, commands) { + match (command, idx) { + (Command::MoveCall(move_call), Some(idx)) => { + let arg_type = arg_type_of_move_call_input(called_packages, move_call, idx)?; + if matches!( + arg_type, + move_binary_format::normalized::Type::MutableReference(_) + | move_binary_format::normalized::Type::Struct { .. } + ) { + mutable = true; + } + } + (Command::SplitCoins(_) | Command::MergeCoins(_) | Command::MakeMoveVector(_), _) => { + mutable = true; + } + _ => {} + } + // Early break out of the loop if we've already determined that the shared object + // is needed to be mutable + if mutable { + break; + } + } + + Ok(ObjectArg::SharedObject { + id: object_id, + initial_shared_version, + mutable, + }) } /// Given an particular input argument, find all of its uses. /// /// The returned iterator contains all commands where the argument is used and an optional index -/// for where the argument is used in that command. +/// to indicate where the argument is used in that command. fn find_arg_uses( arg_idx: usize, commands: &[Command], ) -> impl Iterator)> { + fn matches_input_arg(arg: Argument, arg_idx: usize) -> bool { + matches!(arg, Argument::Input(idx) if idx as usize == arg_idx) + } + commands.iter().filter_map(move |command| { match command { Command::MoveCall(move_call) => move_call @@ -505,13 +728,27 @@ fn find_arg_uses( .iter() .position(|elem| matches_input_arg(*elem, arg_idx)) .map(Some), - Command::TransferObjects(transfer_objects) => transfer_objects - .objects - .iter() - .position(|elem| matches_input_arg(*elem, arg_idx)) - .map(Some), + Command::TransferObjects(transfer_objects) => { + if matches_input_arg(transfer_objects.address, arg_idx) { + Some(None) + } else { + transfer_objects + .objects + .iter() + .position(|elem| matches_input_arg(*elem, arg_idx)) + .map(Some) + } + } Command::SplitCoins(split_coins) => { - matches_input_arg(split_coins.coin, arg_idx).then_some(None) + if matches_input_arg(split_coins.coin, arg_idx) { + Some(None) + } else { + split_coins + .amounts + .iter() + .position(|amount| matches_input_arg(*amount, arg_idx)) + .map(Some) + } } Command::MergeCoins(merge_coins) => { if matches_input_arg(merge_coins.coin, arg_idx) { @@ -536,10 +773,6 @@ fn find_arg_uses( }) } -fn matches_input_arg(arg: Argument, arg_idx: usize) -> bool { - matches!(arg, Argument::Input(idx) if idx as usize == arg_idx) -} - /// Estimate the gas budget using the gas_cost_summary from a previous DryRun /// /// The estimated gas budget is computed as following: @@ -569,6 +802,7 @@ fn select_gas( max_gas_payment_objects: u32, input_objects: &[ObjectID], ) -> Result> { + //TODO implement index of gas coins sorted in order of decreasing value let gas_coins = reader .inner() .account_owned_objects_info_iter(owner, None)?