ink icon indicating copy to clipboard operation
ink copied to clipboard

Make ink! contracts viable dependencies for the runtime

Open athei opened this issue 2 years ago • 6 comments

When a contract A depends on another contract B and sets the ink-as-dependency feature we generate code that allows A to easily call into B. The generated code is specifically constructed to do a cross contract call and can't be used for other purposes easily. We want to change this.

The objective is to generate additional code that is more general and can be consumed by other targets than an ink! contract. The target we have in mind here are substrate runtimes: We want to make it convenient for substrate runtimes to call into ink! contracts.

The idea is that a runtime or pallet can depend on a ink! contract in its Cargo.toml just like a contract would. This will enable the runtime to learn about the messages and constructors of the contract and conveniently call these functions.

The e2e framework is probably doing something similar. I didn't check. I think it is most easily understood with an example. I wrote some example code how I imagine the system working. It is basically how I think the generated code could look like and how I would integrate it into pallet-contracts. It is by no means perfect. But I think it gets the point across: https://github.com/athei/call-ink-example/blob/master/src/main.rs

Its quite a bit of code. But it is a complete example.

athei avatar Feb 23 '23 07:02 athei

The option of calling SCs from pallets conveniently (as it's possible right now but definitely not conveniently) would be very useful! We were discussing recently several use cases for that, e.g sth similar to this PR https://github.com/paritytech/substrate/pull/13203 but using a SC DEX instead of a DEX pallet.

mike1729 avatar Feb 23 '23 08:02 mike1729

@ascjones Please specify this out in a way that an external contributor could work on it.

cmichi avatar Apr 11 '23 15:04 cmichi

We are looking into this as well atm.

deep-ink-ventures avatar Apr 18 '23 16:04 deep-ink-ventures

The generated code is specifically constructed to do a cross contract call and can't be used for other purposes easily. We want to change this.

The first thing I would do here is to see what is possible to achieve with the existing CallBuilder codegen that we are currently using for cross contract calls. It would be nice to have a similar API to that for consistency, and less codegen for different usage contexts.

The e2e framework is probably doing something similar.

Have a look at this PR which was an experiment I did to see if it was possible to use the existing contract ref messages which return a CallBuilder for which you can set parameters e.g. (gas limit, transferred value). The outcome was that it was possible with minimal modifications to the CallBuilder. See the erc20 example in that PR.

To get started with experimenting with this, you could reference a contract using ink-as-dependency from your pallet, exactly as you would for a cross contract call. This contract could then have an ink dependency pointing at your local copy of the ink source where you could make any tweaks to the API.

Again, ideally I would like to see a unified API for building contract calls for cross contract/e2e testing and calling contracts from substrate pallets. And I believe this will be possible while reusing most of the codegen we already have.

ascjones avatar May 10 '23 16:05 ascjones

The first thing I would do here is to see what is possible to achieve with the existing CallBuilder codegen that we are currently using for cross contract calls. It would be nice to have a similar API to that for consistency, and less codegen for different usage contexts.

It is possible to achieve this with minor changes in the call builder and a new trait. The call builder's current implementation(invoking of the contract) only works with the EnvInstance, which limits it only to the contract's context.

But if we make it configurable, we will be able to work with e2e or substrate runtime through the same API.

image

Have a look at https://github.com/paritytech/ink/pull/1669 which was an experiment I did to see if it was possible to use the existing contract ref messages which return a CallBuilder for which you can set parameters e.g. (gas limit, transferred value). The outcome was that it was possible with minimal modifications to the CallBuilder. See the erc20 example in that PR.

It is clear that we need a selector, arguments, and output type from the CallBuilder. All other information belongs to the executor/invoker and only defines the parameters of the call.

The substrate already has access to this information via ink-as-depdendecy, so it should be possible to do.

Again, ideally I would like to see a unified API for building contract calls for cross contract/e2e testing and calling contracts from substrate pallets. And I believe this will be possible while reusing most of the codegen we already have.

Maybe we could have something like this:

pub trait InkInvoker {
    fn invoke<Input, Output>(&self, selector: Selector, arguments: Input) -> Output {
        self.try_invoke(selector, arguments).expect("Got an error durign invoking")
    }
    
    fn try_invoke<Input, Output>(&self, selector: Selector, arguments: Input) -> Result<Output, SomeError>;
}

/// `ContractInvoker` has `AccountId`, some configuration about `CallType` and `CallFlags`.
impl<E: Environment> InkInvoker for ContractInvoker<E> {
    fn try_invoke<Input, Output>(&self, selector: Selector, arguments: Input) -> Result<Output, SomeError> {
        let params = CallParams::from(self, selector, arguments);
        <EnvInstance as OnInstance>::on_instance(|instance| {
            TypedEnvBackend::invoke_contract::<E, Input, Output>(instance, params)
        })
    }
}

/// `E2EDryRunInvoker` has `&E2EClient` and dry run configuration.
/// The same ideas is appliable to `E2ECallInvoker`.
impl InkInvoker for E2EDryRunInvoker {
    fn try_invoke<Input, Output>(&self, selector: Selector, arguments: Input) -> Result<Output, SomeError> {
        let exec_input = ExecInput::from(selector, arguments);
        self
         .api
         .call_dry_run(
             Signer::account_id(self.signer).clone(),
             dest,
             exec_input,
             self.value,
             self.storage_deposit_limit,
         )
    }
}

impl InkInvoker for SubstrateInvoker {
    ...
}

Selector, Input, and Output are the core of the any call. Having them, we can create an invoker instance for a specific method in a specific context. The usage transforms only into defining the right type and implementing the try_invoke function, but mostly it is one-time work.

xgreenx avatar Jul 30 '23 14:07 xgreenx

I’m refactoring the envcrate to make agnostic of OnInstance implementation as part of the #1279. This might also be relevant for this issue

https://github.com/paritytech/ink/compare/master...gn/configurable-buffer

SkymanOne avatar Jul 30 '23 18:07 SkymanOne