Complex Values
In the previous section, we discussed representations of simple types here. These representations are suitable for arguments of types like usize
, BigUint
, or &[u8]
. However, there is a need to specify complex types such as custom structs or lists of items.
Concatenation
Concatenating multiple expressions is possible using the pipe operator (|
). The pipe operator takes precedence over everything else, so it is not currently possible to concatenate and then apply a function to the whole result.
This is ideal for short lists or small structs.
Example
- A
Vec<u32>
can be expressed as"u32:1|u32:2|u32:3"
. - A
(BigUint, BigUint)
tuple can be expressed as"biguint:1|biguint:2"
. - A
SimpleStruct { a: u8, b: BoxedBytes }
can be expressed as"u8:4|nested:str:value-b"
.
Please note that the pipe operator only takes care of the concatenation itself. You are responsible for ensuring that nested encoding is used where appropriate.
Using JSON lists as values
Scenarios allow using JSON lists to express longer values, especially when the value being represented is itself a list in the smart contract.
Example
- A
Vec<u32>
can also be expressed as["u32:1", "u32:2", "u32:3"]
. - A
(BigUint, BigUint)
tuple can also be expressed as["biguint:1", "biguint:2"]
. - A
SimpleStruct { a: u8, b: BoxedBytes }
can also be expressed as["u8:4", "nested:str:value-b"]
, although in this case, a JSON map might be more appropriate.
Make sure not to confuse values expressed as JSON lists with other elements of scenario syntax.
Example
{
"step": "scCall",
"txId": "echo_managed_vec_of_managed_vec",
"tx": {
"from": "address:an_account",
"to": "sc:basic-features",
"value": "0",
"function": "echo_managed_vec_of_managed_vec",
"arguments": [
[
"u32:3",
["u32:1", "u32:2", "u32:3"],
"u32:0",
"u32:2",
["u32:5", "u32:6"]
]
],
"gasLimit": "50,000,000",
"gasPrice": "0"
}
}
In the example above, there is a single argument being passed to the endpoint. The outer brackets in "arguments": [ ... ]
are scenario syntax for the list of arguments. The brackets immediately nested signal a JSON list value. Notice how the list itself contains some more lists inside it, and they all get concatenated into a single value in the end.
In this example, the only argument is 0x0000000300000001000000020000000300000000000000020000000500000006
.
Tip
We mentioned earlier how the developer needs to take care of the serialization of the nested items. This is actually a good example of that. The endpoint echo_managed_vec_of_managed_vec
takes a list of lists, so we need to serialize the lengths of the lists on the second level. Notice how the lengths are given as JSON strings, and the contents as JSON lists. The first "u32:3"
is the serialized length of the first item, which is ["u32:1", "u32:2", "u32:3"]
, and so forth.
Using JSON maps as values
JSON lists are suitable for representing series of items, but for structs, JSON maps are more expressive.
The rules for using JSON maps as values are as follows:
- The interpreter will concatenate all JSON map values and leave the keys out.
- The keys need to be in alphanumerical order, so we customarily prefix them with numbers. Map keys in JSON are fundamentally unordered, and this is the easiest way to enforce a deterministic order for the values.
- Map values can be either JSON strings, lists, or other maps, and all scenario value rules apply the same way all the way down.
Example
This is an abridged section of the actual lottery contract in the examples.
{
"step": "checkState",
"accounts": {
"sc:lottery": {
"storage": {
"str:lotteryInfo|nested:str:lottery_name": {
"0-token_identifier": "nested:str:LOTTERY-123456",
"1-ticket_price": "biguint:100",
"2-tickets-left": "u32:0",
"3-deadline": "u64:123,456",
"4-max_entries_per_user": "u32:1",
"5-prize_distribution": ["u32:2", "u8:75", "u8:25"],
"6-whitelist": [
"u32:3",
"address:acc1",
"address:acc2",
"address:acc3"
],
"7-prize_pool": "biguint:500"
}
},
"code": "file:../output/lottery-Kda.wasm"
}
}
}
The Rust struct this translates to is:
#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, TypeAbi)]
pub struct LotteryInfo<M: ManagedTypeApi> {
pub token_identifier: TokenIdentifier<M>,
pub ticket_price: BigUint<M>,
pub tickets_left: u32,
pub deadline: u64,
pub max_entries_per_user: u32,
pub prize_distribution: Vec<u8>,
pub whitelist: Vec<Address>,
pub prize_pool: BigUint<M>,
}
Tip
Once again, note that all contained values are in nested encoding format:
- The token identifier has its length automatically prepended by the
nested:
prefix. - Big integers are given with the
biguint:
syntax, which prepends their byte length. - Small integers are given in full length.
- Lists have their length explicitly encoded at the start, always on 4 bytes (as
u32
).
A note about enums
In Rust, there are two types of enums that we use:
- Simple enums are simply encoded as
u8
. - Complex enums are encoded just like structures, with an added
u8
discriminant at the beginning.
Discriminants are not explicit in the Rust code; they get generated automatically, starting from 0
.
Example
This is an abridged section of a Multisig contract test.
{
"step": "checkState",
"accounts": {
"sc:multisig": {
"storage": {
"str:action_data.item|u32:3": {
"1-discriminant": "0x06",
"2-amount": "u32:0",
"3-code": "nested:file:../test-contracts/adder.wasm",
"4-code_metadata": "0x0000",
"5-arguments": ["u32:1", "u32:2|1234"]
},
"+": ""
},
"code": "file:../output/multisig.wasm"
},
"+": ""
}
}
In this example, we have an SCDeploy
enum:
#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, TypeAbi)]
pub enum Action<M: ManagedTypeApi> {
Nothing,
AddBoardMember(ManagedAddress<M>),
AddProposer(ManagedAddress<M>),
RemoveUser(ManagedAddress<M>),
ChangeQuorum(usize),
SendKlv {
to: ManagedAddress<M>,
amount: BigUint<M>,
data: BoxedBytes,
},
SCDeploy {
amount: BigUint<M>,
code: ManagedBuffer<M>,
code_metadata: CodeMetadata,
arguments: Vec<BoxedBytes>,
},
SCCall {
to: ManagedAddress<M>,
klv_payment: BigUint<M>,
endpoint_name: BoxedBytes,
arguments: Vec<BoxedBytes>,
},
}