5

Wondering if anyone here has some example rust code or rust patterns for encoding Call objects for use with cross-chain XCM::Transact operations?

Don't want to necessarily re-invent the wheel ourselves. I guess the encoder could depend on the foreign chain's runtime crate, but that seems like quite a heavy solution, increasing compile times.

2 Answers 2

6

This is very interesting, thanks for bringing this up. Definitely a scenario that can rise questions at the moment.

A quick note before starting, one can do all the transact payload prep encoding with the help of double_encoded. But how does a runtime propoerly encodes a call for another one, a different one ? The sending side will need to have the following into account:

A certain call, let's call it c, has an index within the calls of a certain pallet, call_index, and said pallet at the same time has an index within the runtime it is installed in, a pallet index. Not only this, but c has a certain function signature. These three elements may change over time making the encoding of c change too.

The current work around looks like this, runtime A will maintain the call struct, or a similar enough one, of a certain call on runtimeB with all the information needed for its encoding.

Check the following example - source

// The fixed index of `pallet-ethereum-xcm` within various runtimes.
#[derive(Clone, Eq, PartialEq, Encode)]
#[allow(dead_code)]
pub enum EthereumXcm {
    #[codec(index = 38u8)]
    Moonbase(EthereumXcmCall),
}

// The fixed index of calls available within `pallet-ethereum-xcm`.
#[derive(Clone, Eq, PartialEq, Encode)]
#[allow(dead_code)]
pub enum EthereumXcmCall {
    #[codec(index = 0u8)]
    Transact { xcm_transaction: EthereumXcmTransaction },
    #[codec(index = 1u8)]
    TransactThroughProxy { transact_as: H160, xcm_transaction: EthereumXcmTransaction },
}

These structs can now be used for encoding the desired call

pub(crate) fn transact(
    contract_address: impl Into<H160>,
    call_data: BoundedVec<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>,
    gas_limit: impl Into<U256>,
    value: Option<U256>,
) -> Vec<u8> {
    EthereumXcm::Moonbase(EthereumXcmCall::Transact {
        xcm_transaction: EthereumXcmTransaction::V2(EthereumXcmTransactionV2 {
            gas_limit: gas_limit.into(),
            action: TransactionAction::Call(contract_address.into()),
            value: value.unwrap_or_default(),
            input: call_data,
            access_list: None,
        }),
    })
    .encode()
}

These helper struct are maintained on the sender's side, not the receiver's, so they might get outdated, right ? Indeed, this is prone to happen with this approach, even though changing call indices is not the most common thing, it can definitely happen. So, next question, if one has gone this far, runtime A is properly encoding calls for B and observing the expected results after its execution, how to avoid triggering the execution of another random call in the future ?

ExpectPallet instruction comes to save the day, including this one before Transact will assure the expected version of the target pallet on B, this implies that the call indices haven't changed.

Unfortunately at this moment there is not an instruction that give us the same assurance for a pallet index even though these usually don't change much.

Another helpful instruction could be SetErrorHandler where other instructions can be set in case an error is triggered. In this scenario could be used in case the pallet is not present in B.

-1

Sure, here's an example Rust code snippet for encoding a Call object:

use codec::{Encode, Decode};
use frame_support::traits::Currency;
use xcm::v0::{Junction, MultiLocation, NetworkId, Xcm, SendXcm};

fn encode_call<C: Currency>(currency: &C, dest_chain_id: u8, dest_account: MultiLocation) -> Vec<u8> {
    // Create the call object with the desired function and arguments
    let encoded_call = Call::my_custom_function(42).encode();

    // Create the XCM message with the encoded call as its body
    let xcm_message = Xcm::WithdrawAsset {
        assets: vec![(currency.get_currency_id(), 100)],
        effects: vec![
            Xcm::DepositAsset {
                assets: vec![(currency.get_currency_id(), 100)],
                dest: Junction::Parent.into(),
            }.into(),
            Xcm::Transact {
                origin_type: MultiLocation::parent(),
                require_weight_at_most: 1_000_000_000,
                call: (NetworkId::Any, MultiLocation::new(dest_chain_id.into(), dest_account)).into(),
            }.into(),
        ],
    };

    // Wrap the XCM message in a `SendXcm` object with the specified destination
    let send_xcm = SendXcm {
        message: xcm_message,
        max_weight: 1_000_000_000,
        recipient: Junction::Account(dest_account),
        from: MultiLocation::new(NetworkId::Any.into(), MultiLocation::SelfAddressing(vec![0; 32])),
        settles: false,
    };

    // Encode the `SendXcm` object as bytes using SCALE codec
    send_xcm.encode()
}

This code assumes that you have defined a custom function my_custom_function within the Call type, and that you have a currency object (C) which implements the Currency trait from the frame_support crate. The code creates an XCM message which withdraws and deposits some currency on the current chain, and then sends a cross-chain transaction to execute the custom function on the specified destination chain and account.

Note that the above code is just an example and may not be exactly what you need for your specific use case. You will need to customize it according to your own requirements.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.