CIS2 Transfer not working when trying to Pass params to OnReceivingCis2 hook

I had my cis2 transfer hook implemented like this

/// onReceivingCIS2 hook supplied when making transfers to the contract
#[receive(contract = "game_room", name = "onReceivingCIS2", error = "Error")]
fn process_cis2_deposit<S: HasStateApi>(
    _ctx: &impl HasReceiveContext,
    _host: &impl HasHost<State, StateApiType = S>,
) -> ReceiveResult<()> {
    Ok(())
}

I want to adjust my smart contract logic to add users to the game room participants (a set in state), when they make a cis2 transfer to my smart contract. However there might be other reasons for which my smart contract might receive a transfer, so I only want to enroll the transfer sender to game room if they indicate interest by passing a “should_enroll” : true in the data field being sent with the transfer.
I have now updated my “onReceivingCIS2” to look like this

/// The `additionData` that has to be passed to the `onReceivingCIS2` entry point.
#[derive(Serialize, SchemaType)]
pub struct AdditionalDataParams {
    pub should_enroll: bool,
}

/// Hook called when smart contract receives a cis2 token transfer
/// Attempts to add person who made the transfer to the game room
/// If the person has indicated interest by
#[receive(
    contract = "first_game_room",
    name = "onReceivingCIS2",
    mutable,
    parameter = "OnReceivingCis2DataParams<ContractTokenId, ContractTokenAmount, AdditionalDataParams>",
    error = "Error",
    enable_logger
)]
fn process_cis2_deposit(
    ctx: &impl HasReceiveContext,
    host: &mut Host<State>,
    logger: &mut impl HasLogger,
) -> Result<(), Error> {
    // Ensure the sender is the cis2 token contract.
    ensure!(
        ctx.sender().matches_contract(&host.state().cis2_contract),
        Error::NotTokenContract
    );

    // Getting input parameters.
    let params: OnReceivingCis2DataParams<
    ContractTokenId,
    ContractTokenAmount,
    AdditionalDataParams,
>= ctx.parameter_cursor().get()?;

    
    if params.data.should_enroll {
        // ... Logic for adding to game room
    }

    Ok(())
}

My json parameter file look like this

[{
  "amount": "200",
  "data": [
    123,  34, 115, 104, 111, 117,
    108, 100,  95, 101, 110, 114,
    111, 108, 108,  34,  58, 116,
    114, 117, 101, 125
  ],
  "from": {
    "Account": ["4ezLXX1oNXdZ5M8msooxoKhyPheKRuyiZ7ZfuYPpedi2MSicsq"]
  },
  "to": {
    "Contract": [
      {
        "index": 8663,
        "subindex": 0
      },
      "onReceivingCIS2"
    ]
  },
  "token_id": ""
}]

for the data I took an object { “should_enroll”: true} and converted it to hex, then to bytes.

when I invoke the euroeE contract using the following command

concordium-client contract invoke 7260 --entrypoint transfer --parameter-json parameters/fund.json --invoker-account jerry-ccd-wallet-testnet.json --energy 60000 --grpc-port 20001

I get back an InvokeContractError (-6). I have tried debugging the issue with my update and haven’t been able to spot the problem. Once I remove the AdditionalDataParams i.e revert back to my initial “onReceivingCIS2” everything works well. please any idea on what I’m doing wrong?

Hi @jerry

Straight to the solution:

To encode {"should_enroll": true} use

[{
  ...,
  "data": [1],
  ...
}]

To encode {"should_enroll": false} use

[{
  ...,
  "data": [0],
  ...
}]

Explanation:
So #[derive(Serialize)] defines how the struct is serialized to bytes, and in order to reduce costs, this serialization only encodes the values and not the field names (field names are stored separate in the smart contract schema), this is a lot more compact and therefore saves on cost.

@limemloh I just tried your recommendation, using

[{
  ...,
  "data": [0],
  ...
}]

successfully Invokes the contract, although what ccd scan shows is that it invokes with should_enroll: true, but the code block that executes seems to be correct (the code that runs when should_enroll is false seems to be executed)

when I try invoking the contract with

[{
  ...,
  "data": [1],
  ...
}]

I still get an InvokeContractError (-6)

Thanks @limemloh your solution actually did work, the invokation with
[{
…,
“data”: [1],
…
}]
was failing because of other error checks I had the If block when should_enroll is true. something to note though is that ccd scan shows that what is passed to “onReceivingCIS2” is should_enroll: true even when its false that is passed

1 Like

Hi @jerry
It seems that we have a bug in the schema type for OnReceivingCis2DataParams from concordium-cis2 library, which explains why CCD scan is displaying it wrongly.
The bug should only be relevant for the JSON representation of the parameter, so it should work for the smart contract.

1 Like

@limemloh please one more question, although unrelated. I want to use the logger to log some events in my smart contract. where can I find these logs? I’m guessing it the Logs that appear on ccd scan as hexadecimal right? If it is, how can I easily deserialise these logs so I can see the actual json i logged?

@jerry
If you include the events in the embedded smart contract schema it should be displayed as JSON on CCDScan instead of the hex strings.

You do this by adding the event type for the init-macro:

#[init(..., event = "MyEvent")]
fn my_init(...) -> ... {

The wCCD example does this.

1 Like

Thank you @limemloh worked well. Please I still got another question. In my if block I got a couple of ensure checks

if params.data.should_enroll {
        // Ensure that only accounts can join game room
        let potential_participant = match params.from {
            Address::Contract(_) => bail!(Error::OnlyAccount),
            Address::Account(account_address) => account_address,
        };

        let state = host.state();
        let entry_fee = state.entry_fee;

        ensure!(params.amount >= entry_fee, Error::AmountLessThanEntryFee);

        // esnure token sent is the token used for this game room
        ensure_eq!(state.token_id, params.token_id, Error::WrongTokenID);
}

when I invoke the euroe transfer entrypoint with should_enroll as true and any of these error occurs e.g AmountLessThanEntryFee , I would think that this error should be returned, instead what gets returned is still the InvokeContractError from euroE smart contract. Is there a way to have the error from my onReceivingCIS2 be returned, so It can help me handle failures better

I am afraid not.
The EUROe smart contract does not forward the error message back, and we currently don’t have tools for debug tracing a call.

1 Like

Thank you @limemloh. Really appreciate your help so far :raised_hands:. sorry to still bother you again. I updated my AdditonalDataParams to take more optional fields

pub struct AdditionalDataParams {
    pub should_enroll: bool,
    pub user_id: Option<String>,
    pub game_room_id: Option<String>,
    pub team_id: Option<String>,
}

so now when testing in my json file I have been able to test for two scenarios, but having difficulty testing for the last scenario. The two senarios I have been able to test for are

  1. when should_enroll is false and all other properties (user_id, team_id,game_room_id) are “None”. I simply send data: [0,0,0,0] which works
  2. when should_enroll is true and all other properties are “None”. I simply send data: [1,0,00] which works

The final scenario I want to test for is when should_enroll is true and all other properties (user_id, team_id,game_room_id) are defined. how do I come up with the byte array for this. for instance if I had my data as

 {
    should_enroll: true,
    user_id: "5a6281b8-a9ff-4f58-9714-2547df49d41a",
    game_room_id: "dc316815-4ada-402f-8cb1-92aa1c4f6088",
    team_id: "8e37b79f-bab9-4173-9641-b5660082172f",
}

how do I convert the value fields to a byte array or maybe atleast to a hex string (if I can get it to hex I believe I can get it to byte array). The conversion I tried so far doesn’t seem to work correctly

So some of our SDKs have helpers for this, but a quick solution from a smart contract project could be to print the bytes in a test.
In your smart contract lib.rs you could add a testcase where you construct the type and serialize it:

#[test]
fn test_print() {
    let p = AdditionalDataParams {
        should_enroll: true,
        ...
    };
    println!("Byte encoding: {:?}", to_bytes(&p));
}

To see the output, run:

cargo test test_print -- --nocapture