Unable to deserialize smart contract v1 state with the nodejs sdk

Hello,

I’m playing a bit around smart contract to familiarize myself with dapp principle and I am stuck trying to deserialize my contract state.
I have a website with the nodeJS SDK, the browser app and a smartcontract V1 on the TestNet.

I can instantiate the smartcontract with the following call :

concordiumHelpers.detectConcordiumProvider()
.then((provider) => {
	provider
		.connect()
		.then((acc) => {
			console.log("Account selected in the extension " + acc);

			provider.sendTransaction(
				acc,
				concordiumSDK.AccountTransactionType.InitializeSmartContractInstance,
				{
					amount: new concordiumSDK.GtuAmount(0n),
					moduleRef: new concordiumSDK.ModuleReference(
						'6dd75e22895dc69e471a8a9f4199959f2779507850c4dfb7702d41655e458169'
					),
					contractName: 'tictactoebet',
					maxContractExecutionEnergy: 30000n
				} as concordiumSDK.InitContractPayload,
				{
					game_name: 'Game 1',
					end: '2022-09-12T11:30:25Z',
				},
				'//8CAQAAAAwAAAB0aWN0YWN0b2ViZXQBABQAAgAAAAkAAABnYW1lX25hbWUWAgMAAABlbmQNBAAAAAMAAABiZXQEFAACAAAABAAAAG5hbWUWAhAAAABwbGF5ZXJfdG9fYmV0X29uFQIAAAADAAAAT25lAgMAAABUd28CFQUAAAALAAAAT25seUFjY291bnQCCgAAAEJldFRvb0xhdGUCCQAAAEJldENsb3NlZAIKAAAAQWxyZWFkeUJldAIQAAAATWlzc2luZ1BhcmFtZXRlcgIEAAAAam9pbgQUAAEAAAAEAAAAbmFtZRYCFQQAAAALAAAAT25seUFjY291bnQCCQAAAExvYmJ5RnVsbAIOAAAAQWxyZWFkeUFQbGF5ZXICEAAAAE1pc3NpbmdQYXJhbWV0ZXICBAAAAHZpZXcBFAAGAAAACQAAAGdhbWVfbmFtZRYCCgAAAGdhbWVfc3RhdGUVBAAAABEAAABXYWl0aW5nRm9yUGxheWVycwIFAAAAUmVhZHkCCgAAAEluUHJvZ3Jlc3MCCAAAAEZpbmlzaGVkAgkAAABiZXRfc3RhdGUVAgAAAAQAAABPcGVuAgYAAABDbG9zZWQCBwAAAHBsYXllcnMQAhQABAAAAAcAAABhY2NvdW50CwYAAABhbW91bnQKBAAAAG5hbWUWAggAAABwb3NpdGlvbhUCAAAAAwAAAE9uZQIDAAAAVHdvAggAAABnYW1ibGVycxACFAAEAAAABwAAAGFjY291bnQLBgAAAGFtb3VudAoGAAAAYmV0X29uFQIAAAADAAAAT25lAgMAAABUd28CBAAAAG5hbWUWAgMAAABlbmQNCgAAAHZpZXdfc3Rha2UBCg=='
			)
				.then((transaction_address) => {
					console.log("Init contract response : " + transaction_address);
					id = setInterval(checkTransactionStatus, 2000, transaction_address);
				})
				.catch((error) => console.log(error));
		});
});

I get the index id that I can use later to call my smart contract instance.
I can call the view function of my contract to get the state to display in my page, but I’m a little bit stuck when trying to deserialize the response with the contract schema.

concordiumHelpers.detectConcordiumProvider()
.then((provider) => {
	provider
		.connect()
		.then((acc) => {
			console.log("Account selected in the extension  " + acc);

			provider.getJsonRpcClient().invokeContract({
				method: `tictactoebet.view`,
				contract: { index: index, subindex: 0n },
			})
			.then((view_state) => {
				let rawContractState = Buffer.from(view_state.returnValue, 'hex');
				let schema = Buffer.from('//8CAQAAAAwAAAB0aWN0YWN0b2ViZXQBABQAAgAAAAkAAABnYW1lX25hbWUWAgMAAABlbmQNBAAAAAMAAABiZXQEFAACAAAABAAAAG5hbWUWAhAAAABwbGF5ZXJfdG9fYmV0X29uFQIAAAADAAAAT25lAgMAAABUd28CFQUAAAALAAAAT25seUFjY291bnQCCgAAAEJldFRvb0xhdGUCCQAAAEJldENsb3NlZAIKAAAAQWxyZWFkeUJldAIQAAAATWlzc2luZ1BhcmFtZXRlcgIEAAAAam9pbgQUAAEAAAAEAAAAbmFtZRYCFQQAAAALAAAAT25seUFjY291bnQCCQAAAExvYmJ5RnVsbAIOAAAAQWxyZWFkeUFQbGF5ZXICEAAAAE1pc3NpbmdQYXJhbWV0ZXICBAAAAHZpZXcBFAAGAAAACQAAAGdhbWVfbmFtZRYCCgAAAGdhbWVfc3RhdGUVBAAAABEAAABXYWl0aW5nRm9yUGxheWVycwIFAAAAUmVhZHkCCgAAAEluUHJvZ3Jlc3MCCAAAAEZpbmlzaGVkAgkAAABiZXRfc3RhdGUVAgAAAAQAAABPcGVuAgYAAABDbG9zZWQCBwAAAHBsYXllcnMQAhQABAAAAAcAAABhY2NvdW50CwYAAABhbW91bnQKBAAAAG5hbWUWAggAAABwb3NpdGlvbhUCAAAAAwAAAE9uZQIDAAAAVHdvAggAAABnYW1ibGVycxACFAAEAAAABwAAAGFjY291bnQLBgAAAGFtb3VudAoGAAAAYmV0X29uFQIAAAADAAAAT25lAgMAAABUd28CBAAAAG5hbWUWAgMAAABlbmQNCgAAAHZpZXdfc3Rha2UBCg==', 'base64');
				let state = concordiumSDK.deserializeContractState("tictactoebet", schema, rawContractState);
			})
			.catch((error) => console.log(error));
		});
});

I’m try to follow this concordium-node-sdk-js/packages/common at 780163f40afb00572bf6f20ed1f786bffa9c4663 · Concordium/concordium-node-sdk-js · GitHub

Raw data returned : '0600000047616d6520310000000000000000000068fc763183010000'  
hexadecimal schema: ffff02010000000c000000746963746163746f6562657401001400020000000900000067616d655f6e616d65160203000000656e640d040000000300000062657404140002000000040000006e616d65160210000000706c617965725f746f5f6265745f6f6e1502000000030000004f6e65020300000054776f0215050000000b0000004f6e6c794163636f756e74020a000000426574546f6f4c6174650209000000426574436c6f736564020a000000416c726561647942657402100000004d697373696e67506172616d6574657202040000006a6f696e04140001000000040000006e616d65160215040000000b0000004f6e6c794163636f756e7402090000004c6f62627946756c6c020e000000416c726561647941506c6179657202100000004d697373696e67506172616d65746572020400000076696577011400060000000900000067616d655f6e616d6516020a00000067616d655f737461746515040000001100000057616974696e67466f72506c617965727302050000005265616479020a000000496e50726f6772657373020800000046696e697368656402090000006265745f73746174651502000000040000004f70656e0206000000436c6f7365640207000000706c61796572731002140004000000070000006163636f756e740b06000000616d6f756e740a040000006e616d65160208000000706f736974696f6e1502000000030000004f6e65020300000054776f020800000067616d626c6572731002140004000000070000006163636f756e740b06000000616d6f756e740a060000006265745f6f6e1502000000030000004f6e65020300000054776f02040000006e616d65160203000000656e640d0a000000766965775f7374616b65010a  

Result:

Error: unable to deserialize state, due to: unable to parse schema: ParseError  
    at I.deserializeContractState (concordium.min.js:2:11127)  

The js seems to point to webassembly rust code in the rust-binding crate: concordium-node-sdk-js\packages\rust-bindings\src\aux_functions.rs:557
I reproduced the parse error with this sample test:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_deserialize_state_aux() {
        let contract_name = "tictactoebet";
        let state_bytes = "0600000047616d6520310000000000000000000068fc763183010000".to_string();
        let schema = "ffff02010000000c000000746963746163746f6562657401001400020000000900000067616d655f6e616d65160203000000656e640d040000000300000062657404140002000000040000006e616d65160210000000706c617965725f746f5f6265745f6f6e1502000000030000004f6e65020300000054776f0215050000000b0000004f6e6c794163636f756e74020a000000426574546f6f4c6174650209000000426574436c6f736564020a000000416c726561647942657402100000004d697373696e67506172616d6574657202040000006a6f696e04140001000000040000006e616d65160215040000000b0000004f6e6c794163636f756e7402090000004c6f62627946756c6c020e000000416c726561647941506c6179657202100000004d697373696e67506172616d65746572020400000076696577011400060000000900000067616d655f6e616d6516020a00000067616d655f737461746515040000001100000057616974696e67466f72506c617965727302050000005265616479020a000000496e50726f6772657373020800000046696e697368656402090000006265745f73746174651502000000040000004f70656e0206000000436c6f7365640207000000706c61796572731002140004000000070000006163636f756e740b06000000616d6f756e740a040000006e616d65160208000000706f736974696f6e1502000000030000004f6e65020300000054776f020800000067616d626c6572731002140004000000070000006163636f756e740b06000000616d6f756e740a060000006265745f6f6e1502000000030000004f6e65020300000054776f02040000006e616d65160203000000656e640d0a000000766965775f7374616b65010a".to_string();

        let result = deserialize_state_aux(contract_name, state_bytes, schema);

        dbg!(result); // ParseError
    }
}

I see in the code contract V0, does this method works for smart-contract V1 ?
Is there another way to de-serialize data from the nodeJS SDK ?
Am I missing something obvious here ?

Thanks for any help :slight_smile:

Hi @jt117,

Thank you for the detailed question. It is always much easier to help when the details are in order :wink:
Unfortunately, you are not missing something obvious. It is the NodeJS SDK that is missing the ability to deserialize the return values using a schema. We’ll try to fix the issue soon.
And as you correctly guessed, getting the whole contract state is only supported in v0 contracts. V1 contracts can have a very large state, so for that, you need to use view functions.

But you actually don’t need the schema to deserialize the return value, if you know the expected types.
You can see how most of the contract types are serialized here: concordium-contracts-common/impls.rs at main · Concordium/concordium-contracts-common · GitHub
Please note that all numbers use little-endian.

There are probably some js libraries that can help you with decoding it, but even a simple solution like this should work (this is based on a quick example, but I haven’t tested it with the modifications):

function decodeString(buffer, offset) {
    // By default, the length of strings are encoded with 4 bytes. But you can change that with attributes in rust, i.e.: #[concordium(size_length = 1)].
    // Read more about it here: https://docs.rs/concordium-contracts-common-derive/1.0.1/concordium_contracts_common_derive/derive.Serial.html
    const length = buffer.readUInt32LE(offset);
    offset += 4; //  Offset with 4 bytes for the length
    offset += length; // Offset with length bytes for the actual string
    // Return the string and the new offset in the buffer
    return [buffer.slice(offset, offset + length).toString('utf8'), offset];
}

// Assuming your return value is a tuple of strings, you might use it like so:
function decodeView(view_state) {
    const offset_0 = 0;
    const buffer = toBuffer(view_state.returnValue, 'hex');
    const [string_0, offset_1] = decodeString(buffer, offset_0);
    const [string_1, offset_2] = decodeString(buffer, offset_1);
    // ...
}

I hope this is enough to get you started on the deserialization. Otherwise, please let us know.

Best regards,
Kasper

1 Like

Thanks :slight_smile:

With your sample and by returning not all the state in the view function but only what I needed to display, I was able to decode the data.
It was less tiresome than I expected :upside_down_face:

My more complex structure to parse is a Vec<(String, Amount)>, I’ll put the snippet in here in case someone stumble on this thread.

function parseArray(buffer, offset) {
    let array_size = buffer.readUInt16LE(offset);// Array length
    offset += 4; //  Offset with 4 bytes for the length
    let myArray= new Array(array_size);

    for (let i = 0; i < array_size; i++) {
      let name;
      [name, offset] = parseString(buffer, offset);

      let amount = 0;
      [amount, offset] = parseAmount(buffer, offset);

      myArray[i] = [name, amount];
    }

    return [myArray, offset];
  }

  function parseAmount(buffer, offset) {
    let amount = buffer.readBigUInt64LE(offset);
    offset += 8;

    return [amount, offset];
  }

  function parseString(buffer, offset) {
    let stringLength = buffer.readUInt32LE(offset);
    offset += 4; //  Offset with 4 bytes for the length
    let string = buffer.slice(offset, offset + stringLength).toString("utf8");
    offset += stringLength; // Offset with length bytes for the actual string
    return [string, offset];
  }

That’s great! Nicely done :wink:
We’ve added the functionality needed for deserializing data using a schema in version 5.2.

I noticed a small issue in your code, on these lines:

Here you are reading a u16, which is 2 bytes long, and then you move the offset by 4 bytes.
It should work correctly for numbers smaller than 2^16 since it is little-endian, but you probably want to use readUInt32LE :blush:

/ Kasper

1 Like

Thanks for pointing that out and keeping me updated :slight_smile:
My bad indeed :sweat_smile:
I just tried out the “deserializeReceiveReturnValue” works like a charm :star_struck:

1 Like