From e9c59d584a87130fc8421d9612b0ad49c4021505 Mon Sep 17 00:00:00 2001 From: green Date: Thu, 6 Apr 2023 00:00:14 +0100 Subject: [PATCH 1/7] Implemented `call_builder` macro from the [proposal](https://github.com/paritytech/ink/issues/631#issuecomment-1475389406). --- crates/env/src/call/call_builder.rs | 183 ++++++++++++++++++ crates/env/src/call/mod.rs | 1 + crates/env/src/contract.rs | 8 + crates/ink/Cargo.toml | 1 + .../generator/as_dependency/contract_ref.rs | 17 ++ .../src/generator/trait_def/definition.rs | 31 ++- crates/ink/src/codegen/mod.rs | 1 + crates/ink/src/contract_ref.rs | 26 ++- crates/ink/src/reflect/message.rs | 51 +++++ crates/ink/src/reflect/mod.rs | 2 + .../builder_contract_caller/.gitignore | 9 + .../builder_contract_caller/Cargo.toml | 37 ++++ .../builder_contract_caller/lib.rs | 134 +++++++++++++ .../other_contract/.gitignore | 9 + .../other_contract/Cargo.toml | 28 +++ .../other_contract/lib.rs | 44 +++++ 16 files changed, 573 insertions(+), 9 deletions(-) create mode 100644 crates/ink/src/reflect/message.rs create mode 100644 integration-tests/builder_contract_caller/.gitignore create mode 100644 integration-tests/builder_contract_caller/Cargo.toml create mode 100755 integration-tests/builder_contract_caller/lib.rs create mode 100644 integration-tests/builder_contract_caller/other_contract/.gitignore create mode 100644 integration-tests/builder_contract_caller/other_contract/Cargo.toml create mode 100644 integration-tests/builder_contract_caller/other_contract/lib.rs diff --git a/crates/env/src/call/call_builder.rs b/crates/env/src/call/call_builder.rs index eedc2e15bfe..5490cc39d61 100644 --- a/crates/env/src/call/call_builder.rs +++ b/crates/env/src/call/call_builder.rs @@ -24,6 +24,7 @@ use crate::{ ExecutionInput, }, types::Gas, + ContractEnv, Environment, Error, }; @@ -785,3 +786,185 @@ where self.params().try_invoke() } } + +/// The analog of the [`build_call`] but from variable to get the [`ContractEnv::Env`]. +#[doc(hidden)] +#[allow(clippy::type_complexity)] +pub fn build_call_from_variable( + _: V, +) -> CallBuilder< + V::Env, + Unset>, + Unset>, + Unset>, +> +where + V: ContractEnv, +{ + build_call::() +} + +/// The helper macro reverses the order of the token stream. +/// +/// ```rust +/// use ink_env::reverse_tokens_order; +/// assert_eq!( +/// core::any::TypeId::of::(), +/// reverse_tokens_order!(()>u64<::of::TypeId::any::core) +/// ) +/// ``` +#[doc(hidden)] +#[macro_export] +macro_rules! reverse_tokens_order { + (@main_loop [ $( $heads:tt, )+ ],) => { + $( $heads )* + }; + (@main_loop [ $( $heads:tt, )+ ], $head:tt $( $tail:tt )*) => { + $crate::reverse_tokens_order!(@main_loop [ $head, $( $heads, )+ ], $( $tail )*) + }; + ($head:tt $( $tail:tt )*) => { + $crate::reverse_tokens_order!(@main_loop [ $head, ], $( $tail )*) + }; +} + +/// The primary internal implementation of the [`call_builder`](crate::call_builder) +/// macro. It parses the token stream and generates [`CallBuilder`]. +/// +/// The macro works as a state machine with the next steps: +/// - Reverses the token stream. +/// - Tries to find the template `(args)method.` or `(args, receiver)method::`. +/// - Collects arguments, receiver, and message descriptor getter. +/// - Reverse the token stream back. +/// - Expands into [`CallBuilder`]. +#[doc(hidden)] +#[macro_export] +macro_rules! call_builder_inner { + // Reverses the initial token stream to simplify handling in the `@reversed`. + (@reverse $head:tt $( $tail:tt )*) => { + $crate::call_builder_inner!(@reverse_main_loop [ $head, ], $( $tail )*) + }; + (@reverse_main_loop [ $( $heads:tt, )+ ],) => { + $crate::call_builder_inner!(@reversed $( $heads )*) + }; + (@reverse_main_loop [ $( $heads:tt, )+ ], $head:tt $( $tail:tt )*) => { + $crate::call_builder_inner!(@reverse_main_loop [ $head, $( $heads, )+ ], $( $tail )*) + }; + + // Entry point for the `... .method(inputs)`. + // The `$input_bindings` are not reversed because it is part + // of the `( $( $input_bindings:expr ),* )` that is single `tt`. + (@reversed ( $( $input_bindings:expr ),* ) $method:ident . $( $rest:tt )+ ) => { + $crate::call_builder_inner!( + @final + $crate::reverse_tokens_order!( $( $rest )+ ), + $crate::reverse_tokens_order!( ( $( $input_bindings ),* ) $method . $( $rest )+ ), + ::ink::codegen::paste! { + $crate::reverse_tokens_order!( () [<__ink_ $method _description>] . $( $rest )+ ) + }, + $( $input_bindings )* + ) + }; + + // Entry point for the `... ::method(self, inputs)`. + // The `$receiver` and `$input_bindings` are not reversed because it is part + // of the `( $receiver:expr $(, $input_bindings:expr )* )` that is single `tt`. + (@reversed ( $receiver:expr $(, $input_bindings:expr )* ) $method:ident :: $( $rest:tt )+ ) => { + $crate::call_builder_inner!( + @final + $receiver, + $crate::reverse_tokens_order!( ( $receiver $(, $input_bindings )* ) $method :: $( $rest )+ ), + ::ink::codegen::paste! { + $crate::reverse_tokens_order!( ($receiver) [<__ink_ $method _description>] :: $( $rest )+ ) + }, + $( $input_bindings )* + ) + }; + + // The final generated code by the macro. + (@final $caller:expr, $call:expr, $description:expr, $( $input_bindings:expr)* ) => {{ + // Gets the message description with selector information. + let message_description = $description; + let call_builder = match $caller { + ref caller => { + // Creates the call builder with the selector from + // the message descriptor with input arguments. + let call_builder = $crate::call::build_call_from_variable(caller) + .call(::ink::ToAccountId::to_account_id(caller)) + .exec_input( + $crate::call::ExecutionInput::new( + message_description.selector() + ) + $( + .push_arg($input_bindings) + )* + ) + .returns::<_>(); + + call_builder + } + }; + + // Forces setting of the return type of the call builder. + if false { + let _ = $call == call_builder.invoke(); + unreachable!(); + }; + call_builder + }}; +} + +/// Returns a [`CallBuilder`] based on the message call signature. +/// +/// +/// ```should_panic +/// use ink::contract_ref; +/// use ink_env::{ +/// call_builder, +/// CallFlags, +/// DefaultEnvironment, +/// }; +/// use ink_primitives::AccountId; +/// +/// #[ink::trait_definition] +/// pub trait Erc20 { +/// /// Returns the total supply of the ERC-20 smart contract. +/// #[ink(message)] +/// fn total_supply(&self) -> u128; +/// +/// /// Transfers balance from the caller to the given address. +/// #[ink(message)] +/// fn transfer(&mut self, amount: u128, to: AccountId) -> bool; +/// } +/// +/// let mut callee: contract_ref!(Erc20, DefaultEnvironment) = +/// AccountId::from([0; 32]).into(); +/// call_builder!(callee.total_supply()) +/// .transferred_value(1000) +/// .invoke(); +/// call_builder!(callee.transfer(20, AccountId::from([1; 32]))) +/// .call_flags(CallFlags::default().set_allow_reentry(true)) +/// .invoke(); +/// let ink_err = call_builder!(callee.total_supply()).try_invoke(); +/// let message_err = ink_err.unwrap(); +/// let supply = message_err.unwrap(); +/// +/// // Other supported syntax +/// call_builder!(Erc20::total_supply(&callee)).invoke(); +/// call_builder!(Erc20::transfer(&mut callee, 20, AccountId::from([2; 32]))).invoke(); +/// call_builder!(<_ as Erc20>::total_supply(&callee)).invoke(); +/// call_builder!(<_ as Erc20>::transfer( +/// &mut callee, +/// 20, +/// AccountId::from([3; 32]) +/// )) +/// .invoke(); +/// call_builder!(Erc20::total_supply(&callee)).invoke(); +/// ``` +#[macro_export] +macro_rules! call_builder { +( $( $tokens:tt )* ) => {{ + // Forces the compiler to check first that the expression is valid. + let _ = || { $( $tokens )* }; + $crate::call_builder_inner!(@reverse $( $tokens )* ) + }}; +} diff --git a/crates/env/src/call/mod.rs b/crates/env/src/call/mod.rs index f5fd923cef7..a032247e5fb 100644 --- a/crates/env/src/call/mod.rs +++ b/crates/env/src/call/mod.rs @@ -42,6 +42,7 @@ pub mod utils { pub use self::{ call_builder::{ build_call, + build_call_from_variable, Call, CallBuilder, CallParams, diff --git a/crates/env/src/contract.rs b/crates/env/src/contract.rs index 94dfd3d16c2..5faecdf7321 100644 --- a/crates/env/src/contract.rs +++ b/crates/env/src/contract.rs @@ -93,6 +93,14 @@ pub trait ContractEnv { type Env: crate::Environment; } +impl ContractEnv for &T { + type Env = T::Env; +} + +impl ContractEnv for &mut T { + type Env = T::Env; +} + /// Refers to the generated ink! smart contract reference type. /// /// # Note diff --git a/crates/ink/Cargo.toml b/crates/ink/Cargo.toml index 10ff807003d..a69ec6c47f8 100644 --- a/crates/ink/Cargo.toml +++ b/crates/ink/Cargo.toml @@ -23,6 +23,7 @@ ink_metadata = { workspace = true, optional = true } ink_prelude = { workspace = true } ink_macro = { workspace = true } +paste = { workspace = true } scale = { package = "parity-scale-codec", workspace = true } derive_more = { workspace = true, features = ["from"] } diff --git a/crates/ink/codegen/src/generator/as_dependency/contract_ref.rs b/crates/ink/codegen/src/generator/as_dependency/contract_ref.rs index 91f00534b3f..b155845f67d 100644 --- a/crates/ink/codegen/src/generator/as_dependency/contract_ref.rs +++ b/crates/ink/codegen/src/generator/as_dependency/contract_ref.rs @@ -23,6 +23,7 @@ use ir::{ }; use proc_macro2::TokenStream as TokenStream2; use quote::{ + format_ident, quote, quote_spanned, }; @@ -392,6 +393,13 @@ impl ContractRef<'_> { let input_types = message.inputs().map(|input| &input.ty).collect::>(); let output_type = message.output().map(|ty| quote! { -> #ty }); let wrapped_output_type = message.wrapped_output(); + + let cfg_attrs = message.get_cfg_attrs(span); + let description_ident = format_ident!("__ink_{}_description", message_ident); + let is_mutable = message.receiver().is_ref_mut(); + let is_payable = message.is_payable(); + let selector_bytes = message.composed_selector().hex_lits(); + quote_spanned!(span=> #( #attrs )* #[inline] @@ -424,6 +432,15 @@ impl ContractRef<'_> { error, )) } + + #(#cfg_attrs)* + pub const fn #description_ident(&self) -> ::ink::reflect::MessageDescription { + ::ink::reflect::MessageDescription::new( + #is_mutable, + #is_payable, + [ #( #selector_bytes ),* ] + ) + } ) } diff --git a/crates/ink/codegen/src/generator/trait_def/definition.rs b/crates/ink/codegen/src/generator/trait_def/definition.rs index edf7b1ed1a4..ce44e157043 100644 --- a/crates/ink/codegen/src/generator/trait_def/definition.rs +++ b/crates/ink/codegen/src/generator/trait_def/definition.rs @@ -16,6 +16,7 @@ use super::TraitDefinition; use heck::ToLowerCamelCase as _; +use ir::Selector; use proc_macro2::TokenStream as TokenStream2; use quote::{ format_ident, @@ -24,7 +25,10 @@ use quote::{ }; impl<'a> TraitDefinition<'a> { - fn generate_for_message(message: ir::InkTraitMessage<'a>) -> TokenStream2 { + fn generate_for_message( + message: ir::InkTraitMessage<'a>, + selector: Selector, + ) -> TokenStream2 { let span = message.span(); let attrs = message.attrs(); let sig = message.sig(); @@ -37,6 +41,12 @@ impl<'a> TraitDefinition<'a> { }; let output_ident = format_ident!("{}Output", ident.to_string().to_lower_camel_case()); + + let description_ident = format_ident!("__ink_{}_description", sig.ident); + let is_mutable = message.receiver().is_ref_mut(); + let is_payable = message.ink_attrs().is_payable(); + let selector_bytes = selector.hex_lits(); + quote_spanned!(span => /// Output type of the respective trait message. #(#cfg_attrs)* @@ -44,6 +54,16 @@ impl<'a> TraitDefinition<'a> { #(#attrs)* fn #ident(#inputs) -> Self::#output_ident; + + #(#cfg_attrs)* + #[inline(always)] + fn #description_ident(&self) -> ::ink::reflect::MessageDescription { + ::ink::reflect::MessageDescription::new( + #is_mutable, + #is_payable, + [ #( #selector_bytes ),* ] + ) + } ) } } @@ -54,11 +74,10 @@ impl TraitDefinition<'_> { let span = item.span(); let attrs = item.attrs(); let ident = item.ident(); - let messages = item - .iter_items() - .map(|(item, _)| item) - .flat_map(ir::InkTraitItem::filter_map_message) - .map(Self::generate_for_message); + let messages = item.iter_items().flat_map(|(item, selector)| { + ir::InkTraitItem::filter_map_message(item) + .map(|message| Self::generate_for_message(message, selector)) + }); quote_spanned!(span => #(#attrs)* pub trait #ident: ::ink::env::ContractEnv { diff --git a/crates/ink/src/codegen/mod.rs b/crates/ink/src/codegen/mod.rs index d7ebf7e84e8..96e63e0658d 100644 --- a/crates/ink/src/codegen/mod.rs +++ b/crates/ink/src/codegen/mod.rs @@ -40,3 +40,4 @@ pub use self::{ TraitMessageSelector, }, }; +pub use paste::paste; diff --git a/crates/ink/src/contract_ref.rs b/crates/ink/src/contract_ref.rs index 2fe16e1f9ea..b80efe1dfe4 100644 --- a/crates/ink/src/contract_ref.rs +++ b/crates/ink/src/contract_ref.rs @@ -211,10 +211,30 @@ macro_rules! contract_ref { /// Implemented by contracts that are compiled as dependencies. /// /// Allows them to return their underlying account identifier. -pub trait ToAccountId +pub trait ToAccountId where - T: Environment, + E: Environment, { /// Returns the underlying account identifier of the instantiated contract. - fn to_account_id(&self) -> ::AccountId; + fn to_account_id(&self) -> ::AccountId; +} + +impl ToAccountId for &T +where + T: ToAccountId, + E: Environment, +{ + fn to_account_id(&self) -> ::AccountId { + T::to_account_id(self) + } +} + +impl ToAccountId for &mut T +where + T: ToAccountId, + E: Environment, +{ + fn to_account_id(&self) -> ::AccountId { + T::to_account_id(self) + } } diff --git a/crates/ink/src/reflect/message.rs b/crates/ink/src/reflect/message.rs new file mode 100644 index 00000000000..6eacb3e6362 --- /dev/null +++ b/crates/ink/src/reflect/message.rs @@ -0,0 +1,51 @@ +// Copyright 2018-2022 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use ink_env::call::Selector; + +/// Description of the ink! message. +pub struct MessageDescription { + /// Yields `true` if the ink! message mutates the ink! storage. + mutates: bool, + /// Yields `true` if the ink! message is payable. + payable: bool, + /// The selector of the ink! message. + selector: Selector, +} + +impl MessageDescription { + /// Creates the description of the message. + pub const fn new(mutates: bool, payable: bool, selector: [u8; 4]) -> Self { + Self { + mutates, + payable, + selector: Selector::new(selector), + } + } + + /// Returns `true` of the ink! message mutates the ink! storage. + pub const fn mutates(&self) -> bool { + self.mutates + } + + /// Returns `true` of the ink! message is payable. + pub const fn payable(&self) -> bool { + self.payable + } + + /// Returns the selector of the ink! message. + pub const fn selector(&self) -> Selector { + self.selector + } +} diff --git a/crates/ink/src/reflect/mod.rs b/crates/ink/src/reflect/mod.rs index 4df2d8e1c5c..fa52c658c24 100644 --- a/crates/ink/src/reflect/mod.rs +++ b/crates/ink/src/reflect/mod.rs @@ -25,6 +25,7 @@ mod contract; mod dispatch; +mod message; mod trait_def; pub use self::{ @@ -40,6 +41,7 @@ pub use self::{ DispatchableMessageInfo, ExecuteDispatchable, }, + message::MessageDescription, trait_def::{ TraitDefinitionRegistry, TraitInfo, diff --git a/integration-tests/builder_contract_caller/.gitignore b/integration-tests/builder_contract_caller/.gitignore new file mode 100644 index 00000000000..8de8f877e47 --- /dev/null +++ b/integration-tests/builder_contract_caller/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/integration-tests/builder_contract_caller/Cargo.toml b/integration-tests/builder_contract_caller/Cargo.toml new file mode 100644 index 00000000000..33ec0f088b0 --- /dev/null +++ b/integration-tests/builder_contract_caller/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "builder-contract-caller" +version = "4.1.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../crates/ink", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.5", default-features = false, features = ["derive"], optional = true } + +# Note: We **need** to specify the `ink-as-dependency` feature. +# +# If we don't we will end up with linking errors! +other-contract = { path = "other_contract", default-features = false, features = ["ink-as-dependency"] } + +[dev-dependencies] +ink_e2e = { path = "../../crates/e2e" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", + + # Note: The metadata generation step requires `std`. If we don't specify this the metadata + # generation for our contract will fail! + "other-contract/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/builder_contract_caller/lib.rs b/integration-tests/builder_contract_caller/lib.rs new file mode 100755 index 00000000000..2e92b3fbbde --- /dev/null +++ b/integration-tests/builder_contract_caller/lib.rs @@ -0,0 +1,134 @@ +//! The integration test is the same as `../basic_contract_caller` +//! but it uses the [`ink::env::call::CallBuilder`] to do cross contract calls. + +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod builder_contract_caller { + use ink::env::call_builder; + /// We import the generated `ContractRef` of our other contract. + /// + /// Note that the other contract must have re-exported it (`pub use + /// OtherContractRef`) for us to have access to it. + use other_contract::OtherContractRef; + + #[ink(storage)] + pub struct BuilderContractCaller { + /// We specify that our contract will store a reference to the `OtherContract`. + other_contract: OtherContractRef, + } + + impl BuilderContractCaller { + /// In order to use the `OtherContract` we first need to **instantiate** it. + /// + /// To do this we will use the uploaded `code_hash` of `OtherContract`. + #[ink(constructor)] + pub fn new(other_contract_code_hash: Hash) -> Self { + let other_contract = OtherContractRef::new(true) + .code_hash(other_contract_code_hash) + .endowment(0) + .salt_bytes([0xDE, 0xAD, 0xBE, 0xEF]) + .instantiate(); + + Self { other_contract } + } + + /// Return the value of the `OtherContract`. + #[ink(message)] + pub fn get(&self) -> bool { + use other_contract::Trait; + call_builder!(self.other_contract.get()).invoke() + } + + /// Using the `ContractRef` we can call all the messages of the `OtherContract` as + /// if they were normal Rust methods (because at the end of the day, they + /// are!). + #[ink(message)] + pub fn flip_and_get(&mut self) -> bool { + call_builder!(self.other_contract.flip()).invoke(); + call_builder!(other_contract::Trait::get(&self.other_contract)).invoke() + } + } +} + +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests { + use super::builder_contract_caller::BuilderContractCallerRef; + use ink_e2e::build_message; + + type E2EResult = Result>; + + /// A test deploys and instantiates the + /// `builder_contract_caller::BuilderContractCaller` contract. + /// + /// The test verifies that we can call `BuilderContractCaller::flip_and_get`. + #[ink_e2e::test(additional_contracts = "other_contract/Cargo.toml")] + async fn e2e_cross_contract_calls( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let other_code_hash = client + .upload("other-contract", &ink_e2e::alice(), None) + .await + .expect("uploading `other-contract` failed") + .code_hash; + + let _ = client + .upload("builder-contract-caller", &ink_e2e::alice(), None) + .await + .expect("uploading `builder-contract-caller` failed") + .code_hash; + + let constructor = BuilderContractCallerRef::new(other_code_hash); + + let caller_account_id = client + .instantiate( + "builder-contract-caller", + &ink_e2e::alice(), + constructor, + 0, + None, + ) + .await + .expect("instantiate failed") + .account_id; + + // Check that the `get` return `true`(default value). + let get = build_message::(caller_account_id.clone()) + .call(|contract| contract.get()); + let value = client + .call_dry_run(&ink_e2e::alice(), &get, 0, None) + .await + .return_value(); + assert_eq!(value, true); + + // Flip the value + let flip_and_get = + build_message::(caller_account_id.clone()) + .call(|contract| contract.flip_and_get()); + let _ = client + .call(&ink_e2e::alice(), flip_and_get, 0, None) + .await + .expect("calling `flip_and_get` failed"); + + // The value should be updated + let get = build_message::(caller_account_id.clone()) + .call(|contract| contract.get()); + let value = client + .call_dry_run(&ink_e2e::alice(), &get, 0, None) + .await + .return_value(); + assert_eq!(value, false); + + // The dry run for `flip_and_get` should return `true` again. + let flip_and_get = + build_message::(caller_account_id.clone()) + .call(|contract| contract.flip_and_get()); + let value = client + .call_dry_run(&ink_e2e::alice(), &flip_and_get, 0, None) + .await + .return_value(); + assert_eq!(value, true); + + Ok(()) + } +} diff --git a/integration-tests/builder_contract_caller/other_contract/.gitignore b/integration-tests/builder_contract_caller/other_contract/.gitignore new file mode 100644 index 00000000000..8de8f877e47 --- /dev/null +++ b/integration-tests/builder_contract_caller/other_contract/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/integration-tests/builder_contract_caller/other_contract/Cargo.toml b/integration-tests/builder_contract_caller/other_contract/Cargo.toml new file mode 100644 index 00000000000..5e88384d0c9 --- /dev/null +++ b/integration-tests/builder_contract_caller/other_contract/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "other-contract" +version = "4.1.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.5", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/builder_contract_caller/other_contract/lib.rs b/integration-tests/builder_contract_caller/other_contract/lib.rs new file mode 100644 index 00000000000..d09c63e009c --- /dev/null +++ b/integration-tests/builder_contract_caller/other_contract/lib.rs @@ -0,0 +1,44 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +/// Re-export the `ContractRef` generated by the ink! codegen. +/// +/// This let's other crates which pull this contract in as a dependency to interact +/// with this contract in a type-safe way. +pub use self::other_contract::{ + OtherContractRef, + Trait, +}; + +#[ink::contract] +mod other_contract { + + #[ink(storage)] + pub struct OtherContract { + value: bool, + } + + #[ink::trait_definition] + pub trait Trait { + #[ink(message)] + fn get(&self) -> bool; + } + + impl OtherContract { + #[ink(constructor)] + pub fn new(init_value: bool) -> Self { + Self { value: init_value } + } + + #[ink(message)] + pub fn flip(&mut self) { + self.value = !self.value; + } + } + + impl Trait for OtherContract { + #[ink(message)] + fn get(&self) -> bool { + self.value + } + } +} From a776ed587d4497dc5e0582cc04e023c45e4a16b5 Mon Sep 17 00:00:00 2001 From: green Date: Thu, 6 Apr 2023 10:59:28 +0100 Subject: [PATCH 2/7] Fix doc test compilation --- crates/env/src/call/call_builder.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/env/src/call/call_builder.rs b/crates/env/src/call/call_builder.rs index 5490cc39d61..dd78b84f8dc 100644 --- a/crates/env/src/call/call_builder.rs +++ b/crates/env/src/call/call_builder.rs @@ -906,7 +906,11 @@ macro_rules! call_builder_inner { // Forces setting of the return type of the call builder. if false { - let _ = $call == call_builder.invoke(); + let _ = if false { + $call + } else { + call_builder.invoke() + }; unreachable!(); }; call_builder From 3e3f0d5d9914cdc416410f9d016d5a4b296f6c58 Mon Sep 17 00:00:00 2001 From: green Date: Sun, 9 Apr 2023 22:22:18 +0100 Subject: [PATCH 3/7] Replaced old test with more advanced test --- .../builder_contract_caller/lib.rs | 97 ++++++++++++------- .../other_contract/lib.rs | 63 +++++++++--- 2 files changed, 112 insertions(+), 48 deletions(-) diff --git a/integration-tests/builder_contract_caller/lib.rs b/integration-tests/builder_contract_caller/lib.rs index 2e92b3fbbde..6db0e3e51dd 100755 --- a/integration-tests/builder_contract_caller/lib.rs +++ b/integration-tests/builder_contract_caller/lib.rs @@ -1,5 +1,7 @@ -//! The integration test is the same as `../basic_contract_caller` -//! but it uses the [`ink::env::call::CallBuilder`] to do cross contract calls. +//! The integration test does cross contract calls to the +//! [`other_contract::OtherContract`] via the [`ink::env::call::CallBuilder`]. +//! It tests how to call read-only methods, mutable methods, methods that return +//! something, and methods with several input arguments. #![cfg_attr(not(feature = "std"), no_std, no_main)] @@ -7,9 +9,6 @@ mod builder_contract_caller { use ink::env::call_builder; /// We import the generated `ContractRef` of our other contract. - /// - /// Note that the other contract must have re-exported it (`pub use - /// OtherContractRef`) for us to have access to it. use other_contract::OtherContractRef; #[ink(storage)] @@ -24,7 +23,7 @@ mod builder_contract_caller { /// To do this we will use the uploaded `code_hash` of `OtherContract`. #[ink(constructor)] pub fn new(other_contract_code_hash: Hash) -> Self { - let other_contract = OtherContractRef::new(true) + let other_contract = OtherContractRef::new() .code_hash(other_contract_code_hash) .endowment(0) .salt_bytes([0xDE, 0xAD, 0xBE, 0xEF]) @@ -33,20 +32,38 @@ mod builder_contract_caller { Self { other_contract } } - /// Return the value of the `OtherContract`. + /// Return the total supply of the `OtherContract`. + #[ink(message)] + pub fn total_supply(&self) -> u128 { + use other_contract::Erc20; + call_builder!(self.other_contract.total_supply()).invoke() + } + + /// Return the balance of the `owner` in the `OtherContract`. #[ink(message)] - pub fn get(&self) -> bool { - use other_contract::Trait; - call_builder!(self.other_contract.get()).invoke() + pub fn balance_of(&self, owner: AccountId) -> u128 { + call_builder!(<_ as other_contract::Erc20>::balance_of( + &self.other_contract, + owner + )) + .invoke() } - /// Using the `ContractRef` we can call all the messages of the `OtherContract` as - /// if they were normal Rust methods (because at the end of the day, they - /// are!). + /// Using the `{Contract}Ref` we can call all the messages of the `OtherContract` + /// as if they were normal Rust methods (because at the end of the day, + /// they are!). #[ink(message)] - pub fn flip_and_get(&mut self) -> bool { - call_builder!(self.other_contract.flip()).invoke(); - call_builder!(other_contract::Trait::get(&self.other_contract)).invoke() + pub fn mint_and_transfer(&mut self, to: AccountId, amount: u128) { + // Mint tokens to self. + call_builder!(self.other_contract.mint(self.env().account_id(), amount)) + .invoke(); + // Transfer tokens from self to `to`. + call_builder!(other_contract::Erc20::transfer( + &mut self.other_contract, + to, + amount + )) + .invoke(); } } } @@ -54,6 +71,7 @@ mod builder_contract_caller { #[cfg(all(test, feature = "e2e-tests"))] mod e2e_tests { use super::builder_contract_caller::BuilderContractCallerRef; + use ink::primitives::AccountId; use ink_e2e::build_message; type E2EResult = Result>; @@ -61,7 +79,8 @@ mod e2e_tests { /// A test deploys and instantiates the /// `builder_contract_caller::BuilderContractCaller` contract. /// - /// The test verifies that we can call `BuilderContractCaller::flip_and_get`. + /// The test verifies that we can call `BuilderContractCaller::mint_and_transfer` and + /// `BuilderContractCaller::total_supply`. #[ink_e2e::test(additional_contracts = "other_contract/Cargo.toml")] async fn e2e_cross_contract_calls( mut client: ink_e2e::Client, @@ -92,42 +111,46 @@ mod e2e_tests { .expect("instantiate failed") .account_id; - // Check that the `get` return `true`(default value). - let get = build_message::(caller_account_id.clone()) - .call(|contract| contract.get()); + // Check that the `total_supply` return `0`(default value). + let total_supply = + build_message::(caller_account_id.clone()) + .call(|contract| contract.total_supply()); let value = client - .call_dry_run(&ink_e2e::alice(), &get, 0, None) + .call_dry_run(&ink_e2e::alice(), &total_supply, 0, None) .await .return_value(); - assert_eq!(value, true); + assert_eq!(value, 0); - // Flip the value - let flip_and_get = + // Mint tokens and transfer them to `to`. + let to = AccountId::from([13; 32]); + let amount = 100; + let mint_and_transfer = build_message::(caller_account_id.clone()) - .call(|contract| contract.flip_and_get()); + .call(|contract| contract.mint_and_transfer(to, amount)); let _ = client - .call(&ink_e2e::alice(), flip_and_get, 0, None) + .call(&ink_e2e::alice(), mint_and_transfer, 0, None) .await - .expect("calling `flip_and_get` failed"); + .expect("calling `mint_and_transfer` failed"); - // The value should be updated - let get = build_message::(caller_account_id.clone()) - .call(|contract| contract.get()); + // The total supply should be equal to `amount`. + let total_supply = + build_message::(caller_account_id.clone()) + .call(|contract| contract.total_supply()); let value = client - .call_dry_run(&ink_e2e::alice(), &get, 0, None) + .call_dry_run(&ink_e2e::alice(), &total_supply, 0, None) .await .return_value(); - assert_eq!(value, false); + assert_eq!(value, amount); - // The dry run for `flip_and_get` should return `true` again. - let flip_and_get = + // The balance of the `to` should be equal to `amount`. + let balance_of = build_message::(caller_account_id.clone()) - .call(|contract| contract.flip_and_get()); + .call(|contract| contract.balance_of(to)); let value = client - .call_dry_run(&ink_e2e::alice(), &flip_and_get, 0, None) + .call_dry_run(&ink_e2e::alice(), &balance_of, 0, None) .await .return_value(); - assert_eq!(value, true); + assert_eq!(value, amount); Ok(()) } diff --git a/integration-tests/builder_contract_caller/other_contract/lib.rs b/integration-tests/builder_contract_caller/other_contract/lib.rs index d09c63e009c..38d344ce67c 100644 --- a/integration-tests/builder_contract_caller/other_contract/lib.rs +++ b/integration-tests/builder_contract_caller/other_contract/lib.rs @@ -5,40 +5,81 @@ /// This let's other crates which pull this contract in as a dependency to interact /// with this contract in a type-safe way. pub use self::other_contract::{ + Erc20, + OtherContract, OtherContractRef, - Trait, }; #[ink::contract] mod other_contract { + use ink::storage::Mapping; + #[derive(Default)] #[ink(storage)] pub struct OtherContract { - value: bool, + balances: Mapping, + total_supply: u128, } #[ink::trait_definition] - pub trait Trait { + pub trait Erc20 { #[ink(message)] - fn get(&self) -> bool; + fn total_supply(&self) -> u128; + + #[ink(message)] + fn balance_of(&self, account_id: AccountId) -> u128; + + #[ink(message)] + fn transfer(&mut self, to: AccountId, amount: u128); } impl OtherContract { #[ink(constructor)] - pub fn new(init_value: bool) -> Self { - Self { value: init_value } + pub fn new() -> Self { + Default::default() } #[ink(message)] - pub fn flip(&mut self) { - self.value = !self.value; + pub fn mint(&mut self, to: AccountId, amount: u128) { + self._mint(&to, amount) + } + + fn _balance_of(&self, owner: &AccountId) -> u128 { + self.balances.get(owner).unwrap_or_default() + } + + fn _mint(&mut self, to: &AccountId, amount: u128) { + let mut balance = self._balance_of(to); + balance += amount; + self.balances.insert(to, &balance); + self.total_supply += amount; } } - impl Trait for OtherContract { + impl Erc20 for OtherContract { #[ink(message)] - fn get(&self) -> bool { - self.value + fn total_supply(&self) -> u128 { + self.total_supply + } + + #[ink(message)] + fn balance_of(&self, account_id: AccountId) -> u128 { + self._balance_of(&account_id) + } + + #[ink(message)] + fn transfer(&mut self, to: AccountId, amount: u128) { + let caller = self.env().caller(); + let mut balance = self._balance_of(&caller); + + if balance < amount { + panic!("The balance is too low"); + } + balance -= amount; + self.balances.insert(&caller, &balance); + self.total_supply -= amount; + + self._mint(&to, amount) } } } From 830b7550f08fb46a5f275de094fb550731871e24 Mon Sep 17 00:00:00 2001 From: green Date: Sun, 9 Apr 2023 22:37:48 +0100 Subject: [PATCH 4/7] Make clippy happy --- crates/env/src/call/call_builder.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/env/src/call/call_builder.rs b/crates/env/src/call/call_builder.rs index dd78b84f8dc..cca0dfa7d8e 100644 --- a/crates/env/src/call/call_builder.rs +++ b/crates/env/src/call/call_builder.rs @@ -883,6 +883,7 @@ macro_rules! call_builder_inner { // The final generated code by the macro. (@final $caller:expr, $call:expr, $description:expr, $( $input_bindings:expr)* ) => {{ // Gets the message description with selector information. + #[allow(clippy::unnecessary_mut_passed)] let message_description = $description; let call_builder = match $caller { ref caller => { From 5c449f4d49495369ff1c001d96303df28abaabad Mon Sep 17 00:00:00 2001 From: Green Baneling Date: Sun, 16 Apr 2023 14:47:57 +0100 Subject: [PATCH 5/7] Update crates/env/src/call/call_builder.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael Müller --- crates/env/src/call/call_builder.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/env/src/call/call_builder.rs b/crates/env/src/call/call_builder.rs index cca0dfa7d8e..2f81c747b78 100644 --- a/crates/env/src/call/call_builder.rs +++ b/crates/env/src/call/call_builder.rs @@ -920,7 +920,6 @@ macro_rules! call_builder_inner { /// Returns a [`CallBuilder`] based on the message call signature. /// -/// /// ```should_panic /// use ink::contract_ref; /// use ink_env::{ From 2af6bf90532b9e14550f82bd77881c49dd24b573 Mon Sep 17 00:00:00 2001 From: green Date: Wed, 26 Jul 2023 16:06:44 +0100 Subject: [PATCH 6/7] Fix compialtion --- crates/e2e/macro/src/codegen.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/e2e/macro/src/codegen.rs b/crates/e2e/macro/src/codegen.rs index fb81939c4c1..6cf9c168990 100644 --- a/crates/e2e/macro/src/codegen.rs +++ b/crates/e2e/macro/src/codegen.rs @@ -293,6 +293,7 @@ fn build_contract(path_to_cargo_toml: &str) -> String { output_type: OutputType::HumanReadable, skip_wasm_validation: false, target: Target::Wasm, + ..ExecuteArgs::default() }; match contract_build::execute(args) { From ec7e6698229839fe8b993336d2db65ce2ab36933 Mon Sep 17 00:00:00 2001 From: green Date: Wed, 26 Jul 2023 17:41:32 +0100 Subject: [PATCH 7/7] Fix compilation to use latest e2e primitives --- .../builder_contract_caller/lib.rs | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/integration-tests/builder_contract_caller/lib.rs b/integration-tests/builder_contract_caller/lib.rs index 6db0e3e51dd..7feb08ea3a7 100755 --- a/integration-tests/builder_contract_caller/lib.rs +++ b/integration-tests/builder_contract_caller/lib.rs @@ -70,9 +70,11 @@ mod builder_contract_caller { #[cfg(all(test, feature = "e2e-tests"))] mod e2e_tests { - use super::builder_contract_caller::BuilderContractCallerRef; + use super::builder_contract_caller::{ + BuilderContractCaller, + BuilderContractCallerRef, + }; use ink::primitives::AccountId; - use ink_e2e::build_message; type E2EResult = Result>; @@ -99,7 +101,7 @@ mod e2e_tests { let constructor = BuilderContractCallerRef::new(other_code_hash); - let caller_account_id = client + let contract = client .instantiate( "builder-contract-caller", &ink_e2e::alice(), @@ -108,46 +110,38 @@ mod e2e_tests { None, ) .await - .expect("instantiate failed") - .account_id; + .expect("instantiate failed"); + let mut call = contract.call::(); // Check that the `total_supply` return `0`(default value). - let total_supply = - build_message::(caller_account_id.clone()) - .call(|contract| contract.total_supply()); let value = client - .call_dry_run(&ink_e2e::alice(), &total_supply, 0, None) + .call_dry_run(&ink_e2e::alice(), &call.total_supply(), 0, None) .await .return_value(); assert_eq!(value, 0); - // Mint tokens and transfer them to `to`. let to = AccountId::from([13; 32]); let amount = 100; - let mint_and_transfer = - build_message::(caller_account_id.clone()) - .call(|contract| contract.mint_and_transfer(to, amount)); let _ = client - .call(&ink_e2e::alice(), mint_and_transfer, 0, None) + .call( + &ink_e2e::alice(), + &call.mint_and_transfer(to, amount), + 0, + None, + ) .await .expect("calling `mint_and_transfer` failed"); // The total supply should be equal to `amount`. - let total_supply = - build_message::(caller_account_id.clone()) - .call(|contract| contract.total_supply()); let value = client - .call_dry_run(&ink_e2e::alice(), &total_supply, 0, None) + .call_dry_run(&ink_e2e::alice(), &call.total_supply(), 0, None) .await .return_value(); assert_eq!(value, amount); // The balance of the `to` should be equal to `amount`. - let balance_of = - build_message::(caller_account_id.clone()) - .call(|contract| contract.balance_of(to)); let value = client - .call_dry_run(&ink_e2e::alice(), &balance_of, 0, None) + .call_dry_run(&ink_e2e::alice(), &call.balance_of(to), 0, None) .await .return_value(); assert_eq!(value, amount);