Build Reference

How to: Basic Build

To build a contract, simply navigate to your contract crate and run the following command:

sc-meta all build

Configuring the Build

The build process is primarily configured using the build configuration file, currently named multicontract.toml, and it is placed in the root of the contract crate.

It is also possible to configure the build process to produce more than one contract per project crate.


Contract Build Process Deep Dive

This section provides an in-depth overview for those who want to understand the system at a deeper level. If you are simply interested in building contracts, feel free to skip this section.

Building a contract is a complex process, but fortunately, it is handled transparently by the framework. We will walk through the components step by step and provide explanations for this architecture.

a. The Smart Contract Itself

The smart contract is defined as a trait without an implementation. This design allows the contract to execute on various platforms. Some of the implementation is generated automatically by the [klever_sc::contract] macro.

However, not everything can be accomplished here. Notably, macros cannot access data from other modules or crates; all processing is confined to the current contract or module. Therefore, we need another mechanism to work with the complete contract data.

b. The (Generated) ABI Generator

ABIs consist of metadata about the contract. To build an ABI, we also need data from the modules. The module macros cannot be called from the contract macros (macros are executed at compilation, and we cannot guarantee that modules will need to be recompiled). However, modules can be called. This is why we generate ABI generator functions for each module, which can call each other to obtain the complete picture.

Note: The ABI generator is implemented as a trait called ContractAbiProvider.

c. Meta Crate: Generating the ABI

The next question is how to call these functions. Whenever we compile the WASM contracts, we also produce the ABIs in JSON format. Rust has something called build scripts, which are executed after compiling a project. However, these build scripts are not powerful enough for our use case.

Therefore, we decided to include an additional crate in each smart contract project. This crate is always found in the meta folder within the contract and manages the entire build process. To minimize boilerplate, it typically contains only one line that defers execution to the framework:

fn main() {
    klever_sc_meta::cli_main::<my_contract_crate::AbiProvider>();
}

To generate the ABI, it is enough to run the following command within the meta folder:

cd meta
cargo run abi

The meta crate has access to the ABI generator because it always has a dependency on the contract crate. This is represented by my_contract_crate::AbiProvider in the example above.

This is also the step where the meta crate parses and processes the multicontract.toml file. If multiple outputs are specified, one ABI will be produced for each output.

d. Meta Crate: Generating wasm Crate Code

Each contract must contain at least one wasm crate. This crate is separate from the contract crate because it serves a different purpose: it only needs to provide the foundation for compiling WASM. Consider it an intermediary step between the contract logic and the Rust-to-WASM compiler. This separation is advantageous because it allows the smart contract crate to function as a pure Rust crate without any knowledge of WebAssembly. This simplifies testing, coverage analysis, and integration with unrelated technologies.

The wasm crates do not add meaningful code to the smart contract. Their main task is to provide an adapter to the WASM function syntax. Specifically, they expose an external function for each desired endpoint, which delegates execution to the corresponding smart contract method.

Without proper care, there is a risk of adding unintended endpoints to the contract. For example, when a crate has multiple modules, and only one is imported into the smart contract, older versions might have included unwanted endpoints from the other modules. To prevent this, we use the ABI to generate a curated list of endpoints for each wasm crate. This ensures that our contracts always have exactly the same endpoints as those specified in the ABIs.

This process requires code generation, and the meta crate handles this code generation as well. Here's an example of such generated code:

// Code generated by the klever-sc multi-contract system. DO NOT EDIT.

////////////////////////////////////////////////////
////////////////// AUTO-GENERATED //////////////////
////////////////////////////////////////////////////

// Init:                                 1
// Endpoints:                            2
// Total number of exported functions:   3

#![no_std]
#![feature(alloc_error_handler, lang_items)]

klever_sc_wasm_adapter::allocator!();
klever_sc_wasm_adapter::panic_handler!();

klever_sc_wasm_adapter::endpoints! {
    adder
    (
        getSum
        add
    )
}

The macros provided by klever_sc_wasm_adapter help keep even this generated code concise.

For multi-contract builds, one wasm crate needs to be generated for each output contract:

  • The main wasm crate always resides in the wasm folder. While the source file is auto-generated, the Cargo.toml file must be provided by the developer.
  • Other wasm contracts (referred to as "secondary" contracts) are placed in folders starting with wasm-, for example, wasm-multisig-view. These crates are entirely generated based on data from multicontract.toml. The Cargo.toml files for these crates are derived from the main wasm crate's Cargo.toml. All configurations are inherited from the main crate except for the crate name.
  • Warning: Any folders beginning with wasm- that are not accounted for will be deleted without warning to maintain a clean folder structure in case of renames.

e. Meta Crate: The Actual WASM Build

The previous two steps can be executed by running cargo run in the meta crate. To perform a build, you must use the cargo run build command.

With the ABI information and generated code in hand, the meta crate can build all the WASM contracts, one for each output contract. The Rust compiler places the resulting files in the designated target folder, but for convenience, the meta crate moves the executables to the project's output folder and renames them according to the configured names.

simply calls the meta crate to perform this job. This is because, at this point, only the meta crate has access to the ABIs and can perform this task effortlessly.

f. Meta Crate: Build Post-Processing

After building the contracts, there are three more operations to perform based on the compiled WebAssembly outputs:

  1. All contracts are optimized using wasm-opt. This operation can be disabled with the --no-wasm-opt flag.
  2. A WAT (WebAssembly Text Format) file is generated for each contract. This feature is not enabled by default but can be turned on using the --wat flag. The framework simply calls the wasm2wat tool to perform this operation.
  3. An .imports.json file is generated for each contract. This feature can be disabled with the --no-imports flag. The framework utilizes the wasm-objdump tool to extract imports. It parses the output and saves it in JSON format.

g. Cleaning a Project

Running cargo run clean in the meta crate will execute cargo clean in all wasm crates and delete the output folder.

Build Process Summary

To summarize, the build process consists of the following steps:

  1. Generate code using contract/module macros, including the ABI generator.
  2. Invoke the ABI generator to produce an ABI in memory.
  3. Parse the multicontract.toml file (if present).
  4. Based on the parsed configuration, determine which endpoints go to which output contracts.
  5. Save the ABI as JSON in the output folder (one for each output contract).
  6. Generate the wasm crates for all output contracts (all source code generated, Cargo.toml contents copied from the main wasm crate).
  7. Build all wasm crates.
  8. Copy binaries from the target folder(s) to the output folder.
  9. Perform post-processing for each contract: wasm-opt, wasm2wat, imports.

Fortunately, the framework can automate all of these steps with a single click.

Was this page helpful?