5

I'm trying to write an off-chain test for an ink! smart-contract that uses chain extensions. The example showing how to mock an extension for tests is never using the supplied _input argument.

The functions of my chain extension have the following signatures:

#[ink::chain_extension]
pub trait BalanceExtension {
    type ErrorCode = BalanceReadErr;

    #[ink(extension = 1101, returns_result = false)]
    fn fetch_balance(owner: ink_env::AccountId, asset: Asset) -> u128;

    #[ink(extension = 1102, returns_result = false, handle_status = false)]
    fn transfer_balance(
        from: ink_env::AccountId,
        to: ink_env::AccountId,
        asset: Asset,
        amount: u128,
    ) -> ();
}

.

For the tests I created a mock as follows:

        type FetchBalanceInput = [u8; 32 + 32 + 12]; // 1-> owner:AccountId, 2-> asset_issuer: [u8;32], 3-> asset_code: [u8;12]

        struct MockedBalanceExtension {
            balances: Rc<RefCell<BalanceMapping>>,
        }

        impl ink_env::test::ChainExtension for MockedBalanceExtension {
            /// The static function id of the chain extension.
            fn func_id(&self) -> u32 {
                1101
            }

            fn call(&mut self, mut _input: &[u8], output: &mut Vec<u8>) -> u32 {
                // log input by panicking
                // panic!("input: {:?}, length: {:?}", _input, _input.len(),);
                let input: FetchBalanceInput =
                    <FetchBalanceInput as scale::Decode>::decode(&mut input).unwrap();

                let mut account_array: [u8; 32] = Default::default();
                account_array.copy_from_slice(&input[0..32]);
                let account_id = AccountId::from(account_array);

                let mut issuer_array: [u8; 32] = Default::default();
                issuer_array.copy_from_slice(&input[32..64]);

                let mut asset_code_array: [u8; 12] = Default::default();
                asset_code_array.copy_from_slice(&input[64..]);

                let asset: Asset = (issuer_array, asset_code_array);

                let map = self.balances.borrow_mut();
                let balance = map.get(&(account_id, asset)).unwrap_or_else(|| {
                    panic!(
                        "No entry found for account_id: {:?}, asset: {:?}",
                        account_id, asset,
                    )
                });

                scale::Encode::encode_to(&balance, output);

                0 // 0 is error code
            }
        }

Now when I test my smart contract and call the fetch_balance function with the following arguments:

let account_id = AccountId::from([0x01; 32]);
let asset = ([0x02; 32], [0x03; 12]);
fetch_balance(account_id, asset);

the panic! logs an input of [49, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] and I don't understand why. The 32x 1s are obviously from the account_id but the passed asset argument is completely ignored and also what does the leading 49 mean?

I looked a bit into the implementation of the chain extension and it seems that the _input that is passed to the call() is encoded beforehand, so I tried to decode it. But I logged both the passed _input without decoding it and with decoding (like shown in the snippet) and it does not change much.

Has someone experience with testing a chain extension of ink! smart contracts while using the provided input argument? Am I missing something here? I'd appreciate any help.

4
  • For me all arguments are encoded correctly, but I need to skip the first few bytes. The number of bytes I need to skip is different depending on arguments, and I can't figure out what the first few bytes are for. Commented Jun 22, 2022 at 13:54
  • Try adding the chain-extension tag. I want an answer to this also. Commented Jun 22, 2022 at 14:02
  • I found the answer by digging through the source code. :D Commented Jun 22, 2022 at 16:05
  • I also noticed that I can just skip the first two bytes and the rest is the data I'm looking for without having to call decode(). But I didn't notice that it depends on the arguments. I would also be curious as to why this is the case.
    – ebma
    Commented Jun 28, 2022 at 7:52

1 Answer 1

3

The value is encoded again to a vector of bytes, so you can decode it like this:

let bytes: Vec<u8> = Decode::decode(&mut &input[..]).unwrap();
let (owner, asset): (AccountId, Asset) = Decode::decode(&mut &bytes[..]).unwrap();

You can see a working example here.

1
  • Thanks a lot for sharing that example. Happy to see another project using and testing with chain extensions. You can find the code I used for solving it here.
    – ebma
    Commented Jun 28, 2022 at 7:54

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.