Build a Calculator Application
In this tutorial, we will build a simple Calculator application to illustrate how requests are sent and processed within Cartesi Rollups Infrastructure.
We provide JavaScript, Python, Rust, Go, and C++ implementations so you can use your preferred backend language.
Set up your environment
Install these to set up your environment for quick building:
-
Cartesi CLI: A simple tool for building applications on Cartesi. Install Cartesi CLI for your OS of choice.
-
Docker Desktop 4.x: The tool you need to run the Cartesi Machine and its dependencies. Install Docker for your OS of choice.
Create the backend application
To create a calculator backend, run the template command for your language:
- JavaScript
- Python
- Rust
- Go
- C++
cartesi create calculator --template javascript
cartesi create calculator --template python
cartesi create calculator --template rust
cartesi create calculator --template go
cartesi create calculator --template cpp
This creates a calculator/ directory with all required files.
Your backend entry point depends on language (src/index.js, dapp.py, src/main.rs, main.go, or src/main.cpp).
Review the default backend template
Before implementing the calculator logic, review the default rollup request loop generated by the templates:
- JavaScript
- Python
- Rust
- Go
- C++
const rollup_server = process.env.ROLLUP_HTTP_SERVER_URL;
console.log("HTTP rollup_server url is " + rollup_server);
async function handle_advance(data) {
console.log("Received advance request data " + JSON.stringify(data));
return "accept";
}
async function handle_inspect(data) {
console.log("Received inspect request data " + JSON.stringify(data));
return "accept";
}
var handlers = {
advance_state: handle_advance,
inspect_state: handle_inspect,
};
var finish = { status: "accept" };
(async () => {
while (true) {
const finish_req = await fetch(rollup_server + "/finish", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ status: "accept" }),
});
console.log("Received finish status " + finish_req.status);
if (finish_req.status == 202) {
console.log("No pending rollup request, trying again");
} else {
const rollup_req = await finish_req.json();
var handler = handlers[rollup_req["request_type"]];
finish["status"] = await handler(rollup_req["data"]);
}
}
})();
from os import environ
import logging
import requests
logging.basicConfig(level="INFO")
logger = logging.getLogger(__name__)
rollup_server = environ["ROLLUP_HTTP_SERVER_URL"]
logger.info(f"HTTP rollup_server url is {rollup_server}")
def handle_advance(data):
logger.info(f"Received advance request data {data}")
return "accept"
def handle_inspect(data):
logger.info(f"Received inspect request data {data}")
return "accept"
handlers = {
"advance_state": handle_advance,
"inspect_state": handle_inspect,
}
finish = {"status": "accept"}
while True:
logger.info("Sending finish")
response = requests.post(rollup_server + "/finish", json=finish)
logger.info(f"Received finish status {response.status_code}")
if response.status_code == 202:
logger.info("No pending rollup request, trying again")
else:
rollup_request = response.json()
data = rollup_request["data"]
handler = handlers[rollup_request["request_type"]]
finish["status"] = handler(rollup_request["data"])
use json::{object, JsonValue};
use std::env;
pub async fn handle_advance(
_client: &hyper::Client<hyper::client::HttpConnector>,
_server_addr: &str,
request: JsonValue,
) -> Result<&'static str, Box<dyn std::error::Error>> {
println!("Received advance request data {}", &request);
let _payload = request["data"]["payload"]
.as_str()
.ok_or("Missing payload")?;
// TODO: add application logic here
Ok("accept")
}
pub async fn handle_inspect(
_client: &hyper::Client<hyper::client::HttpConnector>,
_server_addr: &str,
request: JsonValue,
) -> Result<&'static str, Box<dyn std::error::Error>> {
println!("Received inspect request data {}", &request);
let _payload = request["data"]["payload"]
.as_str()
.ok_or("Missing payload")?;
// TODO: add application logic here
Ok("accept")
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = hyper::Client::new();
let server_addr = env::var("ROLLUP_HTTP_SERVER_URL")?;
let mut status = "accept";
loop {
println!("Sending finish");
let response = object! {"status" => status.clone()};
let request = hyper::Request::builder()
.method(hyper::Method::POST)
.header(hyper::header::CONTENT_TYPE, "application/json")
.uri(format!("{}/finish", &server_addr))
.body(hyper::Body::from(response.dump()))?;
let response = client.request(request).await?;
println!("Received finish status {}", response.status());
if response.status() == hyper::StatusCode::ACCEPTED {
println!("No pending rollup request, trying again");
} else {
let body = hyper::body::to_bytes(response).await?;
let utf = std::str::from_utf8(&body)?;
let req = json::parse(utf)?;
let request_type = req["request_type"]
.as_str()
.ok_or("request_type is not a string")?;
status = match request_type {
"advance_state" => handle_advance(&client, &server_addr[..], req).await?,
"inspect_state" => handle_inspect(&client, &server_addr[..], req).await?,
&_ => {
eprintln!("Unknown request type");
"reject"
}
};
}
}
}
package main
import (
"dapp/rollups"
"encoding/json"
"fmt"
"io"
"log"
"os"
"strconv"
)
var (
infolog = log.New(os.Stderr, "[ info ] ", log.Lshortfile)
errlog = log.New(os.Stderr, "[ error ] ", log.Lshortfile)
)
func HandleAdvance(data *rollups.AdvanceResponse) error {
dataMarshal, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("HandleAdvance: failed marshaling json: %w", err)
}
infolog.Println("Received advance request data", string(dataMarshal))
return nil
}
func HandleInspect(data *rollups.InspectResponse) error {
dataMarshal, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("HandleInspect: failed marshaling json: %w", err)
}
infolog.Println("Received inspect request data", string(dataMarshal))
return nil
}
func Handler(response *rollups.FinishResponse) error {
var err error
switch response.Type {
case "advance_state":
data := new(rollups.AdvanceResponse)
if err = json.Unmarshal(response.Data, data); err != nil {
return fmt.Errorf("Handler: Error unmarshaling advance: %w", err)
}
err = HandleAdvance(data)
case "inspect_state":
data := new(rollups.InspectResponse)
if err = json.Unmarshal(response.Data, data); err != nil {
return fmt.Errorf("Handler: Error unmarshaling inspect: %w", err)
}
err = HandleInspect(data)
}
return err
}
func main() {
finish := rollups.FinishRequest{Status: "accept"}
for {
infolog.Println("Sending finish")
res, err := rollups.SendFinish(&finish)
if err != nil {
errlog.Panicln("Error: error making http request: ", err)
}
infolog.Println("Received finish status ", strconv.Itoa(res.StatusCode))
if res.StatusCode == 202 {
infolog.Println("No pending rollup request, trying again")
} else {
resBody, err := io.ReadAll(res.Body)
if err != nil {
errlog.Panicln("Error: could not read response body: ", err)
}
var response rollups.FinishResponse
err = json.Unmarshal(resBody, &response)
if err != nil {
errlog.Panicln("Error: unmarshaling body:", err)
}
finish.Status = "accept"
err = Handler(&response)
if err != nil {
errlog.Println(err)
finish.Status = "reject"
}
}
}
}
#include <stdio.h>
#include <iostream>
#include "3rdparty/cpp-httplib/httplib.h"
#include "3rdparty/picojson/picojson.h"
std::string handle_advance(httplib::Client &cli, picojson::value data)
{
std::cout << "Received advance request data " << data << std::endl;
return "accept";
}
std::string handle_inspect(httplib::Client &cli, picojson::value data)
{
std::cout << "Received inspect request data " << data << std::endl;
return "accept";
}
int main(int argc, char **argv)
{
std::map<std::string, decltype(&handle_advance)> handlers = {
{std::string("advance_state"), &handle_advance},
{std::string("inspect_state"), &handle_inspect},
};
httplib::Client cli(getenv("ROLLUP_HTTP_SERVER_URL"));
cli.set_read_timeout(20, 0);
std::string status("accept");
std::string rollup_address;
while (true)
{
std::cout << "Sending finish" << std::endl;
auto finish = std::string("{\"status\":\"") + status + std::string("\"}");
auto r = cli.Post("/finish", finish, "application/json");
std::cout << "Received finish status " << r.value().status << std::endl;
if (r.value().status == 202)
{
std::cout << "No pending rollup request, trying again" << std::endl;
}
else
{
picojson::value rollup_request;
picojson::parse(rollup_request, r.value().body);
picojson::value metadata = rollup_request.get("data").get("metadata");
auto request_type = rollup_request.get("request_type").get<std::string>();
auto handler = handlers.find(request_type)->second;
auto data = rollup_request.get("data");
status = (*handler)(cli, data);
}
}
return 0;
}
Build the backend application
For this tutorial, we parse math expressions from advance payloads and emit calculation results as notices. Install the minimal dependencies for your language:
- JavaScript
- Python
- Rust
- Go
- C++
npm add expr-eval
cat > requirements.txt << 'EOF'
requests==2.32.5
py_expression_eval==0.3.14
EOF
cargo add meval hex
go get github.com/Knetic/govaluate
# No extra package needed for this tutorial snippet.
# The generated C++ template already includes httplib + picojson.
For example, an advance request to the backend with payload “1+2” should emit a notice with the response “3”.
Implement the application logic
Copy the snippet for your language and replace the contents of your local backend entry point file:
- JavaScript
- Python
- Rust
- Go
- C++
import { Parser } from "expr-eval";
const rollup_server = process.env.ROLLUP_HTTP_SERVER_URL;
console.log("HTTP rollup_server url is " + rollup_server);
const parser = new Parser();
function hex2str(hex) {
const normalized = hex.startsWith("0x") ? hex.slice(2) : hex;
return Buffer.from(normalized, "hex").toString("utf8");
}
function str2hex(text) {
return "0x" + Buffer.from(text, "utf8").toString("hex");
}
async function emitNotice(payload) {
await fetch(rollup_server + "/notice", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload }),
});
}
async function emitReport(payload) {
await fetch(rollup_server + "/report", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload }),
});
}
async function handle_advance(data) {
console.log("Received advance request data " + JSON.stringify(data));
try {
const expression = hex2str(data.payload);
const result = parser.evaluate(expression);
await emitNotice(str2hex(String(result)));
return "accept";
} catch (error) {
await emitReport(str2hex(`Error processing input: ${error}`));
return "reject";
}
}
async function handle_inspect(data) {
console.log("Received inspect request data " + JSON.stringify(data));
await emitReport(data.payload);
return "accept";
}
const handlers = {
advance_state: handle_advance,
inspect_state: handle_inspect,
};
const finish = { status: "accept" };
(async () => {
while (true) {
const finish_req = await fetch(rollup_server + "/finish", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: finish.status }),
});
if (finish_req.status === 202) {
console.log("No pending rollup request, trying again");
continue;
}
const rollup_req = await finish_req.json();
const handler = handlers[rollup_req.request_type];
finish.status = await handler(rollup_req.data);
}
})();
from os import environ
import traceback
import logging
import requests
from py_expression_eval import Parser
logging.basicConfig(level="INFO")
logger = logging.getLogger(__name__)
rollup_server = environ["ROLLUP_HTTP_SERVER_URL"]
logger.info(f"HTTP rollup_server url is {rollup_server}")
def hex2str(hex_value):
return bytes.fromhex(hex_value[2:]).decode("utf-8")
def str2hex(text):
return "0x" + text.encode("utf-8").hex()
def handle_advance(data):
logger.info(f"Received advance request data {data}")
status = "accept"
try:
expression = hex2str(data["payload"])
parser = Parser()
output = parser.parse(expression).evaluate({})
response = requests.post(
rollup_server + "/notice",
json={"payload": str2hex(str(output))}
)
logger.info(f"Received notice status {response.status_code} body {response.content}")
except Exception:
status = "reject"
msg = f"Error processing data {data}\n{traceback.format_exc()}"
logger.error(msg)
response = requests.post(
rollup_server + "/report",
json={"payload": str2hex(msg)}
)
logger.info(f"Received report status {response.status_code} body {response.content}")
return status
def handle_inspect(data):
logger.info(f"Received inspect request data {data}")
response = requests.post(rollup_server + "/report", json={"payload": data["payload"]})
logger.info(f"Received report status {response.status_code}")
return "accept"
handlers = {
"advance_state": handle_advance,
"inspect_state": handle_inspect,
}
finish = {"status": "accept"}
while True:
logger.info("Sending finish")
response = requests.post(rollup_server + "/finish", json=finish)
logger.info(f"Received finish status {response.status_code}")
if response.status_code == 202:
logger.info("No pending rollup request, trying again")
else:
rollup_request = response.json()
handler = handlers[rollup_request["request_type"]]
finish["status"] = handler(rollup_request["data"])
use json::{object, JsonValue};
use std::env;
fn hex2str(payload: &str) -> Result<String, Box<dyn std::error::Error>> {
let normalized = payload.strip_prefix("0x").unwrap_or(payload);
let bytes = hex::decode(normalized)?;
Ok(String::from_utf8(bytes)?)
}
fn str2hex(text: &str) -> String {
format!("0x{}", hex::encode(text))
}
async fn post_payload(
client: &hyper::Client<hyper::client::HttpConnector>,
server_addr: &str,
route: &str,
payload_hex: String,
) -> Result<(), Box<dyn std::error::Error>> {
let body = object! { "payload" => payload_hex };
let request = hyper::Request::builder()
.method(hyper::Method::POST)
.header(hyper::header::CONTENT_TYPE, "application/json")
.uri(format!("{}/{}", server_addr, route))
.body(hyper::Body::from(body.dump()))?;
let _ = client.request(request).await?;
Ok(())
}
pub async fn handle_advance(
client: &hyper::Client<hyper::client::HttpConnector>,
server_addr: &str,
request: JsonValue,
) -> Result<&'static str, Box<dyn std::error::Error>> {
println!("Received advance request data {}", &request);
let payload = request["data"]["payload"].as_str().ok_or("missing payload")?;
match hex2str(payload) {
Ok(expression) => match meval::eval_str(expression) {
Ok(value) => {
post_payload(client, server_addr, "notice", str2hex(&value.to_string())).await?;
Ok("accept")
}
Err(err) => {
let msg = format!("evaluation error: {}", err);
post_payload(client, server_addr, "report", str2hex(&msg)).await?;
Ok("reject")
}
},
Err(err) => {
let msg = format!("decode error: {}", err);
post_payload(client, server_addr, "report", str2hex(&msg)).await?;
Ok("reject")
}
}
}
pub async fn handle_inspect(
client: &hyper::Client<hyper::client::HttpConnector>,
server_addr: &str,
request: JsonValue,
) -> Result<&'static str, Box<dyn std::error::Error>> {
println!("Received inspect request data {}", &request);
let payload = request["data"]["payload"].as_str().ok_or("missing payload")?;
post_payload(client, server_addr, "report", payload.to_string()).await?;
Ok("accept")
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = hyper::Client::new();
let server_addr = env::var("ROLLUP_HTTP_SERVER_URL")?;
let mut status = "accept";
loop {
println!("Sending finish");
let finish = object! { "status" => status };
let request = hyper::Request::builder()
.method(hyper::Method::POST)
.header(hyper::header::CONTENT_TYPE, "application/json")
.uri(format!("{}/finish", &server_addr))
.body(hyper::Body::from(finish.dump()))?;
let response = client.request(request).await?;
if response.status() == hyper::StatusCode::ACCEPTED {
println!("No pending rollup request, trying again");
continue;
}
let body = hyper::body::to_bytes(response).await?;
let req = json::parse(std::str::from_utf8(&body)?)?;
let request_type = req["request_type"].as_str().ok_or("request_type is not string")?;
status = match request_type {
"advance_state" => handle_advance(&client, &server_addr, req).await?,
"inspect_state" => handle_inspect(&client, &server_addr, req).await?,
_ => "reject",
};
}
}
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"dapp/rollups"
"github.com/Knetic/govaluate"
)
var (
infolog = log.New(os.Stderr, "[ info ] ", log.Lshortfile)
errlog = log.New(os.Stderr, "[ error ] ", log.Lshortfile)
)
func HandleAdvance(data *rollups.AdvanceResponse) error {
infolog.Printf("Received advance request data %+v\n", data)
expression, err := rollups.Hex2Str(data.Payload)
if err != nil {
report := rollups.ReportRequest{Payload: rollups.Str2Hex(fmt.Sprintf("decode error: %v", err))}
_, _ = rollups.SendReport(&report)
return err
}
evaluable, err := govaluate.NewEvaluableExpression(expression)
if err != nil {
report := rollups.ReportRequest{Payload: rollups.Str2Hex(fmt.Sprintf("parse error: %v", err))}
_, _ = rollups.SendReport(&report)
return err
}
result, err := evaluable.Evaluate(nil)
if err != nil {
report := rollups.ReportRequest{Payload: rollups.Str2Hex(fmt.Sprintf("evaluation error: %v", err))}
_, _ = rollups.SendReport(&report)
return err
}
notice := rollups.NoticeRequest{Payload: rollups.Str2Hex(fmt.Sprintf("%v", result))}
_, err = rollups.SendNotice(¬ice)
return err
}
func HandleInspect(data *rollups.InspectResponse) error {
infolog.Printf("Received inspect request data %+v\n", data)
report := rollups.ReportRequest{Payload: data.Payload}
_, err := rollups.SendReport(&report)
return err
}
func main() {
finish := rollups.FinishRequest{Status: "accept"}
for {
infolog.Println("Sending finish")
res, err := rollups.SendFinish(&finish)
if err != nil {
errlog.Panicln("Error calling /finish:", err)
}
if res.StatusCode == 202 {
infolog.Println("No pending rollup request, trying again")
continue
}
response, err := rollups.ParseFinishResponse(res)
if err != nil {
errlog.Panicln("Error parsing finish response:", err)
}
finish.Status = "accept"
if response.Type == "advance_state" {
data := new(rollups.AdvanceResponse)
_ = json.Unmarshal(response.Data, data)
if err = HandleAdvance(data); err != nil {
finish.Status = "reject"
}
} else if response.Type == "inspect_state" {
data := new(rollups.InspectResponse)
_ = json.Unmarshal(response.Data, data)
if err = HandleInspect(data); err != nil {
finish.Status = "reject"
}
}
}
}
#include <iomanip>
#include <iostream>
#include <map>
#include <sstream>
#include <stdexcept>
#include <string>
#include "3rdparty/cpp-httplib/httplib.h"
#include "3rdparty/picojson/picojson.h"
static std::string hex_to_string(const std::string &hex)
{
std::string raw = hex.rfind("0x", 0) == 0 ? hex.substr(2) : hex;
std::string out;
out.reserve(raw.size() / 2);
for (size_t i = 0; i + 1 < raw.size(); i += 2)
{
out.push_back(static_cast<char>(std::stoi(raw.substr(i, 2), nullptr, 16)));
}
return out;
}
static std::string string_to_hex(const std::string &text)
{
std::ostringstream oss;
oss << "0x";
for (unsigned char c : text)
{
oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(c);
}
return oss.str();
}
static double evaluate_expression(const std::string &expr)
{
// Minimal parser for "number op number" expressions (e.g. 1+2, 7*3).
std::stringstream ss(expr);
double lhs = 0.0;
double rhs = 0.0;
char op = 0;
ss >> lhs >> op >> rhs;
if (ss.fail())
{
throw std::runtime_error("invalid expression format");
}
switch (op)
{
case '+': return lhs + rhs;
case '-': return lhs - rhs;
case '*': return lhs * rhs;
case '/':
if (rhs == 0.0)
{
throw std::runtime_error("division by zero");
}
return lhs / rhs;
default:
throw std::runtime_error("unsupported operator");
}
}
static void emit_notice(httplib::Client &cli, const std::string &text)
{
picojson::object notice;
notice["payload"] = picojson::value(string_to_hex(text));
cli.Post("/notice", picojson::value(notice).serialize(), "application/json");
}
static void emit_report(httplib::Client &cli, const std::string &payload_hex)
{
picojson::object report;
report["payload"] = picojson::value(payload_hex);
cli.Post("/report", picojson::value(report).serialize(), "application/json");
}
std::string handle_advance(httplib::Client &cli, picojson::value data)
{
std::cout << "Received advance request data " << data << std::endl;
try
{
const std::string payload_hex = data.get("payload").get<std::string>();
const std::string expression = hex_to_string(payload_hex);
const double output = evaluate_expression(expression);
emit_notice(cli, std::to_string(output));
return "accept";
}
catch (const std::exception &e)
{
emit_report(cli, string_to_hex(std::string("Error processing input: ") + e.what()));
return "reject";
}
}
std::string handle_inspect(httplib::Client &cli, picojson::value data)
{
std::cout << "Received inspect request data " << data << std::endl;
emit_report(cli, data.get("payload").get<std::string>());
return "accept";
}
int main(int argc, char **argv)
{
std::map<std::string, decltype(&handle_advance)> handlers = {
{"advance_state", &handle_advance},
{"inspect_state", &handle_inspect},
};
httplib::Client cli(getenv("ROLLUP_HTTP_SERVER_URL"));
std::string status = "accept";
while (true)
{
auto finish_body = std::string("{\"status\":\"") + status + "\"}";
auto response = cli.Post("/finish", finish_body, "application/json");
if (!response)
{
status = "reject";
continue;
}
if (response->status == 202)
{
std::cout << "No pending rollup request, trying again" << std::endl;
status = "accept";
continue;
}
picojson::value req;
picojson::parse(req, response->body);
auto request_type = req.get("request_type").get<std::string>();
auto data = req.get("data");
status = handlers[request_type](cli, data);
}
}
With Docker running, “build your backend” application by running:
cartesi build
“Building” in this context installs the libraries in the requirements.txt, compiles your application into RISC-V architecture, and consequently builds a Cartesi machine that contains your backend application.
The anvil node can now run your application.
To run your application, enter the command:
cartesi run
Sending inputs with the CLI
We can send inputs to your application with a custom JavaScript frontend, Cast, or Cartesi CLI.
To send a string encoded input to your application, run the below command:
cartesi send "1+2"
Example: Send 1+2 as an input to the application.
(base) user@user-MacBook-Pro calculator % cartesi send "1 + 2"
(node:64729) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
✔ Input sent: 0x1866ab903f8fa5bd712fc2d322b8c0ba119bb31d30885a06e0fe6005ff079ff2
Retrieving outputs from the application
The cartesi send generic sends a request to the calculator application to process the computation (1 + 2), this is computed and a payload (notice) containing the result is sent to the Rollup Server's /notice endpoint.
Notice payloads will be returned in hexadecimal format; developers will need to decode these to convert them into plain text.
We can query these notices using the JSON-RPC server running on http://127.0.0.1:6751/rpc or with a custom frontend client.
You can retrieve all notices sent to the rollup server by executing this request on a terminal:
curl -X POST http://127.0.0.1:6751/rpc \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "cartesi_listOutputs",
"params": {
"application": "calculator",
"limit": 10,
"offset": 0
},
"id": 1
}'
Alternatively you can make a post request to the JsonRPC server like below:
const response = await fetch("http://127.0.0.1:6751/rpc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "cartesi_listOutputs",
params: {
application: "calculator",
limit: 10,
offset: 0
},
id: 1
})
});
const result = await response.json();
const outputs = result?.result?.data ?? [];
for (const output of outputs) {
const decoded = output.decoded_data;
console.log("Output payload:", decoded);
}
You can also query a notice based on its input index.
Congratulations, you have successfully built a dApp on Cartesi Rollups!
You can access the complete project implementation here!