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:

bulletin_board/lib.rs
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:

bulletin_board/lib.rs
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've probably noticed that we didn't use the called method's name in the call above. Instead, we needed to use a selector, which is a number associated with each message in Ink!. There are two ways of going about the selectors: the explicit one, presented 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:

highlighted_posts/lib.rs
#[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:

highlighted_posts/lib.rs
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:

bulletin_board/lib.rs
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:

bulletin_board/lib.rs
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 the last remark, please remember to always test your contracts on the Testnet first, taking into account different edge cases (including high load and potentially malicious actors interacting with your contract). We are huge advocates of testing even the simplest of contracts but if you're making cross-contract calls, the need for thorough testing is even more apparent.

Last updated