diff --git a/CHANGELOG.md b/CHANGELOG.md index 29471ad9400..18d812d6073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fail when decoding from storage and not all bytes consumed - [#1897](/~https://github.com/paritytech/ink/pull/1897) - [E2E] resolve DispatchError error details for dry-runs - [#1944](/~https://github.com/paritytech/ink/pull/1994) - [E2E] update to new `drink` API - [#2005](/~https://github.com/paritytech/ink/pull/2005) +- Support multiple chain extensions - [#1958](/~https://github.com/paritytech/ink/pull/1958) + - New example of how to use multiple chain extensions in one contract. + - Affects the usage of the `#[ink::chain_extension]` macro and the definition of the chain extension. ## Version 5.0.0-alpha diff --git a/crates/engine/src/chain_extension.rs b/crates/engine/src/chain_extension.rs index 1e44a3ce2a4..c0600dc1476 100644 --- a/crates/engine/src/chain_extension.rs +++ b/crates/engine/src/chain_extension.rs @@ -29,29 +29,29 @@ pub struct ChainExtensionHandler { output: Vec, } -/// The unique ID of the registered chain extension method. +/// The unique ID of the registered chain extension. #[derive( Debug, From, scale::Encode, scale::Decode, PartialEq, Eq, PartialOrd, Ord, Hash, )] -pub struct ExtensionId(u32); +pub struct ExtensionId(u16); /// Types implementing this trait can be used as chain extensions. /// /// This trait is only useful for testing contract via the off-chain environment. pub trait ChainExtension { - /// The static function ID of the chain extension. + /// The static ID of the chain extension. /// /// # Note /// /// This is expected to return a constant value. - fn func_id(&self) -> u32; + fn ext_id(&self) -> u16; /// Calls the chain extension with the given input. /// /// Returns an error code and may fill the `output` buffer with a SCALE encoded /// result. #[allow(clippy::ptr_arg)] - fn call(&mut self, input: &[u8], output: &mut Vec) -> u32; + fn call(&mut self, func_id: u16, input: &[u8], output: &mut Vec) -> u32; } impl Default for ChainExtensionHandler { @@ -79,20 +79,24 @@ impl ChainExtensionHandler { /// Register a new chain extension. pub fn register(&mut self, extension: Box) { - let func_id = extension.func_id(); - self.registered - .insert(ExtensionId::from(func_id), extension); + let ext_id = extension.ext_id(); + self.registered.insert(ExtensionId::from(ext_id), extension); } /// Evaluates the chain extension with the given parameters. /// /// Upon success returns the values returned by the evaluated chain extension. - pub fn eval(&mut self, func_id: u32, input: &[u8]) -> Result<(u32, &[u8]), Error> { + pub fn eval(&mut self, id: u32, input: &[u8]) -> Result<(u32, &[u8]), Error> { self.output.clear(); - let extension_id = ExtensionId::from(func_id); + + let func_id = (id & 0x0000FFFF) as u16; + let ext_id = (id >> 16) as u16; + + let extension_id = ExtensionId::from(ext_id); match self.registered.entry(extension_id) { Entry::Occupied(occupied) => { - let status_code = occupied.into_mut().call(input, &mut self.output); + let status_code = + occupied.into_mut().call(func_id, input, &mut self.output); Ok((status_code, &mut self.output)) } Entry::Vacant(_vacant) => Err(Error::UnregisteredChainExtension), diff --git a/crates/engine/src/ext.rs b/crates/engine/src/ext.rs index 387eb57127c..475d5fa1286 100644 --- a/crates/engine/src/ext.rs +++ b/crates/engine/src/ext.rs @@ -442,14 +442,14 @@ impl Engine { /// Calls the chain extension method registered at `func_id` with `input`. pub fn call_chain_extension( &mut self, - func_id: u32, + id: u32, input: &[u8], output: &mut &mut [u8], ) { let encoded_input = input.encode(); let (status_code, out) = self .chain_extension_handler - .eval(func_id, &encoded_input) + .eval(id, &encoded_input) .unwrap_or_else(|error| { panic!( "Encountered unexpected missing chain extension method: {error:?}" diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index 99455b5786e..880e7c1da03 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -327,7 +327,7 @@ pub trait EnvBackend { /// drive the decoding and error management process from the outside. fn call_chain_extension( &mut self, - func_id: u32, + id: u32, input: &I, status_to_result: F, decode_to_result: D, diff --git a/crates/env/src/chain_extension.rs b/crates/env/src/chain_extension.rs index 91b50e22202..e0033cc3c8d 100644 --- a/crates/env/src/chain_extension.rs +++ b/crates/env/src/chain_extension.rs @@ -80,7 +80,7 @@ pub trait FromStatusCode: Sized { /// type. The method just returns `O`. #[derive(Debug)] pub struct ChainExtensionMethod { - func_id: u32, + id: u32, #[allow(clippy::type_complexity)] state: PhantomData (I, O, ErrorCode)>, } @@ -88,9 +88,9 @@ pub struct ChainExtensionMethod { impl ChainExtensionMethod<(), (), (), false> { /// Creates a new chain extension method instance. #[inline] - pub fn build(func_id: u32) -> Self { + pub fn build(id: u32) -> Self { Self { - func_id, + id, state: Default::default(), } } @@ -112,7 +112,7 @@ impl I: scale::Encode, { ChainExtensionMethod { - func_id: self.func_id, + id: self.id, state: Default::default(), } } @@ -136,7 +136,7 @@ impl ChainExtensionMethod { O: scale::Decode, { ChainExtensionMethod { - func_id: self.func_id, + id: self.id, state: Default::default(), } } @@ -159,7 +159,7 @@ impl ChainExtensionMethod { self, ) -> ChainExtensionMethod { ChainExtensionMethod { - func_id: self.func_id, + id: self.id, state: Default::default(), } } @@ -178,7 +178,7 @@ impl ChainExtensionMethod { ErrorCode: FromStatusCode, { ChainExtensionMethod { - func_id: self.func_id, + id: self.id, state: Default::default(), } } @@ -269,7 +269,7 @@ where _, >( instance, - self.func_id, + self.id, input, ErrorCode::from_status_code, |mut output| scale::Decode::decode(&mut output).map_err(Into::into), @@ -338,7 +338,7 @@ where _, >( instance, - self.func_id, + self.id, input, |_status_code| Ok(()), |mut output| scale::Decode::decode(&mut output).map_err(Into::into), @@ -399,7 +399,7 @@ where ::on_instance(|instance| { EnvBackend::call_chain_extension::( instance, - self.func_id, + self.id, input, ErrorCode::from_status_code, |mut output| { @@ -449,7 +449,7 @@ where ::on_instance(|instance| { EnvBackend::call_chain_extension::( instance, - self.func_id, + self.id, input, |_status_code| Ok(()), |mut output| { diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index f170a8ab537..d0757d17818 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -358,7 +358,7 @@ impl EnvBackend for EnvInstance { fn call_chain_extension( &mut self, - func_id: u32, + id: u32, input: &I, status_to_result: F, decode_to_result: D, @@ -374,7 +374,7 @@ impl EnvBackend for EnvInstance { let mut output: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; self.engine - .call_chain_extension(func_id, enc_input, &mut &mut output[..]); + .call_chain_extension(id, enc_input, &mut &mut output[..]); let (status, out): (u32, Vec) = scale::Decode::decode(&mut &output[..]) .unwrap_or_else(|error| { panic!("could not decode `call_chain_extension` output: {error:?}") diff --git a/crates/env/src/engine/off_chain/mod.rs b/crates/env/src/engine/off_chain/mod.rs index 1f463b4427f..dc9a27bd18a 100644 --- a/crates/env/src/engine/off_chain/mod.rs +++ b/crates/env/src/engine/off_chain/mod.rs @@ -20,8 +20,6 @@ mod types; #[cfg(test)] mod tests; -pub use call_data::CallData; - use super::OnInstance; use crate::Error; diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs index 3c5df7613ea..babfada8d1c 100644 --- a/crates/env/src/engine/on_chain/impls.rs +++ b/crates/env/src/engine/on_chain/impls.rs @@ -344,7 +344,7 @@ impl EnvBackend for EnvInstance { fn call_chain_extension( &mut self, - func_id: u32, + id: u32, input: &I, status_to_result: F, decode_to_result: D, @@ -359,7 +359,7 @@ impl EnvBackend for EnvInstance { let mut scope = self.scoped_buffer(); let enc_input = scope.take_encoded(input); let output = &mut scope.take_rest(); - status_to_result(ext::call_chain_extension(func_id, enc_input, output))?; + status_to_result(ext::call_chain_extension(id, enc_input, output))?; let decoded = decode_to_result(&output[..])?; Ok(decoded) } diff --git a/crates/env/src/lib.rs b/crates/env/src/lib.rs index 881de493441..2f0bbf5277e 100644 --- a/crates/env/src/lib.rs +++ b/crates/env/src/lib.rs @@ -35,7 +35,6 @@ overflowing_literals, path_statements, patterns_in_fns_without_body, - private_in_public, unconditional_recursion, unused_allocation, unused_comparisons, diff --git a/crates/ink/codegen/src/generator/chain_extension.rs b/crates/ink/codegen/src/generator/chain_extension.rs index dff7bd8f7c3..906caa6b644 100644 --- a/crates/ink/codegen/src/generator/chain_extension.rs +++ b/crates/ink/codegen/src/generator/chain_extension.rs @@ -36,7 +36,7 @@ impl ChainExtension<'_> { let span = method.span(); let attrs = method.attrs(); let ident = method.ident(); - let func_id = method.id().into_u32(); + let id = method.id().into_u32(); let sig = method.sig(); let inputs = &sig.inputs; let input_bindings = method.inputs().map(|pat_type| &pat_type.pat); @@ -110,7 +110,7 @@ impl ChainExtension<'_> { where #where_output_impls_from_error_code { - ::ink::env::chain_extension::ChainExtensionMethod::build(#func_id) + ::ink::env::chain_extension::ChainExtensionMethod::build(#id) .input::<#compound_input_type>() .output::<#output_type, {::ink::is_result_type!(#output_type)}>() #error_code_handling diff --git a/crates/ink/ir/src/ast/attr_args.rs b/crates/ink/ir/src/ast/attr_args.rs index 81b59bda011..ba8d598b2c2 100644 --- a/crates/ink/ir/src/ast/attr_args.rs +++ b/crates/ink/ir/src/ast/attr_args.rs @@ -26,11 +26,17 @@ use syn::{ /// /// For example, the segment `env = ::my::env::Environment` /// in `#[ink::contract(env = ::my::env::Environment)]`. -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct AttributeArgs { args: Punctuated, } +impl quote::ToTokens for AttributeArgs { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.args.to_tokens(tokens) + } +} + impl IntoIterator for AttributeArgs { type Item = MetaNameValue; type IntoIter = syn::punctuated::IntoIter; diff --git a/crates/ink/ir/src/ir/attrs.rs b/crates/ink/ir/src/ir/attrs.rs index 100efa06548..34d3f5feba5 100644 --- a/crates/ink/ir/src/ir/attrs.rs +++ b/crates/ink/ir/src/ir/attrs.rs @@ -38,7 +38,7 @@ use crate::{ error::ExtError as _, ir, ir::{ - ExtensionId, + chain_extension::FunctionId, Selector, }, }; @@ -363,8 +363,8 @@ pub enum AttributeArgKind { /// `#[ink(selector = _)]` /// `#[ink(selector = 0xDEADBEEF)]` Selector, - /// `#[ink(extension = N: u32)]` - Extension, + /// `#[ink(function = N: u16)]` + Function, /// `#[ink(namespace = "my_namespace")]` Namespace, /// `#[ink(impl)]` @@ -435,13 +435,13 @@ pub enum AttributeArg { /// Note that ink! messages and constructors still need to be explicitly /// flagged as such. Implementation, - /// `#[ink(extension = N: u32)]` + /// `#[ink(function = N: u16)]` /// /// Applies on ink! chain extension method to set their `func_id` parameter. - /// Every chain extension method must have exactly one ink! `extension` attribute. + /// Every chain extension method must have exactly one ink! `function` attribute. /// /// Used by the `#[ink::chain_extension]` procedural macro. - Extension(ExtensionId), + Function(FunctionId), /// `#[ink(handle_status = flag: bool)]` /// /// Used by the `#[ink::chain_extension]` procedural macro. @@ -462,8 +462,8 @@ impl core::fmt::Display for AttributeArgKind { Self::Selector => { write!(f, "selector = S:[u8; 4] || _") } - Self::Extension => { - write!(f, "extension = N:u32)") + Self::Function => { + write!(f, "function = N:u16)") } Self::Namespace => { write!(f, "namespace = N:string") @@ -486,7 +486,7 @@ impl AttributeArg { Self::Constructor => AttributeArgKind::Constructor, Self::Payable => AttributeArgKind::Payable, Self::Selector(_) => AttributeArgKind::Selector, - Self::Extension(_) => AttributeArgKind::Extension, + Self::Function(_) => AttributeArgKind::Function, Self::Namespace(_) => AttributeArgKind::Namespace, Self::Implementation => AttributeArgKind::Implementation, Self::HandleStatus(_) => AttributeArgKind::HandleStatus, @@ -505,8 +505,8 @@ impl core::fmt::Display for AttributeArg { Self::Constructor => write!(f, "constructor"), Self::Payable => write!(f, "payable"), Self::Selector(selector) => core::fmt::Display::fmt(&selector, f), - Self::Extension(extension) => { - write!(f, "extension = {:?}", extension.into_u32()) + Self::Function(function) => { + write!(f, "function = {:?}", function.into_u16()) } Self::Namespace(namespace) => { write!(f, "namespace = {:?}", namespace.as_bytes()) @@ -925,19 +925,19 @@ impl Parse for AttributeFrag { Namespace::try_from(&name_value.value) .map(AttributeArg::Namespace) } - "extension" => { + "function" => { if let Some(lit_int) = name_value.value.as_lit_int() { - let id = lit_int.base10_parse::() + let id = lit_int.base10_parse::() .map_err(|error| { format_err_spanned!( lit_int, - "could not parse `N` in `#[ink(extension = N)]` into a `u32` integer: {}", error) + "could not parse `N` in `#[ink(function = N)]` into a `u16` integer: {}", error) })?; - Ok(AttributeArg::Extension(ExtensionId::from_u32(id))) + Ok(AttributeArg::Function(FunctionId::from_u16(id))) } else { Err(format_err_spanned!( name_value.value, - "expected `u32` integer type for `N` in #[ink(extension = N)]", + "expected `u16` integer type for `N` in #[ink(function = N)]", )) } } @@ -977,10 +977,10 @@ impl Parse for AttributeFrag { "default" => Ok(AttributeArg::Default), "impl" => Ok(AttributeArg::Implementation), _ => match ident.to_string().as_str() { - "extension" => Err(format_err_spanned!( + "function" => Err(format_err_spanned!( path, - "encountered #[ink(extension)] that is missing its `id` parameter. \ - Did you mean #[ink(extension = id: u32)] ?" + "encountered #[ink(function)] that is missing its `id` parameter. \ + Did you mean #[ink(function = id: u16)] ?" )), "handle_status" => Err(format_err_spanned!( path, @@ -1292,10 +1292,10 @@ mod tests { fn extension_works() { assert_attribute_try_from( syn::parse_quote! { - #[ink(extension = 42)] + #[ink(function = 42)] }, - Ok(test::Attribute::Ink(vec![AttributeArg::Extension( - ExtensionId::from_u32(42), + Ok(test::Attribute::Ink(vec![AttributeArg::Function( + FunctionId::from_u16(42), )])), ); } @@ -1304,9 +1304,9 @@ mod tests { fn extension_invalid_value_type() { assert_attribute_try_from( syn::parse_quote! { - #[ink(extension = "string")] + #[ink(function = "string")] }, - Err("expected `u32` integer type for `N` in #[ink(extension = N)]"), + Err("expected `u16` integer type for `N` in #[ink(function = N)]"), ); } @@ -1314,9 +1314,9 @@ mod tests { fn extension_negative_integer() { assert_attribute_try_from( syn::parse_quote! { - #[ink(extension = -1)] + #[ink(function = -1)] }, - Err("could not parse `N` in `#[ink(extension = N)]` into a `u32` integer: invalid digit found in string") + Err("could not parse `N` in `#[ink(function = N)]` into a `u16` integer: invalid digit found in string") ); } @@ -1325,9 +1325,9 @@ mod tests { let max_u32_plus_1 = (u32::MAX as u64) + 1; assert_attribute_try_from( syn::parse_quote! { - #[ink(extension = #max_u32_plus_1)] + #[ink(function = #max_u32_plus_1)] }, - Err("could not parse `N` in `#[ink(extension = N)]` into a `u32` integer: number too large to fit in target type"), + Err("could not parse `N` in `#[ink(function = N)]` into a `u16` integer: number too large to fit in target type"), ); } @@ -1335,11 +1335,11 @@ mod tests { fn extension_missing_parameter() { assert_attribute_try_from( syn::parse_quote! { - #[ink(extension)] + #[ink(function)] }, Err( - "encountered #[ink(extension)] that is missing its `id` parameter. \ - Did you mean #[ink(extension = id: u32)] ?", + "encountered #[ink(function)] that is missing its `id` parameter. \ + Did you mean #[ink(function = id: u16)] ?", ), ); } diff --git a/crates/ink/ir/src/ir/chain_extension.rs b/crates/ink/ir/src/ir/chain_extension.rs index 0e28e5d0b37..30e5b768ec9 100644 --- a/crates/ink/ir/src/ir/chain_extension.rs +++ b/crates/ink/ir/src/ir/chain_extension.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::{ + ast, error::ExtError, ir, ir::idents_lint, @@ -29,6 +30,7 @@ use syn::{ #[derive(Debug, PartialEq, Eq)] pub struct ChainExtension { item: syn::ItemTrait, + config: Config, error_code: syn::TraitItemType, methods: Vec, } @@ -66,13 +68,74 @@ impl ChainExtension { } } +/// The chain extension configuration. +#[derive(Default, Debug, PartialEq, Eq)] +pub struct Config { + ext_id: ExtensionId, +} + +impl TryFrom for Config { + type Error = syn::Error; + + fn try_from(args: ast::AttributeArgs) -> Result { + let mut ext_id: Option = None; + + for arg in args.clone().into_iter() { + if arg.name.is_ident("extension") { + if ext_id.is_some() { + return Err(format_err_spanned!( + arg.value, + "encountered duplicate ink! contract `extension` configuration argument", + )) + } + + if let Some(lit_int) = arg.value.as_lit_int() { + let id = lit_int.base10_parse::() + .map_err(|error| { + format_err_spanned!( + lit_int, + "could not parse `N` in `extension = N` into a `u16` integer: {}", error) + })?; + ext_id = Some(ExtensionId::from_u16(id)); + } else { + return Err(format_err_spanned!( + arg.value, + "expected `u16` integer type for `N` in `extension = N`", + )) + } + } else { + return Err(format_err_spanned!( + arg, + "encountered unknown or unsupported chain extension configuration argument", + )) + } + } + + if let Some(ext_id) = ext_id { + Ok(Config { ext_id }) + } else { + Err(format_err_spanned!( + args, + "missing required `extension = N: u16` argument on ink! chain extension", + )) + } + } +} + +impl Config { + /// Returns the chain extension identifier. + pub fn ext_id(&self) -> ExtensionId { + self.ext_id + } +} + /// An ink! chain extension method. #[derive(Debug, PartialEq, Eq)] pub struct ChainExtensionMethod { /// The underlying validated AST of the chain extension method. item: syn::TraitItemFn, /// The unique identifier of the chain extension method. - id: ExtensionId, + id: GlobalMethodId, /// If `false` the `u32` status code of the chain extension method call is going to /// be ignored and assumed to be always successful. The output buffer in this /// case is going to be queried and decoded into the chain extension method's @@ -118,7 +181,7 @@ impl ChainExtensionMethod { } /// Returns the unique ID of the chain extension method. - pub fn id(&self) -> ExtensionId { + pub fn id(&self) -> GlobalMethodId { self.id } @@ -158,39 +221,100 @@ impl<'a> Iterator for ChainExtensionMethodInputs<'a> { } } -/// The unique ID of an ink! chain extension method. +/// The unique ID of an chain extension. /// /// # Note /// -/// The ink! attribute `#[ink(extension = N: u32)]` for chain extension methods. -/// -/// Has a `func_id` extension ID to identify the associated chain extension method. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +/// The ink! attribute `#[ink::chain_extension(extension = N: u16)]` for chain extension. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ExtensionId { - index: u32, + index: u16, } impl ExtensionId { - /// Creates a new chain extension method ID from the given `u32`. - pub fn from_u32(index: u32) -> Self { + /// Creates a new chain extension ID from the given `u16`. + pub fn from_u16(index: u16) -> Self { + Self { index } + } + + /// Returns the underlying raw `u16` index. + pub fn into_u16(self) -> u16 { + self.index + } +} + +/// The unique ID of the method within the chain extension. +/// +/// # Note +/// +/// The ink! attribute `#[ink(function = N: u16)]` for chain extension methods. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FunctionId { + index: u16, +} + +impl FunctionId { + /// Creates a new chain extension function ID from the given `u16`. + pub fn from_u16(index: u16) -> Self { Self { index } } + /// Returns the underlying raw `u16` index. + pub fn into_u16(self) -> u16 { + self.index + } +} + +/// The unique ID of a chain extension method across all chain extensions. +/// +/// # Note +/// +/// It is a combination of the [`ExtensionId`] and [`FunctionId`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GlobalMethodId { + index: u32, +} + +impl GlobalMethodId { + /// Creates a new chain extension method global ID. + pub fn new(ext_id: ExtensionId, func_id: FunctionId) -> Self { + Self { + index: ((ext_id.index as u32) << 16) | func_id.index as u32, + } + } + + /// Returns the identifier of the function within the chain extension. + pub fn func_id(&self) -> FunctionId { + FunctionId::from_u16((self.index & 0x0000FFFF) as u16) + } + + /// Returns the identifier of the chain extension. + pub fn ext_id(&self) -> ExtensionId { + ExtensionId::from_u16((self.index >> 16) as u16) + } + /// Returns the underlying raw `u32` index. pub fn into_u32(self) -> u32 { self.index } } -impl TryFrom for ChainExtension { - type Error = syn::Error; - - fn try_from(item_trait: syn::ItemTrait) -> core::result::Result { +impl ChainExtension { + /// Creates a new ink! chain extension from the given configuration and trait item. + /// + /// # Errors + /// + /// Returns an error if some of the chain extension rules are violated. + pub fn try_from( + item_trait: syn::ItemTrait, + config: Config, + ) -> core::result::Result { idents_lint::ensure_no_ink_identifiers(&item_trait)?; Self::analyse_properties(&item_trait)?; - let (error_code, methods) = Self::analyse_items(&item_trait)?; + let (error_code, methods) = Self::analyse_items(config.ext_id, &item_trait)?; Ok(Self { item: item_trait, + config, error_code, methods, }) @@ -200,14 +324,10 @@ impl TryFrom for ChainExtension { impl ChainExtension { /// Returns `Ok` if the trait matches all requirements for an ink! chain extension. pub fn new(attr: TokenStream2, input: TokenStream2) -> Result { - if !attr.is_empty() { - return Err(format_err_spanned!( - attr, - "unexpected attribute input for ink! chain extension" - )) - } + let args = syn::parse2::(attr)?; + let config = Config::try_from(args)?; let item_trait = syn::parse2::(input)?; - ChainExtension::try_from(item_trait) + ChainExtension::try_from(item_trait, config) } /// Analyses the properties of the ink! chain extension. @@ -320,8 +440,8 @@ impl ChainExtension { /// - If the trait contains methods which do not respect the ink! trait definition /// requirements: /// - All trait methods must not have a `self` receiver. - /// - All trait methods must have an `#[ink(extension = N: u32)]` attribute that - /// is the ID that corresponds with the function ID of the respective chain + /// - All trait methods must have an `#[ink(function = N: u16)]` attribute that is + /// the ID that corresponds with the function ID of the respective chain /// extension call. /// /// # Note @@ -329,6 +449,7 @@ impl ChainExtension { /// The input Rust trait item is going to be replaced with a concrete chain extension /// type definition as a result of this procedural macro invocation. fn analyse_items( + ext_id: ExtensionId, item_trait: &syn::ItemTrait, ) -> Result<(syn::TraitItemType, Vec)> { let mut methods = Vec::new(); @@ -358,7 +479,7 @@ impl ChainExtension { )) } syn::TraitItem::Fn(fn_trait_item) => { - let method = Self::analyse_methods(fn_trait_item)?; + let method = Self::analyse_methods(ext_id, fn_trait_item)?; let method_id = method.id(); if let Some(previous) = seen_ids.get(&method_id) { return Err(format_err!( @@ -396,12 +517,15 @@ impl ChainExtension { /// /// # Errors /// - /// - If the method is missing the `#[ink(extension = N: u32)]` attribute. + /// - If the method is missing the `#[ink(function = N: u16)]` attribute. /// - If the method has a `self` receiver. /// - If the method declared as `unsafe`, `const` or `async`. /// - If the method has some explicit API. /// - If the method is variadic or has generic parameters. - fn analyse_methods(method: &syn::TraitItemFn) -> Result { + fn analyse_methods( + ext_id: ExtensionId, + method: &syn::TraitItemFn, + ) -> Result { if let Some(default_impl) = &method.default { return Err(format_err_spanned!( default_impl, @@ -446,19 +570,19 @@ impl ChainExtension { } match ir::first_ink_attribute(&method.attrs)? .map(|attr| attr.first().kind().clone()) { - Some(ir::AttributeArg::Extension(extension)) => { - Self::analyse_chain_extension_method(method, extension) + Some(ir::AttributeArg::Function(func_id)) => { + Self::analyse_chain_extension_method(method, ext_id, func_id) } Some(_unsupported) => { Err(format_err_spanned!( method, - "encountered unsupported ink! attribute for ink! chain extension method. expected #[ink(extension = N: u32)] attribute" + "encountered unsupported ink! attribute for ink! chain extension method. expected #[ink(function = N: u16)] attribute" )) } None => { Err(format_err_spanned!( method, - "missing #[ink(extension = N: u32)] flag on ink! chain extension method" + "missing #[ink(function = N: u16)] flag on ink! chain extension method" )) } } @@ -471,16 +595,18 @@ impl ChainExtension { /// - If the chain extension method has a `self` receiver as first argument. fn analyse_chain_extension_method( item_method: &syn::TraitItemFn, - extension: ExtensionId, + ext_id: ExtensionId, + func_id: FunctionId, ) -> Result { let (ink_attrs, _) = ir::sanitize_attributes( item_method.span(), item_method.attrs.clone(), - &ir::AttributeArgKind::Extension, + &ir::AttributeArgKind::Function, |arg| { match arg.kind() { - ir::AttributeArg::Extension(_) - | ir::AttributeArg::HandleStatus(_) => Ok(()), + ir::AttributeArg::Function(_) | ir::AttributeArg::HandleStatus(_) => { + Ok(()) + } _ => Err(None), } }, @@ -492,7 +618,7 @@ impl ChainExtension { )) } let result = ChainExtensionMethod { - id: extension, + id: GlobalMethodId::new(ext_id, func_id), item: item_method.clone(), handle_status: ink_attrs.is_handle_status(), }; @@ -509,9 +635,12 @@ mod tests { macro_rules! assert_ink_chain_extension_eq_err { ( error: $err_str:literal, $($chain_extension:tt)* ) => { assert_eq!( - >::try_from(syn::parse_quote! { - $( $chain_extension )* - }) + ChainExtension::try_from( + syn::parse_quote! { + $( $chain_extension )* + }, + Config::default() + ) .map_err(|err| err.to_string()), Err( $err_str.to_string() @@ -632,19 +761,19 @@ mod tests { #[test] fn chain_extension_containing_non_flagged_method_is_denied() { assert_ink_chain_extension_eq_err!( - error: "missing #[ink(extension = N: u32)] flag on ink! chain extension method", + error: "missing #[ink(function = N: u16)] flag on ink! chain extension method", pub trait MyChainExtension { fn non_flagged_1(&self); } ); assert_ink_chain_extension_eq_err!( - error: "missing #[ink(extension = N: u32)] flag on ink! chain extension method", + error: "missing #[ink(function = N: u16)] flag on ink! chain extension method", pub trait MyChainExtension { fn non_flagged_2(&mut self); } ); assert_ink_chain_extension_eq_err!( - error: "missing #[ink(extension = N: u32)] flag on ink! chain extension method", + error: "missing #[ink(function = N: u16)] flag on ink! chain extension method", pub trait MyChainExtension { fn non_flagged_3() -> Self; } @@ -667,7 +796,7 @@ mod tests { assert_ink_chain_extension_eq_err!( error: "const ink! chain extension methods are not supported", pub trait MyChainExtension { - #[ink(extension = 1)] + #[ink(function = 1)] const fn const_constructor() -> Self; } ); @@ -678,7 +807,7 @@ mod tests { assert_ink_chain_extension_eq_err!( error: "async ink! chain extension methods are not supported", pub trait MyChainExtension { - #[ink(extension = 1)] + #[ink(function = 1)] async fn const_constructor() -> Self; } ); @@ -689,7 +818,7 @@ mod tests { assert_ink_chain_extension_eq_err!( error: "unsafe ink! chain extension methods are not supported", pub trait MyChainExtension { - #[ink(extension = 1)] + #[ink(function = 1)] unsafe fn const_constructor() -> Self; } ); @@ -700,7 +829,7 @@ mod tests { assert_ink_chain_extension_eq_err!( error: "ink! chain extension methods with non default ABI are not supported", pub trait MyChainExtension { - #[ink(extension = 1)] + #[ink(function = 1)] extern fn const_constructor() -> Self; } ); @@ -711,7 +840,7 @@ mod tests { assert_ink_chain_extension_eq_err!( error: "variadic ink! chain extension methods are not supported", pub trait MyChainExtension { - #[ink(extension = 1)] + #[ink(function = 1)] fn const_constructor(...) -> Self; } ); @@ -722,7 +851,7 @@ mod tests { assert_ink_chain_extension_eq_err!( error: "generic ink! chain extension methods are not supported", pub trait MyChainExtension { - #[ink(extension = 1)] + #[ink(function = 1)] fn const_constructor() -> Self; } ); @@ -733,7 +862,7 @@ mod tests { assert_ink_chain_extension_eq_err!( error: "\ encountered unsupported ink! attribute for ink! chain extension method. \ - expected #[ink(extension = N: u32)] attribute", + expected #[ink(function = N: u16)] attribute", pub trait MyChainExtension { #[ink(message)] fn unsupported_ink_attribute(&self); @@ -751,34 +880,34 @@ mod tests { #[test] fn chain_extension_containing_method_with_invalid_marker() { assert_ink_chain_extension_eq_err!( - error: "could not parse `N` in `#[ink(extension = N)]` into a `u32` integer: \ + error: "could not parse `N` in `#[ink(function = N)]` into a `u16` integer: \ invalid digit found in string", pub trait MyChainExtension { - #[ink(extension = -1)] + #[ink(function = -1)] fn has_self_receiver(); } ); - let too_large = (u32::MAX as u64) + 1; + let too_large = (u16::MAX as u64) + 1; assert_ink_chain_extension_eq_err!( - error: "could not parse `N` in `#[ink(extension = N)]` into a `u32` integer: \ + error: "could not parse `N` in `#[ink(function = N)]` into a `u16` integer: \ number too large to fit in target type", pub trait MyChainExtension { - #[ink(extension = #too_large)] + #[ink(function = #too_large)] fn has_self_receiver(); } ); assert_ink_chain_extension_eq_err!( - error: "expected `u32` integer type for `N` in #[ink(extension = N)]", + error: "expected `u16` integer type for `N` in #[ink(function = N)]", pub trait MyChainExtension { - #[ink(extension = "Hello, World!")] + #[ink(function = "Hello, World!")] fn has_self_receiver(); } ); assert_ink_chain_extension_eq_err!( - error: "encountered #[ink(extension)] that is missing its `id` parameter. \ - Did you mean #[ink(extension = id: u32)] ?", + error: "encountered #[ink(function)] that is missing its `id` parameter. \ + Did you mean #[ink(function = id: u16)] ?", pub trait MyChainExtension { - #[ink(extension)] + #[ink(function)] fn has_self_receiver(); } ); @@ -786,23 +915,23 @@ mod tests { assert_ink_chain_extension_eq_err!( error: "encountered duplicate ink! attribute", pub trait MyChainExtension { - #[ink(extension = 42)] - #[ink(extension = 42)] + #[ink(function = 42)] + #[ink(function = 42)] fn duplicate_attributes() -> Self; } ); assert_ink_chain_extension_eq_err!( error: "encountered ink! attribute arguments with equal kinds", pub trait MyChainExtension { - #[ink(extension = 1)] - #[ink(extension = 2)] + #[ink(function = 1)] + #[ink(function = 2)] fn duplicate_attributes() -> Self; } ); assert_ink_chain_extension_eq_err!( error: "encountered conflicting ink! attribute argument", pub trait MyChainExtension { - #[ink(extension = 1)] + #[ink(function = 1)] #[ink(message)] fn conflicting_attributes() -> Self; } @@ -816,7 +945,7 @@ mod tests { pub trait MyChainExtension { type ErrorCode = (); - #[ink(extension = 1)] + #[ink(function = 1)] fn has_self_receiver(&self) -> Self; } ); @@ -825,7 +954,7 @@ mod tests { pub trait MyChainExtension { type ErrorCode = (); - #[ink(extension = 1)] + #[ink(function = 1)] fn has_self_receiver(&mut self) -> Self; } ); @@ -834,7 +963,7 @@ mod tests { pub trait MyChainExtension { type ErrorCode = (); - #[ink(extension = 1)] + #[ink(function = 1)] fn has_self_receiver(self) -> Self; } ); @@ -843,7 +972,7 @@ mod tests { pub trait MyChainExtension { type ErrorCode = (); - #[ink(extension = 1)] + #[ink(function = 1)] fn has_self_receiver(self: &Self) -> Self; } ); @@ -852,7 +981,7 @@ mod tests { pub trait MyChainExtension { type ErrorCode = (); - #[ink(extension = 1)] + #[ink(function = 1)] fn has_self_receiver(self: Self) -> Self; } ); @@ -863,9 +992,9 @@ mod tests { assert_ink_chain_extension_eq_err!( error: "encountered duplicate extension identifiers for the same chain extension", pub trait MyChainExtension { - #[ink(extension = 1)] + #[ink(function = 1)] fn same_id_1(); - #[ink(extension = 1)] + #[ink(function = 1)] fn same_id_2(); } ); @@ -873,22 +1002,22 @@ mod tests { #[test] fn chain_extension_is_ok() { - let chain_extension = >::try_from(syn::parse_quote! { + let chain_extension = ChainExtension::try_from(syn::parse_quote! { pub trait MyChainExtension { type ErrorCode = (); - #[ink(extension = 1)] + #[ink(function = 1)] fn extension_1(); - #[ink(extension = 2)] + #[ink(function = 2)] fn extension_2(input: i32); - #[ink(extension = 3)] + #[ink(function = 3)] fn extension_3() -> i32; - #[ink(extension = 4)] + #[ink(function = 4)] fn extension_4(input: i32) -> i32; - #[ink(extension = 5)] + #[ink(function = 5)] fn extension_5(in1: i8, in2: i16, in3: i32, in4: i64) -> (u8, u16, u32, u64); } - }).unwrap(); + }, Config::default()).unwrap(); assert_eq!(chain_extension.methods.len(), 5); for (actual, expected) in chain_extension .methods @@ -920,28 +1049,30 @@ mod tests { #[test] fn chain_extension_with_params_is_ok() { - let chain_extension = - >::try_from(syn::parse_quote! { + let chain_extension = ChainExtension::try_from( + syn::parse_quote! { pub trait MyChainExtension { type ErrorCode = (); - #[ink(extension = 1, handle_status = false)] + #[ink(function = 1, handle_status = false)] fn extension_a(); - #[ink(extension = 2)] + #[ink(function = 2)] fn extension_b(); - #[ink(extension = 3, handle_status = false)] + #[ink(function = 3, handle_status = false)] fn extension_c(); - #[ink(extension = 4)] + #[ink(function = 4)] #[ink(handle_status = false)] fn extension_d(); - #[ink(extension = 5)] + #[ink(function = 5)] fn extension_e(); - #[ink(extension = 6)] + #[ink(function = 6)] #[ink(handle_status = false)] fn extension_f(); } - }) - .unwrap(); + }, + Config::default(), + ) + .unwrap(); let expected_methods = 6; assert_eq!(chain_extension.methods.len(), expected_methods); for (actual, expected) in chain_extension @@ -972,4 +1103,64 @@ mod tests { assert_eq!(actual, expected); } } + + /// Asserts that the given input configuration attribute argument are converted + /// into the expected ink! configuration or yields the expected error message. + fn assert_config( + input: ast::AttributeArgs, + expected: core::result::Result, + ) { + assert_eq!( + >::try_from(input) + .map_err(|err| err.to_string()), + expected.map_err(ToString::to_string), + ); + } + + #[test] + fn empty_config_fails() { + assert_config( + syn::parse_quote! {}, + Err("missing required `extension = N: u16` argument on ink! chain extension"), + ) + } + + #[test] + fn extension_works() { + assert_config( + syn::parse_quote! { + extension = 13 + }, + Ok(Config { + ext_id: ExtensionId::from_u16(13), + }), + ) + } + + #[test] + fn extension_invalid_value_fails() { + assert_config( + syn::parse_quote! { extension = "invalid" }, + Err("expected `u16` integer type for `N` in `extension = N`"), + ); + } + + #[test] + fn unknown_arg_fails() { + assert_config( + syn::parse_quote! { unknown = argument }, + Err("encountered unknown or unsupported chain extension configuration argument"), + ); + } + + #[test] + fn duplicate_args_fails() { + assert_config( + syn::parse_quote! { + extension = 13, + extension = 123, + }, + Err("encountered duplicate ink! contract `extension` configuration argument"), + ); + } } diff --git a/crates/ink/macro/src/lib.rs b/crates/ink/macro/src/lib.rs index ba1e2e5cafb..d55fbc97c84 100644 --- a/crates/ink/macro/src/lib.rs +++ b/crates/ink/macro/src/lib.rs @@ -906,35 +906,45 @@ pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { /// and use its associated environment definition in order to make use of /// the methods provided by the chain extension. /// -/// # Attributes +/// # Macro Attributes +/// +/// The macro supports only one required argument: +/// +/// - `extension = N: u16`: +/// +/// The runtime may have several chain extensions at the same time. The `extension` +/// identifier points to the corresponding chain extension in the runtime. +/// The value should be the same as during the definition of the chain extension. +/// +/// # Method Attributes /// /// There are three different attributes with which the chain extension methods /// can be flagged: /// /// | Attribute | Required | Default Value | Description | /// |:----------|:--------:|:--------------|:-----------:| -/// | `ink(extension = N: u32)` | Yes | - | Determines the unique function ID of the chain -/// extension method. | | `ink(handle_status = flag: bool)` | Optional | `true` | Assumes +/// | `ink(function = N: u16)` | Yes | - | Determines the unique function ID within the +/// chain extension. | | `ink(handle_status = flag: bool)` | Optional | `true` | Assumes /// that the returned status code of the chain extension method always indicates success /// and therefore always loads and decodes the output buffer of the call. | /// /// As with all ink! attributes multiple of them can either appear in a contiguous list: /// ``` /// # type Access = i32; -/// # #[ink::chain_extension] +/// # #[ink::chain_extension(extension = 1)] /// # pub trait MyChainExtension { /// # type ErrorCode = i32; -/// #[ink(extension = 5, handle_status = false)] +/// #[ink(function = 5, handle_status = false)] /// fn key_access_for_account(key: &[u8], account: &[u8]) -> Access; /// # } /// ``` /// …or as multiple stand alone ink! attributes applied to the same item: /// ``` /// # type Access = i32; -/// # #[ink::chain_extension] +/// # #[ink::chain_extension(extension = 1)] /// # pub trait MyChainExtension { /// # type ErrorCode = i32; -/// #[ink(extension = 5)] +/// #[ink(function = 5)] /// #[ink(handle_status = false)] /// fn key_access_for_account(key: &[u8], account: &[u8]) -> Access; /// # } @@ -996,7 +1006,7 @@ pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { /// Every chain extension defines exactly one `ErrorCode` using the following syntax: /// /// ``` -/// #[ink::chain_extension] +/// #[ink::chain_extension(extension = 0)] /// pub trait MyChainExtension { /// type ErrorCode = MyErrorCode; /// @@ -1019,7 +1029,7 @@ pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// ``` /// /// Custom chain extension to read to and write from the runtime. -/// #[ink::chain_extension] +/// #[ink::chain_extension(extension = 0)] /// pub trait RuntimeReadWrite { /// type ErrorCode = ReadWriteErrorCode; /// @@ -1028,7 +1038,7 @@ pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// # Note /// /// /// /// Actually returns a value of type `Result, Self::ErrorCode>`. -/// #[ink(extension = 1)] +/// #[ink(function = 1)] /// fn read(key: &[u8]) -> Vec; /// /// /// Reads from runtime storage. @@ -1045,7 +1055,7 @@ pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// /// /// This requires `ReadWriteError` to implement `From` /// /// and may potentially return any `Self::ErrorCode` through its return value. -/// #[ink(extension = 2)] +/// #[ink(function = 2)] /// fn read_small(key: &[u8]) -> Result<(u32, [u8; 32]), ReadWriteError>; /// /// /// Writes into runtime storage. @@ -1053,7 +1063,7 @@ pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// # Note /// /// /// /// Actually returns a value of type `Result<(), Self::ErrorCode>`. -/// #[ink(extension = 3)] +/// #[ink(function = 3)] /// fn write(key: &[u8], value: &[u8]); /// /// /// Returns the access allowed for the key for the caller. @@ -1061,7 +1071,7 @@ pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// # Note /// /// /// /// Assumes to never fail the call and therefore always returns `Option`. -/// #[ink(extension = 4, handle_status = false)] +/// #[ink(function = 4, handle_status = false)] /// fn access(key: &[u8]) -> Option; /// /// /// Unlocks previously acquired permission to access key. @@ -1074,7 +1084,7 @@ pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// /// /// Assumes the call to never fail and therefore does _NOT_ require `UnlockAccessError` /// /// to implement `From` as in the `read_small` method above. -/// #[ink(extension = 5, handle_status = false)] +/// #[ink(function = 5, handle_status = false)] /// fn unlock_access(key: &[u8], access: Access) -> Result<(), UnlockAccessError>; /// } /// # #[derive(scale::Encode, scale::Decode, scale_info::TypeInfo)] @@ -1227,18 +1237,18 @@ pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { /// } /// } /// # /// Custom chain extension to read to and write from the runtime. -/// # #[ink::chain_extension] +/// # #[ink::chain_extension(extension = 13)] /// # pub trait RuntimeReadWrite { /// # type ErrorCode = ReadWriteErrorCode; -/// # #[ink(extension = 1)] +/// # #[ink(function = 1)] /// # fn read(key: &[u8]) -> Vec; -/// # #[ink(extension = 2)] +/// # #[ink(function = 2)] /// # fn read_small(key: &[u8]) -> Result<(u32, [u8; 32]), ReadWriteError>; -/// # #[ink(extension = 3)] +/// # #[ink(function = 3)] /// # fn write(key: &[u8], value: &[u8]); -/// # #[ink(extension = 4, handle_status = false)] +/// # #[ink(function = 4, handle_status = false)] /// # fn access(key: &[u8]) -> Option; -/// # #[ink(extension = 5, handle_status = false)] +/// # #[ink(function = 5, handle_status = false)] /// # fn unlock_access(key: &[u8], access: Access) -> Result<(), UnlockAccessError>; /// # } /// # #[derive(scale::Encode, scale::Decode, scale_info::TypeInfo)] diff --git a/crates/ink/src/chain_extension.rs b/crates/ink/src/chain_extension.rs index d7b79b68a1a..26980bd9b17 100644 --- a/crates/ink/src/chain_extension.rs +++ b/crates/ink/src/chain_extension.rs @@ -98,3 +98,78 @@ mod private { /// Seals the `Output` trait so that it cannot be implemented outside this module. pub trait OutputSealed {} } + +/// Macro defines the combined chain extension via structure definition. +/// Each sub-extension can be accessed by the corresponding field. +/// +/// The macro expects a structure definition as an input where each field should +/// implement [`ChainExtensionInstance`]. Usually, this trait is implemented +/// by the `#[ink::chain_extension]` macro during the definition of the chain extension. +/// +/// ```rust +/// #[ink::scale_derive(TypeInfo)] +/// struct ExtensionOne; +/// impl ink::ChainExtensionInstance for ExtensionOne { +/// type Instance = Self; +/// +/// fn instantiate() -> Self::Instance { +/// Self {} +/// } +/// } +/// +/// #[ink::scale_derive(TypeInfo)] +/// struct ExtensionTwo; +/// impl ink::ChainExtensionInstance for ExtensionTwo { +/// type Instance = Self; +/// +/// fn instantiate() -> Self::Instance { +/// Self {} +/// } +/// } +/// +/// ink::combine_extensions! { +/// /// Defines a combined extension with [`ExtensionOne`] and [`ExtensionTwo`]. +/// struct Combined { +/// /// This field can be used to access the first extension like +/// /// `self.env().extension().extension_one` in the contract's context. +/// extension_one: ExtensionOne, +/// /// This field can be used to access the second extension like +/// /// `self.env().extension().extension_two` in the contract's context. +/// extension_two: ExtensionTwo, +/// } +/// } +/// ``` +#[macro_export] +macro_rules! combine_extensions { + ($(#[$meta:meta])* $vis:vis struct $name:ident { + $($(#[$field_meta:meta])* $field_vis:vis $field_name:ident: $field_type:ty,)* + }) => { + $(#[$meta])* + #[::ink::scale_derive(TypeInfo)] + $vis struct $name { + $($(#[$field_meta])* $field_vis $field_name: $field_type,)* + } + + const _: () = { + /// Each chain extension has an abstract type `$name` that describes + /// it and the actual instance that provides access to methods. + /// This structure is an instance that is returned by the `self.env().extension()` call. + /// + /// Because it is a combination of corresponding sub-instances, we need to initialize + /// each sub-instance in the same way by calling [`ChainExtensionInstance::instantiate`]. + pub struct Private { + $($(#[$field_meta])* $field_vis $field_name: <$field_type as ::ink::ChainExtensionInstance>::Instance,)* + } + + impl ::ink::ChainExtensionInstance for $name { + type Instance = Private; + + fn instantiate() -> Self::Instance { + Self::Instance { + $($field_name: <$field_type as ::ink::ChainExtensionInstance>::instantiate(),)* + } + } + } + }; + } +} diff --git a/crates/ink/tests/ui/chain_extension/E-01-simple.rs b/crates/ink/tests/ui/chain_extension/E-01-simple.rs index 5e2ae3ab185..fd732644a94 100644 --- a/crates/ink/tests/ui/chain_extension/E-01-simple.rs +++ b/crates/ink/tests/ui/chain_extension/E-01-simple.rs @@ -1,12 +1,12 @@ use ink_env::Environment; /// Custom chain extension to read to and write from the runtime. -#[ink::chain_extension] +#[ink::chain_extension(extension = 0)] pub trait RuntimeReadWrite { type ErrorCode = ReadWriteErrorCode; /// Reads from runtime storage. - #[ink(extension = 1)] + #[ink(function = 1)] fn read(key: &[u8]) -> Vec; /// Reads from runtime storage. @@ -18,15 +18,15 @@ pub trait RuntimeReadWrite { /// /// If the runtime storage cell stores a value that requires more than /// 32 bytes. - #[ink(extension = 2)] + #[ink(function = 2)] fn read_small(key: &[u8]) -> Result<(u32, [u8; 32]), ReadWriteError>; /// Writes into runtime storage. - #[ink(extension = 3)] + #[ink(function = 3)] fn write(key: &[u8], value: &[u8]); /// Returns the access allowed for the key for the caller. - #[ink(extension = 4, handle_status = false)] + #[ink(function = 4, handle_status = false)] fn access(key: &[u8]) -> Option; /// Unlocks previously acquired permission to access key. @@ -34,7 +34,7 @@ pub trait RuntimeReadWrite { /// # Errors /// /// If the permission was not granted. - #[ink(extension = 5, handle_status = false)] + #[ink(function = 5, handle_status = false)] fn unlock_access(key: &[u8], access: Access) -> Result<(), UnlockAccessError>; } diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 266a31c2f26..86a9ad30b58 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -35,7 +35,6 @@ overflowing_literals, path_statements, patterns_in_fns_without_body, - private_in_public, unconditional_recursion, unused_allocation, unused_comparisons, diff --git a/integration-tests/combined-extension/.gitignore b/integration-tests/combined-extension/.gitignore new file mode 100755 index 00000000000..8de8f877e47 --- /dev/null +++ b/integration-tests/combined-extension/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/integration-tests/combined-extension/Cargo.toml b/integration-tests/combined-extension/Cargo.toml new file mode 100755 index 00000000000..8c432461fad --- /dev/null +++ b/integration-tests/combined-extension/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "combined_extension" +version = "5.0.0-alpha" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../crates/ink", default-features = false } +psp22_extension = { path = "../psp22-extension", default-features = false, features = ["ink-as-dependency"] } +rand_extension = { path = "../rand-extension", default-features = false, features = ["ink-as-dependency"] } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "psp22_extension/std", + "rand_extension/std", +] +ink-as-dependency = [ + "psp22_extension/ink-as-dependency", + "rand_extension/ink-as-dependency", +] diff --git a/integration-tests/combined-extension/README.md b/integration-tests/combined-extension/README.md new file mode 100644 index 00000000000..f71d8cbaeaa --- /dev/null +++ b/integration-tests/combined-extension/README.md @@ -0,0 +1,19 @@ +# Combined Chain Extension Example + +## What is this example about? + +It demonstrates how to combine several chain extensions and call them from ink!. + +See [this chapter](https://use.ink/macros-attributes/chain-extension) +in our ink! documentation for more details about chain extensions. + + +This example uses two chain extensions, `FetchRandom`(from [rand-extension](../rand-extension)) +and `Psp22Extension`(from [psp22-extension](../psp22-extension)) defined in other examples. +The example shows how to combine two chain extensions together +and use them in the contract along with each other. +Also example shows how to mock each chain extension for testing. + +This example doesn't show how to define a chain extension and how +to implement in on the runtime side. For that purpose, you can +check the two examples mentioned above. diff --git a/integration-tests/combined-extension/lib.rs b/integration-tests/combined-extension/lib.rs new file mode 100755 index 00000000000..76824771354 --- /dev/null +++ b/integration-tests/combined-extension/lib.rs @@ -0,0 +1,189 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use ink::env::{ + DefaultEnvironment, + Environment, +}; +use psp22_extension::Psp22Extension; +use rand_extension::{ + FetchRandom, + RandomReadErr, +}; + +ink::combine_extensions! { + /// This extension combines the [`FetchRandom`] and [`Psp22Extension`] extensions. + /// It is possible to combine any number of extensions in this way. + /// + /// This structure is an instance that is returned by the `self.env().extension()` call. + pub struct CombinedChainExtension { + /// The instance of the [`Psp22Extension`] chain extension. + /// + /// It provides you access to `PSP22` functionality. + pub psp22: Psp22Extension, + /// The instance of the [`FetchRandom`] chain extension. + /// + /// It provides you access to randomness functionality. + pub rand: FetchRandom, + } +} + +/// An environment using default ink environment types, with PSP-22 extension included +#[derive(Debug, Clone, PartialEq, Eq)] +#[ink::scale_derive(TypeInfo)] +pub enum CustomEnvironment {} + +/// We use the same types and values as for [`DefaultEnvironment`] except the +/// [`Environment::ChainExtension`] type. +impl Environment for CustomEnvironment { + const MAX_EVENT_TOPICS: usize = ::MAX_EVENT_TOPICS; + + type AccountId = ::AccountId; + type Balance = ::Balance; + type Hash = ::Hash; + type Timestamp = ::Timestamp; + type BlockNumber = ::BlockNumber; + + /// Setting up the combined chain extension as a primary extension. + /// + /// The `self.env().extension()` call returns the [`CombinedInstance`] + /// that provides access two both chain extensions. + type ChainExtension = CombinedChainExtension; +} + +#[ink::contract(env = crate::CustomEnvironment)] +mod combined_extension { + use super::*; + use psp22_extension::Psp22Error; + + /// Defines the storage of our contract. + /// + /// The example shows how to call each extension and test it, + /// so we don't need any state to save. + #[ink(storage)] + #[derive(Default)] + pub struct CombinedExtensionContract; + + impl CombinedExtensionContract { + /// Constructor that initializes empty storage. + #[ink(constructor)] + pub fn new() -> Self { + Self {} + } + + /// Returns the random value from extension. + #[ink(message)] + pub fn get_rand(&self) -> Result<[u8; 32], RandomReadErr> { + self.env().extension().rand.fetch_random([0; 32] /* seed */) + } + + /// Returns the total supply from PSP22 extension. + #[ink(message)] + pub fn get_total_supply(&self) -> Result { + self.env().extension().psp22.total_supply(0) + } + } + + /// Unit tests in Rust are normally defined within such a `#[cfg(test)]` + #[cfg(test)] + mod tests { + /// Imports all the definitions from the outer scope so we can use them here. + use super::*; + + const RANDOM_VALUE: [u8; 32] = [3; 32]; + + /// Mocking the random extension to return results that we want in the tests. + struct MockedRandExtension; + impl ink::env::test::ChainExtension for MockedRandExtension { + fn ext_id(&self) -> u16 { + // It is identifier used by [`rand_extension::FetchRandom`] extension. + 666 + } + + fn call( + &mut self, + _func_id: u16, + _input: &[u8], + output: &mut Vec, + ) -> u32 { + ink::scale::Encode::encode_to(&RANDOM_VALUE, output); + 0 + } + } + + #[ink::test] + fn rand_chain_extension_works() { + let contract = CombinedExtensionContract::new(); + + // given + let result = std::panic::catch_unwind(|| contract.get_rand()); + // The call to random extension should fail because it is not registered. + assert!(result.is_err()); + + // when + ink::env::test::register_chain_extension(MockedRandExtension); + + // then + assert_eq!(contract.get_rand(), Ok(RANDOM_VALUE)); + } + + const TOTAL_SUPPLY: u128 = 1377; + + /// Mocking the PSP22 extension to return results that we want in the tests. + /// + /// Because this extension has many methods, we want to implement only one of + /// them: + /// - `total_supply` with corresponding `func_id` - `0x162d`. + struct MockedPSP22Extension; + impl ink::env::test::ChainExtension for MockedPSP22Extension { + fn ext_id(&self) -> u16 { + // It is identifier used by [`psp22_extension::Psp22Extension`] extension. + 13 + } + + fn call(&mut self, func_id: u16, _input: &[u8], output: &mut Vec) -> u32 { + match func_id { + 0x162d /* `func_id` of the `total_supply` function */ => { + ink::scale::Encode::encode_to(&TOTAL_SUPPLY, output); + 0 + } + _ => { + 1 + } + } + } + } + + #[ink::test] + fn psp22_chain_extension_works() { + let contract = CombinedExtensionContract::new(); + + // given + let result = std::panic::catch_unwind(|| contract.get_total_supply()); + // The call to PSP22 extension should fail because it is not registered. + assert!(result.is_err()); + + // when + ink::env::test::register_chain_extension(MockedPSP22Extension); + + // then + assert_eq!(contract.get_total_supply(), Ok(TOTAL_SUPPLY)); + } + + #[ink::test] + fn both_chain_extensions_work() { + let contract = CombinedExtensionContract::new(); + + // given + assert!(std::panic::catch_unwind(|| contract.get_rand()).is_err()); + assert!(std::panic::catch_unwind(|| { contract.get_total_supply() }).is_err()); + + // when + ink::env::test::register_chain_extension(MockedRandExtension); + ink::env::test::register_chain_extension(MockedPSP22Extension); + + // then + assert_eq!(contract.get_rand(), Ok(RANDOM_VALUE)); + assert_eq!(contract.get_total_supply(), Ok(TOTAL_SUPPLY)); + } + } +} diff --git a/integration-tests/psp22-extension/lib.rs b/integration-tests/psp22-extension/lib.rs index 22c63070462..3691ca280f4 100755 --- a/integration-tests/psp22-extension/lib.rs +++ b/integration-tests/psp22-extension/lib.rs @@ -8,30 +8,30 @@ use ink::{ type DefaultAccountId = ::AccountId; type DefaultBalance = ::Balance; -#[ink::chain_extension] +#[ink::chain_extension(extension = 13)] pub trait Psp22Extension { type ErrorCode = Psp22Error; // PSP22 Metadata interfaces - #[ink(extension = 0x3d26)] + #[ink(function = 0x3d26)] fn token_name(asset_id: u32) -> Result>; - #[ink(extension = 0x3420)] + #[ink(function = 0x3420)] fn token_symbol(asset_id: u32) -> Result>; - #[ink(extension = 0x7271)] + #[ink(function = 0x7271)] fn token_decimals(asset_id: u32) -> Result; // PSP22 interface queries - #[ink(extension = 0x162d)] + #[ink(function = 0x162d)] fn total_supply(asset_id: u32) -> Result; - #[ink(extension = 0x6568)] + #[ink(function = 0x6568)] fn balance_of(asset_id: u32, owner: DefaultAccountId) -> Result; - #[ink(extension = 0x4d47)] + #[ink(function = 0x4d47)] fn allowance( asset_id: u32, owner: DefaultAccountId, @@ -39,12 +39,12 @@ pub trait Psp22Extension { ) -> Result; // PSP22 transfer - #[ink(extension = 0xdb20)] + #[ink(function = 0xdb20)] fn transfer(asset_id: u32, to: DefaultAccountId, value: DefaultBalance) -> Result<()>; // PSP22 transfer_from - #[ink(extension = 0x54b3)] + #[ink(function = 0x54b3)] fn transfer_from( asset_id: u32, from: DefaultAccountId, @@ -53,7 +53,7 @@ pub trait Psp22Extension { ) -> Result<()>; // PSP22 approve - #[ink(extension = 0xb20f)] + #[ink(function = 0xb20f)] fn approve( asset_id: u32, spender: DefaultAccountId, @@ -61,7 +61,7 @@ pub trait Psp22Extension { ) -> Result<()>; // PSP22 increase_allowance - #[ink(extension = 0x96d6)] + #[ink(function = 0x96d6)] fn increase_allowance( asset_id: u32, spender: DefaultAccountId, @@ -69,7 +69,7 @@ pub trait Psp22Extension { ) -> Result<()>; // PSP22 decrease_allowance - #[ink(extension = 0xfecb)] + #[ink(function = 0xfecb)] fn decrease_allowance( asset_id: u32, spender: DefaultAccountId, @@ -77,6 +77,7 @@ pub trait Psp22Extension { ) -> Result<()>; } +#[derive(Debug, PartialEq, Eq)] #[ink::scale_derive(Encode, Decode, TypeInfo)] pub enum Psp22Error { TotalSupplyFailed, diff --git a/integration-tests/rand-extension/lib.rs b/integration-tests/rand-extension/lib.rs index fe059618105..4fad0867c39 100755 --- a/integration-tests/rand-extension/lib.rs +++ b/integration-tests/rand-extension/lib.rs @@ -7,14 +7,14 @@ use ink::env::Environment; /// file `runtime/chain-extension-example.rs` for that implementation. /// /// Here we define the operations to interact with the Substrate runtime. -#[ink::chain_extension] +#[ink::chain_extension(extension = 666)] pub trait FetchRandom { type ErrorCode = RandomReadErr; /// Note: this gives the operation a corresponding `func_id` (1101 in this case), /// and the chain-side chain extension will get the `func_id` to do further /// operations. - #[ink(extension = 1101)] + #[ink(function = 1101)] fn fetch_random(subject: [u8; 32]) -> [u8; 32]; } @@ -122,11 +122,11 @@ mod rand_extension { #[ink::test] fn chain_extension_works() { // given - struct MockedExtension; - impl ink::env::test::ChainExtension for MockedExtension { + struct MockedRandExtension; + impl ink::env::test::ChainExtension for MockedRandExtension { /// The static function id of the chain extension. - fn func_id(&self) -> u32 { - 1101 + fn ext_id(&self) -> u32 { + 666 } /// The chain extension is called with the given input. @@ -135,13 +135,18 @@ mod rand_extension { /// SCALE encoded result. The error code is taken from the /// `ink::env::chain_extension::FromStatusCode` implementation for /// `RandomReadErr`. - fn call(&mut self, _input: &[u8], output: &mut Vec) -> u32 { + fn call( + &mut self, + _func_id: u16, + _input: &[u8], + output: &mut Vec, + ) -> u32 { let ret: [u8; 32] = [1; 32]; ink::scale::Encode::encode_to(&ret, output); 0 } } - ink::env::test::register_chain_extension(MockedExtension); + ink::env::test::register_chain_extension(MockedRandExtension); let mut rand_extension = RandExtension::new_default(); assert_eq!(rand_extension.get(), [0; 32]);