Introduction

Welcome to the Getting Started guide for building applications on essential.
This guide is aimed at ambitious developers that want to try building declarative applications.

We are currently very early in our development process so things may change rapidly.
Some things will be broken. If you run into any issues we would love an issue on the appropriate repository. This guide is currently a work in progress and will be expanding further into building more complex applications.

Getting Started

This guide will walk you through the process of creating your first essential application.
We will create a simple counter application that increments a counter.
This will introduce you to the concepts involved in creating essential applications.
The counter application is our version of a "Hello World" application.
It is a little trivial but it will give you a good understanding of how to create more complex applications.
Essential is a declarative system that is capable of expressing powerful applications in elegant ways.

Installation

To get started building Pint applications you will need to install a few tools.
The easiest way to do this is to use the Nix package manager.

However there are some other alternatives if you don't want to use Nix.

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-server and some other things that will be useful for developing your application.

nix develop github:essential-contributions/essential-integration#dev

From source

Cargo

There are instructions on how to install Cargo and Rust imperatively here.

Pint

Once you have Cargo installed you can build Pint from source.

git clone git@github.com:essential-contributions/pint.git
cd pint

To build the pint binary run:

cargo build --release -p pint-cli

The binary will be located at target/release/pint.

To run the pint binary you can use:

cargo run --release -p pint-cli

To install pint on your path you can run:

cargo install --path pint-cli/

Essential Server

Clone the server repo

git clone git@github.com:essential-contributions/essential-server.git
cd essential-server

To build the essential-rest-server binary run:

cargo build --release -p essential-rest-server

The binary will be located at target/release/essential-rest-server.

To run the essential-rest-server binary you can use:

cargo run --release -p essential-rest-server

To install essential-rest-server on your path you can run:

cargo install -p crates/rest-server/

Optional

These are not strictly necessary but are useful for testing and deploying contracts.

Essential Wallet

Clone the wallet repo

git clone git@github.com:essential-contributions/essential-wallet.git
cd essential-wallet

To build the essential-wallet binary run:

cargo build --release -p essential-wallet

The binary will be located at target/release/essential-wallet.

To run the essential-wallet binary you can use:

cargo run --release -p essential-wallet

To install essential-wallet on your path you can run:

cargo install -p crates/wallet/

Essential Deploy Contract

Clone the integration repo

git clone git@github.com:essential-contributions/essential-integration.git
cd essential-integration

To build the essential-deploy-contract binary run:

cargo build --release -p essential-deploy-contract

The binary will be located at target/release/essential-deploy-contract.

To run the essential-deploy-contract binary you can use:

cargo run --release --p essential-deploy-contract

To install essential-deploy-contract on your path you can run:

cargo install -p crates/essential-deploy-contract/

Download binaries

Are CI builds binaries for macOS apple silicon and linux.

Curl

MacOS

curl -L https://github.com/essential-contributions/essential-integration/releases/latest/download/essential-rest-server-macos-latest -o essential-rest-server && chmod 755 essential-rest-server && mkdir -p ~/.local/bin && mv -f essential-rest-server ~/.local/bin/essential-rest-server
curl -L https://github.com/essential-contributions/essential-integration/releases/latest/download/pint-macos-latest -o pint && chmod 755 pint && mkdir -p ~/.local/bin && mv -f pint ~/.local/bin/pint

Linux

These are built on ubuntu.

curl -L https://github.com/essential-contributions/essential-integration/releases/latest/download/essential-rest-server-ubuntu-latest -o essential-rest-server && chmod 755 essential-rest-server && mkdir -p ~/.local/bin && mv -f essential-rest-server ~/.local/bin/essential-rest-server
curl -L https://github.com/essential-contributions/essential-integration/releases/latest/download/pint-ubuntu-latest -o pint && chmod 755 pint && mkdir -p ~/.local/bin && mv -f pint ~/.local/bin/pint

Github

You can find the binaries as assets on the latest release page.

Due to macOS restrictions, the binaries will not run at first and you will need to right click on them and select open.
After doing this once you will be able to run them in the terminal as normal.

Warning: It's not a great idea to download binaries from the internet and run them on your machine.
We recommend you use nix or build from source.

We make an effort at keeping these binaries up to date but they may not be the latest version.
To see which version a binary is you can check the flake.lock file on the commit tagged in the release.

Syntax highlighting

We are currently in the process of adding syntax highlighting for Pint. So far we have support for the following editors.

VScode

Search the market place for pint syntax or use this link

Essential Applications 101

Essential applications at the highest level are composed of contracts. Although this terminology will be familiar to developers coming from imperative blockchain languages (e.g. Solidity), a declarative contract is a fundamentally different thing.

Imperative "contracts" take a set of inputs, and update state as a side-effect of the execution of a sequence of opcodes over these inputs. In particular, a set of storage opcodes exist which directly act upon state.

You may have heard that Essential achieves state updates "declaratively"; without the need for execution. This means that from the point of view of an Essential application, things happen in reverse when compared to the imperative approach. We start with a (proposed) atomic state mutation (i.e. a set of proposed new state values), and then substitute those values into a contract to check their validity. A Pint program, then, exists to validate a given state mutation against a set of predefined rules. These predefined rules are what make up a Pint contract.

We have said that the starting point for an Essential application is a state mutation. You may therefore be wondering where these state mutations come from. Discovery of optimal state mutations (or "solutions") is the responsibility of solvers. A solver may be a third-party entity which competes to find optimal solutions. It may also be simply a centralized program (e.g. a server, or a front-end app or wallet) which serves solutions for a specific application. The techniques solvers use to find optimal solutions (and the mechanism governing their inclusion in blocks) is beyond the scope of this guide. For now, it is sufficient to note that incentivized actors exist in the system to discover these solutions, and that this discovery occurs off-chain. We will see a simple solution later in this guide, when we come to test our application.

A Pint contract may declare a storage block. If it does, this state belongs to that contract. In general, state can only be updated if the new values are validated by the contract which owns it.

Note : a contract does not have to define a storage block. It may simply apply additional constraints to state mutations on other contracts. In this case, both the constraints of this contract and the constraints contract which owns the state must be satisfied for a solution to be valid.

Validation occurs through the satisfaction of one of the contract's predicates. You can think of predicates as "pathways to validity" for the contract: in order for the contract to be satisfied (and therefore, its state updated), one of its predicates must be satisfied.

A predicate is a block of code comprising one or more constraints. A constraint is simply a boolean expression which must evaluate to True for the predicate containing it to be satisfied. From a code organization point of view, a predicate may look a bit like a function. However, the distinction is very important. A predicate is in no sense "called" in the same way a function is. It is simply a target that individual solutions may seek to satisfy.

In the rest of this guide, we will see these concepts implemented in Pint for a simple counter application.

Essential server

The essential server is a public centralized test server that implements the declarative constraint checking system. The server is built on top of the same software stack that the node will be using. This allows app developers and solvers who want to experiment with building declarative applications to get started in a realistic environment.

Functionally the server acts like a single block builder but does not post blocks to an L1.

The plan is to incrementally swap out parts of the server until there is a fully decentralized node and builder. We are aiming to keep the changes to the API and constraint checking system as minimal as possible although it is still early in the project and there will most likely be some breaking changes. These will be well documented and communicated so developers can easily update their applications.

The server is using a persistent database so that you can interact with other applications that have been deployed.

In the following section we will walk you through building a simple counter app. At first you will use the server running locally on your machine but then you will try using the public server.

The server has the same API as the local server you will be running. The API is defined here.

Address

The server is running at https://server.essential.builders. It only accepts HTTPS connections and requires HTTP/2. You don't need to worry about this unless you are writing your own client libraries.

Note: You may be wondering what happens in you deploy the same app to the server as someone else. This is completely fine but you should keep in mind that the state of the app doesn't reset on a redeploy (redeploying is idempotent). So the state may already contain values from other users. It will be interesting to see what the count is up to once you go finish the tutorial.

Note: Although we will make an effort to keep the state of the server, there may be times where the databases need to be wiped due to changes in the database schema.

Note: If the pintc changes how it compiles your application or there is a change to our assembly you 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.

Counter

This chapter will take you through the process of creating a simple counter application.
The aim is to include every step required to create a working application.
We use Rust for our front end in this example but you can use any language you like for your own applications.
There is currently better support for Rust due to most essential software being written in Rust however we plan to expand this in the future.

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 {
    state counter: int = storage::counter;
}

When a storage value is read like this there are actually two values read:

  • The pre-state value of the storage, denoted by counter.
  • The post-state value of the storage, denoted by counter'. (Notice the apostrophe ').

Constraint

Let's add our first constraint:

predicate Increment {
    state counter: int = 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.

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 {
    state counter: int = 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 {
    state counter: int = 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 {
    state counter: int = 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.

This is the file you can sign and deploy.

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": "",
      "vars": [],
      "pub_vars": []
    },
    {
      "name": "::Increment",
      "vars": [],
      "pub_vars": []
    }
  ],
  "storage": [
    {
      "name": "counter",
      "ty": {
        "Int": [
          0
        ]
      }
    }
  ]
}

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

Now that you have compiled your counter contract you could go ahead and deploy it to the test server at https://server.essential.builders.

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

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.

We will cover how to make contracts unique later on.

Deploying a contract doesn't really let you do much with it though.
To interact with the contract you will need to create a front end application.
This can be done in any language. We will demonstrate in 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.

As previously mentioned there is no reason you have to use Rust to build a front end application.
You could use any language you like.
This chapter is optional but you may find it useful to see how to interact with the contract (even if you don't know Rust).

Install Rust

If you don't have Rust and Cargo installed you can follow the instructions here.

If you are using Nix you can follow these instructions to get Rust.

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#dev

Or you can create your own flake using:

cd counter
nix flake init -t github:essential-contributions/essential-integration
nix develop

Create the cargo project

Create a new cargo project by running following command from the root of your counter project:

cargo new --lib counter-app

You project should look like:

counter/contract/pint.toml
counter/contract/contract.pnt
counter/contract/src/contract.pnt
counter/counter-app/Cargo.toml
counter/counter-app/src/lib.rs

Dependencies

Now add the following dependencies to your rust project:

cd counter-app
cargo add essential-app-utils
cargo add essential-app-utils --features test-utils --dev
cargo add essential-deploy-contract
cargo add essential-hash
cargo add essential-rest-client
cargo add essential-types
cargo add essential-wallet --features test-utils --dev
cargo add anyhow
cargo add tokio --features full
cargo add clap

Your cargo toml should look something like this:

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

[dependencies]
anyhow = "1.0.86"
clap = "4.5.8"
essential-app-utils = "0.1.0"
essential-deploy-contract = "0.1.0"
essential-hash = "0.1.0"
essential-rest-client = "0.1.0"
essential-types = "0.1.0"
tokio = { version = "1.38.0", features = ["full"] }

[dev-dependencies]
essential-app-utils = { version = "0.1.0", features = ["test-utils"] }
essential-wallet = { version = "0.1.0", features = ["test-utils"] }

Add a test

Lastly add a test to your front end application:

mkdir tests
touch tests/counter.rs

Your project should now look like:

counter/contract/pint.toml                        
counter/contract/contract.pnt                         
counter/contract/src/contract.pnt                             
counter/counter-app/Cargo.toml                             
counter/counter-app/tests/counter.rs                   
counter/counter-app/src/lib.rs    

App

We are going to write the application functionality.
This is what we will use in our tests to interact with the contract.
The aim of this code is to read state and create solutions.

Start by adding the imports you are going to need.

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

This is the main struct that allows us to interact with the essential-server. It contains the client that let's us send requests to the server and the predicate address for our counter contract.

#![allow(unused)]
fn main() {
pub struct App {
    client: EssentialClient,
    predicate: PredicateAddress,
}
}

Add an impl block.

#![allow(unused)]
fn main() {
impl App {

}
}

The COUNTER_KEY is the key that points to the counter: int storage.

#![allow(unused)]
fn main() {
impl App {
    pub const COUNTER_KEY: Word = 0;
}
}

Add a new method so the App can be created. \ This takes the address of the server and the contract.

#![allow(unused)]
fn main() {
impl App {
    // ...

    pub fn new(addr: String, predicate: PredicateAddress) -> anyhow::Result<Self> {
        let client = EssentialClient::new(addr)?;
        Ok(Self {
            client,
            predicate,
        })
    }
}
}

Read storage

Read the current count from storage.
Using the essential-client we make a query to the state at the address of the counter contract and the COUNTER_KEY.

#![allow(unused)]
fn main() {
impl App {
    // ...

    pub async fn read_count(&self) -> anyhow::Result<Word> {
        let output = self
            .client
            .query_state(&self.predicate.contract, &vec![Self::COUNTER_KEY])
            .await?;

        // ...
    }

    // ...
}
}

State can return a value of any size including empty.
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() {
impl App {
    // ...

    pub async fn read_count(&self) -> anyhow::Result<Word> {
        let output = self
            .client
            .query_state(&self.predicate.contract, &vec![Self::COUNTER_KEY])
            .await?;

        let count = match &output[..] {
            [] => 0,
            [count] => *count,
            _ => bail!("Expected one word, got: {:?}", output),
        };
        Ok(count)
    }

    // ...
}
}

Create a solution

Add this function (outside the impl) that takes the predicate address and the count we are trying to set the state to.
The solution has a single SolutionData that solves this predicate (other solutions may solve multiple predicates).
There's no decision variables or transient data so those are set to default.
Add in a single state Mutation. The key is the COUNTER_KEY and the value is the new count.

#![allow(unused)]
fn main() {
pub fn create_solution(predicate: PredicateAddress, new_count: Word) -> Solution {
    Solution {
        data: vec![SolutionData {
            predicate_to_solve: predicate,
            decision_variables: Default::default(),
            transient_data: Default::default(),
            state_mutations: vec![Mutation {
                key: vec![App::COUNTER_KEY],
                value: vec![new_count],
            }],
        }],
    }
}
}

Back in the impl App add a method to create and submit a solution that will increment the count.

#![allow(unused)]
fn main() {
impl App {
    // ...

    pub async fn increment(&self) -> anyhow::Result<Word> {
        let new_count = self.read_count().await? + 1;
        let solution = create_solution(self.predicate.clone(), new_count);
        self.client.submit_solution(solution).await?;
        Ok(new_count)
    }

    // ...
}
}
Check your `lib.rs` matches this.
#![allow(unused)]
fn main() {
use anyhow::bail;
use essential_rest_client::EssentialClient;
use essential_types::{
    solution::{Mutation, Solution, SolutionData},
    PredicateAddress, Word,
};

pub struct App {
    client: EssentialClient,
    predicate: PredicateAddress,
}

impl App {
    pub const COUNTER_KEY: Word = 0;

    pub fn new(addr: String, predicate: PredicateAddress) -> anyhow::Result<Self> {
        let client = EssentialClient::new(addr)?;
        Ok(Self {
            client,
            predicate,
        })
    }

    pub async fn read_count(&self) -> anyhow::Result<Word> {
        let output = self
            .client
            .query_state(&self.predicate.contract, &vec![Self::COUNTER_KEY])
            .await?;

        let count = match &output[..] {
            [] => 0,
            [count] => *count,
            _ => bail!("Expected one word, got: {:?}", output),
        };
        Ok(count)
    }

    pub async fn increment(&self) -> anyhow::Result<Word> {
        let new_count = self.read_count().await? + 1;
        let solution = create_solution(self.predicate.clone(), new_count);
        self.client.submit_solution(solution).await?;
        Ok(new_count)
    }
}

pub fn create_solution(predicate: PredicateAddress, new_count: Word) -> Solution {
    Solution {
        data: vec![SolutionData {
            predicate_to_solve: predicate,
            decision_variables: Default::default(),
            transient_data: Default::default(),
            state_mutations: vec![Mutation {
                key: vec![App::COUNTER_KEY],
                value: vec![new_count],
            }],
        }],
    }
}
}

Compile and deploy

Now we will create a test that compiles, signs and deploys the counter app on a locally running essential-server.

Start by adding the imports you will need.

#![allow(unused)]
fn main() {
use essential_app_utils::{compile::compile_pint_project, local_server::setup_server};
use counter_app::App;
use essential_types::{PredicateAddress, Word};
}

Add a tokio test because we are using async code.

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

}
}

Run a local essential-server in the background.

Note that this requires that the essential-server binary be your $PATH. See the installation section for more details.

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_counter() {
    let (addr, _server) = setup_server().await.unwrap();
}
}

Compile

Compile the pint project.

Note that this requires that the pint binary be your $PATH. See the installation section for more details.

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

    let counter = compile_pint_project(
        concat!(env!("CARGO_MANIFEST_DIR"), "/../contract").into(),
    )
    .await
    .unwrap();
}
}

Create the PredicateAddress. This is the ContentAddress of the overall contract and the ContentAddress if the predicate.

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

    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,
    };
}
}

Our contract only has a single predicate but other contracts can have multiple predicates and therefor multiple PredicateAddresss.

Sign and deploy

Using the essential-wallet create a temporary wallet and a new key for alice using the Secp256k1 scheme.
This is the key that you will use to sign the contract before deploying it.

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

    let mut wallet = essential_wallet::Wallet::temp().unwrap();
    wallet
        .new_key_pair("alice", essential_wallet::Scheme::Secp256k1)
        .unwrap();
}
}

Note that the essential-wallet has not been audited and is only for testing purposes. In this test we create a temporary key that is deleted at the end of the test.
To interact with the hosted essential-server you will want to create a key pair that's stored locally using essential-wallet.
Our wallet is just a convenience for testing.

Sign and deploy the contract using alice's key.

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

    essential_deploy_contract::sign_and_deploy(addr.clone(), "alice", &mut wallet, counter)
        .await
        .unwrap();
}
}

Now we are done getting everything setup and deployed.
In the next section we will interact with the deployed contract.

Check your `tests/counter.rs` matches this.
#![allow(unused)]
fn main() {
use essential_app_utils::{compile::compile_pint_project, local_server::setup_server};
use counter_app::App;
use essential_types::{PredicateAddress, Word};

#[tokio::test]
async fn test_counter() {
    let (addr, _server) = setup_server().await.unwrap();

    let counter = compile_pint_project(
        concat!(env!("CARGO_MANIFEST_DIR"), "/../contract").into(),
    )
    .await
    .unwrap();

    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,
    };

    let mut wallet = essential_wallet::Wallet::temp().unwrap();
    wallet
        .new_key_pair("alice", essential_wallet::Scheme::Secp256k1)
        .unwrap();

    essential_deploy_contract::sign_and_deploy(addr.clone(), "alice", &mut wallet, counter)
        .await
        .unwrap();
}
}

Test

Continuing on from where we left off in the last test we will now read and increment the counter.

Continue adding to the bottom of the same test.

`tests/counter.rs` code from previous section.
#![allow(unused)]
fn main() {
use essential_app_utils::{compile::compile_pint_project, local_server::setup_server};
use counter_app::App;
use essential_types::{PredicateAddress, Word};

#[tokio::test]
async fn test_counter() {
    let (addr, _server) = setup_server().await.unwrap();

    let counter = compile_pint_project(
        concat!(env!("CARGO_MANIFEST_DIR"), "/../contract").into(),
    )
    .await
    .unwrap();

    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,
    };

    let mut wallet = essential_wallet::Wallet::temp().unwrap();
    wallet
        .new_key_pair("alice", essential_wallet::Scheme::Secp256k1)
        .unwrap();

    essential_deploy_contract::sign_and_deploy(addr.clone(), "alice", &mut wallet, counter)
        .await
        .unwrap();

    // Add new code here.

}
}

Create a new App we defined earlier in the lib.rs.

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

    let app = App::new(addr, predicate_address).unwrap();
}
}

Read the current count value and assert that it's 0.

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

    assert_eq!(app.read_count().await.unwrap(), 0);
}
}

Increment the counter. Remember this creates and submits the solution.

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

    app.increment().await.unwrap();
}
}

Stepping out of the test function for a second. We want to check for new state however the state only changes once a solution has been included in a block.
Add this wait_for_change function that will check if the state is the expected value. If it is not then it will wait one second and check again.

#![allow(unused)]
fn main() {
async fn wait_for_change(app: &App, expected: Word) {
    loop {
        if app.read_count().await.unwrap() == expected {
            break;
        }
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    }
}
}

Back in the test function now let's use the wait_for_change we just wrote to wait for the count to reach 1.

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

    wait_for_change(&app, 1).await;
}
}

Exciting the your solution has successfully satisfied the constraints and the state has been mutated.

Now just to make sure let's increment it again.

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

    app.increment().await.unwrap();
}
}

And wait for the state to change.

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

    wait_for_change(&app, 2).await;
}
}

The state has changed again to 2!

Check your `tests/counter.rs` matches this.
#![allow(unused)]
fn main() {
use essential_app_utils::{compile::compile_pint_project, local_server::setup_server};
use counter_app::App;
use essential_types::{PredicateAddress, Word};

#[tokio::test]
async fn test_counter() {
    let (addr, _server) = setup_server().await.unwrap();

    let counter = compile_pint_project(
        concat!(env!("CARGO_MANIFEST_DIR"), "/../contract").into(),
    )
    .await
    .unwrap();

    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,
    };

    let mut wallet = essential_wallet::Wallet::temp().unwrap();
    wallet
        .new_key_pair("alice", essential_wallet::Scheme::Secp256k1)
        .unwrap();

    essential_deploy_contract::sign_and_deploy(addr.clone(), "alice", &mut wallet, counter)
        .await
        .unwrap();

    let app = App::new(addr, predicate_address).unwrap();

    assert_eq!(app.read_count().await.unwrap(), 0);

    app.increment().await.unwrap();

    wait_for_change(&app, 1).await;
    
    app.increment().await.unwrap();

    wait_for_change(&app, 2).await;
}

async fn wait_for_change(app: &App, expected: Word) {
    loop {
        if app.read_count().await.unwrap() == expected {
            break;
        }
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    }
}
}

Run the test

Run the test and check it all works.

cargo test

Congratulations on building your first essential application.
It may be a very simple example but this declarative way of creating applications is very powerful. \

In future sections we will dive into more complex and interesting applications.

This concludes the "hello world" introduction to the essential system.

Deploy to server

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

Compared to the test this deploys the counter persistently to the test net. 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/

Start by creating an account in essential wallet (you can skip this if you already have an account). Name your account something you will remember:

essential-deploy-contract create-account "alice"

You will be prompted with:

Enter password to unlock wallet:

If you have not yet created an essential wallet this will set your password for all keys stored locally in the wallet. If you have already created a wallet you will need to enter the password you used to create it. If you have forgotten your password you delete the wallet at ~/.essential-wallet. You will loose any keys you have already created but you can start over with a new password.

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

Now sign and deploy the counter app:

essential-deploy-contract deploy "https://server.essential.builders" "alice" "contract/out/debug/counter.json"

You will be prompted with:

Enter password to unlock wallet:

This is to unlock your wallet so the contract can be signed. Then you should see something similar to:

Deployed contract to: B1D2E4A1CA7822903AF93E9D395ED7037A79AD8E10084BA25E75B18D6C92FAB8

The address you see might be different.

Rust CLI

Now that the counter is deployed we need a way to interact with it.
We could use curl and manually create solutions but that would be cumbersome.
Instead we will create a simple CLI in Rust to interact with the counter. This will reuse the functionality you have already written in the app.

Commands

Add in a main.rs file that will be used to run the counter-app CLI:

cd counter-app
touch src/main.rs

Now in the main.rs file add the use statements:

#![allow(unused)]
fn main() {
use clap::{Args, Parser, Subcommand};
use counter_app::App;
use essential_app_utils::compile::compile_pint_project;
use essential_types::PredicateAddress;
use std::path::PathBuf;
}

Using the cli crate clap add two commands:

#![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 {
        #[command(flatten)]
        server: Shared,
    },
}

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

The command ReadCount with become read-count <SERVER> <PINT_DIRECTORY> which will read the current count from the server.
The command IncrementCount with become increment-count <SERVER> <PINT_DIRECTORY> which will increment the current count on the server.
Both commands take the same arguments so we use a Shared struct to hold the arguments.

Add the main function to run the CLI:

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

This is fairly simple and just handles errors in a nice way for the user.

For both commands we need to compile the pint project to get the address of the predicate and create a new App.
Add this helper function:

#![allow(unused)]
fn main() {
async fn create_app(pint_directory: PathBuf, server: String) -> Result<App, 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,
    };
    let app = App::new(server, predicate_address)?;
    Ok(app)
}
}

The core of the cli is the run function.
It should handle each command and use the App to complete the actions like:

#![allow(unused)]
fn main() {
async fn run(cli: Cli) -> anyhow::Result<()> {
    let Cli { command } = cli;
    match command {
        Command::ReadCount {
            server: Shared {
                server,
                pint_directory,
            },
        } => {
            let app = create_app(pint_directory, server).await?;
            let count = app.read_count().await?;
            println!("Current count is: {}", count);
        }
        Command::IncrementCount {
            server: Shared {
                server,
                pint_directory,
            },
        } => {
            let app = create_app(pint_directory, server).await?;
            let new_count = app.increment().await?;
            println!("Incremented count to: {}", new_count);
        }
    }
    Ok(())
}
}
Check your `main.rs` matches this.
use clap::{Args, Parser, Subcommand};
use counter_app::App;
use essential_app_utils::compile::compile_pint_project;
use essential_types::PredicateAddress;
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 {
        #[command(flatten)]
        server: Shared,
    },
}

#[derive(Args)]
pub struct Shared {
    /// The address of the server to connect to.
    pub server: 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 {
                server,
                pint_directory,
            },
        } => {
            let app = create_app(pint_directory, server).await?;
            let count = app.read_count().await?;
            println!("Current count is: {}", count);
        }
        Command::IncrementCount {
            server: Shared {
                server,
                pint_directory,
            },
        } => {
            let app = create_app(pint_directory, server).await?;
            let new_count = app.increment().await?;
            println!("Incremented count to: {}", new_count);
        }
    }
    Ok(())
}

async fn create_app(pint_directory: PathBuf, server: String) -> Result<App, 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,
    };
    let app = App::new(server, predicate_address)?;
    Ok(app)
}

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://server.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://server.essential.builders" "../contract"

And check the count again:

cargo run -- read-count "https://server.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.