AI agent documentation index: llms.txt. Raw markdown for any page is available by appending .md to the URL. Full content snapshot: llms-full.txt.
llms.txt — complete Cartesi documentation index. Append .md to any page URL for raw Markdown (e.g. /cartesi-rollups/2.0/development/building-an-application.md).
Skip to main content

Managing balances

The CMA ledger records which account owns which asset, and how much of it. Think of it as a plug in wallet for your application: instead of writing your own balance bookkeeping, you call deposit, withdraw and transfer, and the ledger keeps the state consistent.

Core ideas

The ledger works with three things:

  • Accounts. An account usually maps to a wallet address, but it can also map to a generic 32 byte ID. Internally each account gets a small numeric account_id.
  • Assets. An asset is anything you track: Ether, an ERC20 token (identified by its address), an ERC721 or ERC1155 token (identified by address plus token ID). Internally each asset gets a numeric asset_id.
  • Balances. The amount of an asset that an account holds. The ledger also tracks the total supply of each asset inside your application.

Most ledger calls follow a retrieve pattern: you describe the account or asset, and the ledger finds it or creates it and hands back its ID. After that, you work with IDs only.

The ledger comes in two flavors. The multi asset ledger described above tracks many assets at once. The single asset ledger tracks one fixed asset and stores balances in a form the default emergency withdrawal tooling can prove. See Single-asset ledger below.

Creating a ledger

Create an in memory ledger with no arguments, or a file backed ledger that survives restarts by passing a memory file and size limits:

from pycma import Ledger

# In memory
ledger = Ledger()

# File backed, sized for your application. The file is created and sized
# when missing, and validated when it already exists.
ledger = Ledger(
memory_filename="/dev/pmem2",
offset=0,
mem_length=64 * 1024 * 1024,
n_accounts=16 * 1024,
n_assets=8,
n_balances=8 * 16 * 1024,
)

Single-asset ledger

The single asset ledger is a variant that tracks one fixed asset for the life of the store. You choose the asset when you create it, either Ether or one ERC20, and it cannot change after that. Use it when your application handles a single token and you want balances that are easy to recover on the base layer.

It differs from the multi asset ledger in a few ways:

  • One immutable asset. There is always exactly one asset, with id 0. Reopening the store with a different asset is rejected.
  • 64 bit balances. Amounts are 64 bit values rather than 256 bit.
  • A 32 byte account record. Each withdrawable account is stored as a 32 byte record: an 8 byte balance in little endian order, then the 20 byte owner address, then 4 bytes of padding. This is the standard accounts drive layout, so the default emergency withdrawal tooling can prove a balance and release it without a custom output builder.
  • Withdrawable and virtual accounts. Accounts keyed by a wallet address are withdrawable and live in the drive records. Accounts keyed by an internal id are virtual and live off the drive, so they are not part of the provable set.

Create it from a file. The deposit, withdraw, transfer and query calls are the same as the multi asset ledger, and the asset id is always 0.

from pycma import Ledger

# Single ERC20 drive. Omit account_drive_token for an Ether drive.
ledger = Ledger(
memory_filename="/dev/pmem1",
offset=0,
mem_length=4 * 1024 * 1024,
n_accounts=4096,
n_assets=1, # unused by the single asset ledger, but must be non zero
n_balances=1, # unused by the single asset ledger, but must be non zero
single_asset_account_drive=True,
account_drive_token="0x88A2120B7068E78692C8fd12E751d610B6377E4d",
)

Accounts and assets

retrieve_account and retrieve_asset find an entry or create it when it does not exist yet. Both return a dictionary that includes the internal ID:

# Account from a wallet address
account = ledger.retrieve_account(account="0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")
account_id = account["account_id"]

# The Ether asset (the base token has no contract address)
ether = ledger.retrieve_asset(base_token=True)

# An ERC20 asset, identified by its contract address
erc20 = ledger.retrieve_asset(token="0x491604c0fdf08347dd1fa4ee062a822a5dd06b5d")

# An ERC721 asset, identified by address plus token ID
nft = ledger.retrieve_asset(token="0x...", token_id=1)

# An ERC1155 asset, where each token ID has its own amount
sft = ledger.retrieve_asset(token="0x...", token_id=1, token_id_with_amount=True)

# Find only, do not create
existing = ledger.retrieve_asset(base_token=True, force_find=True)

Moving assets

Three operations change balances. Each one fails with a clear error instead of leaving partial state, for example when an account tries to spend more than it holds.

# Credit an account, for example after a decoded deposit
ledger.deposit(asset_id, account_id, amount)

# Move assets between two accounts inside the application
ledger.transfer(asset_id, from_account_id, to_account_id, amount)

# Debit an account, for example before emitting a withdrawal voucher
ledger.withdraw(asset_id, account_id, amount)

Reading balances

Queries never change state, so you can call them from inspect handlers.

balance = ledger.balance(asset_id, account_id)   # int
supply = ledger.supply(asset_id) # int, total supply of the asset

A complete deposit flow

This is how the parser and the ledger work together when an ERC20 deposit arrives. The snippet comes from the Python wallet sample:

if msg_sender == ERC20_PORTAL_ADDRESS:
deposit = decode_erc20_deposit(advance)

asset = ledger.retrieve_asset(token=deposit["token"])
account = ledger.retrieve_account(account=deposit["sender"])

ledger.deposit(asset["asset_id"], account["account_id"], deposit["amount"])
return True

Error handling

Ledger operations report failures such as insufficient funds, unknown accounts or full storage. The full code list is in Types and selectors. In Python a failed call raises an exception, in Rust it returns a LedgerError, and in C and C++ it returns a negative code with details available from cma_ledger_get_last_error_message().

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.