ABI
ABI Overview
To interact with a smart contract it is essential to understand its inputs and outputs. This is valid both for on-chain calls, and for off-chain tools, and can in most cases also tell us a lot about what the smart contract does and how it does it.
For this reason, blockchain smart contracts have so-called ABIs, expressed in a platform-agnostic language - JSON in our case.
Note that the name ABI is short for Application Binary Interface, which is a concept borrowed from low-level and systems programming. The word binary does not refer to its representation, but rather to the fact that it describes the binary encoding of inputs and outputs.
Minimal example
At its base minimum, an ABI contains:
- Some general build information, mostly used by humans, rather than tools:
- the compiler version;
- the name and version of the contract crate;
- the framework version used.
- The name of the contract crate and the Rust docs associated to it (again, mostly for documentation purposes).
- Under it, the list of all endpoints. For each endpoint we get:
- The Rust docs associated to them;
- Mutability, meaning whether or not the endpoint can modify smart contract state. At this point in the evolution of the framework, this mutability is purely cosmetic, not enforced. It can be viewed as a form of documentation, or declaration of intent. This might change, though, in the future into a hard guarantee.
- Whether the endpoint is payable.
- The list of all inputs, with their corresponding names and types.
- The list of all outputs, with their corresponding types. It is rare but possible to have more than one declared output value. It is also rare but possible to have output values named.
{
"buildInfo": {
"rustc": {
"version": "1.71.0-nightly",
"commitHash": "a2b1646c597329d0a25efa3889b66650f65de1de",
"commitDate": "2023-05-25",
"channel": "Nightly",
"short": "rustc 1.71.0-nightly (a2b1646c5 2023-05-25)"
},
"contractCrate": {
"name": "adder",
"version": "0.0.0",
"gitVersion": "v0.43.2-5-gfe62c37d2"
},
"framework": {
"name": "multiversx-sc",
"version": "0.43.2"
}
},
"docs": [
"One of the simplest smart contracts possible,",
"it holds a single variable in storage, which anyone can increment."
],
"name": "Adder",
"constructor": {
"inputs": [
{
"name": "initial_value",
"type": "BigUint"
}
],
"outputs": []
},
"endpoints": [
{
"name": "getSum",
"mutability": "readonly",
"inputs": [],
"outputs": [
{
"type": "BigUint"
}
]
},
{
"docs": [
"Add desired amount to the storage variable."
],
"name": "add",
"mutability": "mutable",
"inputs": [
{
"name": "value",
"type": "BigUint"
}
],
"outputs": []
}
],
"events": [],
"esdtAttributes": [],
"hasCallback": false,
"types": {}
}
Data types
Smart contract inputs and outputs almost universally use the standard MultiversX serialization format.
The ABI is supposed to contain enough information, so that, knowing this standard, a developer can write an encoder or decoder for the data of a smart contract in any language.
Please note that the type names are not necessarily the ones from Rust, we are trying to keep this language-agnostic to some extent.
Basic types
First off, there are a number of basic types that are known, and which have a universal representation:
- Numerical types:
BigUint
,BigInt
,u64
,i32
, etc. - Booleans:
bool
. - Raw byte arrays are all specified as
bytes
, irrespective of the underlying implementation in the contract. Someone who just interacts with the contract does not care whether the contracts works withManagedBuffer
,Vec<u8>
, or something else, it's all the same to the exterior. - Text:
utf-8 string
. - 32 byte account address:
Address
. - ESDT token identifier:
TokenIdentifier
. Encoded the same asbytes
, but with more specific semantics.
Composite types
Then, there are several standard composite types. They also have type arguments that describe the type of their content:
- Variable-length lists:
List<T>
, whereT
can be any type; e.g.List<u32>
. - Fixed-length arrays:
arrayN<T>
, whereN
is a number andT
can be any type; e.g.array5<u8>
represents 5 bytes. - Heterogeneous fixed-length tuples,
tuple<T1,T2,...,TN>
, no spaces, whereT1
,T2
, ... ,TN
can be any types; e.g.tuple<i16,bytes>
. - Optional data,
Option<T>
, whereT
can be any type. Represented as either nothing, or a byte of0x01
followed by the serialized contents.
Custom types: overview
All the types until here were standard and it is expected that any project using the ABI knows about them.
But here it gets interesting: the ABI also needs to describe types that are defined the smart contract itself.
There is simply not enough room to do it inline with the arguments, so a separate section is necessary, which contains all these descriptions. This section is called "types"
, and it can describe struct
and enum
types.
Have a look at this example with custom types.
Let's take the following enum
and struct
:
#[type_abi]
pub struct MyAbiStruct<M: ManagedTypeApi> {
pub field1: BigUint<M>,
pub field2: ManagedVec<M, Option<u32>>,
pub field3: (bool, i32)
}
#[type_abi]
pub enum MyAbiEnum<M: ManagedTypeApi> {
Nothing,
Something(i32),
SomethingMore(u8, MyAbiStruct<M>),
}
And this is their json representation:
{
"buildInfo": {},
"docs": [
"Struct & Enum example"
],
"name": "TypesExample",
"constructor": {
"inputs": [],
"outputs": []
},
"endpoints": [
{
"name": "doSomething",
"onlyOwner": true,
"mutability": "mutable",
"inputs": [
{
"name": "s",
"type": "MyAbiStruct"
}
],
"outputs": [
{
"type": "MyAbiEnum"
}
]
}
],
"events": [],
"esdtAttributes": [],
"hasCallback": false,
"types": {
"MyAbiStruct": {
"type": "struct",
"docs": [
"ABI example of a struct."
],
"fields": [
{
"docs": [
"Fields can also have docs."
],
"name": "field1",
"type": "BigUint"
},
{
"name": "field2",
"type": "List<Option<u32>>"
},
{
"name": "field3",
"type": "tuple<bool, i32>"
}
]
},
"MyAbiEnum": {
"type": "enum",
"docs": [
"ABI example of an enum."
],
"variants": [
{
"name": "Nothing",
"discriminant": 0
},
{
"name": "Something",
"discriminant": 1,
"fields": [
{
"name": "0",
"type": "i32"
}
]
},
{
"name": "SomethingMore",
"discriminant": 2,
"fields": [
{
"name": "0",
"type": "u8"
},
{
"name": "1",
"type": "MyAbiStruct"
}
]
}
]
}
}
}
Custom types: struct
ABI structures are defined by:
- Name;
- Docs (optionally);
- A list of fields. Each field has:
- Name;
- Docs (optionally);
- The type of the field. Any type is allowed, so:
- simple types,
- composite types,
- other custom types,
- even the type itself (if you manage to pull that off).
In the example above, we are declaring a structure called MyAbiStruct
, with 3 fields, called field1
, field2
, and field3
.
Custom types: enum
Similarly, enums are defined by:
- Name;
- Docs (optionally);
- A list of variants. Each variant has:
- A name;
- Docs (optionally);
- The discriminant. This is the index of the variant (starts from 0). It is always serialized as the first byte.
- Optionally, data fields associated with the enum.
- It is most common to have single unnamed field, which will pe named
0
. There are, however, other options. Rust syntax allows: - Tuple variants, named
0
,1
,2
, etc. - Struct-like variants, with named fields.
- It is most common to have single unnamed field, which will pe named
You can read more about Rust enums here.
ESDT Attribute ABI
Overview
The framework will export all data types found in arguments, results, and events, but it doesn't intrinsically know abut the data that we use in SFT and NFT attributes. This is why there is a special annotation to specify this explicitly.
Starting with the framework version 0.44
, developers can use the new trait annotation #[esdt_attribute("name", Type)]
in order to export ESDT attributes types in the ABI file.
The name field is an arbitrary name provided by the developer, to identify the token. Token identifiers are not hard-coded in contracts, but it can make sense to use the ticker here, if known.
The type field is simply the name of the type, as it would show up in regular smart contract code.
The annotation can only be used at trait level along with #[multiversx_sc::contract]
or #[multiversx_sc::module]
annotations. Using it anywhere else will not work.
The exported data will end up in 2 places:
- In the contract ABI, in a special
"esdt_attributes"
section; - In a special ESDT ABI file (
name.esdt-abi.json
), one for each such declared ESDT.
More examples of this below.
Details
A new field called esdtAttributes
was added to the ABI file, where developers can find the structs (name, type) exported using the esdt_attribute
trait annotation. Additionally, each esdt_attribute
will create a new json file with the name given by the developer (followed by .esdt-abi
) and containing its exported structs (names, types and descriptions).
The name/ticker is just a way to identify the idea of the token because we do not have the exact identifier or the possibility to create it through this annotation. We only use this annotation as a mark up for a specific ESDT, in order to define its fields' attributes type. It is useful to define ESDT attributes' type beforehand in order to get more specific and overall better results fetching data from other services.
Example using basic types
Let's take a simple contract SomeContract
as an example and try out the new annotation.
#![no_std]
multiversx_sc::imports!();
multiversx_sc::derive_imports!();
#[multiversx_sc::contract]
#[esdt_attribute("testBasic", BigUint)]
pub trait SomeContract {
#[init]
fn init(&self) {}
}
Adding the #[esdt_attribute("testBasic", BigUint)]
at trait level along with #[multiversx_sc::contract]
should export a new structure named testBasic
with a BigUint
field type. This structure resembles an ESDT with the ticker testBasic
and the attributes fields of type BigUint
.
The abi can be generated calling sc-meta all abi
in the contract folder, or by building the contract using sc-meta all build
(this command also adds the wasm
file to the output
folder).
Building the contract using sc-meta all build
will generate the following folder structure:
some_contract
├── output
│ ├── some_contract.abi.json
│ ├── some_contract.imports.json
| ├── some_contract.mxsc.json
| ├── some_contract.wasm
│ ├── testBasic.esdt-abi.json
Let's check out the some_contract.abi.json
file first. Here we discover the new esdtAttributes
field, containing the value mentioned in the annotation.
{
"esdtAttributes": [
{
"ticker": "testBasic",
"type": "BigUint"
}
]
}
We can also check the specific json file exported for the newly defined type where we can find information about the type separated from the main abi file.
{
"esdtAttribute": {
"ticker": "testBasic",
"type": "BigUint"
}
}
Using more complex types
Now, let's see what happens when we use other types than basic ones. Let's add a Vec
, an Enum (MyAbiEnum)
and an Option
to our esdt attributes.
#![no_std]
multiversx_sc::imports!();
multiversx_sc::derive_imports!();
#[multiversx_sc::contract]
#[esdt_attribute("testBasic", BigUint)]
#[esdt_attribute("testEnum", MyAbiEnum<Self::Api>)]
#[esdt_attribute("testOption", Option<TokenIdentifier>)]
#[esdt_attribute("testVec", ManagedVec<u64>)]
pub trait SomeContract {
#[init]
fn init(&self) {}
}
If we call sc-meta all abi
(or sc-meta all build
if we also wish to build the contract), the new attributes will be added to our some_contract.abi.json file and new separate json files will be created for each attribute. Now, our esdtAttributes
section from our abi file should look like this:
{
"esdtAttributes": [
{
"ticker": "testBasic",
"type": "BigUint"
},
{
"ticker": "testEnum",
"type": "MyAbiEnum"
},
{
"ticker": "testOption",
"type": "Option<TokenIdentifier>"
},
{
"ticker": "testVec",
"type": "List<u64>"
}
],
}
Now, if we take a look into the folder structure of the contract, we should see the following updated folder structure containing the newly generated files in output
:
some_contract
├── output
│ ├── some_contract.abi.json
│ ├── some_contract.imports.json
| ├── some_contract.mxsc.json
| ├── some_contract.wasm
│ ├── testBasic.esdt-abi.json
│ ├── testEnum.esdt-abi.json
│ ├── testOption.esdt-abi.json
│ ├── testVec.esdt-abi.json
Each file contains the new struct with its name and the type field's description such as:
{
"esdtAttribute": {
"ticker": "testOption",
"type": "Option<TokenIdentifier>"
}
}
Let's also add a custom struct
into the mix. For this example we are going to use MyAbiStruct
declared above.
Here is the updated code for lib.rs:
#![no_std]
multiversx_sc::imports!();
multiversx_sc::derive_imports!();
#[multiversx_sc::contract]
#[esdt_attribute("testBasic", BigUint)]
#[esdt_attribute("testEnum", MyAbiEnum<Self::Api>)]
#[esdt_attribute("testOption", Option<TokenIdentifier>)]
#[esdt_attribute("testVec", ManagedVec<u64>)]
#[esdt_attribute("testStruct", MyAbiStruct<Self::Api>)]
pub trait SomeContract {
#[init]
fn init(&self) {}
}
Same as before, we use sc-meta all abi
and a new file named testStruct.esdt-abi.json
shows up in our folder structure:
some_contract
├── output
│ ├── some_contract.abi.json
│ ├── some_contract.imports.json
| ├── some_contract.mxsc.json
| ├── some_contract.wasm
│ ├── testBasic.esdt-abi.json
│ ├── testEnum.esdt-abi.json
│ ├── testOption.esdt-abi.json
│ ├── testStruct.esdt-abi.json
│ ├── testVec.esdt-abi.json
As a final check, let's take a look at what changed in the main abi file, some_contract.abi.json
, after adding multiple new attributes.
{
"esdtAttributes": [
{
"ticker": "testBasic",
"type": "BigUint"
},
{
"ticker": "testEnum",
"type": "MyAbiEnum"
},
{
"ticker": "testOption",
"type": "Option<TokenIdentifier>"
},
{
"ticker": "testVec",
"type": "List<u64>"
},
{
"ticker": "testStruct",
"type": "MyAbiStruct"
}
],
}
You can find more examples containing multiple data types in the abi-tester
from here.