Skip to main content

Build a wallet dApp with Python

Let’s build a simple dApp that uses the cartesi-wallet package to handle different deposits, transfers, and withdrawals of various assets.

Cartesi Wallet is a Python-based wallet implementation for asset handling within Cartesi dApps.

Installation

To install the cartesi-wallet library, you can use pip:

pip install cartesi-wallet

You must compile some source code for this lib to run on Docker. Include build-essential in your Dockerfile in the apt-get install. Like this:

apt-get install -y --no-install-recommends build-essential=12.9ubuntu3 busybox-static=1:1.30.1-7ubuntu3 ca-certificates=20230311ubuntu0.22.04.1 curl=7.81.0-1ubuntu1.16

Usage

Add the cartesi-wallet module to your requirements.txt file:

cartesi-wallet == 0.0.4

To use the cartesi-wallet module in your project, you need to import the module and necessary utilities:

import cartesi_wallet.wallet as Wallet
from cartesi_wallet.util import hex_to_str, str_to_hex
import json

Create an instance of the Wallet:

wallet = Wallet
rollup_address = ""

Initialize the wallet with the portal contract and relay addresses:

dapp_relay_address = "0x...aE"
ether_portal_address = "0x...44"
erc20_portal_address = "0x...DB"
erc721_portal_address = "0x...87"

You can obtain the relevant addresses by running cartesi address-book.

Checking Balance

To retrieve balance information from the wallet, use the balance_get method:

balance = wallet.balance_get(account)

The balance object includes methods to access specific balance information, such as:

  • ether_get(self) -> int: Returns the Ether balance.
  • erc20_get(self, erc20: str) -> int: Returns the balance of a specific ERC20 token.
  • erc721_get(self, erc721: str) -> set[int]: Returns a set of indexes of the ERC721 tokens owned by the user.

Asset Handling Methods

For operations like deposits, transfers, and withdrawals, use the methods inside the handle_advance function.

Deposits

To process a deposit, ensure the sender is the designated portal smart contract. Here's an example of an ERC20 deposit:

msg_sender = data["metadata"]["msg_sender"]
payload = data["payload"]

if msg_sender.lower() == erc20_portal_address.lower():
notice = wallet.erc20_deposit_process(payload)
response = requests.post(rollup_server + "/notice", json={"payload": notice.payload})
return "accept"

Transfers and Withdrawals

Below is an example of ERC20 transfer and withdrawal:

msg_sender = data["metadata"]["msg_sender"]
payload = data["payload"]

def decode_json(data):
return json.loads(hex_to_str(data))

try:
req_json = decode_json(payload)

if req_json["method"] == "erc20_transfer":
notice = wallet.erc20_transfer(req_json["from"].lower(), req_json["to"].lower(), req_json["erc20"].lower(), req_json["amount"])
response = requests.post(rollup_server + "/notice", json={"payload": notice.payload})

if req_json["method"] == "erc20_withdraw":
voucher = wallet.erc20_withdraw(req_json["from"].lower(), req_json["erc20"].lower(), req_json["amount"])
response = requests.post(rollup_server + "/voucher", json={"payload": voucher.payload, "destination": voucher.destination})

return "accept"

except Exception as error:
error_msg = f"Failed to process command '{payload}'. {error}"
response = requests.post(rollup_server + "/report", json={"payload": encode(error_msg)})
logger.debug(error_msg, exc_info=True)
return "reject"

The payload format for transfers and withdrawals may vary.

# Example payload for "transfer" method
{
"method": "erc20_transfer",
"from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"to": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"erc20": "0xae7f61eCf06C65405560166b259C54031428A9C4",
"amount": 5000000000000000000
}

# Example payload for "withdraw" method
{
"method": "erc20_withdraw",
"from": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"erc20": "0xae7f61eCf06C65405560166b259C54031428A9C4",
"amount": 3000000000000000000
}

Other Tokens

The method signatures are similar for other token types (e.g., Ether, ERC721), and the logic for deposits, transfers, and withdrawals follows the same pattern.

Here are the functions:

wallet.balance_get(account):

wallet.ether_deposit_process(payload: str):
wallet.ether_withdraw(rollup_address, account, amount):
wallet.erc20_transfer(account, to, erc20, amount):

wallet.erc20_deposit_process(payload:str):
wallet.erc20_withdraw(account, erc20, amount):
wallet.erc20_transfer(account, to, erc20, amount):

wallet.erc721_deposit_process(payload:str):
wallet.erc721_withdraw(rollup_address, sender, erc721, token_id):
wallet.erc721_transfer(account, to, erc721, token_id):

Ensure that the rollup_address for withdrawal operations is set trustless. Here's an example:

if msg_sender.lower() == dapp_relay_address.lower():
global rollup_address
rollup_address = payload
response = requests.post(rollup_server + "/notice", json={"payload": str_to_hex(f"Set rollup_address {rollup_address}")})
return "accept"

With this, your dApp is ready to process deposits, transfers, and withdrawals for various assets.

Code snippets

Here are some useful code snippets that you can utilize in your Cartesi dApp development:

Setup

rollup_server = environ["ROLLUP_HTTP_SERVER_URL"]
logger.info(f"HTTP rollup_server url is {rollup_server}")

# The following addresses are for the local development environment
dapp_relay_address = "0xF5DE34d6BbC0446E2a45719E718efEbaaE179daE"
ether_portal_address = "0xFfdbe43d4c855BF7e0f105c400A50857f53AB044"
erc20_portal_address = "0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB"
erc721_portal_address = "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87"

wallet = Wallet
rollup_address = ""

# define a function to convert a hexadecimal string to a json
def decode_json(data):
return json.loads(hex_to_str(data))

Ether

def handle_advance(data):
logger.info(f"Received advance request data {data}")
msg_sender = data["metadata"]["msg_sender"]
payload = data["payload"]
print(payload)

# Set Relay
if msg_sender.lower() == dapp_relay_address.lower():
global rollup_address
logger.info(f"Received advance from dapp relay")
rollup_address = payload
response = requests.post(rollup_server + "/notice", json={"payload": str_to_hex(f"Set rollup_address {rollup_address}")})
return "accept"

# Deposit
try:
notice = None
if msg_sender.lower() == ether_portal_address.lower():
notice = wallet.ether_deposit_process(payload)
response = requests.post(rollup_server + "/notice", json={"payload": notice.payload})
if notice:
logger.info(f"Received notice status {response.status_code} body {response.content}")
return "accept"
except Exception as error:
error_msg = f"Failed to process deposit '{payload}'. {error}"
logger.debug(error_msg, exc_info=True)
return "reject"

# Transfer and Withdraw
try:
req_json = decode_json(payload)
if req_json["method"] == "ether_transfer":
notice = wallet.ether_transfer(req_json["from"].lower(), req_json["to"].lower(), req_json["amount"])
response = requests.post(rollup_server + "/notice", json={"payload": notice.payload})
if req_json["method"] == "ether_withdraw":
voucher = wallet.ether_withdraw(rollup_address, req_json["from"].lower(), req_json["amount"])
response = requests.post(rollup_server + "/voucher", json={"payload": voucher.payload, "destination": voucher.destination})
return "accept"
except Exception as error:
error_msg = f"Failed to process command '{payload}'. {error}"
response = requests.post(rollup_server + "/report", json={"payload": encode(error_msg)})
logger.debug(error_msg, exc_info=True)
return "reject"

ERC20

def handle_advance(data):
logger.info(f"Received advance request data {data}")
msg_sender = data["metadata"]["msg_sender"]
payload = data["payload"]


# Deposit
try:
notice = None
if msg_sender.lower() == erc20_portal_address.lower():
notice = wallet.erc20_deposit_process(payload)
response = requests.post(rollup_server + "/notice", json={"payload": notice.payload})
if notice:
logger.info(f"Received notice status {response.status_code} body {response.content}")
return "accept"
except Exception as error:
error_msg = f"Failed to process deposit '{payload}'. {error}"
logger.debug(error_msg, exc_info=True)
return "reject"

# Transfer and Withdraw
try:
req_json = decode_json(payload)
if req_json["method"] == "erc20_transfer":
notice = wallet.erc20_transfer(req_json["from"].lower(), req_json["to"].lower(), req_json["erc20"].lower(), req_json["amount"])
response = requests.post(rollup_server + "/notice", json={"payload": notice.payload})

if req_json["method"] == "erc20_withdraw":
voucher = wallet.erc20_withdraw(req_json["from"].lower(), req_json["erc20"].lower(), req_json["amount"])
response = requests.post(rollup_server + "/voucher", json={"payload": voucher.payload, "destination": voucher.destination})
return "accept"
except Exception as error:
error_msg = f"Failed to process command '{payload}'. {error}"
response = requests.post(rollup_server + "/report", json={"payload": encode(error_msg)})
logger.debug(error_msg, exc_info=True)
return "reject"

ERC721

def handle_advance(data):
logger.info(f"Received advance request data {data}")
msg_sender = data["metadata"]["msg_sender"]
payload = data["payload"]

# Set Relay
if msg_sender.lower() == dapp_relay_address.lower():
global rollup_address
logger.info(f"Received advance from dapp relay")
rollup_address = payload
response = requests.post(rollup_server + "/notice", json={"payload": str_to_hex(f"Set rollup_address {rollup_address}")})
return "accept"

# Deposit
try:
notice = None
if msg_sender.lower() == erc721_portal_address.lower():
notice = wallet.erc721_deposit_process(payload)
response = requests.post(rollup_server + "/notice", json={"payload": notice.payload})
if notice:
logger.info(f"Received notice status {response.status_code} body {response.content}")
return "accept"
except Exception as error:
error_msg = f"Failed to process deposit '{payload}'. {error}"
logger.debug(error_msg, exc_info=True)
return "reject"

# Transfer and Withdraw
try:
req_json = decode_json(payload)
if req_json["method"] == "erc721_transfer":
notice = wallet.erc721_transfer(req_json["from"].lower(), req_json["to"].lower(), req_json["erc721"].lower(), req_json["token_id"])
response = requests.post(rollup_server + "/notice", json={"payload": notice.payload})
if req_json["method"] == "erc721_withdraw":
voucher = wallet.erc721_withdraw(rollup_address, req_json["from"].lower(), req_json["erc721"].lower(), req_json["token_id"])
response = requests.post(rollup_server + "/voucher", json={"payload": voucher.payload, "destination": voucher.destination})
return "accept"
except Exception as error:
error_msg = f"Failed to process command '{payload}'. {error}"
response = requests.post(rollup_server + "/report", json={"payload": encode(error_msg)})
logger.debug(error_msg, exc_info=True)
return "reject"

Balance

def handle_inspect(data):
logger.info(f"Received inspect request data {data}")
try:
url = urlparse(hex_to_str(data["payload"]))
if url.path.startswith("balance/"):
info = url.path.replace("balance/", "").split("/")
token_type, account = info[0].lower(), info[1].lower()
token_address, token_id, amount = "", 0, 0

if (token_type == "ether"):
amount = wallet.balance_get(account).ether_get()
elif (token_type == "erc20"):
token_address = info[2]
amount = wallet.balance_get(account).erc20_get(token_address.lower())
elif (token_type == "erc721"):
token_address, token_id = info[2], info[3]
amount = 1 if token_id in wallet.balance_get(account).erc721_get(token_address.lower()) else 0

report = {"payload": encode({"token_id": token_id, "amount": amount, "token_type": token_type})}
response = requests.post(rollup_server + "/report", json=report)
logger.info(f"Received report status {response.status_code} body {response.content}")
return "accept"
except Exception as error:
error_msg = f"Failed to process inspect request. {error}"
logger.debug(error_msg, exc_info=True)
return "reject"

The routes for the Balance inspects implemented above are as follows:

Ether

balance/ether/{wallet}
balance/ether/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

ERC20

balance/ether/{wallet}/{token_address}
balance/erc20/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266/0xae7f61eCf06C65405560166b259C54031428A9C4

ERC721

balance/ether/{wallet}/{token_addres}/{token_id}
balance/erc721/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266/0xae7f61eCf06C65405560166b259C54031428A9C4/0

Frontend Integration

You can use a couple of options for frontend integration in your backend wallet.

  • Frontend Console: The terminal can interact directly with your backend wallet. Here is a sample frontend console application ready to be used!

  • Web User Interface: Alternatively, you can develop a user-friendly web interface for your dApp. This approach offers a more polished user experience and is suitable for production-ready applications. Here are React.js starter and Angular starter templates you can use.

© 2024 Cartesi Foundation Ltd. All rights reserved.

The Cartesi Project is commissioned by the Cartesi Foundation.

We use cookies to ensure that we give you the best experience on our website. By using the website, you agree to the use of cookies.