From 77b7863dd426b938678fc6ec34e0910dc797e2b3 Mon Sep 17 00:00:00 2001 From: Timothy Zakian Date: Thu, 13 Jul 2023 12:31:17 -0700 Subject: [PATCH] [4/n][tto-sdks] Add Rust SDK support for receiving arguments --- crates/sui-json/src/lib.rs | 17 + crates/sui-transaction-builder/src/lib.rs | 23 +- crates/sui/tests/cli_tests.rs | 383 ++++++++++++++++++++ crates/sui/tests/data/tto/Move.toml | 9 + crates/sui/tests/data/tto/sources/tto1.move | 29 ++ 5 files changed, 458 insertions(+), 3 deletions(-) create mode 100644 crates/sui/tests/data/tto/Move.toml create mode 100644 crates/sui/tests/data/tto/sources/tto1.move diff --git a/crates/sui-json/src/lib.rs b/crates/sui-json/src/lib.rs index 7714c9e265072..86a7015f5b39a 100644 --- a/crates/sui-json/src/lib.rs +++ b/crates/sui-json/src/lib.rs @@ -34,6 +34,7 @@ use sui_types::base_types::{ }; use sui_types::id::{ID, RESOLVED_SUI_ID}; use sui_types::move_package::MovePackage; +use sui_types::transfer::RESOLVED_RECEIVING_STRUCT; use sui_types::MOVE_STDLIB_ADDRESS; const HEX_PREFIX: &str = "0x"; @@ -818,6 +819,22 @@ fn resolve_call_arg( } } +pub fn is_receiving_argument(view: &BinaryIndexedView, arg_type: &SignatureToken) -> bool { + use SignatureToken as ST; + + // Progress down into references to determine if the underlying type is a receiving + // type or not. + let mut token = arg_type; + while let ST::Reference(inner) | ST::MutableReference(inner) = token { + token = inner; + } + + matches!( + token, + ST::StructInstantiation(idx, targs) if resolve_struct(view, *idx) == RESOLVED_RECEIVING_STRUCT && targs.len() == 1 + ) +} + fn resolve_call_args( view: &BinaryIndexedView, type_args: &[TypeTag], diff --git a/crates/sui-transaction-builder/src/lib.rs b/crates/sui-transaction-builder/src/lib.rs index 4f73b88ae20d0..428947bdc3b5f 100644 --- a/crates/sui-transaction-builder/src/lib.rs +++ b/crates/sui-transaction-builder/src/lib.rs @@ -9,11 +9,13 @@ use std::sync::Arc; use anyhow::{anyhow, bail, ensure, Ok}; use async_trait::async_trait; use futures::future::join_all; +use move_binary_format::binary_views::BinaryIndexedView; use move_binary_format::file_format::SignatureToken; +use move_binary_format::file_format_common::VERSION_MAX; use move_core_types::identifier::Identifier; use move_core_types::language_storage::{StructTag, TypeTag}; -use sui_json::{resolve_move_function_args, ResolvedCallArg, SuiJsonValue}; +use sui_json::{is_receiving_argument, resolve_move_function_args, ResolvedCallArg, SuiJsonValue}; use sui_json_rpc_types::{ RPCTransactionRequestParams, SuiData, SuiObjectDataOptions, SuiObjectResponse, SuiRawData, SuiTypeTag, @@ -325,6 +327,8 @@ impl TransactionBuilder { id: ObjectID, objects: &mut BTreeMap, is_mutable_ref: bool, + view: &BinaryIndexedView<'_>, + arg_type: &SignatureToken, ) -> Result { let response = self .0 @@ -335,6 +339,9 @@ impl TransactionBuilder { let obj_ref = obj.compute_object_reference(); let owner = obj.owner; objects.insert(id, obj); + if is_receiving_argument(view, arg_type) { + return Ok(ObjectArg::Receiving(obj_ref)); + } Ok(match owner { Owner::Shared { initial_shared_version, @@ -385,6 +392,8 @@ impl TransactionBuilder { let mut args = Vec::new(); let mut objects = BTreeMap::new(); + let module = package.deserialize_module(module, VERSION_MAX, true)?; + let view = BinaryIndexedView::Module(&module); for (arg, expected_type) in json_args_and_tokens { args.push(match arg { ResolvedCallArg::Pure(p) => builder.input(CallArg::Pure(p)), @@ -394,6 +403,8 @@ impl TransactionBuilder { id, &mut objects, matches!(expected_type, SignatureToken::MutableReference(_)), + &view, + &expected_type, ) .await?, )), @@ -402,8 +413,14 @@ impl TransactionBuilder { let mut object_ids = vec![]; for id in v { object_ids.push( - self.get_object_arg(id, &mut objects, /* is_mutable_ref */ false) - .await?, + self.get_object_arg( + id, + &mut objects, + /* is_mutable_ref */ false, + &view, + &expected_type, + ) + .await?, ) } builder.make_obj_vec(object_ids) diff --git a/crates/sui/tests/cli_tests.rs b/crates/sui/tests/cli_tests.rs index f30a1ddd4f860..cb06323c6abbb 100644 --- a/crates/sui/tests/cli_tests.rs +++ b/crates/sui/tests/cli_tests.rs @@ -1,8 +1,10 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use std::collections::BTreeSet; use std::io::Read; use std::os::unix::prelude::FileExt; +use std::str::FromStr; use std::{fmt::Write, fs::read_dir, path::PathBuf, str, thread, time::Duration}; use expect_test::expect; @@ -633,6 +635,387 @@ async fn test_package_publish_command() -> Result<(), anyhow::Error> { Ok(()) } +#[sim_test] +async fn test_receive_argument() -> Result<(), anyhow::Error> { + let mut test_cluster = TestClusterBuilder::new().build().await; + let rgp = test_cluster.get_reference_gas_price().await; + let address = test_cluster.get_address_0(); + let context = &mut test_cluster.wallet; + + let client = context.get_client().await?; + let object_refs = client + .read_api() + .get_owned_objects( + address, + Some(SuiObjectResponseQuery::new_with_options( + SuiObjectDataOptions::new() + .with_type() + .with_owner() + .with_previous_transaction(), + )), + None, + None, + ) + .await? + .data; + + let gas_obj_id = object_refs.first().unwrap().object().unwrap().object_id; + + // Provide path to well formed package sources + let mut package_path = PathBuf::from(TEST_DATA_DIR); + package_path.push("tto"); + let build_config = BuildConfig::new_for_testing().config; + let resp = SuiClientCommands::Publish { + package_path, + build_config, + gas: Some(gas_obj_id), + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + skip_dependency_verification: false, + with_unpublished_dependencies: false, + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + lint: false, + } + .execute(context) + .await?; + + let owned_obj_ids = if let SuiClientCommandResult::Publish(response) = resp { + let x = response.effects.unwrap(); + x.created().to_vec() + } else { + unreachable!("Invalid response"); + }; + + // Check the objects + for OwnedObjectRef { reference, .. } in &owned_obj_ids { + get_parsed_object_assert_existence(reference.object_id, context).await; + } + + let package_id = owned_obj_ids + .into_iter() + .find(|OwnedObjectRef { owner, .. }| owner == &Owner::Immutable) + .expect("Must find published package ID") + .reference; + + // Start and then receive the object + let start_call_result = SuiClientCommands::Call { + package: (*package_id.object_id).into(), + module: "tto".to_string(), + function: "start".to_string(), + type_args: vec![], + gas: None, + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + args: vec![], + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + } + .execute(context) + .await?; + + let (parent, child) = if let SuiClientCommandResult::Call(response) = start_call_result { + let created = response.effects.unwrap().created().to_vec(); + let owners: BTreeSet = created + .iter() + .flat_map(|refe| { + refe.owner + .get_address_owner_address() + .ok() + .map(|x| x.into()) + }) + .collect(); + let child = created + .iter() + .find(|refe| !owners.contains(&refe.reference.object_id)) + .unwrap(); + let parent = created + .iter() + .find(|refe| owners.contains(&refe.reference.object_id)) + .unwrap(); + (parent.reference.clone(), child.reference.clone()) + } else { + unreachable!("Invalid response"); + }; + + let receive_result = SuiClientCommands::Call { + package: (*package_id.object_id).into(), + module: "tto".to_string(), + function: "receiver".to_string(), + type_args: vec![], + gas: None, + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + args: vec![ + SuiJsonValue::from_str(&parent.object_id.to_string()).unwrap(), + SuiJsonValue::from_str(&child.object_id.to_string()).unwrap(), + ], + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + } + .execute(context) + .await?; + + if let SuiClientCommandResult::Call(response) = receive_result { + assert!(response.effects.unwrap().into_status().is_ok()); + } else { + unreachable!("Invalid response"); + }; + + Ok(()) +} + +#[sim_test] +async fn test_receive_argument_by_immut_ref() -> Result<(), anyhow::Error> { + let mut test_cluster = TestClusterBuilder::new().build().await; + let rgp = test_cluster.get_reference_gas_price().await; + let address = test_cluster.get_address_0(); + let context = &mut test_cluster.wallet; + + let client = context.get_client().await?; + let object_refs = client + .read_api() + .get_owned_objects( + address, + Some(SuiObjectResponseQuery::new_with_options( + SuiObjectDataOptions::new() + .with_type() + .with_owner() + .with_previous_transaction(), + )), + None, + None, + ) + .await? + .data; + + let gas_obj_id = object_refs.first().unwrap().object().unwrap().object_id; + + // Provide path to well formed package sources + let mut package_path = PathBuf::from(TEST_DATA_DIR); + package_path.push("tto"); + let build_config = BuildConfig::new_for_testing().config; + let resp = SuiClientCommands::Publish { + package_path, + build_config, + gas: Some(gas_obj_id), + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + skip_dependency_verification: false, + with_unpublished_dependencies: false, + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + lint: false, + } + .execute(context) + .await?; + + let owned_obj_ids = if let SuiClientCommandResult::Publish(response) = resp { + let x = response.effects.unwrap(); + x.created().to_vec() + } else { + unreachable!("Invalid response"); + }; + + // Check the objects + for OwnedObjectRef { reference, .. } in &owned_obj_ids { + get_parsed_object_assert_existence(reference.object_id, context).await; + } + + let package_id = owned_obj_ids + .into_iter() + .find(|OwnedObjectRef { owner, .. }| owner == &Owner::Immutable) + .expect("Must find published package ID") + .reference; + + // Start and then receive the object + let start_call_result = SuiClientCommands::Call { + package: (*package_id.object_id).into(), + module: "tto".to_string(), + function: "start".to_string(), + type_args: vec![], + gas: None, + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + args: vec![], + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + } + .execute(context) + .await?; + + let (parent, child) = if let SuiClientCommandResult::Call(response) = start_call_result { + let created = response.effects.unwrap().created().to_vec(); + let owners: BTreeSet = created + .iter() + .flat_map(|refe| { + refe.owner + .get_address_owner_address() + .ok() + .map(|x| x.into()) + }) + .collect(); + let child = created + .iter() + .find(|refe| !owners.contains(&refe.reference.object_id)) + .unwrap(); + let parent = created + .iter() + .find(|refe| owners.contains(&refe.reference.object_id)) + .unwrap(); + (parent.reference.clone(), child.reference.clone()) + } else { + unreachable!("Invalid response"); + }; + + let receive_result = SuiClientCommands::Call { + package: (*package_id.object_id).into(), + module: "tto".to_string(), + function: "invalid_call_immut_ref".to_string(), + type_args: vec![], + gas: None, + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + args: vec![ + SuiJsonValue::from_str(&parent.object_id.to_string()).unwrap(), + SuiJsonValue::from_str(&child.object_id.to_string()).unwrap(), + ], + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + } + .execute(context) + .await?; + + if let SuiClientCommandResult::Call(response) = receive_result { + assert!(response.effects.unwrap().into_status().is_ok()); + } else { + unreachable!("Invalid response"); + }; + + Ok(()) +} + +#[sim_test] +async fn test_receive_argument_by_mut_ref() -> Result<(), anyhow::Error> { + let mut test_cluster = TestClusterBuilder::new().build().await; + let rgp = test_cluster.get_reference_gas_price().await; + let address = test_cluster.get_address_0(); + let context = &mut test_cluster.wallet; + + let client = context.get_client().await?; + let object_refs = client + .read_api() + .get_owned_objects( + address, + Some(SuiObjectResponseQuery::new_with_options( + SuiObjectDataOptions::new() + .with_type() + .with_owner() + .with_previous_transaction(), + )), + None, + None, + ) + .await? + .data; + + let gas_obj_id = object_refs.first().unwrap().object().unwrap().object_id; + + // Provide path to well formed package sources + let mut package_path = PathBuf::from(TEST_DATA_DIR); + package_path.push("tto"); + let build_config = BuildConfig::new_for_testing().config; + let resp = SuiClientCommands::Publish { + package_path, + build_config, + gas: Some(gas_obj_id), + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + skip_dependency_verification: false, + with_unpublished_dependencies: false, + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + lint: false, + } + .execute(context) + .await?; + + let owned_obj_ids = if let SuiClientCommandResult::Publish(response) = resp { + let x = response.effects.unwrap(); + x.created().to_vec() + } else { + unreachable!("Invalid response"); + }; + + // Check the objects + for OwnedObjectRef { reference, .. } in &owned_obj_ids { + get_parsed_object_assert_existence(reference.object_id, context).await; + } + + let package_id = owned_obj_ids + .into_iter() + .find(|OwnedObjectRef { owner, .. }| owner == &Owner::Immutable) + .expect("Must find published package ID") + .reference; + + // Start and then receive the object + let start_call_result = SuiClientCommands::Call { + package: (*package_id.object_id).into(), + module: "tto".to_string(), + function: "start".to_string(), + type_args: vec![], + gas: None, + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + args: vec![], + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + } + .execute(context) + .await?; + + let (parent, child) = if let SuiClientCommandResult::Call(response) = start_call_result { + let created = response.effects.unwrap().created().to_vec(); + let owners: BTreeSet = created + .iter() + .flat_map(|refe| { + refe.owner + .get_address_owner_address() + .ok() + .map(|x| x.into()) + }) + .collect(); + let child = created + .iter() + .find(|refe| !owners.contains(&refe.reference.object_id)) + .unwrap(); + let parent = created + .iter() + .find(|refe| owners.contains(&refe.reference.object_id)) + .unwrap(); + (parent.reference.clone(), child.reference.clone()) + } else { + unreachable!("Invalid response"); + }; + + let receive_result = SuiClientCommands::Call { + package: (*package_id.object_id).into(), + module: "tto".to_string(), + function: "invalid_call_mut_ref".to_string(), + type_args: vec![], + gas: None, + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + args: vec![ + SuiJsonValue::from_str(&parent.object_id.to_string()).unwrap(), + SuiJsonValue::from_str(&child.object_id.to_string()).unwrap(), + ], + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + } + .execute(context) + .await?; + + if let SuiClientCommandResult::Call(response) = receive_result { + assert!(response.effects.unwrap().into_status().is_ok()); + } else { + unreachable!("Invalid response"); + }; + + Ok(()) +} + #[sim_test] async fn test_package_publish_command_with_unpublished_dependency_succeeds( ) -> Result<(), anyhow::Error> { diff --git a/crates/sui/tests/data/tto/Move.toml b/crates/sui/tests/data/tto/Move.toml new file mode 100644 index 0000000000000..310a863195d6c --- /dev/null +++ b/crates/sui/tests/data/tto/Move.toml @@ -0,0 +1,9 @@ +[package] +name = "tto" +version = "0.0.1" + +[dependencies] +Sui = { local = "../../../../sui-framework/packages/sui-framework" } + +[addresses] +tto = "0x0" diff --git a/crates/sui/tests/data/tto/sources/tto1.move b/crates/sui/tests/data/tto/sources/tto1.move new file mode 100644 index 0000000000000..0274f9e51b5cc --- /dev/null +++ b/crates/sui/tests/data/tto/sources/tto1.move @@ -0,0 +1,29 @@ +module tto::tto { + use sui::object::{Self, UID}; + use sui::tx_context::{Self, TxContext}; + use sui::transfer::{Self, Receiving}; + + struct A has key, store { + id: UID, + } + + struct B has key, store { + id: UID, + } + + public fun start(ctx: &mut TxContext) { + let a = A { id: object::new(ctx) }; + let a_address = object::id_address(&a); + let b = B { id: object::new(ctx) }; + transfer::public_transfer(a, tx_context::sender(ctx)); + transfer::public_transfer(b, a_address); + } + + public entry fun receiver(parent: &mut A, x: Receiving) { + let b = transfer::receive(&mut parent.id, x); + transfer::public_transfer(b, @tto); + } + + public fun invalid_call_immut_ref(_parent: &mut A, _x: &Receiving) { } + public fun invalid_call_mut_ref(_parent: &mut A, _x: &mut Receiving) { } +}