Getting Started

Welcome to the Getting Started guide for building applications on essential. This guide is designed for ambitious developers looking to build declarative applications.

Note: We are in the early stages of development, so things may change rapidly, and some features may be broken. If you encounter any issues, we encourage you to report them in the appropriate repository. This guide is a work in progress and will continue expanding to cover more complex applications.

Overview

In this guide, you'll learn how to create your first essential application. We’ll start with a simple counter application that increments a counter — the essential equivalent of a "Hello World" app. While trivial, it introduces key concepts that will help you build more complex applications.

Essential is a declarative system that allows for expressing powerful applications in an elegant and intuitive way.

What You'll Learn

  • How to create an essential application
  • The basics of declarative programming
  • Key concepts involved in essential application development

Let’s dive in and start building!

Essential Applications

At the core, Essential applications are built around contracts. If you're familiar with imperative blockchain languages like Solidity, this terminology might sound familiar. However, a declarative contract in Essential is fundamentally different from its imperative counterpart.

The Difference: Imperative vs. Declarative

In imperative contracts, a set of inputs is processed through a sequence of opcodes, which updates the state as a side effect. These contracts often rely on storage-related opcodes that directly manipulate the state.

However, Essential applications operate declaratively, meaning that state updates occur without direct execution. Unlike the imperative approach, Essential applications work in reverse. They begin with a proposed atomic state mutation — a set of proposed new state values — and then check their validity against a contract.

In short, a Pint program exists to validate a given state mutation against predefined rules. These rules form the core of a Pint contract.

In the rest of this guide, we will implement these concepts in Pint using a simple counter application. You’ll see how contracts, predicates, and constraints come together in practice.

State Mutations and Solvers

You may be wondering: Where do these state mutations come from?

  • State mutations (or "solutions") are discovered by solvers.
  • Solvers can be third-party entities competing to find optimal solutions, or centralized programs such as servers, front-end apps, or wallets, which provide solutions for specific applications.
  • The techniques used by solvers to discover these solutions, and the mechanism for including them in blocks, are outside the scope of this guide. For now, know that incentivized actors in the system discover these solutions off-chain.

Later in this guide, we’ll explore a simple solution when we test our application.

Storage and Contracts

A Pint contract may declare a storage block. If it does, the contract owns that state, and in general, state updates can only occur if the new values are validated by the contract.

Note: A contract is not required to define a storage block. It may impose additional constraints on state mutations related to other contracts. In such cases, both the constraints of the current contract and the contract owning the state must be satisfied for a solution to be valid.

Predicates and Constraints

Validation of a contract’s state occurs through the satisfaction of one of the contract's predicates.

  • Think of predicates as "pathways to validity." For a contract to be satisfied (and for its state to be updated), one of its predicates must be met.
  • A predicate consists of one or more constraints. A constraint is a boolean expression that must evaluate to True for the predicate to be satisfied.

From a code organization perspective, predicates might look like functions, but there's a key difference: predicates are not called like functions. Instead, they are targets that individual solutions attempt to satisfy.

Installation

To start building Pint applications, you'll need to install a few essential tools. Below are three installation options, depending on your preference:

The easiest and most convenient method is to use the Nix package manager. Nix automatically handles dependencies, making setup hassle-free. Follow the instructions in the Nix installation guide to get started.

2. Installing from crates.io

You can install the Essential tools directly from crates.io using Cargo. This method allows you to easily install prebuilt tools, though they are built from source by Cargo. To use this method, ensure you have Cargo installed on your system. Detailed instructions can be found here.


Additional Setup

Rust Installation

If you are using nix, you can simply run the following command to launch a dev shell with all the necessary tools installed:

nix develop github:essential-contributions/essential-integration

If you don't have Rust and Cargo installed, follow the official installation instructions here.

Syntax Highlighting

To improve the development experience, we are adding syntax highlighting support for Pint across popular code editors. Currently, support is available for the following editor:

Visual Studio Code (VSCode)

To enable syntax highlighting for Pint in VSCode, you can search for pint syntax in the marketplace or use this link.

Nix

Nix is the easiest way to get everything you need to start developing with essential.

Install Nix

If you don't already have Nix installed you can install it by running the following command:

curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install

This uses the Determinate Systems installer.
There are other alternatives here.
You can lean more about Nix here.

Enter development shell

This will enter you into a shell with cargo, pint, essential-builder and some other things that will be useful for developing your application.

nix develop github:essential-contributions/essential-integration

Crates IO

You can install the Essential tools from crates.io.
Note that this method requires you to have Cargo installed and check the versions of the tools you are installing are compatible with the version of the Essential network you are using.

Install Pint

Once you have Cargo installed, you can build Pint from crates.io.

To install pint on your system path, run:

cargo install pint-cli

Install Essential REST Client

To install essential-rest-client on your system path, run:

cargo install essential-rest-client

Install Essential Wallet

Warning: Essential wallet is for testing purposes only. Do not use it for production. It has never been audited and should not be used to store real value.

To install essential-wallet on your system path, run:

cargo install essential-wallet

Install Essential Builder

To install essential-builder-cli on your system path, run:

cargo install essential-builder-cli

Install Essential Node

To install essential-node-cli on your system path, run:

cargo install essential-node-cli

Essential Architecture


Note:
While efforts are made to maintain the Builder / Node state, schema changes may require wiping the database, resulting in a reset of stored data.


Builder

The Essential Builder is responsible for block construction within the network. It gathers proposed solutions (such as state changes or transactions), validates them, and assembles new blocks for the blockchain. The Builder ensures state consistency by maintaining pre- and post-state views during block construction.

Node

The Essential Node is the backbone of the blockchain network, handling core operations like block validation, state synchronization, and peer-to-peer communication. The Node processes and validates transactions while maintaining the integrity and synchronization of the network.

Essential Integration

The Essential Integration includes the Essential REST Client, which provides tools to interact with both the Node and Builder for managing contracts and state.

Essential REST Client Usage

Usage: essential-rest-client <COMMAND>

Commands:
  list-blocks                     List blocks in the given block number range
  query-state                     Query the state of a contract
  deploy-contract                 Deploy a contract
  submit-solution                 Submit a solution
  latest-solution-failures        Get the latest failures for solution

This client simplifies interactions with the blockchain, offering key functionality for querying state and managing solutions.

Note: If the pintc changes how it compiles your application or there is a change to our assembly, your contracts will have a new address. This means that if you deploy an app with the same source as another app that was compiled with a different compiler then the two apps will have different addresses and separate state.

Example Application: Counter

In this chapter, we will walk you step-by-step through the process of building a simple counter application. This guide is designed to cover every step required to create a fully working application.

Language Choice

For this example, we will be using Rust for the front end. Rust is currently the most supported language for essential software, as much of the existing codebase is written in Rust.

However, you are free to use any programming language of your choice for your own applications. While Rust offers the best support at the moment, we are actively working to extend support for other languages in the future.


Follow along with the steps, and feel free to adapt them based on your preferred language or setup.

New Project

Create a new pint project with the following command:

mkdir counter
pint new --name counter counter/contract

Our entire application is going to live in the counter directory.
The pint project will be created in the contract subdirectory.

Define the Storage

Define the storage for the counter.
The storage is a simple integer that will be incremented by solutions that satisfy the predicate.

Add the following to the contract/src/contract.pnt file:

storage {
    counter: int,
}

Write the Predicate

Start by adding a new predicate called Increment to the contract/src/contract.pnt file:

predicate Increment() {

}

Storage

The first thing we want to do within this predicate is to read the counter storage value:

predicate Increment() {
    let counter: int = mut storage::counter;
}

Constraint

Let's add our first constraint:

predicate Increment() {
    let counter: int = mut storage::counter;

    constraint counter' == counter + 1;
}

A constraint is a boolean expression which must be true for the predicate to be satisfied.
This constraint says that the post-state of the counter must be equal to the pre-state plus one.

Note: The post-state value of the counter storage variable is denoted by counter'. The trailing apostrophe ' is the syntax for accessing the post-state value of a storage variable.

But there is a problem with this constraint.
What if the pre-state of the counter is not yet set to anything?

We can use a nil check to handle this case:

predicate Increment() {
    let counter: int = mut storage::counter;

    constraint counter == nil && counter' == 1;
}

This says that if the pre-state of the counter is nil then the post-state must be 1.

Now let's put it all together:

predicate Increment() {
    let counter: int = mut storage::counter;

    constraint (counter == nil && counter' == 1) || counter' == counter + 1;
}

This constraint is satisfied if either the counter pre-state is nil and the post-state is 1, or the counter pre-state is n and the post-state is n + 1.

Your complete contract/src/contract.pnt file should look like this:

storage {
    counter: int,
}

predicate Increment() {
    let counter: int = mut storage::counter;

    constraint (counter == nil && counter' == 1) || counter' == counter + 1;
}

Congratulations on writing your first pint predicate!

Compile

Now that we have our predicate written, we can compile the code using the pint tool.

cd counter/contract
pint build

This will create a new directory inside the contract directory called out.
Because this is a debug build, you can find the compiled contract at counter/contract/out/debug/counter.json.

There is also a counter-abi.json file in the same directory that contains the counter contract's ABI.

The ABI for the counter looks like this:

{
  "predicates": [
    {
      "name": "::Increment",
      "params": []
    }
  ],
  "storage": [
    {
      "name": "counter",
      "ty": "Int"
    }
  ]
}

Note that yours may look slightly different depending on the version of the compiler you are using.

In the next section, we'll learn how to run a local test node, deploy our contract, and update the onchain counter state by solving its predicate.

Deploy & Solve

Now that we've built our simple counter contract, let's deploy it to a local test network, and update the counter by solving our Increment predicate.

Before we begin, we'll make the essential-builder, pint, and essential-rest-client tools available to our current shell:

nix shell github:essential-contributions/essential-integration#essential

Running a Test Builder

Before we can deploy our contract, we need somewhere to deploy it to. Let's run a local, in-memory test instance of the essential-builder tool.

The builder will run forever so let's run it in a seperate terminal. Open a new terminal, run the nix shell command above to make the builder tool available, and run it like so:

essential-builder --node-api-bind-address "0.0.0.0:3553" --builder-api-bind-address "0.0.0.0:3554"

Now we're running a local instance of the builder. We should see some output like the following:

2024-11-14T11:58:14.359260Z  INFO essential_builder_cli: Initializing node DB
2024-11-14T11:58:14.364424Z  INFO essential_builder_cli: Starting node API server at 0.0.0.0:3553
2024-11-14T11:58:14.364439Z  INFO essential_builder_cli: Initializing builder DB
2024-11-14T11:58:14.365863Z  INFO essential_builder_cli: Starting builder API server at 0.0.0.0:3554
2024-11-14T11:58:14.365887Z  INFO essential_builder_cli: Running the block builder

We can see that the builder exposes 2 APIs:

  1. A node API at localhost on port 3553. This allows for querying the state of our local single-node blockchain.
  2. A builder API at localhost on port 3554. This allows for submitting solutions to have them included in a block.

Note: If we do not specify the --node-api-bind-address or --builder-api-bind-address options, the builder will randomly select available ports for the node and builder APIs respectively.

Tip: To see more detailed logs from the builder, we can run it with the RUST_LOG environment variable set to debug or trace, e.g.

RUST_LOG=trace essential-builder

Contract Deployment

Using the pint deploy plugin, we can deploy our built counter contract to the local test builder:

pint deploy --builder-address "http://127.0.0.1:3554" --contract "./out/debug/counter.json"

Upon success, the builder will send us the content address of the solution used to deploy the contract. We'll learn more about solutions in the following sections. For now, this is enough to know our contract is deployed!

Solving Predicates

In order to update contract state, a solution must solve one of the contract's predicates.

Let's try updating our contract's counter state by solving its Increment predicate.

A solution requires 3 components:

  1. predicate_to_solve: The full address of the predicate we wish to solve. This includes both the contract and predicate content addresses. If we look back at our pint build step, we'll notice that these addresses are printed to the command line:

    $ pint build
       Compiling counter [contract] (/Users/mindtree/Desktop/counter/contract)
        Finished build [debug] in 13.38825ms
        contract counter            1899743AA94972DDD137D039C2E670ADA63969ABF93191FA1A4506304D4033A2
             └── counter::Increment 355A12DCB600C302FFD5D69C4B7B79E60BA3C72DDA553B7D43F4C36CB7CC0948
    

    Note that your addresses might be slightly different if using a newer compiler version.

  2. decision_variables: A list of parameters (aka decision variables) that are expected by the predicate. Our Increment predicate takes no parameters, so here we can specify an empty list.

  3. state_mutations: The state mutations we wish to propose. In our case, we want to initialise the counter storage variable to 1.

    Note: In state mutations, both keys and values are variable-sized arrays of Words. As counter is the only variable, we can assume it is at key [0]. As the type of counter is int, we only need a single Word to represent the value, e.g. [1].

When represented as JSON, our full solution looks as follows:

{
    "data": [
        {
            "predicate_to_solve": {
                "contract": "1899743AA94972DDD137D039C2E670ADA63969ABF93191FA1A4506304D4033A2",
                "predicate": "355A12DCB600C302FFD5D69C4B7B79E60BA3C72DDA553B7D43F4C36CB7CC0948"
            },
            "decision_variables": [],
            "state_mutations": [
                {
                    "key": [0],
                    "value": [1]
                }
            ]
        }
    ]
}

Lets put the above JSON in a solution.json file.

To submit our solution to the local builder, we can now use the following command:

pint submit --builder-address "http://127.0.0.1:3554" --solution "./solution.json"

As confirmation that the builder received our solution, it responds with its content address.

However, this is not enough to know whether or not our solution was included in a block, or whether it passed the contract's constraints at all.

To check whether or not our solution was successful, we can query the state of our contract's counter storage variable using the builder's node API:

pint query --node-address "http://127.0.0.1:3553" --contract-address "1899743AA94972DDD137D039C2E670ADA63969ABF93191FA1A4506304D4033A2" counter

Here, we're providing the address of the node API:

--node-address "http://127.0.0.1:3553"

the counter's contract address:

--contract-address "1899743AA94972DDD137D039C2E670ADA63969ABF93191FA1A4506304D4033A2"

and the name of the storage variable which we want to query (currently this is only supported for scalar types):

counter

As an alternative to the name of the variable, you can provide an 8-byte hex-formatted key, as such:

--key 0000000000000000

Tip: Refer to the relevant section in The Book of Pint for more information on storage keys.

Upon success, the node responds with:

[1]

And that's it! We can continue to submit solutions and update state in this manner - as long as our solutions satisfy the contract's predicates.

Tip: Use essential-rest-client --help to find more useful queries, including list-blocks and latest-solution-failures.

Constructing Solutions for Apps

Rather than manually writing JSON files to construct solutions and interact with contracts, it will be more common to automatically create solutions from application code.

In the following chapter, we will demonstrate how to do so with the Rust programming language.

It is not a requirement to use Rust in order to use Essential, we just happen to be Rust devs. The following section is completely optional but you may find it useful to see how we interact with the contract (even if you don't know Rust).

Rust Front End

In this section, we will build a simple front-end application in Rust to interact with the counter contract.

While Rust is used for this example, you are not required to use it for your front-end development. You can choose any programming language that suits your needs.

This chapter is optional, but it can be helpful to follow along to understand how to interact with the contract, even if you are not familiar with Rust.

Setup Cargo Project

In this section, we'll create a new Cargo project to serve as the front-end application for interacting with the counter contract. Follow the steps below to set up your project and include the necessary dependencies.

Step 1: Create a New Cargo Project

Run the following command from the root directory of your counter project to create a new Cargo project:

cargo new --lib counter-app

Your project structure should now look like this:

counter/
├── contract
│   ├── out
│   │   └── debug
│   │       ├── counter-abi.json
│   │       └── counter.json
│   ├── pint.toml
│   └── src
│       └── contract.pnt
└── counter-app
    ├── Cargo.toml
    └── src
        └── lib.rs

Step 2: Add Dependencies

Now, add the necessary dependencies to your Rust project by running the following command:

cd counter-app
cargo add anyhow
cargo add clap --features derive
cargo add essential-app-utils
cargo add essential-hash
cargo add essential-rest-client
cargo add essential-types
cargo add pint-abi
cargo add tokio --features full
cargo add essential-app-utils --features test-utils --dev
cargo add essential-builder --dev
cargo add essential-builder-db --dev
cargo add essential-node --dev
cargo add serde_json --dev

Your Cargo.toml file should now look like this:

[package]
name = "counter-app"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.93"
clap = { version = "4.5.21", features = ["derive"] }
essential-app-utils = "0.6.0"
essential-hash = "0.7.0"
essential-rest-client = "0.6.0"
essential-types = "0.5.0"
pint-abi = "0.6.0"
tokio = { version = "1.41.1", features = ["full"] }

[dev-dependencies]
essential-app-utils = { version = "0.6.0", features = ["test-utils"] }
essential-builder = "0.10.0"
essential-builder-db = "0.5.0"
essential-node = "0.8.0"
serde_json = "1.0.132"

Step 3: Add a Test

Lastly, add a test to your front-end application by using the following command:

mkdir tests
touch tests/counter.rs

After adding the test, your project structure should look like this:

counter
├── contract
│   ├── out
│   │   └── debug
│   │       ├── counter-abi.json
│   │       └── counter.json
│   ├── pint.toml
│   └── src
│       └── contract.pnt
└── counter-app
    ├── Cargo.lock
    ├── Cargo.toml
    ├── src
    │   └── lib.rs
    └── tests
        └── counter.rs

At this point, your Rust project is set up with all the necessary dependencies, and a basic test has been added to your front-end application.

Application Functionality

In this section, we will write the core application functionality that will be used in our tests to interact with the contract.

The purpose of this code is to generate the data required to read the contract's state and submit valid solutions.

The following section will be written in:

counter/
└── counter-app/
    └── src/
        └── lib.rs

Dependencies

Start by adding the imports you are going to need.

#![allow(unused)]
fn main() {
use anyhow::bail;
use essential_types::{
    solution::{Mutation, Solution, SolutionData},
    Value, Word,
};
}

Rust Items Generation Using the ABI

The Application Binary Interface (ABI) is genearted after compiling your Pint project and lives under out/debug. The ABI can be used to generate Rust types and modules that will help you generate solutions. Add the following macro call produce all the required Rust items from the ABI:

#![allow(unused)]
fn main() {
pint_abi::gen_from_file! {
    abi: "../contract/out/debug/counter-abi.json",
    contract: "../contract/out/debug/counter.json",
}
}

Keys

Create a type that will be used to query the counter state.
Also create a function to construct this type from the ABI.
Keys are vectors of words so here we create a vector with a single word.

#![allow(unused)]
fn main() {
#[derive(Clone)]
pub struct CounterKey(pub Vec<Word>);

pub fn counter_key() -> CounterKey {
    let keys: Vec<_> = storage::keys().counter().into();
    CounterKey(keys.first().unwrap().clone())
}
}

Here, we're extract the storage key for the storage variable counter by using the method counter().

Extract Count

State will be read using the data from the keys page however state returns an optional vector of words.
This is because state can return any size of data including empty.

Create a function to extract the count from the state.

Add this match expression that maps:

  • empty to 0.
  • a single word to the count.
  • anything else to an error.

Then return the count.

#![allow(unused)]
fn main() {
/// Given a query of the current count, extract the count.
pub fn extract_count(count: Option<Value>) -> anyhow::Result<Word> {
    match count {
        Some(count) => match &count[..] {
            [] => Ok(0),
            [count] => Ok(*count),
            _ => bail!("Expected single word, got: {:?}", count),
        },
        None => Ok(0),
    }
}
}

Create Solution

In this step, we will add a function that accepts the predicate address and the desired count for setting the state.

The solution includes a single SolutionData that satisfies the given predicate (although other solutions may solve multiple predicates). In this case, there are no decision variables so that field is set to the default.

We will also add a single state mutation where the counter is updated to some new_count.

#![allow(unused)]
fn main() {
pub fn create_solution(new_count: Word) -> Solution {
    let state_mutations: Vec<Mutation> = storage::mutations().counter(new_count).into();
    Solution {
        data: vec![SolutionData {
            predicate_to_solve: Increment::ADDRESS,
            decision_variables: Default::default(),
            state_mutations,
        }],
    }
}
}

Here, we're using method counter(..) to provide the new value for the counter. Note that storage::mutations(), counter(..), and Increment::ADDRESS are available from the expansion of the gen_from_file macro.

Increment

Now we can put it all together and create the increment function.
This function will take an optional Value and return a Solution with the new expected count. \

#![allow(unused)]
fn main() {
pub fn incremented_solution(count: Option<Value>) -> anyhow::Result<(Solution, Word)> {
    let count = extract_count(count)?;
    let new_count = count + 1;
    Ok((create_solution(new_count), new_count))
}
}

Summary

Check your lib.rs matches this.

#![allow(unused)]
fn main() {
use anyhow::bail;
use essential_types::{
    solution::{Mutation, Solution, SolutionData},
    Value, Word,
};

pint_abi::gen_from_file! {
    abi: "../contract/out/debug/counter-abi.json",
    contract: "../contract/out/debug/counter.json",
}

#[derive(Clone)]
pub struct CounterKey(pub Vec<Word>);

pub fn counter_key() -> CounterKey {
    let keys: Vec<_> = storage::keys().counter().into();
    CounterKey(keys.first().unwrap().clone())
}

pub fn incremented_solution(count: Option<Value>) -> anyhow::Result<(Solution, Word)> {
    let count = extract_count(count)?;
    let new_count = count + 1;
    Ok((create_solution(new_count), new_count))
}

/// Given a query of the current count, extract the count.
pub fn extract_count(count: Option<Value>) -> anyhow::Result<Word> {
    match count {
        Some(count) => match &count[..] {
            [] => Ok(0),
            [count] => Ok(*count),
            _ => bail!("Expected single word, got: {:?}", count),
        },
        None => Ok(0),
    }
}

pub fn create_solution(new_count: Word) -> Solution {
    let state_mutations: Vec<Mutation> = storage::mutations().counter(new_count).into();
    Solution {
        data: vec![SolutionData {
            predicate_to_solve: Increment::ADDRESS,
            decision_variables: Default::default(),
            state_mutations,
        }],
    }
}
}

Test

In this section, we will test the functionality of the Rust front-end application by interacting with the counter contract. These tests will verify that the application can:

  1. Correctly read the current state of the contract.
  2. Submit valid solutions to modify the contract’s state.
  3. Confirm that state changes are processed accurately.

Each test will focus on a specific aspect of interacting with the contract, ensuring that the front-end functions as expected and communicates correctly with the contract.

The following section will be written in:

counter/
└── counter-app/
    └── tests/
        └── test.rs

Dependencies

In this section, we're importing the necessary modules and types for our counter test. We're bringing in the counter_app module, which contains our counter-related functions. We also import utilities from essential_app_utils for compiling Pint projects and working with databases.

#![allow(unused)]
fn main() {
use counter_app::*;
use essential_app_utils as utils;
}

Generate Address

Define an asynchronous test function using the #[tokio::test] attribute. This sets up our test environment and allows us to use async/await syntax within our test.

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test() {
}
}

Then add a section to read the "counter" contract from its bytecode which was generated earlier and can be found under the out/debug directory.

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test() {
    let path: std::path::PathBuf = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../contract/out/debug/counter.json"
    )
    .into();

    let counter =
        serde_json::from_reader(std::io::BufReader::new(std::fs::File::open(path).unwrap()))
            .unwrap();
}
}

Deploy to Test Builder

Add these lines to deploy the contract.
Here, we're setting up our test databases and deploying the counter contract to our test builder. This step is crucial for having a contract to interact with in our subsequent tests.

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test() {
    // ...

    let dbs = utils::db::new_dbs().await;

    // Deploy the contract
    essential_app_utils::deploy::deploy_contract(&dbs.builder, &counter)
        .await
        .unwrap();
}
}

Read Count

Create the read_count function that queries the current count from the Essential node using the provided node client.

#![allow(unused)]
fn main() {
async fn read_count(dbs: &utils::db::Dbs) -> essential_types::Word {
    let r = utils::node::query_state_head(&dbs.node, &ADDRESS, &counter_key().0)
        .await
        .unwrap();
    extract_count(r).unwrap()
}
}

Add this section that reads the counter. In this part, we're reading the initial count from our deployed counter contract and assert that it starts at zero. This verifies that our counter is deployed and hasn't been incremented yet.

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test() {
    // ...

    let count = read_count(&dbs).await;
    assert_eq!(count, 0);
}
}

Increment Count

Add an increment function that increments the counter's value by one. The function reads the current count, increments it, and then creates a new solution with the updated count. The solution is then submitted to the builder to be included in a block.

#![allow(unused)]
fn main() {
async fn increment(dbs: &utils::db::Dbs) {
    let current_count = extract_count(
        utils::node::query_state_head(&dbs.node, &ADDRESS, &counter_key().0)
            .await
            .unwrap(),
    )
    .unwrap();

    utils::builder::submit(&dbs.builder, create_solution(current_count + 1))
        .await
        .unwrap();
}
}

Here, we're calling the increment function to increase the counter's value. This function interacts with the builder to update the counter's state.

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test() {
    // ...

    increment(&dbs).await;
}
}

Build Block

Add a section that builds a block and verifies the results. We check that three solutions succeeded (the initial chain solution, the contract deployment and counter increment) and that no transactions failed. We then read the counter's value again to confirm it has been updated to 1.

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test() {
    // ...

    let o = utils::builder::build_default(&dbs).await.unwrap();
    assert_eq!(o.succeeded.len(), 3);
    assert!(o.failed.is_empty());

    let count = read_count(&dbs).await;
    assert_eq!(count, 1);
}
}

Compete

In this final section test how the counter handles competing increments. We increment the counter twice with two solutions setting the count to the same value. Then verify that only one increment succeeds when we build the block. This demonstrates that when two solutions compete for the same state only one will win.

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test() {
    // ...

    increment(&dbs).await;
    increment(&dbs).await;

    let o = utils::builder::build_default(&dbs).await.unwrap();
    assert_eq!(o.succeeded.len(), 2);

    let count = read_count(&dbs).await;
    assert_eq!(count, 2);
}
}

Currently the builder uses a simple FIFO approach to choosing which solutions wins but this will change to more advanced strategies in the future.

Summary

Your test.rs file should look similar to this:

#![allow(unused)]
fn main() {
use counter_app::*;
use essential_app_utils as utils;

#[tokio::test]
async fn test() {
    let path: std::path::PathBuf = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../contract/out/debug/counter.json"
    )
    .into();

    let counter =
        serde_json::from_reader(std::io::BufReader::new(std::fs::File::open(path).unwrap()))
            .unwrap();

    let dbs = utils::db::new_dbs().await;

    // Deploy the contract
    essential_app_utils::deploy::deploy_contract(&dbs.builder, &counter)
        .await
        .unwrap();

    let count = read_count(&dbs).await;
    assert_eq!(count, 0);

    increment(&dbs).await;

    let o = utils::builder::build_default(&dbs).await.unwrap();
    assert_eq!(o.succeeded.len(), 3);
    assert!(o.failed.is_empty());

    let count = read_count(&dbs).await;
    assert_eq!(count, 1);

    increment(&dbs).await;
    increment(&dbs).await;

    let o = utils::builder::build_default(&dbs).await.unwrap();
    assert_eq!(o.succeeded.len(), 2);

    let count = read_count(&dbs).await;
    assert_eq!(count, 2);
}

async fn read_count(dbs: &utils::db::Dbs) -> essential_types::Word {
    let r = utils::node::query_state_head(&dbs.node, &ADDRESS, &counter_key().0)
        .await
        .unwrap();
    extract_count(r).unwrap()
}

async fn increment(dbs: &utils::db::Dbs) {
    let current_count = extract_count(
        utils::node::query_state_head(&dbs.node, &ADDRESS, &counter_key().0)
            .await
            .unwrap(),
    )
    .unwrap();

    utils::builder::submit(&dbs.builder, create_solution(current_count + 1))
        .await
        .unwrap();
}
}

Rust CLI

After deploying the counter application, you’ll need a way to interact with it. While you could use curl to manually create and send solutions, this approach can be inefficient and error-prone.

Instead, we will create a simple Command Line Interface (CLI) in Rust to interact with the counter. This CLI will reuse much of the functionality you've already written in the app and will streamline interactions with the deployed application.

The following section will be written in:

counter/
└── counter-app/
    └── src/
        └── main.rs

Deploy to Testnet

In this section you will learn how to deploy your counter app to the public testnet builder. The builder is running at https://node.essential.builders.

To do this you can use the pint-deploy tool available in the essential-integration repo.

Compared to the test this deploys the counter persistently to the testnet. This means that the counter will be available to anyone who knows the contract address.

Make sure you have compiled your app before deploying it. It may have changed since you last compiled.

enter the top level of your project:

counter/

Now deploy the counter app:

pint deploy --builder-address "https://node.essential.builders" --contract "contract/out/debug/counter.json"

It's very possible that someone else has already deployed this contract as contracts are stored via their content hash but don't worry, deploying the same contract twice will not cause any issues.

CLI Program

In this section, we will write the command line application functionality that will be used in our tests to interact with the contract and deployed builder testnet.

The purpose of this code is to provide the user with a way to interact with the contract and deployed builder testnet.

Dependencies

Import the necessary dependencies for the counter application. This includes libraries for command-line argument parsing (clap), counter application, utility functions for compiling Pint projects, and types for working with the Essential protocol.

#![allow(unused)]
fn main() {
use clap::{Args, Parser, Subcommand};
use counter_app::{counter_key, extract_count, incremented_solution, CounterKey};
use essential_app_utils::compile::compile_pint_project;
use essential_rest_client::node_client::EssentialNodeClient;
use essential_types::{ContentAddress, PredicateAddress, Value};
use std::path::PathBuf;
}

Generate Address

Add the compile_address function that compiles the Pint project located in the specified directory and returns the PredicateAddress, which includes both the contract address and the predicate address:

#![allow(unused)]
fn main() {
async fn compile_address(pint_directory: PathBuf) -> Result<PredicateAddress, anyhow::Error> {
    let counter = compile_pint_project(pint_directory).await?;
    let contract_address = essential_hash::contract_addr::from_contract(&counter);
    let predicate_address = essential_hash::content_addr(&counter.predicates[0]);
    let predicate_address = PredicateAddress {
        contract: contract_address,
        predicate: predicate_address,
    };
    Ok(predicate_address)
}
}

This function helps identify which contract and predicate to interact with. While we could simply pass in the addresses directly, generating them here adds convenience and ensures they are correctly derived from the compiled contract.

Args

Define the command-line interface structure using the clap library. Set up two subcommands: ReadCount and IncrementCount, each with their respective arguments. The Shared struct is used to define common arguments for both subcommands.

#![allow(unused)]
fn main() {
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    ReadCount {
        #[command(flatten)]
        server: Shared,
    },
    IncrementCount {
        /// The address of the builder to connect to.
        builder_api: String,
        #[command(flatten)]
        server: Shared,
    },
}

#[derive(Args)]
pub struct Shared {
    /// The address of the node to connect to.
    pub node_api: String,
    /// The directory containing the pint files.
    pub pint_directory: PathBuf,
}
}

Query Count

Create the query_count function to query the current count from the Essential node using the provided node client, contract address, and counter key.

#![allow(unused)]
fn main() {
async fn query_count(
    node: EssentialNodeClient,
    address: ContentAddress,
    key: CounterKey,
) -> anyhow::Result<Option<Value>> {
    Ok(node.query_state(address, key.0).await?)
}
}

Run

Create the run function that handles the execution of the chosen subcommand (ReadCount or IncrementCount).

  • For ReadCount, it compiles the Pint project, queries the current count, and displays it.
  • For IncrementCount, it compiles the project, queries the current count, creates an incremented solution, submits it to the builder, and displays the new count:
#![allow(unused)]
fn main() {
async fn run(cli: Cli) -> anyhow::Result<()> {
    let Cli { command } = cli;
    match command {
        Command::ReadCount {
            server: Shared {
                node_api,
                pint_directory,
            },
        } => {
            let address = compile_address(pint_directory).await?;
            let node = EssentialNodeClient::new(node_api)?;
            let key = counter_key();
            let count = query_count(node, address.contract, key).await?;
            let count_value = extract_count(count)?;
            println!("Current count is: {}", count_value);
        }
        Command::IncrementCount {
            builder_api,
            server: Shared {
                node_api,
                pint_directory,
            },
        } => {
            let address = compile_address(pint_directory).await?;
            let node = EssentialNodeClient::new(node_api)?;
            let key = counter_key();
            let count = query_count(node, address.contract.clone(), key).await?;
            let (solution, new_count) = incremented_solution(count)?; // Pass only count
            let builder =
                essential_rest_client::builder_client::EssentialBuilderClient::new(builder_api)?;
            let ca = builder.submit_solution(&solution).await?;
            println!("Submitted solution: {}", ca);
            println!("Incremented count to: {}", new_count);
        }
    }
    Ok(())
}
}

Add the main entry point of the application. It uses tokio for asynchronous execution, parses the command-line arguments, and calls the run function to execute the appropriate command:

#[tokio::main]
async fn main() {
    let args = Cli::parse();
    if let Err(err) = run(args).await {
        eprintln!("Command failed because: {}", err);
    }
}

Summary

Your final file should look like the code below after following the steps, with a CLI structure for reading and incrementing the contract state, shared arguments, and functions for compiling, querying, and submitting solutions.

use clap::{Args, Parser, Subcommand};
use counter_app::{counter_key, extract_count, incremented_solution, CounterKey};
use essential_app_utils::compile::compile_pint_project;
use essential_rest_client::node_client::EssentialNodeClient;
use essential_types::{ContentAddress, PredicateAddress, Value};
use std::path::PathBuf;

#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    ReadCount {
        #[command(flatten)]
        server: Shared,
    },
    IncrementCount {
        /// The address of the builder to connect to.
        builder_api: String,
        #[command(flatten)]
        server: Shared,
    },
}

#[derive(Args)]
pub struct Shared {
    /// The address of the node to connect to.
    pub node_api: String,
    /// The directory containing the pint files.
    pub pint_directory: PathBuf,
}

#[tokio::main]
async fn main() {
    let args = Cli::parse();
    if let Err(err) = run(args).await {
        eprintln!("Command failed because: {}", err);
    }
}

async fn run(cli: Cli) -> anyhow::Result<()> {
    let Cli { command } = cli;
    match command {
        Command::ReadCount {
            server: Shared {
                node_api,
                pint_directory,
            },
        } => {
            let address = compile_address(pint_directory).await?;
            let node = EssentialNodeClient::new(node_api)?;
            let key = counter_key();
            let count = query_count(node, address.contract, key).await?;
            let count_value = extract_count(count)?;
            println!("Current count is: {}", count_value);
        }
        Command::IncrementCount {
            builder_api,
            server: Shared {
                node_api,
                pint_directory,
            },
        } => {
            let address = compile_address(pint_directory).await?;
            let node = EssentialNodeClient::new(node_api)?;
            let key = counter_key();
            let count = query_count(node, address.contract.clone(), key).await?;
            let (solution, new_count) = incremented_solution(count)?; // Pass only count
            let builder =
                essential_rest_client::builder_client::EssentialBuilderClient::new(builder_api)?;
            let ca = builder.submit_solution(&solution).await?;
            println!("Submitted solution: {}", ca);
            println!("Incremented count to: {}", new_count);
        }
    }
    Ok(())
}

async fn query_count(
    node: EssentialNodeClient,
    address: ContentAddress,
    key: CounterKey,
) -> anyhow::Result<Option<Value>> {
    Ok(node.query_state(address, key.0).await?)
}

async fn compile_address(pint_directory: PathBuf) -> Result<PredicateAddress, anyhow::Error> {
    let counter = compile_pint_project(pint_directory).await?;
    let contract_address = essential_hash::contract_addr::from_contract(&counter);
    let predicate_address = essential_hash::content_addr(&counter.predicates[0]);
    let predicate_address = PredicateAddress {
        contract: contract_address,
        predicate: predicate_address,
    };
    Ok(predicate_address)
}

Interact with Server

Let's try it out!

Simply run your cli with the read-count command, the server address and the path to the contract directory as follows:

cargo run -- read-count "https://node.essential.builders" "../contract"

You should see something like:

Current count is: 1

Your count is probably different.
Are you one of the first people to do this tutorial or is the count already much higher?

Now let's increment the count:

cargo run -- increment-count "https://node.essential.builders" "https://node.essential.builders" "../contract"

The increment-count command requires the builder and nodes api address. They just happen to be the same in this case.

And check the count again:

cargo run -- read-count "https://node.essential.builders" "../contract"

If you don't see the count go up then it's probably because the solution hasn't been included in a block yet.
Just wait a few seconds and try reading again.