Create your first dApp
Once you learned how to run a simple example, it is now time to create one of your own. In order to do this, we will make use of the existing dApps available in Cartesi's rollups-examples Github repository. Once again, make sure you have installed all the necessary requirements before proceeding.
Example dApp
The 'fortune' package in Linux contains a command-line tool that randomly displays quotes from a set of predefined lists, which serve as its quote database. This tutorial demonstrates how to create a dApp that installs this package, calls it from a Python script, and displays the result.
Set up the environment
First of all, clone the rollups-examples repository as follows:
git clone https://github.com/cartesi/rollups-examples.git
Copy an existing dApp
We suggest using a sample dApp that utilizes a new build system, docker-riscv
, instead of the outdated toolchain
system:
In this example, we are using the existing Calculator dApp as a basis to build a new dApp called 'fortune'.
cd rollups-examples
cp -r calculator/ fortune
cd fortune
Adjust build files
- Change
calculator.py
tofortune.py
- Change the dApp name in
entrypoint.sh
torollup-init python3 fortune.py
- Change the dApp name in the Dockerfile to
COPY ./fortune.py
- Change the dApp name in
docker-bake.override.hcl
todapp:fortune
- Change the dApp name in
docker-compose.override.yml
todapp:fortune-devel-server
- Amend the Readme as required.
Test the copied dApp
First of all, check if your Docker supports the RISCV platform by running:
docker buildx ls
If you do not see linux/riscv64
in the platforms list, install QEMU by running:
apt install qemu-user-static
QEMU is a generic and open source machine emulator and virtualizer that will be used by Docker to emulate RISCV instructions to build a Cartesi Machine for your dApp.
After installing QEMU, the platform linux/riscv64
should appear in the platforms list.
Build the copied existing dApp to ensure that the Docker image functions correctly:
docker buildx bake --load
If you have PostgreSQL and Redis already installed on your system, you may encounter port conflicts when the Docker containers attempt to start services on ports that are already in use. To resolve these conflicts, edit the ports for Redis and PostgreSQL in the docker-compose.yml file located in the root directory of your dApp.
Add the fortune package to the Dockerfile
Add the fortune
package to the Docker image as follows:
RUN apt-get update \
&& apt-get install -y fortune \
&& rm -rf /var/apt/lists/*
This command updates the package lists for a Debian-based Linux system, installs the fortune
package without confirmation, and then cleans up the package list files to save space.
So our full Dockerfile will look as follows:
# syntax=docker.io/docker/dockerfile:1.4
FROM --platform=linux/riscv64 cartesi/python:3.10-slim-jammy
WORKDIR /opt/cartesi/dapp
RUN apt-get update \
&& apt-get install -y fortune \
&& rm -rf /var/apt/lists/*
COPY ./requirements.txt .
RUN pip install -r requirements.txt --no-cache \
&& find /usr/local/lib -type d -name __pycache__ -exec rm -r {} +
COPY ./entrypoint.sh .
COPY ./fortune.py .
Modify the dApp logic
- First we need to import the
subprocess package
:
import subprocess
We then delete
from py_expression_eval import Parser
as we are not using theparser
module in our application.Next, we need to add the
FORTUNE_CMD
command to our code as follows:
FORTUNE_CMD = "/usr/games/fortune; exit 0"
This command sets the FORTUNE_CMD variable to a string that, when executed, runs the fortune program to display a random quote and then immediately exits the shell with a success status.
- We then replace the existing
try
function ofdef handle_advance(data)
with the following command that calls thefortune
app:
try:
input = hex2str(data["payload"])
logger.info(f"Received input: {input}")
quote = subprocess.check_output(FORTUNE_CMD, shell=True, stderr=subprocess.STDOUT).decode()
output = f"Received input: {input}. This was the quote {quote}"
# Emits notice with result of calculation
logger.info(f"Adding notice with payload: '{output}'")
response = requests.post(rollup_server + "/notice", json={"payload": str2hex(str(output))})
logger.info(f"Received notice status {response.status_code} body {response.content}")
The quote
runs the command stored in FORTUNE_CMD using a shell, captures the output (including errors), and then decodes it from bytes to a string, storing the result in the variable output. The output
creates a formatted string with placeholders, replacing {input} and {quote} with the values of the variables input and quote, respectively, and assigns the resulting string to the variable output.
- We then change the code of the
def handle_inspect(data):
function to:
logger.info(f"Received inspect request data {data}")
status = "accept"
try:
input = hex2str(data["payload"])
logger.info(f"Received input: {input}")
output = subprocess.check_output(FORTUNE_CMD, shell=True, stderr=subprocess.STDOUT).decode()
# Emits notice with result of calculation
logger.info(f"Adding report with payload: '{output}'")
response = requests.post(rollup_server + "/report", json={"payload": str2hex(str(output))})
logger.info(f"Received report status {response.status_code} body {response.content}")
except Exception as e:
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
Create the Docker build for the new dApp
docker buildx bake --load
The docker buildx
is an extended toolset for Docker, which provides full support for the features of the Moby BuildKit builder toolkit. The bake
command is part of this extension and simplifies the process of defining and running builds by using a declarative format in the form of a docker-bake.hcl
or a docker-compose.yml
. When the --load
flag is added, it instructs BuildKit to build the Docker image and then immediately load it into the local Docker runtime environment.
Start the application
docker compose -f ../docker-compose.yml -f ./docker-compose.override.yml -f ../docker-compose-host.yml up
Check the dApp address
We now will check our dApp address by viewing the contents of the dapp.json
file located in the deployments/localhost/
directory inside the rollups-examples
repository:
cat ../deployments/localhost/dapp.json
It will return a response similar to the one below:
"address": "0x142105FC8dA71191b3a13C738Ba0cF4BC33325e2",
"blockHash":"0xa228c81061345a0db2910c3f253213bfab131cd575c4589b370555bb748b6238"
"blockNumber": 22,
"transactionHash":"0x77b6be9c95b16f7ab880fc7662028ealdea3279fe28f0382706dd693f0d5315d"
Build the frontend console
In the cloned rollups-examples
repository, navigate to the frontend-console
:
cd frontend-console
And build it:
yarn
yarn build
Run an inspect call
From the front-end console directory:
yarn start inspect --payload 'test'
This will get us a qoute, similar to the one below:
yarn run v1.22.19
$ ts-node src/index. ts inspect - -payload test
HTTP status: 200
Inspect status: "Accepted"
Input count: 0
Reports:
0:
My dear People.
My dear Bagginses and Boffins, and my dear Tooks and Brandybucks,
and Grubbs, and Chubbs, and Burrowses, and Hornblowers, and Bolgers,
Bracegirdles, Goodbodies, Brockhouses and Proudfoots. Also my good
Sackville Bagginses that I welcome back at last to Bag End.
one hundred and eleventh birthday: I am eleventy-one today!"
Today is my
J. R. R. Tolkien
If we execute the inspect
command again, it will consistently produce the same result, thereby providing the same quote each time.
Run an advance call
From the front-end console directory:
yarn start input send --payload 'test'
It will give us a response similar to the one below:
yarn run v1.22.19
$ ts-node src/index. ts input send
connecting to http://localhost:8545
payload test
connected to chain 31337
using account "Oxf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
sending "test"
transaction: 0x4ea14167cd19c08379ca22afa568fb952073bb7762e5cd66a36cdccdb875d65f
waiting for confirmation...
input 0 added
Upon executing the inspect
command now, it will generate a new quote.
Request a notice
From the front-end console directory:
yarn start notice list
This will return the previous quote:
yarn run v1.22.19
ts-node src/index. ts notice list
querying http://localhost:4000/graphql for notices of input index "undefined"...
[{"index" :0, Tinput": 0, "payload" : "Received input: test. This was the quote \tMy dear People. \n\tMy dear Bagginses and Boffins, and my dear Tooks and Brandybucks, \nand Grubbs, and Chubbs, and Burrowses,
nd Hornblowers, and Bolgers, \nBracegirdles, Goodbodies, Brockhouses and Proudfoots. Also my good \nSackville Bagginses that welcome back at last to Bag End. Today is my\none hundred and eleventh birt
hday: am eleventy-one today! \"\n\t\t - - J. R. R. Tolkien\n"}]
An intriguing aspect, demonstrating how Cartesi Machine operates, is that the Cartesi Machine is designed to roll back to a previous state every time its state is inspected. Therefore, if we execute the same inspect
command repeatedly, it consistently returns the same result, as the machine keeps reverting to its prior state.
Stop the application
You can shutdown the environment with ctrl+c
and then running:
docker compose -f ../docker-compose.yml -f ./docker-compose.override.yml down -v
Every time you stop the docker compose ... up
command with ctrl+c
, you need to run the docker compose ... down -v
command to remove the volumes and containers. Ignoring this will preserve outdated information in those volumes, causing unexpected behaviors, such as failure to reset the hardhat localchain.
Application code
Now let's have a look at the entire code of our sample application:
# Copyright 2022 Cartesi Pte. Ltd.
#
# SPDX-License-Identifier: Apache-2.0
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use
# this file except in compliance with the License. You may obtain a copy of the
# License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
from os import environ
import traceback
import logging
import requests
import subprocess
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}")
FORTUNE_CMD = "/usr/games/fortune; exit 0"
def hex2str(hex):
"""
Decodes a hex string into a regular string
"""
return bytes.fromhex(hex[2:]).decode("utf-8")
def str2hex(str):
"""
Encodes a string as a hex string
"""
return "0x" + str.encode("utf-8").hex()
def handle_advance(data):
logger.info(f"Received advance request data {data}")
status = "accept"
try:
input = hex2str(data["payload"])
logger.info(f"Received input: {input}")
quote = subprocess.check_output(FORTUNE_CMD, shell=True, stderr=subprocess.STDOUT).decode()
output = f"Received input: {input}. This was the quote {quote}"
# Emits notice with result of calculation
logger.info(f"Adding notice with payload: '{output}'")
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 as e:
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}")
status = "accept"
try:
input = hex2str(data["payload"])
logger.info(f"Received input: {input}")
output = subprocess.check_output(FORTUNE_CMD, shell=True, stderr=subprocess.STDOUT).decode()
# Emits notice with result of calculation
logger.info(f"Adding report with payload: '{output}'")
response = requests.post(rollup_server + "/report", json={"payload": str2hex(str(output))})
logger.info(f"Received report status {response.status_code} body {response.content}")
except Exception as e:
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
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"])
As we conclude this tutorial, we hope that you now have a better understanding of how to build a dApp that uses the fortune
package and how the Cartesi Machine drives the dApp's logic. Don't hesitate to experiment further with these tools and techniques, as they can greatly expand your capabilities in creating your custom dApps. Happy coding!