diff --git a/crates/e2e/src/builders.rs b/crates/e2e/src/builders.rs index 7c3f9b871f3..1ef93fba5a2 100644 --- a/crates/e2e/src/builders.rs +++ b/crates/e2e/src/builders.rs @@ -39,7 +39,7 @@ pub type CreateBuilderPartial = CreateBuilder< Unset<::Balance>, Set>, Unset, - R, + Set>, >; /// Get the encoded constructor arguments from the partially initialized `CreateBuilder` diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs index 0683fcf4b09..90c1294da19 100644 --- a/crates/env/src/api.rs +++ b/crates/env/src/api.rs @@ -311,7 +311,40 @@ where /// # Note /// /// This is a low level way to instantiate another smart contract. -/// Prefer to use the ink! guided and type safe approach to using this. +/// +/// Prefer to use methods on a `ContractRef` or the [`CreateBuilder`](`crate::call::CreateBuilder`) +/// through [`build_create`](`crate::call::build_create`) instead. +/// +/// # Errors +/// +/// - If the code hash is invalid. +/// - If the arguments passed to the instantiation process are invalid. +/// - If the instantiation process traps. +/// - If the instantiation process runs out of gas. +/// - If given insufficient endowment. +/// - If the returned account ID failed to decode properly. +pub fn instantiate_contract( + params: &CreateParams, +) -> Result> +where + E: Environment, + Args: scale::Encode, + Salt: AsRef<[u8]>, +{ + ::on_instance(|instance| { + TypedEnvBackend::instantiate_contract::(instance, params) + }) +} + +/// Attempts to instantiate another contract, returning the instantiation result back to the +/// caller. +/// +/// # Note +/// +/// This is a low level way to instantiate another smart contract. +/// +/// Prefer to use methods on a `ContractRef` or the [`CreateBuilder`](`crate::call::CreateBuilder`) +/// through [`build_create`](`crate::call::build_create`) instead. /// /// # Errors /// @@ -321,16 +354,21 @@ where /// - If the instantiation process runs out of gas. /// - If given insufficient endowment. /// - If the returned account ID failed to decode properly. -pub fn instantiate_contract( - params: &CreateParams, -) -> Result +pub fn instantiate_fallible_contract( + params: &CreateParams, +) -> Result< + ink_primitives::ConstructorResult>, +> where E: Environment, Args: scale::Encode, Salt: AsRef<[u8]>, + ContractError: scale::Decode, { ::on_instance(|instance| { - TypedEnvBackend::instantiate_contract::(instance, params) + TypedEnvBackend::instantiate_fallible_contract::( + instance, params, + ) }) } diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index 39beb23cdee..99b2024f39d 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -439,15 +439,35 @@ pub trait TypedEnvBackend: EnvBackend { /// # Note /// /// For more details visit: [`instantiate_contract`][`crate::instantiate_contract`] - fn instantiate_contract( + fn instantiate_contract( &mut self, - params: &CreateParams, - ) -> Result + params: &CreateParams, + ) -> Result> where E: Environment, Args: scale::Encode, Salt: AsRef<[u8]>; + /// Attempts to instantiate another contract, returning the instantiation result back to the + /// caller. + /// + /// # Note + /// + /// For more details visit: [`instantiate_fallible_contract`][`crate::instantiate_fallible_contract`] + fn instantiate_fallible_contract( + &mut self, + params: &CreateParams, + ) -> Result< + ink_primitives::ConstructorResult< + core::result::Result, + >, + > + where + E: Environment, + Args: scale::Encode, + Salt: AsRef<[u8]>, + ContractError: scale::Decode; + /// Terminates a smart contract. /// /// # Note diff --git a/crates/env/src/call/call_builder.rs b/crates/env/src/call/call_builder.rs index 01c990cde74..ae6ab49edba 100644 --- a/crates/env/src/call/call_builder.rs +++ b/crates/env/src/call/call_builder.rs @@ -132,7 +132,8 @@ where /// # Note /// /// On failure this returns an outer [`ink::env::Error`][`crate::Error`] or inner - /// [`ink_primitives::LangError`], both of which can be handled by the caller. + /// [`ink::primitives::LangError`][`ink_primitives::LangError`], both of which can be handled + /// by the caller. pub fn try_invoke(&self) -> Result, crate::Error> { crate::invoke_contract(self) } @@ -150,7 +151,7 @@ where /// /// # Panics /// - /// This method panics if it encounters an [`ink::env::Error`][`crate::Error`] If you want to + /// This method panics if it encounters an [`ink::env::Error`][`crate::Error`]. If you want to /// handle those use the [`try_invoke`][`CallParams::try_invoke`] method instead. pub fn invoke(&self) -> R { crate::invoke_contract_delegate(self).unwrap_or_else(|env_error| { @@ -691,7 +692,8 @@ where /// # Note /// /// On failure this returns an outer [`ink::env::Error`][`crate::Error`] or inner - /// [`ink_primitives::LangError`], both of which can be handled by the caller. + /// [`ink::primitives::LangError`][`ink_primitives::LangError`], both of which can be handled + /// by the caller. pub fn try_invoke(self) -> Result, Error> { self.params().try_invoke() } @@ -750,7 +752,8 @@ where /// # Note /// /// On failure this returns an outer [`ink::env::Error`][`crate::Error`] or inner - /// [`ink_primitives::LangError`], both of which can be handled by the caller. + /// [`ink::primitives::LangError`][`ink_primitives::LangError`], both of which can be handled + /// by the caller. pub fn try_invoke(self) -> Result, Error> { self.params().try_invoke() } diff --git a/crates/env/src/call/create_builder.rs b/crates/env/src/call/create_builder.rs index 527ede79dcb..6dd80724439 100644 --- a/crates/env/src/call/create_builder.rs +++ b/crates/env/src/call/create_builder.rs @@ -122,31 +122,93 @@ where /// /// # Panics /// - /// This method panics if it encounters an [`ink::env::Error`][`crate::Error`]. If you want to - /// handle those use the [`try_instantiate`][`CreateParams::try_instantiate`] method instead. + /// This method panics if it encounters an [`ink::env::Error`][`crate::Error`] or an + /// [`ink::primitives::LangError`][`ink_primitives::LangError`]. If you want to handle those + /// use the [`try_instantiate`][`CreateParams::try_instantiate`] method instead. #[inline] pub fn instantiate(&self) -> R { crate::instantiate_contract(self) - .map(FromAccountId::from_account_id) .unwrap_or_else(|env_error| { panic!("Cross-contract instantiation failed with {:?}", env_error) }) + .map(FromAccountId::from_account_id) + .unwrap_or_else(|lang_error| { + panic!( + "Received a `LangError` while instantiating: {:?}", + lang_error + ) + }) } /// Instantiates the contract and returns its account ID back to the caller. /// /// # Note /// - /// On failure this returns an [`ink::env::Error`][`crate::Error`] which can be handled by the - /// caller. + /// On failure this returns an outer [`ink::env::Error`][`crate::Error`] or inner + /// [`ink::primitives::LangError`][`ink_primitives::LangError`], both of which can be handled + /// by the caller. #[inline] - pub fn try_instantiate(&self) -> Result { - crate::instantiate_contract(self).map(FromAccountId::from_account_id) + pub fn try_instantiate( + &self, + ) -> Result, crate::Error> { + crate::instantiate_contract(self) + .map(|inner| inner.map(FromAccountId::from_account_id)) + } +} + +impl + CreateParams> +where + E: Environment, + Args: scale::Encode, + Salt: AsRef<[u8]>, + R: FromAccountId, + ContractError: scale::Decode, +{ + /// Attempts to instantiate the contract, returning the execution result back to the caller. + /// + /// # Panics + /// + /// This method panics if it encounters an [`ink_primitives::LangError`]. If you want to handle + /// those use the [`try_instantiate_fallible`][`CreateParams::try_instantiate_fallible`] method + /// instead. + #[inline] + pub fn instantiate_fallible(&self) -> Result { + crate::instantiate_fallible_contract(self) + .unwrap_or_else(|env_error| { + panic!("Cross-contract instantiation failed with {:?}", env_error) + }) + .unwrap_or_else(|lang_error| { + panic!( + "Received a `LangError` while instantiating: {:?}", + lang_error + ) + }) + .map(FromAccountId::from_account_id) + } + + /// Attempts to instantiate the contract, returning the execution result back to the caller. + /// + /// # Note + /// + /// On failure this returns an outer [`ink::env::Error`][`crate::Error`] or inner + /// [`ink::primitives::LangError`][`ink_primitives::LangError`], both of which can be handled + /// by the caller. + #[inline] + pub fn try_instantiate_fallible( + &self, + ) -> Result>, crate::Error> + { + crate::instantiate_fallible_contract(self).map(|constructor_result| { + constructor_result.map(|contract_result| { + contract_result.map(FromAccountId::from_account_id) + }) + }) } } /// Builds up contract instantiations. -pub struct CreateBuilder +pub struct CreateBuilder where E: Environment, { @@ -155,7 +217,7 @@ where endowment: Endowment, exec_input: Args, salt: Salt, - return_type: ReturnType, + return_type: RetType, _phantom: PhantomData E>, } @@ -163,6 +225,12 @@ where /// /// # Example /// +/// **Note:** The shown examples panic because there is currently no cross-calling +/// support in the off-chain testing environment. However, this code +/// should work fine in on-chain environments. +/// +/// ## Example 1: Returns Address of Instantiated Contract +/// /// The below example shows instantiation of contract of type `MyContract`. /// /// The used constructor: @@ -188,7 +256,7 @@ where /// # impl FromAccountId for MyContract { /// # fn from_account_id(account_id: AccountId) -> Self { Self } /// # } -/// let my_contract: MyContract = build_create::() +/// let my_contract: MyContract = build_create::() /// .code_hash(Hash::from([0x42; 32])) /// .gas_limit(4000) /// .endowment(25) @@ -199,26 +267,59 @@ where /// .push_arg(&[0x10u8; 32]) /// ) /// .salt_bytes(&[0xDE, 0xAD, 0xBE, 0xEF]) +/// .returns::() /// .params() /// .instantiate(); /// ``` /// -/// **Note:** The shown example panics because there is currently no cross-calling -/// support in the off-chain testing environment. However, this code -/// should work fine in on-chain environments. +/// ## Example 2: Handles Result from Fallible Constructor +/// +/// ```should_panic +/// # use ::ink_env::{ +/// # Environment, +/// # DefaultEnvironment, +/// # call::{build_create, Selector, ExecutionInput, FromAccountId} +/// # }; +/// # type Hash = ::Hash; +/// # type AccountId = ::AccountId; +/// # type Salt = &'static [u8]; +/// # struct MyContract; +/// # impl FromAccountId for MyContract { +/// # fn from_account_id(account_id: AccountId) -> Self { Self } +/// # } +/// # #[derive(scale::Encode, scale::Decode, Debug)] +/// # #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +/// # pub struct ConstructorError; +/// let my_contract: MyContract = build_create::() +/// .code_hash(Hash::from([0x42; 32])) +/// .gas_limit(4000) +/// .endowment(25) +/// .exec_input( +/// ExecutionInput::new(Selector::new([0xDE, 0xAD, 0xBE, 0xEF])) +/// .push_arg(42) +/// .push_arg(true) +/// .push_arg(&[0x10u8; 32]) +/// ) +/// .salt_bytes(&[0xDE, 0xAD, 0xBE, 0xEF]) +/// .returns::>() +/// .params() +/// .instantiate_fallible() +/// .expect("Constructor should've run without errors."); +/// ``` +/// +/// Note the usage of the [`CreateBuilder::instantiate_fallible`] method. #[allow(clippy::type_complexity)] -pub fn build_create() -> CreateBuilder< +pub fn build_create() -> CreateBuilder< E, Unset, Unset, Unset, Unset>, Unset, - R, + Unset>, > where E: Environment, - R: FromAccountId, { CreateBuilder { code_hash: Default::default(), @@ -231,8 +332,8 @@ where } } -impl - CreateBuilder, GasLimit, Endowment, Args, Salt, R> +impl + CreateBuilder, GasLimit, Endowment, Args, Salt, RetType> where E: Environment, { @@ -241,7 +342,7 @@ where pub fn code_hash( self, code_hash: E::Hash, - ) -> CreateBuilder, GasLimit, Endowment, Args, Salt, R> { + ) -> CreateBuilder, GasLimit, Endowment, Args, Salt, RetType> { CreateBuilder { code_hash: Set(code_hash), gas_limit: self.gas_limit, @@ -254,8 +355,8 @@ where } } -impl - CreateBuilder, Endowment, Args, Salt, R> +impl + CreateBuilder, Endowment, Args, Salt, RetType> where E: Environment, { @@ -264,7 +365,7 @@ where pub fn gas_limit( self, gas_limit: u64, - ) -> CreateBuilder, Endowment, Args, Salt, R> { + ) -> CreateBuilder, Endowment, Args, Salt, RetType> { CreateBuilder { code_hash: self.code_hash, gas_limit: Set(gas_limit), @@ -277,8 +378,8 @@ where } } -impl - CreateBuilder, Args, Salt, R> +impl + CreateBuilder, Args, Salt, RetType> where E: Environment, { @@ -287,7 +388,7 @@ where pub fn endowment( self, endowment: E::Balance, - ) -> CreateBuilder, Args, Salt, R> { + ) -> CreateBuilder, Args, Salt, RetType> { CreateBuilder { code_hash: self.code_hash, gas_limit: self.gas_limit, @@ -300,7 +401,7 @@ where } } -impl +impl CreateBuilder< E, CodeHash, @@ -308,7 +409,7 @@ impl Endowment, Unset>, Salt, - R, + RetType, > where E: Environment, @@ -318,8 +419,15 @@ where pub fn exec_input( self, exec_input: ExecutionInput, - ) -> CreateBuilder>, Salt, R> - { + ) -> CreateBuilder< + E, + CodeHash, + GasLimit, + Endowment, + Set>, + Salt, + RetType, + > { CreateBuilder { code_hash: self.code_hash, gas_limit: self.gas_limit, @@ -332,8 +440,8 @@ where } } -impl - CreateBuilder, R> +impl + CreateBuilder, RetType> where E: Environment, { @@ -342,7 +450,7 @@ where pub fn salt_bytes( self, salt: Salt, - ) -> CreateBuilder, R> + ) -> CreateBuilder, RetType> where Salt: AsRef<[u8]>, { @@ -358,7 +466,38 @@ where } } -impl +impl + CreateBuilder>> +where + E: Environment, +{ + /// Sets the type of the returned value upon the execution of the constructor. + /// + /// # Note + /// + /// Constructors are not able to return arbitrary values. Instead a successful call to a + /// constructor returns the address at which the contract was instantiated. + /// + /// Therefore this must always be a reference (i.e `ContractRef`) to the contract you're trying + /// to instantiate. + #[inline] + pub fn returns( + self, + ) -> CreateBuilder>> + { + CreateBuilder { + code_hash: self.code_hash, + gas_limit: self.gas_limit, + endowment: self.endowment, + exec_input: self.exec_input, + salt: self.salt, + return_type: Set(Default::default()), + _phantom: Default::default(), + } + } +} + +impl CreateBuilder< E, Set, @@ -366,27 +505,27 @@ impl Set, Set>, Set, - R, + Set>, > where E: Environment, GasLimit: Unwrap, { - /// Sets the value transferred upon the execution of the call. + /// Finalizes the create builder, allowing it to instantiate a contract. #[inline] - pub fn params(self) -> CreateParams { + pub fn params(self) -> CreateParams { CreateParams { code_hash: self.code_hash.value(), gas_limit: self.gas_limit.unwrap_or_else(|| 0), endowment: self.endowment.value(), exec_input: self.exec_input.value(), salt_bytes: self.salt.value(), - _return_type: self.return_type, + _return_type: Default::default(), } } } -impl +impl CreateBuilder< E, Set, @@ -394,23 +533,24 @@ impl Set, Set>, Set, - R, + Set>, > where E: Environment, GasLimit: Unwrap, Args: scale::Encode, Salt: AsRef<[u8]>, - R: FromAccountId, + RetType: FromAccountId, { /// Instantiates the contract and returns its account ID back to the caller. /// /// # Panics /// - /// This method panics if it encounters an [`ink::env::Error`][`crate::Error`]. If you want to - /// handle those use the [`try_instantiate`][`CreateBuilder::try_instantiate`] method instead. + /// This method panics if it encounters an [`ink::env::Error`][`crate::Error`] or an + /// [`ink::primitives::LangError`][`ink_primitives::LangError`]. If you want to handle those + /// use the [`try_instantiate`][`CreateBuilder::try_instantiate`] method instead. #[inline] - pub fn instantiate(self) -> R { + pub fn instantiate(self) -> RetType { self.params().instantiate() } @@ -418,10 +558,60 @@ where /// /// # Note /// - /// On failure this returns an [`ink::env::Error`][`crate::Error`] which can be handled by the - /// caller. + /// On failure this returns an outer [`ink::env::Error`][`crate::Error`] or inner + /// [`ink::primitives::LangError`][`ink_primitives::LangError`], both of which can be handled + /// by the caller. #[inline] - pub fn try_instantiate(self) -> Result { + pub fn try_instantiate( + self, + ) -> Result, Error> { self.params().try_instantiate() } } + +impl + CreateBuilder< + E, + Set, + GasLimit, + Set, + Set>, + Set, + Set>>, + > +where + E: Environment, + GasLimit: Unwrap, + Args: scale::Encode, + Salt: AsRef<[u8]>, + RetType: FromAccountId, + ContractError: scale::Decode, +{ + /// Attempts to instantiate the contract, returning the execution result back to the caller. + /// + /// # Panics + /// + /// This method panics if it encounters an [`ink::env::Error`][`crate::Error`] or an + /// [`ink::primitives::LangError`][`ink_primitives::LangError`]. If you want to handle those + /// use the [`try_instantiate_fallible`][`CreateParams::try_instantiate_fallible`] method + /// instead. + #[inline] + pub fn instantiate_fallible(self) -> Result { + self.params().instantiate_fallible() + } + + /// Attempts to instantiate the contract, returning the execution result back to the caller. + /// + /// # Note + /// + /// On failure this returns an outer [`ink::env::Error`][`crate::Error`] or inner + /// [`ink::primitives::LangError`][`ink_primitives::LangError`], both of which can be handled + /// by the caller. + #[inline] + pub fn try_instantiate_fallible( + self, + ) -> Result>, Error> + { + self.params().try_instantiate_fallible() + } +} diff --git a/crates/env/src/engine/mod.rs b/crates/env/src/engine/mod.rs index 2e74d582e12..440cc5b1032 100644 --- a/crates/env/src/engine/mod.rs +++ b/crates/env/src/engine/mod.rs @@ -12,11 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::backend::{ - EnvBackend, - TypedEnvBackend, +use crate::{ + backend::{ + EnvBackend, + TypedEnvBackend, + }, + Result as EnvResult, }; use cfg_if::cfg_if; +use ink_primitives::ConstructorResult; pub trait OnInstance: EnvBackend + TypedEnvBackend { fn on_instance(f: F) -> R @@ -37,3 +41,113 @@ cfg_if! { } } } + +// The `Result` type used to represent the programmer defined contract output. +type ContractResult = core::result::Result; + +// We only use this function when 1) compiling to Wasm 2) compiling for tests. +#[cfg_attr(all(feature = "std", not(test)), allow(dead_code))] +pub(crate) fn decode_fallible_constructor_reverted_return_value( + out_return_value: &mut I, +) -> EnvResult>> +where + I: scale::Input, + E: crate::Environment, + ContractError: scale::Decode, +{ + let out: ConstructorResult> = + scale::Decode::decode(out_return_value)?; + + match out { + ConstructorResult::Ok(ContractResult::Ok(())) => { + // Since the contract reverted we don't expect an `Ok` return value from the + // constructor, otherwise we'd be in the `AccountId` decoding branch. + // + // While we could handle this more gracefully, e.g through a `LangError`, we're going to + // be defensive for now and trap. + panic!( + "The callee reverted, but did not encode an error in the output buffer." + ) + } + ConstructorResult::Ok(ContractResult::Err(contract_error)) => { + Ok(ConstructorResult::Ok(ContractResult::Err(contract_error))) + } + ConstructorResult::Err(lang_error) => Ok(ConstructorResult::Err(lang_error)), + } +} + +#[cfg(test)] +mod fallible_constructor_reverted_tests { + use super::*; + use scale::Encode; + + #[derive(scale::Encode, scale::Decode)] + struct ContractError(String); + + fn encode_and_decode_return_value( + return_value: ConstructorResult>, + ) -> EnvResult>> + { + let encoded_return_value = return_value.encode(); + decode_return_value(&mut &encoded_return_value[..]) + } + + fn decode_return_value( + input: &mut I, + ) -> EnvResult>> + { + decode_fallible_constructor_reverted_return_value::< + I, + crate::DefaultEnvironment, + ContractError, + >(input) + } + + #[test] + #[should_panic( + expected = "The callee reverted, but did not encode an error in the output buffer." + )] + fn revert_branch_rejects_valid_output_buffer_with_success_case() { + let return_value = ConstructorResult::Ok(ContractResult::Ok(())); + + let _decoded_result = encode_and_decode_return_value(return_value); + } + + #[test] + fn succesful_dispatch_with_error_from_contract_constructor() { + let return_value = ConstructorResult::Ok(ContractResult::Err(ContractError( + "Contract's constructor failed.".to_owned(), + ))); + + let decoded_result = encode_and_decode_return_value(return_value); + + assert!(matches!( + decoded_result, + EnvResult::Ok(ConstructorResult::Ok(ContractResult::Err(ContractError(_)))) + )) + } + + #[test] + fn dispatch_error_gets_decoded_correctly() { + let return_value = + ConstructorResult::Err(ink_primitives::LangError::CouldNotReadInput); + + let decoded_result = encode_and_decode_return_value(return_value); + + assert!(matches!( + decoded_result, + EnvResult::Ok(ConstructorResult::Err( + ink_primitives::LangError::CouldNotReadInput + )) + )) + } + + #[test] + fn invalid_bytes_in_output_buffer_fail_decoding() { + let invalid_encoded_return_value = vec![69]; + + let decoded_result = decode_return_value(&mut &invalid_encoded_return_value[..]); + + assert!(matches!(decoded_result, Err(crate::Error::Decode(_)))) + } +} diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index a17bdc3a5ff..771d8608054 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -469,10 +469,10 @@ impl TypedEnvBackend for EnvInstance { ) } - fn instantiate_contract( + fn instantiate_contract( &mut self, - params: &CreateParams, - ) -> Result + params: &CreateParams, + ) -> Result> where E: Environment, Args: scale::Encode, @@ -486,6 +486,28 @@ impl TypedEnvBackend for EnvInstance { unimplemented!("off-chain environment does not support contract instantiation") } + fn instantiate_fallible_contract( + &mut self, + params: &CreateParams, + ) -> Result< + ink_primitives::ConstructorResult< + core::result::Result, + >, + > + where + E: Environment, + Args: scale::Encode, + Salt: AsRef<[u8]>, + ContractError: scale::Decode, + { + let _code_hash = params.code_hash(); + let _gas_limit = params.gas_limit(); + let _endowment = params.endowment(); + let _input = params.exec_input(); + let _salt_bytes = params.salt_bytes(); + unimplemented!("off-chain environment does not support contract instantiation") + } + fn terminate_contract(&mut self, beneficiary: E::AccountId) -> ! where E: Environment, diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs index 6f3b7572c04..3b98fa02ec9 100644 --- a/crates/env/src/engine/on_chain/impls.rs +++ b/crates/env/src/engine/on_chain/impls.rs @@ -479,10 +479,10 @@ impl TypedEnvBackend for EnvInstance { } } - fn instantiate_contract( + fn instantiate_contract( &mut self, - params: &CreateParams, - ) -> Result + params: &CreateParams, + ) -> Result> where E: Environment, Args: scale::Encode, @@ -499,11 +499,63 @@ impl TypedEnvBackend for EnvInstance { let out_address = &mut scoped.take(1024); let salt = params.salt_bytes().as_ref(); let out_return_value = &mut scoped.take_rest(); - // We currently do nothing with the `out_return_value` buffer. - // This should change in the future but for that we need to add support - // for constructors that may return values. - // This is useful to support fallible constructors for example. - ext::instantiate( + + let instantiate_result = ext::instantiate( + enc_code_hash, + gas_limit, + enc_endowment, + enc_input, + out_address, + out_return_value, + salt, + ); + + match instantiate_result { + Ok(()) => { + let account_id = scale::Decode::decode(&mut &out_address[..])?; + Ok(Ok(account_id)) + } + Err(ext::Error::CalleeReverted) => { + // We don't wrap manually with an extra `Err` like we do in the `Ok` case since the + // buffer already comes back in the form of `Err(LangError)` (assuming it's encoded + // by the ink! codegen and not the contract). + let out = ink_primitives::ConstructorResult::::decode( + &mut &out_return_value[..], + )?; + assert!(out.is_err(), "The callee reverted, but did not encode an error in the output buffer."); + Ok(out) + } + Err(actual_error) => Err(actual_error.into()), + } + } + + fn instantiate_fallible_contract( + &mut self, + params: &CreateParams, + ) -> Result< + ink_primitives::ConstructorResult< + core::result::Result, + >, + > + where + E: Environment, + Args: scale::Encode, + Salt: AsRef<[u8]>, + ContractError: scale::Decode, + { + let mut scoped = self.scoped_buffer(); + let gas_limit = params.gas_limit(); + let enc_code_hash = scoped.take_encoded(params.code_hash()); + let enc_endowment = scoped.take_encoded(params.endowment()); + let enc_input = scoped.take_encoded(params.exec_input()); + // We support `AccountId` types with an encoding that requires up to + // 1024 bytes. Beyond that limit ink! contracts will trap for now. + // In the default configuration encoded `AccountId` require 32 bytes. + let out_address = &mut scoped.take(1024); + let salt = params.salt_bytes().as_ref(); + let out_return_value = &mut scoped.take_rest(); + + let instantiate_result = ext::instantiate( enc_code_hash, gas_limit, enc_endowment, @@ -511,9 +563,23 @@ impl TypedEnvBackend for EnvInstance { out_address, out_return_value, salt, - )?; - let account_id = scale::Decode::decode(&mut &out_address[..])?; - Ok(account_id) + ); + + match instantiate_result { + Ok(()) => { + let account_id: E::AccountId = + scale::Decode::decode(&mut &out_address[..])?; + Ok(ink_primitives::ConstructorResult::Ok(Ok(account_id))) + } + Err(ext::Error::CalleeReverted) => { + crate::engine::decode_fallible_constructor_reverted_return_value::< + _, + E, + ContractError, + >(&mut &out_return_value[..]) + } + Err(actual_error) => Err(actual_error.into()), + } } fn terminate_contract(&mut self, beneficiary: E::AccountId) -> ! 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 4c03e90e6f1..a1242d5cdeb 100644 --- a/crates/ink/codegen/src/generator/as_dependency/contract_ref.rs +++ b/crates/ink/codegen/src/generator/as_dependency/contract_ref.rs @@ -403,6 +403,10 @@ impl ContractRef<'_> { let input_bindings = generator::input_bindings(constructor.inputs()); let input_types = generator::input_types(constructor.inputs()); let arg_list = generator::generate_argument_list(input_types.iter().cloned()); + let ret_type = constructor + .output() + .map(quote::ToTokens::to_token_stream) + .unwrap_or_else(|| quote::quote! { Self }); quote_spanned!(span => #( #attrs )* #[inline] @@ -416,9 +420,9 @@ impl ContractRef<'_> { ::ink::env::call::utils::Unset, ::ink::env::call::utils::Set<::ink::env::call::ExecutionInput<#arg_list>>, ::ink::env::call::utils::Unset<::ink::env::call::state::Salt>, - Self, + ::ink::env::call::utils::Set<::ink::env::call::utils::ReturnType<#ret_type>>, > { - ::ink::env::call::build_create::() + ::ink::env::call::build_create::() .exec_input( ::ink::env::call::ExecutionInput::new( ::ink::env::call::Selector::new([ #( #selector_bytes ),* ]) @@ -427,6 +431,7 @@ impl ContractRef<'_> { .push_arg(#input_bindings) )* ) + .returns::<#ret_type>() } ) } diff --git a/crates/ink/src/env_access.rs b/crates/ink/src/env_access.rs index cd691cfb875..59b3ad376b8 100644 --- a/crates/ink/src/env_access.rs +++ b/crates/ink/src/env_access.rs @@ -444,19 +444,28 @@ where /// /// Instantiates another contract. /// #[ink(message)] /// pub fn instantiate_contract(&self) -> AccountId { - /// let create_params = build_create::() + /// let create_params = build_create::() /// .code_hash(Hash::from([0x42; 32])) /// .gas_limit(4000) /// .endowment(25) /// .exec_input( - /// ExecutionInput::new(Selector::new([0xCA, 0xFE, 0xBA, 0xBE])) + /// ExecutionInput::new(Selector::new(ink::selector_bytes!("new"))) /// .push_arg(42) /// .push_arg(true) - /// .push_arg(&[0x10u8; 32]) - /// ) + /// .push_arg(&[0x10u8; 32]), + /// ) /// .salt_bytes(&[0xCA, 0xFE, 0xBA, 0xBE]) + /// .returns::() /// .params(); - /// self.env().instantiate_contract(&create_params).unwrap_or_else(|err| panic!("instantiation must succeed: {:?}", err)) + /// self.env() + /// .instantiate_contract(&create_params) + /// .unwrap_or_else(|error| { + /// panic!( + /// "Received an error from the Contracts pallet while instantiating: {:?}", + /// error + /// ) + /// }) + /// .unwrap_or_else(|error| panic!("Received a `LangError` while instatiating: {:?}", error)) /// } /// # /// # } @@ -469,15 +478,108 @@ where /// # Note /// /// For more details visit: [`ink_env::instantiate_contract`] - pub fn instantiate_contract( + pub fn instantiate_contract( + self, + params: &CreateParams, + ) -> Result> + where + Args: scale::Encode, + Salt: AsRef<[u8]>, + { + ink_env::instantiate_contract::(params) + } + + /// Attempts to instantiate a contract, returning the execution result back to the caller. + /// + /// # Example + /// + /// ``` + /// # #[ink::contract] + /// # pub mod my_contract { + /// # // In order for this to actually work with another contract we'd need a way + /// # // to turn the `ink-as-dependency` crate feature on in doctests, which we + /// # // can't do. + /// # // + /// # // Instead we use our own contract's `Ref`, which is fine for this example + /// # // (just need something that implements the `ContractRef` trait). + /// # pub mod other_contract { + /// # pub use super::MyContractRef as OtherContractRef; + /// # pub use super::ConstructorError as OtherConstructorError; + /// # } + /// use ink::env::{ + /// DefaultEnvironment, + /// call::{build_create, Selector, ExecutionInput} + /// }; + /// use other_contract::{OtherContractRef, OtherConstructorError}; + /// + /// # #[derive(scale::Encode, scale::Decode, Debug)] + /// # #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + /// # pub struct ConstructorError; + /// # + /// # #[ink(storage)] + /// # pub struct MyContract { } + /// # + /// # impl MyContract { + /// # #[ink(constructor)] + /// # pub fn try_new() -> Result { + /// # Ok(Self {}) + /// # } + /// # + /// /// Attempts to instantiate a contract, returning the `AccountId` back to the caller. + /// #[ink(message)] + /// pub fn instantiate_fallible_contract(&self) -> AccountId { + /// let create_params = build_create::() + /// .code_hash(Hash::from([0x42; 32])) + /// .gas_limit(4000) + /// .endowment(25) + /// .exec_input( + /// ExecutionInput::new(Selector::new(ink::selector_bytes!("try_new"))) + /// .push_arg(42) + /// .push_arg(true) + /// .push_arg(&[0x10u8; 32]), + /// ) + /// .salt_bytes(&[0xCA, 0xFE, 0xBA, 0xBE]) + /// .returns::>() + /// .params(); + /// self.env() + /// .instantiate_fallible_contract(&create_params) + /// .unwrap_or_else(|error| { + /// panic!( + /// "Received an error from the Contracts pallet while instantiating: {:?}", + /// error + /// ) + /// }) + /// .unwrap_or_else(|error| panic!("Received a `LangError` while instatiating: {:?}", error)) + /// .unwrap_or_else(|error: ConstructorError| { + /// panic!( + /// "Received a `ConstructorError` while instatiating: {:?}", + /// error + /// ) + /// }) + /// } + /// # + /// # } + /// # } + /// ``` + /// + /// # Note + /// + /// For more details visit: [`ink_env::instantiate_fallible_contract`] + pub fn instantiate_fallible_contract( self, - params: &CreateParams, - ) -> Result + params: &CreateParams, + ) -> Result< + ink_primitives::ConstructorResult< + core::result::Result, + >, + > where + E: Environment, Args: scale::Encode, Salt: AsRef<[u8]>, + ContractError: scale::Decode, { - ink_env::instantiate_contract::(params) + ink_env::instantiate_fallible_contract::(params) } /// Invokes a contract message and returns its result. diff --git a/examples/lang-err-integration-tests/call-builder/lib.rs b/examples/lang-err-integration-tests/call-builder/lib.rs index 2099ebe7e40..6b4638d01a1 100755 --- a/examples/lang-err-integration-tests/call-builder/lib.rs +++ b/examples/lang-err-integration-tests/call-builder/lib.rs @@ -2,8 +2,16 @@ //! //! This contract is used to ensure that the behavior around `LangError`s works as expected. //! -//! It makes use of ink!'s end-to-end testing features, so ensure that you have a node which -//! includes the Contract's pallet running alongside your tests. +//! In particular, it exercises the codepaths that stem from the usage of the +//! [`CallBuilder`](`ink::env::call::CallBuilder`) and [`CreateBuilder`](`ink::env::call::CreateBuilder`) +//! structs. +//! +//! This differs from the codepath used by external tooling, such as `cargo-contract` or the +//! `Contracts-UI` which instead depend on methods from the Contracts pallet which are exposed via +//! RPC. +//! +//! Note that during testing we make use of ink!'s end-to-end testing features, so ensure that you +//! have a node which includes the Contracts pallet running alongside your tests. #![cfg_attr(not(feature = "std"), no_std)] @@ -11,6 +19,8 @@ mod call_builder { use ink::env::{ call::{ + build_call, + build_create, Call, ExecutionInput, Selector, @@ -32,14 +42,15 @@ mod call_builder { /// /// Since we can't use the `CallBuilder` in a test environment directly we need this /// wrapper to test things like crafting calls with invalid selectors. + /// + /// We also wrap the output in an `Option` since we can't return a `Result` directly from a + /// contract message without erroring out ourselves. #[ink(message)] pub fn call( &mut self, address: AccountId, selector: [u8; 4], - ) -> Option<::ink::LangError> { - use ink::env::call::build_call; - + ) -> Option { let result = build_call::() .call_type(Call::new().callee(address)) .exec_input(ExecutionInput::new(Selector::new(selector))) @@ -74,32 +85,80 @@ mod call_builder { .invoke() } + /// Instantiate a contract using the `CreateBuilder`. + /// + /// Since we can't use the `CreateBuilder` in a test environment directly we need this + /// wrapper to test things like crafting calls with invalid selectors. + /// + /// We also wrap the output in an `Option` since we can't return a `Result` directly from a + /// contract message without erroring out ourselves. #[ink(message)] pub fn call_instantiate( &mut self, code_hash: Hash, selector: [u8; 4], init_value: bool, - ) -> Option { - use ink::env::call::build_create; - - let result = build_create::< - DefaultEnvironment, - constructors_return_value::ConstructorsReturnValueRef, - >() - .code_hash(code_hash) - .gas_limit(0) - .endowment(0) - .exec_input(ExecutionInput::new(Selector::new(selector)).push_arg(init_value)) - .salt_bytes(&[0xDE, 0xAD, 0xBE, 0xEF]) - .params() - .try_instantiate(); - - // NOTE: Right now we can't handle any `LangError` from `instantiate`, we can only tell - // that our contract reverted (i.e we see error from the Contracts pallet). - // - // This will be fixed with #1512. - result.ok().map(|id| ink::ToAccountId::to_account_id(&id)) + ) -> Option { + let result = build_create::() + .code_hash(code_hash) + .gas_limit(0) + .endowment(0) + .exec_input( + ExecutionInput::new(Selector::new(selector)).push_arg(init_value), + ) + .salt_bytes(&[0xDE, 0xAD, 0xBE, 0xEF]) + .returns::() + .params() + .try_instantiate() + .expect("Error from the Contracts pallet."); + + match result { + Ok(_) => None, + Err(e @ ink::LangError::CouldNotReadInput) => Some(e), + Err(_) => { + unimplemented!("No other `LangError` variants exist at the moment.") + } + } + } + + /// Attempt to instantiate a contract using the `CreateBuilder`. + /// + /// Since we can't use the `CreateBuilder` in a test environment directly we need this + /// wrapper to test things like crafting calls with invalid selectors. + /// + /// We also wrap the output in an `Option` since we can't return a `Result` directly from a + /// contract message without erroring out ourselves. + #[ink(message)] + pub fn call_instantiate_fallible( + &mut self, + code_hash: Hash, + selector: [u8; 4], + init_value: bool, + ) -> Option< + Result< + Result, + ink::LangError, + >, + > { + let lang_result = build_create::() + .code_hash(code_hash) + .gas_limit(0) + .endowment(0) + .exec_input( + ExecutionInput::new(Selector::new(selector)).push_arg(init_value), + ) + .salt_bytes(&[0xDE, 0xAD, 0xBE, 0xEF]) + .returns::>() + .params() + .try_instantiate_fallible() + .expect("Error from the Contracts pallet."); + + Some(lang_result.map(|contract_result| { + contract_result.map(|inner| ink::ToAccountId::to_account_id(&inner)) + })) } } @@ -119,7 +178,7 @@ mod call_builder { ) -> E2EResult<()> { let constructor = CallBuilderTestRef::new(); let contract_acc_id = client - .instantiate("call_builder", &ink_e2e::charlie(), constructor, 0, None) + .instantiate("call_builder", &ink_e2e::alice(), constructor, 0, None) .await .expect("instantiate failed") .account_id; @@ -128,7 +187,7 @@ mod call_builder { let flipper_acc_id = client .instantiate( "integration_flipper", - &ink_e2e::charlie(), + &ink_e2e::alice(), flipper_constructor, 0, None, @@ -140,16 +199,16 @@ mod call_builder { let flipper_get = build_message::(flipper_acc_id) .call(|contract| contract.get()); let get_call_result = client - .call(&ink_e2e::charlie(), flipper_get, 0, None) + .call(&ink_e2e::alice(), flipper_get, 0, None) .await .expect("Calling `flipper::get` failed"); let initial_value = get_call_result.return_value(); - let invalid_selector = [0x00, 0x00, 0x00, 0x00]; + let selector = ink::selector_bytes!("invalid_selector"); let call = build_message::(contract_acc_id) - .call(|contract| contract.call(flipper_acc_id, invalid_selector)); + .call(|contract| contract.call(flipper_acc_id, selector)); let call_result = client - .call(&ink_e2e::charlie(), call, 0, None) + .call(&ink_e2e::alice(), call, 0, None) .await .expect("Calling `call_builder::call` failed"); @@ -157,13 +216,13 @@ mod call_builder { assert!(matches!( flipper_result, - Some(::ink::LangError::CouldNotReadInput) + Some(ink::LangError::CouldNotReadInput) )); let flipper_get = build_message::(flipper_acc_id) .call(|contract| contract.get()); let get_call_result = client - .call(&ink_e2e::charlie(), flipper_get, 0, None) + .call(&ink_e2e::alice(), flipper_get, 0, None) .await .expect("Calling `flipper::get` failed"); let flipped_value = get_call_result.return_value(); @@ -222,30 +281,31 @@ mod call_builder { ) -> E2EResult<()> { let constructor = CallBuilderTestRef::new(); let contract_acc_id = client - .instantiate("call_builder", &ink_e2e::dave(), constructor, 0, None) + .instantiate("call_builder", &ink_e2e::bob(), constructor, 0, None) .await .expect("instantiate failed") .account_id; let code_hash = client - .upload("constructors_return_value", &ink_e2e::dave(), None) + .upload("constructors_return_value", &ink_e2e::bob(), None) .await .expect("upload `constructors_return_value` failed") .code_hash; - let new_selector = [0x9B, 0xAE, 0x9D, 0x5E]; + let selector = ink::selector_bytes!("new"); + let init_value = true; let call = build_message::(contract_acc_id).call(|contract| { - contract.call_instantiate(code_hash, new_selector, true) + contract.call_instantiate(code_hash, selector, init_value) }); let call_result = client - .call(&ink_e2e::dave(), call, 0, None) + .call(&ink_e2e::bob(), call, 0, None) .await .expect("Client failed to call `call_builder::call_instantiate`.") .return_value(); assert!( - call_result.is_some(), + call_result.is_none(), "Call using valid selector failed, when it should've succeeded." ); @@ -258,34 +318,245 @@ mod call_builder { ) -> E2EResult<()> { let constructor = CallBuilderTestRef::new(); let contract_acc_id = client - .instantiate("call_builder", &ink_e2e::eve(), constructor, 0, None) + .instantiate("call_builder", &ink_e2e::charlie(), constructor, 0, None) .await .expect("instantiate failed") .account_id; let code_hash = client - .upload("constructors_return_value", &ink_e2e::eve(), None) + .upload("constructors_return_value", &ink_e2e::charlie(), None) .await .expect("upload `constructors_return_value` failed") .code_hash; - let invalid_selector = [0x00, 0x00, 0x00, 0x00]; + let selector = ink::selector_bytes!("invalid_selector"); + let init_value = true; let call = build_message::(contract_acc_id).call(|contract| { - contract.call_instantiate(code_hash, invalid_selector, true) + contract.call_instantiate(code_hash, selector, init_value) }); let call_result = client - .call(&ink_e2e::eve(), call, 0, None) + .call(&ink_e2e::charlie(), call, 0, None) .await .expect("Client failed to call `call_builder::call_instantiate`.") .return_value(); assert!( - call_result.is_none(), + matches!(call_result, Some(ink::LangError::CouldNotReadInput)), "Call using invalid selector succeeded, when it should've failed." ); Ok(()) } + + #[ink_e2e::test(additional_contracts = "../constructors-return-value/Cargo.toml")] + async fn e2e_create_builder_with_infallible_revert_constructor_encodes_ok( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let constructor = CallBuilderTestRef::new(); + let contract_acc_id = client + .instantiate("call_builder", &ink_e2e::dave(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let code_hash = client + .upload("constructors_return_value", &ink_e2e::dave(), None) + .await + .expect("upload `constructors_return_value` failed") + .code_hash; + + let selector = ink::selector_bytes!("revert_new"); + let init_value = false; + let call = + build_message::(contract_acc_id).call(|contract| { + contract.call_instantiate(code_hash, selector, init_value) + }); + + let call_result = client.call(&mut ink_e2e::dave(), call, 0, None).await; + assert!( + call_result.is_err(), + "Call execution should've failed, but didn't." + ); + let contains_err_msg = match call_result.unwrap_err() { + ink_e2e::Error::CallDryRun(dry_run) => { + String::from_utf8_lossy(&dry_run.debug_message) + .contains("The callee reverted, but did not encode an error in the output buffer.") + } + _ => false, + }; + assert!( + contains_err_msg, + "Call execution failed for an unexpected reason." + ); + + Ok(()) + } + + #[ink_e2e::test(additional_contracts = "../constructors-return-value/Cargo.toml")] + async fn e2e_create_builder_can_handle_fallible_constructor_success( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let constructor = CallBuilderTestRef::new(); + let contract_acc_id = client + .instantiate("call_builder", &ink_e2e::eve(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let code_hash = client + .upload("constructors_return_value", &ink_e2e::eve(), None) + .await + .expect("upload `constructors_return_value` failed") + .code_hash; + + let selector = ink::selector_bytes!("try_new"); + let init_value = true; + let call = + build_message::(contract_acc_id).call(|contract| { + contract.call_instantiate_fallible(code_hash, selector, init_value) + }); + let call_result = client + .call(&mut ink_e2e::eve(), call, 0, None) + .await + .expect("Calling `call_builder::call_instantiate_fallible` failed") + .return_value(); + + assert!( + matches!(call_result, Some(Ok(_))), + "Call to falliable constructor failed, when it should have succeeded." + ); + + Ok(()) + } + + #[ink_e2e::test(additional_contracts = "../constructors-return-value/Cargo.toml")] + async fn e2e_create_builder_can_handle_fallible_constructor_error( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let constructor = CallBuilderTestRef::new(); + let contract_acc_id = client + .instantiate("call_builder", &ink_e2e::ferdie(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let code_hash = client + .upload("constructors_return_value", &ink_e2e::ferdie(), None) + .await + .expect("upload `constructors_return_value` failed") + .code_hash; + + let selector = ink::selector_bytes!("try_new"); + let init_value = false; + let call = + build_message::(contract_acc_id).call(|contract| { + contract.call_instantiate_fallible(code_hash, selector, init_value) + }); + let call_result = client + .call(&mut ink_e2e::ferdie(), call, 0, None) + .await + .expect("Calling `call_builder::call_instantiate_fallible` failed") + .return_value(); + + let contract_result = call_result + .unwrap() + .expect("Dispatching `constructors_return_value::try_new` failed."); + + assert!( + matches!( + contract_result, + Err(constructors_return_value::ConstructorError) + ), + "Got an unexpected error from the contract." + ); + + Ok(()) + } + + #[ink_e2e::test(additional_contracts = "../constructors-return-value/Cargo.toml")] + async fn e2e_create_builder_with_fallible_revert_constructor_encodes_ok( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let constructor = CallBuilderTestRef::new(); + let contract_acc_id = client + .instantiate("call_builder", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let code_hash = client + .upload("constructors_return_value", &ink_e2e::alice(), None) + .await + .expect("upload `constructors_return_value` failed") + .code_hash; + + let selector = ink::selector_bytes!("try_revert_new"); + let init_value = true; + let call = + build_message::(contract_acc_id).call(|contract| { + contract.call_instantiate_fallible(code_hash, selector, init_value) + }); + let call_result = client.call(&mut ink_e2e::alice(), call, 0, None).await; + + assert!( + call_result.is_err(), + "Call execution should've failed, but didn't." + ); + + let contains_err_msg = match call_result.unwrap_err() { + ink_e2e::Error::CallDryRun(dry_run) => { + String::from_utf8_lossy(&dry_run.debug_message) + .contains("The callee reverted, but did not encode an error in the output buffer.") + } + _ => false, + }; + assert!( + contains_err_msg, + "Call execution failed for an unexpected reason." + ); + + Ok(()) + } + + #[ink_e2e::test(additional_contracts = "../constructors-return-value/Cargo.toml")] + async fn e2e_create_builder_with_fallible_revert_constructor_encodes_err( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let constructor = CallBuilderTestRef::new(); + let contract_acc_id = client + .instantiate("call_builder", &ink_e2e::bob(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let code_hash = client + .upload("constructors_return_value", &ink_e2e::bob(), None) + .await + .expect("upload `constructors_return_value` failed") + .code_hash; + + let selector = ink::selector_bytes!("try_revert_new"); + let init_value = false; + let call = + build_message::(contract_acc_id).call(|contract| { + contract.call_instantiate_fallible(code_hash, selector, init_value) + }); + let call_result = client + .call(&mut ink_e2e::bob(), call, 0, None) + .await + .expect( + "Client failed to call `call_builder::call_instantiate_fallible`.", + ) + .return_value(); + + assert!( + matches!(call_result, Some(Err(ink::LangError::CouldNotReadInput))), + "The callee manually encoded `CouldNotReadInput` to the output buffer, we should've + gotten that back." + ); + + Ok(()) + } } } diff --git a/examples/lang-err-integration-tests/constructors-return-value/lib.rs b/examples/lang-err-integration-tests/constructors-return-value/lib.rs index 35716615f18..018df58ddf9 100644 --- a/examples/lang-err-integration-tests/constructors-return-value/lib.rs +++ b/examples/lang-err-integration-tests/constructors-return-value/lib.rs @@ -34,6 +34,31 @@ pub mod constructors_return_value { } } + /// A constructor which reverts and fills the output buffer with an erronenously encoded + /// return value. + #[ink(constructor)] + pub fn revert_new(_init_value: bool) -> Self { + ink::env::return_value::>( + ink::env::ReturnFlags::new_with_reverted(true), + &Ok(AccountId::from([0u8; 32])), + ) + } + + /// A constructor which reverts and fills the output buffer with an erronenously encoded + /// return value. + #[ink(constructor)] + pub fn try_revert_new(init_value: bool) -> Result { + let value = if init_value { + Ok(Ok(AccountId::from([0u8; 32]))) + } else { + Err(ink::LangError::CouldNotReadInput) + }; + + ink::env::return_value::< + ink::ConstructorResult>, + >(ink::env::ReturnFlags::new_with_reverted(true), &value) + } + /// Returns the current value of the contract storage. #[ink(message)] pub fn get_value(&self) -> bool { @@ -114,7 +139,7 @@ pub mod constructors_return_value { .expect("Instantiate dry run should succeed"); let data = infallible_constructor_result.result.data; - let decoded_result = Result::<(), ::ink::LangError>::decode(&mut &data[..]) + let decoded_result = Result::<(), ink::LangError>::decode(&mut &data[..]) .expect("Failed to decode constructor Result"); assert!( decoded_result.is_ok(), diff --git a/examples/lang-err-integration-tests/contract-ref/lib.rs b/examples/lang-err-integration-tests/contract-ref/lib.rs index bd6e6108e1f..b38d3ff3dd1 100755 --- a/examples/lang-err-integration-tests/contract-ref/lib.rs +++ b/examples/lang-err-integration-tests/contract-ref/lib.rs @@ -22,6 +22,24 @@ mod contract_ref { Self { flipper } } + #[ink(constructor)] + pub fn try_new(version: u32, flipper_code_hash: Hash, succeed: bool) -> Self { + let salt = version.to_le_bytes(); + let flipper = FlipperRef::try_new(succeed) + .endowment(0) + .code_hash(flipper_code_hash) + .salt_bytes(salt) + .instantiate_fallible() + .unwrap_or_else(|error| { + panic!( + "Received an error from the Flipper constructor while instantiating \ + Flipper {:?}", error + ) + }); + + Self { flipper } + } + #[ink(message)] pub fn flip(&mut self) { self.flipper.flip(); @@ -101,5 +119,72 @@ mod contract_ref { Ok(()) } + + #[ink_e2e::test(additional_contracts = "../integration-flipper/Cargo.toml")] + async fn e2e_fallible_ref_can_be_instantiated( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let flipper_hash = client + .upload("integration_flipper", &ink_e2e::bob(), None) + .await + .expect("uploading `flipper` failed") + .code_hash; + + let succeed = true; + let constructor = ContractRefRef::try_new(0, flipper_hash, succeed); + let contract_acc_id = client + .instantiate("contract_ref", &ink_e2e::bob(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let get_check = build_message::(contract_acc_id.clone()) + .call(|contract| contract.get_check()); + let get_call_result = client + .call(&ink_e2e::bob(), get_check, 0, None) + .await + .expect("Calling `get_check` failed"); + let initial_value = get_call_result.return_value(); + assert!(initial_value); + + Ok(()) + } + + #[ink_e2e::test(additional_contracts = "../integration-flipper/Cargo.toml")] + async fn e2e_fallible_ref_fails_to_be_instantiated( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let flipper_hash = client + .upload("integration_flipper", &ink_e2e::charlie(), None) + .await + .expect("uploading `flipper` failed") + .code_hash; + + let succeed = false; + let constructor = ContractRefRef::try_new(0, flipper_hash, succeed); + let instantiate_result = client + .instantiate("contract_ref", &ink_e2e::charlie(), constructor, 0, None) + .await; + + assert!( + instantiate_result.is_err(), + "Call execution should've failed, but didn't." + ); + + let contains_err_msg = match instantiate_result.unwrap_err() { + ink_e2e::Error::InstantiateDryRun(dry_run) => { + String::from_utf8_lossy(&dry_run.debug_message).contains( + "Received an error from the Flipper constructor while instantiating Flipper FlipperError" + ) + } + _ => false, + }; + assert!( + contains_err_msg, + "Call execution failed for an unexpected reason." + ); + + Ok(()) + } } } diff --git a/examples/lang-err-integration-tests/integration-flipper/lib.rs b/examples/lang-err-integration-tests/integration-flipper/lib.rs index 0df61977285..1a9eeaf70f5 100644 --- a/examples/lang-err-integration-tests/integration-flipper/lib.rs +++ b/examples/lang-err-integration-tests/integration-flipper/lib.rs @@ -12,6 +12,10 @@ pub mod integration_flipper { value: bool, } + #[derive(scale::Encode, scale::Decode, Debug)] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub struct FlipperError; + impl Flipper { /// Creates a new integration_flipper smart contract initialized with the given value. #[ink(constructor)] @@ -25,6 +29,17 @@ pub mod integration_flipper { Self::new(Default::default()) } + /// Attemps to create a new integration_flipper smart contract initialized with the given + /// value. + #[ink(constructor)] + pub fn try_new(succeed: bool) -> Result { + if succeed { + Ok(Self::new(true)) + } else { + Err(FlipperError) + } + } + /// Flips the current value of the Flipper's boolean. #[ink(message)] pub fn flip(&mut self) {