-
Notifications
You must be signed in to change notification settings - Fork 431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature] Dynamic trait based contract calling #631
Comments
To help clear out some design requirements I want to demonstrate some "optimal" goals of this implementation proposal. This is just to show certain ways in which this feature is going to be used. #[ink::trait_definition]
pub trait Callback {
#[ink(message)]
fn callback(&self, value: i32) -> bool;
}
#[ink::contract]
pub mod bc {
#[ink::storage]
pub struct BackCaller {
counter: i32,
#[ink(dyn)]
cb: dyn Callback,
}
impl BackCaller {
/// Constructs a new back caller with the given callback.
#[ink(constructor)]
pub fn new(#[ink(dyn)] cb: &dyn Callback) -> Self {
Self { counter: 0, cb }
}
/// Calls the stored callback with the current value and either bump or reset it.
#[ink(message)]
pub fn bump(&mut self) -> bool {
let reset = self.cb(self.counter);
if reset {
self.v = 0;
} else {
self.counter += 1;
}
}
/// Returns the current value of the counter.
#[ink(message)]
pub fn get(&self) -> i32 {
self.counter
}
}
} Note: The above syntax is not final and may change significantly over the course of design and implementation. Example: Use BackCaller As DependencyTODO Example: Use BackCaller StandaloneTODO Idea for Mapping from Trait to Concrete TypeOne of the biggest problems with the design proposed by this feature is that it is really hard to have a semantic level (Rust type system) way to map from a given Rust trait definition (e.g. in We could introduce a known type in
Where we know that this |
So can I get a wrapped instance for a deployed contract, like 'from_account_id'? |
It is not implemented right now in ink!. If the contract that you want to work with is implementing a trait, then you can use wrapper feature from OpenBrush. You can use any |
@cmichi is this still something that will be added in ink!4.0 ? |
@sebastienpattyn93 Sorry for only replying now, your message disappeared in a flood of GitHub notifications. We didn't manage to include it for 4.0, but I just had a conversation with @xgreenx about implementing it in the short term. |
We added basic support for The current codegen adds It is not a problem to remove The primary purpose why we have an It allows us to write But we also can solve it and provide a new syntax to use // `Erc20` is trait defined via `#[ink::trait_definition]` and `account_id` is variable of `AccountId` type
let contract_ref: contract_ref!(Erc20) = account_id.into();
let builder = call_builder!(contract_ref.transfer(20, to));
// or
// let builder = call_builder!(<_ as Erc20>::transfer(&mut contract_ref, 20, to));
builder.transferred_value(0).invoke(); The |
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 withcontract: &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
whereMyContractTrait
is a trait definition that was defined using ink!'s#[ink::trait_definition]
andcontract
is an instance of a smart contract (basically itsAccountId
) 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 oncontract
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. implementsscale::Codec
and has anAccountId
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.Or ...
Where
do_something
is an ink! message defined in theContractCallback
trait definition as&self
message andmutate_something
is an ink! message defined there as well as&mut self
message: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: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 underlyingAccountId
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 internalAccountId
) 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 ofdyn 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.As Dynamic Let-Binding
TODO
The text was updated successfully, but these errors were encountered: