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 theflake.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 contract
s. 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 predicate
s. 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 constraint
s. 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
PredicateAddress
s.
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 hostedessential-server
you will want to create a key pair that's stored locally usingessential-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.