diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 37c0a4b947..84742f4091 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,6 +34,7 @@ variables: ALL_CRATES: "${PURELY_STD_CRATES} ${ALSO_WASM_CRATES}" DELEGATOR_SUBCONTRACTS: "accumulator adder subber" UPGRADEABLE_CONTRACTS: "forward-calls set-code-hash" + LANG_ERR_INTEGRATION_CONTRACTS: "integration-flipper call-builder contract-ref" # TODO `cargo clippy --verbose --all-targets --all-features` for this crate # currently fails on `stable`, but succeeds on `nightly`. This is due to # this fix not yet in stable: https://github.com/rust-lang/rust-clippy/issues/8895. @@ -122,6 +123,7 @@ examples-fmt: # Note that we disable the license header check for the examples, since they are unlicensed. - for example in examples/*/; do if [ "$example" = "examples/upgradeable-contracts/" ]; then continue; fi; + if [ "$example" = "examples/lang-err-integration-tests/" ]; then continue; fi; cargo +nightly fmt --verbose --manifest-path ${example}/Cargo.toml -- --check; done - for contract in ${DELEGATOR_SUBCONTRACTS}; do @@ -130,6 +132,9 @@ examples-fmt: - for contract in ${UPGRADEABLE_CONTRACTS}; do cargo +nightly fmt --verbose --manifest-path ./examples/upgradeable-contracts/${contract}/Cargo.toml -- --check; done + - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do + cargo +nightly fmt --verbose --manifest-path ./examples/lang-err-integration-tests/${contract}/Cargo.toml -- --check; + done - cargo +nightly fmt --verbose --manifest-path ./examples/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml -- --check # This file is not a part of the cargo project, so it wouldn't be formatted the usual way - rustfmt +nightly --verbose --check ./examples/psp22-extension/runtime/psp22-extension-example.rs @@ -161,6 +166,7 @@ examples-clippy-std: script: - for example in examples/*/; do if [ "$example" = "examples/upgradeable-contracts/" ]; then continue; fi; + if [ "$example" = "examples/lang-err-integration-tests/" ]; then continue; fi; cargo clippy --verbose --all-targets --manifest-path ${example}/Cargo.toml -- -D warnings -A $CLIPPY_ALLOWED; done - for contract in ${DELEGATOR_SUBCONTRACTS}; do @@ -169,6 +175,9 @@ examples-clippy-std: - for contract in ${UPGRADEABLE_CONTRACTS}; do cargo clippy --verbose --all-targets --manifest-path ./examples/upgradeable-contracts/${contract}/Cargo.toml -- -D warnings -A $CLIPPY_ALLOWED; done + - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do + cargo clippy --verbose --all-targets --manifest-path ./examples/lang-err-integration-tests/${contract}/Cargo.toml -- -D warnings -A $CLIPPY_ALLOWED; + done - cargo clippy --verbose --all-targets --manifest-path ./examples/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml -- -D warnings -A $CLIPPY_ALLOWED; allow_failure: true @@ -179,6 +188,7 @@ examples-clippy-wasm: script: - for example in examples/*/; do if [ "$example" = "examples/upgradeable-contracts/" ]; then continue; fi; + if [ "$example" = "examples/lang-err-integration-tests/" ]; then continue; fi; cargo clippy --verbose --manifest-path ${example}/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; done - for contract in ${DELEGATOR_SUBCONTRACTS}; do @@ -187,6 +197,9 @@ examples-clippy-wasm: - for contract in ${UPGRADEABLE_CONTRACTS}; do cargo clippy --verbose --manifest-path ./examples/upgradeable-contracts/${contract}/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; done + - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do + cargo clippy --verbose --manifest-path ./examples/lang-err-integration-tests/${contract}/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; + done - cargo clippy --verbose --manifest-path ./examples/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; allow_failure: true @@ -363,6 +376,7 @@ examples-test: - *start-substrate-contracts-node - for example in examples/*/; do if [ "$example" = "examples/upgradeable-contracts/" ]; then continue; fi; + if [ "$example" = "examples/lang-err-integration-tests/" ]; then continue; fi; if grep -q "e2e-tests = \[\]" "${example}/Cargo.toml"; then cargo test --verbose --manifest-path ${example}/Cargo.toml --features e2e-tests; else @@ -375,6 +389,12 @@ examples-test: - for contract in ${UPGRADEABLE_CONTRACTS}; do cargo test --verbose --manifest-path ./examples/upgradeable-contracts/${contract}/Cargo.toml; done + # TODO (#1502): We need to clean before running, otherwise the CI fails with a + # linking error. + - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do + cargo clean --verbose --manifest-path ./examples/lang-err-integration-tests/${contract}/Cargo.toml; + cargo test --verbose --manifest-path ./examples/lang-err-integration-tests/${contract}/Cargo.toml --features e2e-tests; + done - cargo test --verbose --manifest-path ./examples/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml; examples-contract-build: @@ -386,6 +406,7 @@ examples-contract-build: - cargo contract -V - for example in examples/*/; do if [ "$example" = "examples/upgradeable-contracts/" ]; then continue; fi; + if [ "$example" = "examples/lang-err-integration-tests/" ]; then continue; fi; pushd $example && cargo +stable contract build && popd; @@ -394,6 +415,9 @@ examples-contract-build: - for contract in ${UPGRADEABLE_CONTRACTS}; do cargo +stable contract build --manifest-path ./examples/upgradeable-contracts/${contract}/Cargo.toml; done + - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do + cargo +stable contract build --manifest-path ./examples/lang-err-integration-tests/${contract}/Cargo.toml; + done - cargo +stable contract build --manifest-path ./examples/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml examples-docs: @@ -409,6 +433,7 @@ examples-docs: # of this flag. - for example in examples/*/; do if [ "$example" = "examples/upgradeable-contracts/" ]; then continue; fi; + if [ "$example" = "examples/lang-err-integration-tests/" ]; then continue; fi; cargo doc --manifest-path ${example}/Cargo.toml --document-private-items --verbose --no-deps; done - for contract in ${DELEGATOR_SUBCONTRACTS}; do @@ -417,6 +442,9 @@ examples-docs: - for contract in ${UPGRADEABLE_CONTRACTS}; do cargo doc --manifest-path ./examples/upgradeable-contracts/${contract}/Cargo.toml --document-private-items --verbose --no-deps; done + - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do + cargo doc --manifest-path ./examples/lang-err-integration-tests/${contract}/Cargo.toml --document-private-items --verbose --no-deps; + done - cargo doc --manifest-path ./examples/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml --document-private-items --verbose --no-deps diff --git a/crates/ink/codegen/src/generator/as_dependency/call_builder.rs b/crates/ink/codegen/src/generator/as_dependency/call_builder.rs index bea33bf054..9d3232133f 100644 --- a/crates/ink/codegen/src/generator/as_dependency/call_builder.rs +++ b/crates/ink/codegen/src/generator/as_dependency/call_builder.rs @@ -369,10 +369,8 @@ impl CallBuilder<'_> { let input_types = generator::input_types(message.inputs()); let arg_list = generator::generate_argument_list(input_types.iter().cloned()); let mut_tok = callable.receiver().is_ref_mut().then(|| quote! { mut }); - let output = message.output(); - let return_type = - output.map_or_else(|| quote! { () }, |output| quote! { #output }); - let output_span = output.span(); + let return_type = message.wrapped_output(); + let output_span = return_type.span(); let output_type = quote_spanned!(output_span=> ::ink::env::call::CallBuilder< Environment, 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 cfc75401cd..588d14fd8a 100644 --- a/crates/ink/codegen/src/generator/as_dependency/contract_ref.rs +++ b/crates/ink/codegen/src/generator/as_dependency/contract_ref.rs @@ -336,6 +336,7 @@ impl ContractRef<'_> { .filter_attr(message.attrs().to_vec()); let storage_ident = self.contract.module().storage().ident(); let message_ident = message.ident(); + let checked_message_ident = message.checked_ident(); let call_operator = match message.receiver() { ir::Receiver::Ref => quote! { call }, ir::Receiver::RefMut => quote! { call_mut }, @@ -344,6 +345,7 @@ impl ContractRef<'_> { let input_bindings = message.inputs().map(|input| &input.pat).collect::>(); 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(); quote_spanned!(span=> #( #attrs )* #[inline] @@ -351,6 +353,21 @@ impl ContractRef<'_> { & #mut_token self #( , #input_bindings : #input_types )* ) #output_type { + self.#checked_message_ident( #( #input_bindings, )* ) + .unwrap_or_else(|error| ::core::panic!( + "encountered error while calling {}::{}: {:?}", + ::core::stringify!(#storage_ident), + ::core::stringify!(#message_ident), + error, + )) + } + + #( #attrs )* + #[inline] + pub fn #checked_message_ident( + & #mut_token self + #( , #input_bindings : #input_types )* + ) -> #wrapped_output_type { ::#call_operator(self) .#message_ident( #( #input_bindings ),* ) .fire() diff --git a/crates/ink/codegen/src/generator/dispatch.rs b/crates/ink/codegen/src/generator/dispatch.rs index 4df05a9ed6..8875fd8d51 100644 --- a/crates/ink/codegen/src/generator/dispatch.rs +++ b/crates/ink/codegen/src/generator/dispatch.rs @@ -435,16 +435,34 @@ impl Dispatch<'_> { .unwrap_or_else(|error| ::core::panic!("{}", error)) } - ::ink::env::decode_input::< - <#storage_ident as ::ink::reflect::ContractMessageDecoder>::Type>() - .map_err(|_| ::ink::reflect::DispatchError::CouldNotReadInput) - .and_then(|decoder| { - <<#storage_ident as ::ink::reflect::ContractMessageDecoder>::Type - as ::ink::reflect::ExecuteDispatchable>::execute_dispatchable(decoder) - }) - .unwrap_or_else(|error| { - ::core::panic!("dispatching ink! message failed: {}", error) - }) + let dispatchable = match ::ink::env::decode_input::< + <#storage_ident as ::ink::reflect::ContractMessageDecoder>::Type, + >() { + ::core::result::Result::Ok(decoded_dispatchable) => { + decoded_dispatchable + } + ::core::result::Result::Err(_decoding_error) => { + use ::core::default::Default; + let error = ::core::result::Result::Err(::ink::LangError::CouldNotReadInput); + + // At this point we're unable to set the `Ok` variant to be the any "real" + // message output since we were unable to figure out what the caller wanted + // to dispatch in the first place, so we set it to `()`. + // + // This is okay since we're going to only be encoding the `Err` variant + // into the output buffer anyways. + ::ink::env::return_value::<::ink::MessageResult<()>>( + ::ink::env::ReturnFlags::default().set_reverted(true), + &error, + ); + } + }; + + <<#storage_ident as ::ink::reflect::ContractMessageDecoder>::Type + as ::ink::reflect::ExecuteDispatchable>::execute_dispatchable(dispatchable) + .unwrap_or_else(|error| { + ::core::panic!("dispatching ink! message failed: {}", error) + }) } ) } @@ -761,23 +779,24 @@ impl Dispatch<'_> { let failure = ::ink::is_result_type!(#message_output) && ::ink::is_result_err!(result); + // Currently no `LangError`s are raised at this level of the dispatch logic + // so `Ok` is always returned to the caller. + let return_value = ::core::result::Result::Ok(result); + if failure { // We return early here since there is no need to push back the // intermediate results of the contract - the transaction is going to be // reverted anyways. - ::ink::env::return_value::<#message_output>( - ::ink::env::ReturnFlags::default().set_reverted(true), &result + ::ink::env::return_value::<::ink::MessageResult::<#message_output>>( + ::ink::env::ReturnFlags::default().set_reverted(true), &return_value ) } push_contract(contract, #mutates_storage); - if ::core::any::TypeId::of::<#message_output>() != ::core::any::TypeId::of::<()>() { - // In case the return type is `()` we do not return a value. - ::ink::env::return_value::<#message_output>( - ::ink::env::ReturnFlags::default(), &result - ) - } + ::ink::env::return_value::<::ink::MessageResult::<#message_output>>( + ::ink::env::ReturnFlags::default(), &return_value + ) } ) }); @@ -848,8 +867,6 @@ impl Dispatch<'_> { match self { #( #message_execute ),* }; - - ::core::result::Result::Ok(()) } } diff --git a/crates/ink/codegen/src/generator/metadata.rs b/crates/ink/codegen/src/generator/metadata.rs index 04a4e02974..3f7cdc2aa6 100644 --- a/crates/ink/codegen/src/generator/metadata.rs +++ b/crates/ink/codegen/src/generator/metadata.rs @@ -93,6 +93,10 @@ impl Metadata<'_> { .attrs() .iter() .filter_map(|attr| attr.extract_docs()); + let error_ty = syn::parse_quote! { + ::ink::LangError + }; + let error = Self::generate_type_spec(&error_ty); quote! { ::ink::metadata::ContractSpec::new() .constructors([ @@ -107,6 +111,9 @@ impl Metadata<'_> { .docs([ #( #docs ),* ]) + .lang_error( + #error + ) .done() } } @@ -172,6 +179,7 @@ impl Metadata<'_> { fn without_display_name(ty: &syn::Type) -> TokenStream2 { quote! { ::ink::metadata::TypeSpec::of_type::<#ty>() } } + if let syn::Type::Path(type_path) = ty { if type_path.qself.is_some() { return without_display_name(ty) @@ -256,7 +264,7 @@ impl Metadata<'_> { let mutates = message.receiver().is_ref_mut(); let ident = message.ident(); let args = message.inputs().map(Self::generate_dispatch_argument); - let ret_ty = Self::generate_return_type(message.output()); + let ret_ty = Self::generate_return_type(Some(&message.wrapped_output())); quote_spanned!(span => ::ink::metadata::MessageSpec::from_label(::core::stringify!(#ident)) .selector([ diff --git a/crates/ink/ir/src/ir/item_impl/message.rs b/crates/ink/ir/src/ir/item_impl/message.rs index 85ebcb2901..f48814b427 100644 --- a/crates/ink/ir/src/ir/item_impl/message.rs +++ b/crates/ink/ir/src/ir/item_impl/message.rs @@ -291,6 +291,21 @@ impl Message { } } + /// Returns the return type of the message, but wrapped within a `Result`. + /// + /// This is used to to allow callers to handle certain types of errors which are not exposed + /// by messages. + pub fn wrapped_output(&self) -> syn::Type { + let return_type = self + .output() + .map(quote::ToTokens::to_token_stream) + .unwrap_or_else(|| quote::quote! { () }); + + syn::parse_quote! { + ::ink::MessageResult<#return_type> + } + } + /// Returns a local ID unique to the ink! message with respect to its implementation block. /// /// # Note @@ -301,6 +316,11 @@ impl Message { pub fn local_id(&self) -> u32 { utils::local_message_id(self.ident()) } + + /// Returns the identifier of the message with an additional `_checked` suffix attached. + pub fn checked_ident(&self) -> Ident { + quote::format_ident!("{}_checked", self.ident()) + } } #[cfg(test)] diff --git a/crates/ink/src/lib.rs b/crates/ink/src/lib.rs index 2f2370b385..a426811b11 100644 --- a/crates/ink/src/lib.rs +++ b/crates/ink/src/lib.rs @@ -71,3 +71,7 @@ pub use ink_macro::{ test, trait_definition, }; +pub use ink_primitives::{ + LangError, + MessageResult, +}; diff --git a/crates/ink/tests/ui/contract/fail/constructor-return-result-non-codec-error.stderr b/crates/ink/tests/ui/contract/fail/constructor-return-result-non-codec-error.stderr index f26d2e4579..cfcd17fdb1 100644 --- a/crates/ink/tests/ui/contract/fail/constructor-return-result-non-codec-error.stderr +++ b/crates/ink/tests/ui/contract/fail/constructor-return-result-non-codec-error.stderr @@ -36,6 +36,6 @@ error[E0277]: the trait bound `contract::Error: TypeInfo` is not satisfied (A, B, C, D) (A, B, C, D, E) (A, B, C, D, E, F) - and 64 others + and $N others = note: required for `Result<(), contract::Error>` to implement `ConstructorReturnSpec` = note: this error originates in the attribute macro `ink::contract` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/ink/tests/ui/contract/fail/message-hygiene-checked.rs b/crates/ink/tests/ui/contract/fail/message-hygiene-checked.rs new file mode 100644 index 0000000000..4f1c844ceb --- /dev/null +++ b/crates/ink/tests/ui/contract/fail/message-hygiene-checked.rs @@ -0,0 +1,20 @@ +#[ink::contract] +mod contract { + #[ink(storage)] + pub struct Contract {} + + impl Contract { + #[ink(constructor)] + pub fn constructor() -> Self { + Self {} + } + + #[ink(message)] + pub fn message(&self) {} + + #[ink(message)] + pub fn message_checked(&self) {} + } +} + +fn main() {} diff --git a/crates/ink/tests/ui/contract/fail/message-hygiene-checked.stderr b/crates/ink/tests/ui/contract/fail/message-hygiene-checked.stderr new file mode 100644 index 0000000000..cce9b1f97a --- /dev/null +++ b/crates/ink/tests/ui/contract/fail/message-hygiene-checked.stderr @@ -0,0 +1,8 @@ +error[E0201]: duplicate definitions with name `message_checked`: + --> tests/ui/contract/fail/message-hygiene-checked.rs:16:9 + | +1 | #[ink::contract] + | ---------------- previous definition of `message_checked` here +... +16 | pub fn message_checked(&self) {} + | ^^^ duplicate definition diff --git a/crates/ink/tests/ui/contract/fail/message-input-non-codec.stderr b/crates/ink/tests/ui/contract/fail/message-input-non-codec.stderr index 3c4f440e32..e6c2260060 100644 --- a/crates/ink/tests/ui/contract/fail/message-input-non-codec.stderr +++ b/crates/ink/tests/ui/contract/fail/message-input-non-codec.stderr @@ -53,11 +53,11 @@ note: required by a bound in `ExecutionInput::>::push_arg` -error[E0599]: the method `fire` exists for struct `ink::ink_env::call::CallBuilder>, Set, ArgumentList>>>, Set>>`, but its trait bounds were not satisfied +error[E0599]: the method `fire` exists for struct `ink::ink_env::call::CallBuilder>, Set, ArgumentList>>>, Set>>>`, but its trait bounds were not satisfied --> tests/ui/contract/fail/message-input-non-codec.rs:16:9 | 16 | pub fn message(&self, _input: NonCodecType) {} - | ^^^ method cannot be called on `ink::ink_env::call::CallBuilder>, Set, ArgumentList>>>, Set>>` due to unsatisfied trait bounds + | ^^^ method cannot be called on `ink::ink_env::call::CallBuilder>, Set, ArgumentList>>>, Set>>>` due to unsatisfied trait bounds | ::: $WORKSPACE/crates/env/src/call/execution_input.rs | diff --git a/crates/ink/tests/ui/contract/fail/message-returns-non-codec.stderr b/crates/ink/tests/ui/contract/fail/message-returns-non-codec.stderr index 63286eba29..b556536817 100644 --- a/crates/ink/tests/ui/contract/fail/message-returns-non-codec.stderr +++ b/crates/ink/tests/ui/contract/fail/message-returns-non-codec.stderr @@ -21,42 +21,29 @@ note: required by a bound in `DispatchOutput` | T: scale::Encode + 'static; | ^^^^^^^^^^^^^ required by this bound in `DispatchOutput` -error[E0277]: the trait bound `NonCodecType: WrapperTypeEncode` is not satisfied - --> tests/ui/contract/fail/message-returns-non-codec.rs:16:9 - | -16 | pub fn message(&self) -> NonCodecType { - | ^^^ the trait `WrapperTypeEncode` is not implemented for `NonCodecType` - | - = help: the following other types implement trait `WrapperTypeEncode`: - &T - &mut T - Arc - Box - Cow<'a, T> - Rc - String - Vec - parity_scale_codec::Ref<'a, T, U> - = note: required for `NonCodecType` to implement `Encode` +error[E0277]: the trait bound `Result: Encode` is not satisfied + --> tests/ui/contract/fail/message-returns-non-codec.rs:16:9 + | +16 | pub fn message(&self) -> NonCodecType { + | ^^^ the trait `Encode` is not implemented for `Result` + | + = help: the trait `Encode` is implemented for `Result` note: required by a bound in `return_value` - --> $WORKSPACE/crates/env/src/api.rs - | - | R: scale::Encode, - | ^^^^^^^^^^^^^ required by this bound in `return_value` + --> $WORKSPACE/crates/env/src/api.rs + | + | R: scale::Encode, + | ^^^^^^^^^^^^^ required by this bound in `return_value` -error[E0599]: the method `fire` exists for struct `ink::ink_env::call::CallBuilder>, Set>>, Set>>`, but its trait bounds were not satisfied - --> tests/ui/contract/fail/message-returns-non-codec.rs:16:9 - | -4 | pub struct NonCodecType; - | ----------------------- doesn't satisfy `NonCodecType: parity_scale_codec::Decode` -... -16 | pub fn message(&self) -> NonCodecType { - | ^^^ method cannot be called on `ink::ink_env::call::CallBuilder>, Set>>, Set>>` due to unsatisfied trait bounds - | - = note: the following trait bounds were not satisfied: - `NonCodecType: parity_scale_codec::Decode` -note: the following trait must be implemented - --> $CARGO/parity-scale-codec-3.2.1/src/codec.rs - | - | pub trait Decode: Sized { - | ^^^^^^^^^^^^^^^^^^^^^^^ +error[E0599]: the method `fire` exists for struct `ink::ink_env::call::CallBuilder>, Set>>, Set>>>`, but its trait bounds were not satisfied + --> tests/ui/contract/fail/message-returns-non-codec.rs:16:9 + | +16 | pub fn message(&self) -> NonCodecType { + | ^^^ method cannot be called on `ink::ink_env::call::CallBuilder>, Set>>, Set>>>` due to unsatisfied trait bounds + | + ::: $RUST/core/src/result.rs + | + | pub enum Result { + | --------------------- doesn't satisfy `_: parity_scale_codec::Decode` + | + = note: the following trait bounds were not satisfied: + `Result: parity_scale_codec::Decode` diff --git a/crates/ink/tests/ui/contract/pass/message-checked-variant.rs b/crates/ink/tests/ui/contract/pass/message-checked-variant.rs new file mode 100644 index 0000000000..13c42a4f2d --- /dev/null +++ b/crates/ink/tests/ui/contract/pass/message-checked-variant.rs @@ -0,0 +1,23 @@ +#[ink::contract] +mod contract { + #[ink(storage)] + #[derive(Default)] + pub struct Contract {} + + impl Contract { + #[ink(constructor)] + pub fn constructor() -> Self { + Self::default() + } + + #[ink(message)] + pub fn message(&self) {} + } +} + +use contract::Contract; + +fn main() { + let contract = Contract::default(); + contract.message(); +} diff --git a/crates/metadata/Cargo.toml b/crates/metadata/Cargo.toml index b67e6ffd35..7fc22dc550 100644 --- a/crates/metadata/Cargo.toml +++ b/crates/metadata/Cargo.toml @@ -34,6 +34,7 @@ default = [ ] std = [ "ink_prelude/std", + "ink_primitives/std", "serde/std", "scale-info/std", ] diff --git a/crates/metadata/src/lib.rs b/crates/metadata/src/lib.rs index 035bac5a98..bb306a0170 100644 --- a/crates/metadata/src/lib.rs +++ b/crates/metadata/src/lib.rs @@ -28,6 +28,8 @@ pub mod layout; mod specs; mod utils; +pub use ink_primitives::LangError; + pub use self::specs::{ ConstructorReturnSpec, ConstructorSpec, diff --git a/crates/metadata/src/specs.rs b/crates/metadata/src/specs.rs index 567fbe1362..25c6e72d8c 100644 --- a/crates/metadata/src/specs.rs +++ b/crates/metadata/src/specs.rs @@ -57,6 +57,8 @@ pub struct ContractSpec { events: Vec>, /// The contract documentation. docs: Vec, + /// The language specific error type. + lang_error: TypeSpec, } impl IntoPortable for ContractSpec { @@ -79,7 +81,8 @@ impl IntoPortable for ContractSpec { .into_iter() .map(|event| event.into_portable(registry)) .collect::>(), - docs: self.docs.into_iter().map(|s| s.into()).collect(), + docs: registry.map_into_portable(self.docs), + lang_error: self.lang_error.into_portable(registry), } } } @@ -107,6 +110,11 @@ where pub fn docs(&self) -> &[F::String] { &self.docs } + + /// Returns the language error type. + pub fn lang_error(&self) -> &TypeSpec { + &self.lang_error + } } /// The message builder is ready to finalize construction. @@ -193,6 +201,17 @@ where ..self } } + + /// Sets the language error of the contract specification + pub fn lang_error(self, lang_error: TypeSpec) -> Self { + Self { + spec: ContractSpec { + lang_error, + ..self.spec + }, + ..self + } + } } impl ContractSpecBuilder @@ -216,6 +235,7 @@ where impl ContractSpec where F: Form, + TypeSpec: Default, { /// Creates a new contract specification. pub fn new() -> ContractSpecBuilder { @@ -225,6 +245,7 @@ where messages: Vec::new(), events: Vec::new(), docs: Vec::new(), + lang_error: Default::default(), }, marker: PhantomData, } diff --git a/crates/metadata/src/tests.rs b/crates/metadata/src/tests.rs index f1b2b1fb9d..ad396a5ef2 100644 --- a/crates/metadata/src/tests.rs +++ b/crates/metadata/src/tests.rs @@ -112,6 +112,12 @@ fn spec_contract_json() { .done(), ]) .events(Vec::new()) + .lang_error(TypeSpec::with_name_segs::( + ::core::iter::Iterator::map( + ::core::iter::IntoIterator::into_iter(["ink", "LangError"]), + ::core::convert::AsRef::as_ref, + ), + )) .done(); let mut registry = Registry::new(); @@ -168,6 +174,13 @@ fn spec_contract_json() { ], "docs": [], "events": [], + "lang_error": { + "displayName": [ + "ink", + "LangError" + ], + "type": 3 + }, "messages": [ { "args": [ diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 683904c080..e4d5608e2b 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -41,3 +41,21 @@ pub use self::{ Hash, }, }; + +/// An error emitted by the smart contracting language. +/// +/// This is different than errors from: +/// - Errors from the contract, which are programmer defined +/// - Errors from the underlying execution environment (e.g `pallet-contracts`) +#[non_exhaustive] +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, ::scale::Encode, ::scale::Decode)] +#[cfg_attr(feature = "std", derive(::scale_info::TypeInfo))] +pub enum LangError { + /// Failed to read execution input for the dispatchable. + CouldNotReadInput = 1u32, +} + +/// The `Result` type for ink! messages. +#[doc(hidden)] +pub type MessageResult = ::core::result::Result; diff --git a/examples/delegator/lib.rs b/examples/delegator/lib.rs index dbc65516bc..d3e105a0fd 100644 --- a/examples/delegator/lib.rs +++ b/examples/delegator/lib.rs @@ -181,7 +181,8 @@ mod delegator { ) .await .expect("calling `get` failed") - .value; + .value + .expect("calling `get` returned a `LangError`"); assert_eq!(value, 1234); let _ = client .call( @@ -205,7 +206,8 @@ mod delegator { ) .await .expect("calling `get` failed") - .value; + .value + .expect("calling `get` returned a `LangError`"); assert_eq!(value, 1234 + 6); // when @@ -241,7 +243,8 @@ mod delegator { ) .await .expect("calling `get` failed") - .value; + .value + .expect("calling `get` returned a `LangError`"); assert_eq!(value, 1234 + 6 - 3); Ok(()) diff --git a/examples/lang-err-integration-tests/.gitignore b/examples/lang-err-integration-tests/.gitignore new file mode 100755 index 0000000000..8de8f877e4 --- /dev/null +++ b/examples/lang-err-integration-tests/.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/examples/lang-err-integration-tests/call-builder/Cargo.toml b/examples/lang-err-integration-tests/call-builder/Cargo.toml new file mode 100755 index 0000000000..25aaf12a4e --- /dev/null +++ b/examples/lang-err-integration-tests/call-builder/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "call_builder" +version = "4.0.0-alpha.3" +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.3", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e" } + +[lib] +name = "call_builder" +path = "lib.rs" +crate-type = [ + # Used for normal contract Wasm blobs. + "cdylib", +] + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/examples/lang-err-integration-tests/call-builder/lib.rs b/examples/lang-err-integration-tests/call-builder/lib.rs new file mode 100755 index 0000000000..a56fc3b5f3 --- /dev/null +++ b/examples/lang-err-integration-tests/call-builder/lib.rs @@ -0,0 +1,141 @@ +//! # Integration Tests for `LangError` +//! +//! 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. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[ink::contract] +mod call_builder { + + #[ink(storage)] + #[derive(Default)] + pub struct CallBuilderTest {} + + impl CallBuilderTest { + #[ink(constructor)] + pub fn new() -> Self { + Default::default() + } + + /// Call a contract using the `CallBuilder`. + /// + /// 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. + #[ink(message)] + pub fn call( + &mut self, + address: AccountId, + selector: [u8; 4], + ) -> Option<::ink::LangError> { + use ink::env::{ + call::{ + build_call, + Call, + ExecutionInput, + Selector, + }, + DefaultEnvironment, + }; + + let result = build_call::() + .call_type(Call::new().callee(address)) + .exec_input(ExecutionInput::new(Selector::new(selector))) + .returns::>() + .fire() + .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.") + } + } + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + type E2EResult = std::result::Result>; + + #[ink_e2e::test(additional_contracts = "../integration-flipper/Cargo.toml")] + async fn e2e_invalid_selector_can_be_handled( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + use call_builder::contract_types::ink_primitives::{ + types::AccountId as E2EAccountId, + LangError as E2ELangError, + }; + + let constructor = call_builder::constructors::new(); + let contract_acc_id = client + .instantiate(&mut ink_e2e::charlie(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let flipper_constructor = integration_flipper::constructors::default(); + let flipper_acc_id = client + .instantiate(&mut ink_e2e::charlie(), flipper_constructor, 0, None) + .await + .expect("instantiate `flipper` failed") + .account_id; + + let get_call_result = client + .call( + &mut ink_e2e::charlie(), + flipper_acc_id.clone(), + integration_flipper::messages::get(), + 0, + None, + ) + .await + .expect("Calling `flipper::get` failed"); + let initial_value = get_call_result + .value + .expect("Input is valid, call must not fail."); + + let flipper_ink_acc_id = E2EAccountId(flipper_acc_id.clone().into()); + let invalid_selector = [0x00, 0x00, 0x00, 0x00]; + let call_result = client + .call( + &mut ink_e2e::charlie(), + contract_acc_id.clone(), + call_builder::messages::call(flipper_ink_acc_id, invalid_selector), + 0, + None, + ) + .await + .expect("Calling `call_builder::call` failed"); + + let flipper_result = call_result + .value + .expect("Call to `call_builder::call` failed"); + + assert!(matches!( + flipper_result, + Some(E2ELangError::CouldNotReadInput) + )); + + let get_call_result = client + .call( + &mut ink_e2e::charlie(), + flipper_acc_id.clone(), + integration_flipper::messages::get(), + 0, + None, + ) + .await + .expect("Calling `flipper::get` failed"); + let flipped_value = get_call_result + .value + .expect("Input is valid, call must not fail."); + assert!(flipped_value == initial_value); + + Ok(()) + } + } +} diff --git a/examples/lang-err-integration-tests/contract-ref/Cargo.toml b/examples/lang-err-integration-tests/contract-ref/Cargo.toml new file mode 100755 index 0000000000..0c00cff43e --- /dev/null +++ b/examples/lang-err-integration-tests/contract-ref/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "contract_ref" +version = "4.0.0-alpha.3" +authors = ["Parity Technologies "] +edition = "2021" + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"], optional = true } + +integration_flipper = { path = "../integration-flipper", default-features = false, features = ["ink-as-dependency"] } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e" } + +[lib] +name = "contract_ref" +path = "lib.rs" +crate-type = [ + # Used for normal contract Wasm blobs. + "cdylib", +] + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", + + "integration_flipper/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/examples/lang-err-integration-tests/contract-ref/lib.rs b/examples/lang-err-integration-tests/contract-ref/lib.rs new file mode 100755 index 0000000000..5bd914278a --- /dev/null +++ b/examples/lang-err-integration-tests/contract-ref/lib.rs @@ -0,0 +1,129 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[ink::contract] +mod contract_ref { + use integration_flipper::FlipperRef; + + #[ink(storage)] + pub struct ContractRef { + flipper: FlipperRef, + } + + impl ContractRef { + #[ink(constructor)] + pub fn new(version: u32, flipper_code_hash: Hash) -> Self { + let salt = version.to_le_bytes(); + let flipper = FlipperRef::default() + .endowment(0) + .code_hash(flipper_code_hash) + .salt_bytes(salt) + .instantiate() + .unwrap_or_else(|error| { + panic!("failed at instantiating the Flipper contract: {:?}", error) + }); + + Self { flipper } + } + + #[ink(message)] + pub fn flip(&mut self) { + self.flipper.flip(); + } + + #[ink(message)] + pub fn flip_check(&mut self) { + self.flipper + .flip_checked() + .expect("The ink! codegen should've produced a valid call."); + } + + #[ink(message)] + pub fn get(&mut self) -> bool { + self.flipper.get() + } + + #[ink(message)] + pub fn get_check(&mut self) -> bool { + self.flipper + .get_checked() + .expect("The ink! codegen should've produced a valid call.") + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + type E2EResult = std::result::Result>; + + #[ink_e2e::test(additional_contracts = "../integration-flipper/Cargo.toml")] + async fn e2e_ref_can_flip_correctly( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let flipper_hash: ink_e2e::H256 = client + .upload( + &mut ink_e2e::alice(), + integration_flipper::CONTRACT_PATH, + None, + ) + .await + .expect("uploading `flipper` failed") + .code_hash; + let flipper_hash = ink_e2e::utils::runtime_hash_to_ink_hash::< + ink::env::DefaultEnvironment, + >(&flipper_hash); + + let constructor = + contract_ref::constructors::new(Default::default(), flipper_hash); + let contract_acc_id = client + .instantiate(&mut ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let get_call_result = client + .call( + &mut ink_e2e::alice(), + contract_acc_id.clone(), + contract_ref::messages::get_check(), + 0, + None, + ) + .await + .expect("Calling `get_check` failed"); + let initial_value = get_call_result + .value + .expect("Input is valid, call must not fail."); + + let flip_call_result = client + .call( + &mut ink_e2e::alice(), + contract_acc_id.clone(), + contract_ref::messages::flip_check(), + 0, + None, + ) + .await + .expect("Calling `flip` failed"); + assert!( + flip_call_result.value.is_ok(), + "Messages now return a `Result`, which should be `Ok` here." + ); + + let get_call_result = client + .call( + &mut ink_e2e::alice(), + contract_acc_id.clone(), + contract_ref::messages::get_check(), + 0, + None, + ) + .await + .expect("Calling `get_check` failed"); + let flipped_value = get_call_result + .value + .expect("Input is valid, call must not fail."); + assert!(flipped_value != initial_value); + + Ok(()) + } + } +} diff --git a/examples/lang-err-integration-tests/integration-flipper/Cargo.toml b/examples/lang-err-integration-tests/integration-flipper/Cargo.toml new file mode 100644 index 0000000000..7ca0071a07 --- /dev/null +++ b/examples/lang-err-integration-tests/integration-flipper/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "integration_flipper" +version = "4.0.0-alpha.3" +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.3", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e" } + +[lib] +name = "integration_flipper" +path = "lib.rs" +crate-type = [ + # Used for normal contract Wasm blobs. + "cdylib", + # Used for ABI generation. + "rlib", +] + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/examples/lang-err-integration-tests/integration-flipper/lib.rs b/examples/lang-err-integration-tests/integration-flipper/lib.rs new file mode 100644 index 0000000000..a33d95cce8 --- /dev/null +++ b/examples/lang-err-integration-tests/integration-flipper/lib.rs @@ -0,0 +1,171 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use self::integration_flipper::{ + Flipper, + FlipperRef, +}; + +#[ink::contract] +pub mod integration_flipper { + #[ink(storage)] + pub struct Flipper { + value: bool, + } + + impl Flipper { + /// Creates a new integration_flipper smart contract initialized with the given value. + #[ink(constructor)] + pub fn new(init_value: bool) -> Self { + Self { value: init_value } + } + + /// Creates a new integration_flipper smart contract initialized to `false`. + #[ink(constructor)] + pub fn default() -> Self { + Self::new(Default::default()) + } + + /// Flips the current value of the Flipper's boolean. + #[ink(message)] + pub fn flip(&mut self) { + self.value = !self.value; + } + + /// Returns the current value of the Flipper's boolean. + #[ink(message)] + pub fn get(&self) -> bool { + self.value + } + + /// Flips the current value of the Flipper's boolean. + /// + /// We should see the state being reverted here, no write should occur. + #[ink(message)] + #[allow(clippy::result_unit_err)] + pub fn err_flip(&mut self) -> Result<(), ()> { + self.flip(); + Err(()) + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + type E2EResult = std::result::Result>; + + #[ink_e2e::test] + async fn e2e_can_flip_correctly( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let constructor = integration_flipper::constructors::default(); + let contract_acc_id = client + .instantiate(&mut ink_e2e::alice(), constructor, 0, None) + .await + .expect("Instantiate `integration_flipper` failed") + .account_id; + + let get_call_result = client + .call( + &mut ink_e2e::alice(), + contract_acc_id.clone(), + integration_flipper::messages::get(), + 0, + None, + ) + .await + .expect("Calling `get` failed"); + let initial_value = get_call_result + .value + .expect("Input is valid, call must not fail."); + + let flip_call_result = client + .call( + &mut ink_e2e::alice(), + contract_acc_id.clone(), + integration_flipper::messages::flip(), + 0, + None, + ) + .await + .expect("Calling `flip` failed"); + assert!( + flip_call_result.value.is_ok(), + "Messages now return a `Result`, which should be `Ok` here." + ); + + let get_call_result = client + .call( + &mut ink_e2e::alice(), + contract_acc_id.clone(), + integration_flipper::messages::get(), + 0, + None, + ) + .await + .expect("Calling `get` failed"); + let flipped_value = get_call_result + .value + .expect("Input is valid, call must not fail."); + assert!(flipped_value != initial_value); + + Ok(()) + } + + #[ink_e2e::test] + async fn e2e_message_error_reverts_state( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let constructor = integration_flipper::constructors::default(); + let contract_acc_id = client + .instantiate(&mut ink_e2e::bob(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let get_call_result = client + .call( + &mut ink_e2e::bob(), + contract_acc_id.clone(), + integration_flipper::messages::get(), + 0, + None, + ) + .await + .expect("Calling `get` failed"); + let initial_value = get_call_result + .value + .expect("Input is valid, call must not fail."); + + let err_flip_call_result = client + .call( + &mut ink_e2e::bob(), + contract_acc_id.clone(), + integration_flipper::messages::err_flip(), + 0, + None, + ) + .await; + + assert!(matches!( + err_flip_call_result, + Err(ink_e2e::Error::CallExtrinsic(_)) + )); + + let get_call_result = client + .call( + &mut ink_e2e::bob(), + contract_acc_id.clone(), + integration_flipper::messages::get(), + 0, + None, + ) + .await + .expect("Calling `get` failed"); + let flipped_value = get_call_result + .value + .expect("Input is valid, call must not fail."); + assert!(flipped_value == initial_value); + + Ok(()) + } + } +}