Using dynamic calls

If you find yourself needing more expressive power than the references have to offer, you can use the dynamically constructed calls.

Even though the dynamically constructed calls are the more versatile and powerful method, we encourage you to stick to references if your use case allows it. The drawbacks here are the complete lack of the compiler's assistance when constructing calls: the errors will present themselves only at runtime.

There is only one step involved in the whole process, albeit a little more cumbersome.

Building and executing the call

First of all, you will need some imports:

use ink::env::{
    call::{build_call, ExecutionInput, Selector},
    DefaultEnvironment,
};

// contract-specific imports:
use highlighted_posts::{HighlightedPostsError, HIGHLIGHT_POST_SELECTOR};

We will paste the whole code snippet and later explain the less obvious parts:

let call_result: Result<Result<(), HighlightedPostsError>, ink::LangError> =
    build_call::<DefaultEnvironment>()
        .call(highlight_board)
        .exec_input(
            ExecutionInput::new(
                Selector::new(HIGHLIGHT_POST_SELECTOR)
            ).push_arg(author).push_arg(id),
        )
        .transferred_value(cost)
        .returns::<Result<Result<(), HighlightedPostsError>, ink::LangError>>()
        .invoke();

If you come from the OOP world, this style is similar to the 'fluent interface' pattern.

  • We initialize our builder with the build_call method.

  • With call, we specify what contract account we want (this differs from the references, which were initialized with a code hash. Here we need the contract's account, which you can easily find in the Contracts UI).

  • With exec_input we specify which method to call (by using a selector: more on that later) and using push_arg to supply the arguments.

  • By using transferred_value we can send some tokens to the receiving method. Note that this method needs to be marked with the payable macro in order to be able to act on the transfer in any way.

  • We need to specify the return type of the call using returns. Note that each call will have the original return type of the method wrapped in a Result<T, ink::LangError>, on account of all Ink! messages wrapping their return types this way.

  • To actually fire the call we will use the invoke method.

As with references, make sure to inspect the result of the call, handling all the failure cases.

Selectors

You may have noticed that we didn’t use the method’s name directly in the call above. Instead, we used a selector, which is a number associated with each message in Ink!. There are two ways to handle selectors: the explicit method, as shown above, and the macro-based implicit method.

Explicit selectors

In order to use explicit selectors, you first need to declare them on your 'callee' contract. As you're probably able to guess by now, this is done using a macro:

#[ink(message, payable, selector = 7)]
pub fn add()

It is a good practice to create constants for each selector to help eliminate errors. The selectors are actually four-element byte arrays, so we declare it in the following way:

pub const HIGHLIGHT_POST_SELECTOR: [u8; 4] = [0, 0, 0, 7];

Of course, to allow other modules to use it, we need to export our constant:

pub use highlighted_posts::HIGHLIGHT_POST_SELECTOR;

Just as with our reference export, this needs to be placed at the top level.

Now we're able to import it inside our Bulletin Board contract and use it for the ExecutionInput. as in the call snippet above.

Macro-based selectors

In case you don't feel like specifying the selectors manually, you can use the ink::selector_bytes! macro (notice the exclamation mark at the end of the name).

If we use it in our example from before, the Selector instantiation will change to the following:

Selector(ink::selector_bytes("add"))

Which way you end up using is entirely up to you!

Closing remarks

You are now ready to leverage the full potential of Ink! smart contracts and write expressive, complex code. In case you'd like to dive deeper into the cross-contract calls, we encourage you to take a look at the official Ink! documentation.

As a final note, always test your contracts on the Testnet first, considering various edge cases such as high load and interactions from potentially malicious actors. While we advocate testing even the simplest contracts, thorough testing is especially crucial when dealing with cross-contract calls.

Last updated