Description
Since ink! 3.0-rc1 it is possible to define special trait definitions for ink! smart contract using the #[ink::trait_definition]
proc. macro.
Motivation
Defining and implementing such a trait works as expected with regard to some technical limitations.
Smart contracts today can some limited use of traits by calling or instantiating a contract using just the trait it implements.
However, this process is static and not dynamic, meaning that it is currently not possible to store a contract, e.g. in a ink_storage::Lazy<dyn MyContractTrait>
or having an input parameter to a contract constructor or message with contract: &dyn MyContractTrait
and be able to call constructors and messages defined in the trait dynamically on the provided contract instance.
This feature is critical to making trait definitions and implementation actually an integrated user experience for ink! smart contracts.
Syntax
There are several different ways in which a smart contract might want to interact with another smart contract dynamically.
As Message Parameter
It should be possible to declare a smart contract message or constructor with an input argument with contract: &[mut] dyn MyContractTrait
where MyContractTrait
is a trait definition that was defined using ink!'s #[ink::trait_definition]
and contract
is an instance of a smart contract (basically its AccountId
) that implements this exact trait.
Then the ink! constructor or message receiving this argument can call any messages defined on the trait definition using the given contract
instance. Note that it would not be possible to call trait definition constructors on contract
since it represents an already instantiated contract.
Also we have to differentiate between contract: &dyn MyContractTrait
and &mut dyn MyContractTrait
where only the latter allows to call &mut self
defined ink! messages.
It would be possible to syntactically detect usages of &dyn Trait
in the input parameters of an ink! smart contract message or constructor and convert the type into something that is actually usable as input parameter, e.g. implements scale::Codec
and has an AccountId
field for the indirection etc. However, with general ink! design we try to be very explicit about user intentions which is why it might be preferable to start with an ink! annotation again and see how far we can get with it.
The proposed ink! annotation could be something like #[ink(dyn)]
or #[ink(trait)]
.
Example
In the following examples we took the #[ink(trait)]
as proposed ink! annotation. Note that this design is not final.
#[ink(message)]
pub fn call_back(&self, #[ink(trait)] callback: &dyn ContractCallback) {
if self.condition() {
callback.do_something();
}
}
Or ...
#[ink(message)]
pub fn call_back_mut(&self, #[ink(trait)] callback: &mut dyn ContractCallback) {
if self.condition() {
callback.mutate_something();
}
}
Where do_something
is an ink! message defined in the ContractCallback
trait definition as &self
message and mutate_something
is an ink! message defined there as well as &mut self
message:
#[ink::trait_definition]
pub trait ContractCallback {
#[ink(message)]
fn do_something(&self);
#[ink(message)]
fn mutate_something(&mut self);
}
Further complications arise in the case of multiple trait bounds such as &dyn MyContractTraitA + MyContractTraitB + ...
. If we find out that these cases introduce severe complications we might drop them from the initial support and will try to work on these enhancements on a later point in time.
As Storage Entity
It might also be handy to store references to other smart contracts and being able to call them by traits that they implement.
Given the ContractCallback
trait from last section the natural way a Rust programmer would make use an instance implementing this trait is by using the following storage fields and types:
#[ink(storage)]
pub struct MyStorage {
value: Box<dyn ContractCallback>,
vec: Vec<Box<dyn ContractCallback>>, // ... or similar
array: [Box<dyn ContractCallback>] // ... etc
}
So it is all about utilizing Rust's owning smart references which is the Box<T>
abstraction.
However, to a normal Rust developer usage of a Box
would imply usage of dynamic heap memory allocations which we do not really need for our purposes since the underlying AccountId
with which we identify or point to or reference a smart contract through its implemented interface is already serving as a pointer and the smart contract instance itself with its defined trait implementation that is about to be dispatched is already sufficient to perfectly emulate the dynamic dispatch.
So per design we ideally want to convert usages of Box<dyn Trait>
into some other static type that acts like a type that implements the trait and therefore can be used interchangeably. This might count for the input parameters described above as well.
On the other handside making it possible to use Box<T>
in places that are later completely replaced by some other non-allocating type that is to be used to indirect trait based contract calls (scale::Codec
impl and internal AccountId
) might confuse users which is why we should actually be more clever about how exactly we determine usage of dynamic trait based indirect calls for contracts.
The current proposal is to again introduce a new ink! annotation #[ink(dyn)]
or #[ink(trait)]
etc. in order for a user to flag a field of the #[ink(storage)]
struct so that the ink! code analysis can recursively step through the syntactic type and replace uses of dyn Trait
with whatever utility type is to be used in order to actually call traits implemented by contracts via cross contract calling.
As there might be aliases or custom types that might hide such parameters from the #[ink(storage)]
struct definition we need to allow users to apply this new ink! annotation on structs fields, enums variant fields and type alises.
Example
In the following examples we took the #[ink(trait)]
as proposed ink! annotation. Note that this design is not final.
#[ink(storage)]
pub struct MyStorage {
#[ink(trait)]
single: dyn ContractCallback, // replaces dyn Trait directly
#[ink(trait)]
vec: StorageVec<dyn ContractCallback>, // replaces dyn Trait in the generic argument of the StorageVec
#[ink(trait)]
array: [dyn ContractCallback; 10], // replaces dyn Trait of the array argument,
#[ink(trait)]
aliased: Aliased,
}
#[ink(trait)]
type Aliased = dyn ContractCallback; // replaces dyn Trait directly
pub struct MyStruct {
#[ink(trait)]
single: dyn ContractCallback, // replaces dyn Trait directly
}
pub enum MyEnum {
A(#[ink(trait)] dyn ContractCallback), // replaces dyn Trait directly
B,
}