Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
---
id: getting-started
title: Getting started
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

This page shows you how to add the CMA library to a Cartesi application and build a small wallet that accepts deposits of a single ERC20 token and answers balance queries.

## Start from a template

The fastest way to start is with the official [application templates](https://github.com/Mugen-Builders/libcma-app-templates). The repository contains ready to build projects for Python, Rust and C++. Each one already wires the library, the build files and the portal addresses together.

```shell
git clone https://github.com/Mugen-Builders/libcma-app-templates
```

Copy the folder for your language into your workspace and use it as the base of your application.

## Add the library to an existing application

You can also add CMA to an application you already have.

<Tabs groupId="language">
<TabItem value="python" label="Python" default>

Install `pycma` from the prebuilt RISC-V wheels:

```shell
pip3 install pycma --find-links https://prototyp3-dev.github.io/pip-wheels-riscv/wheels/
```

Or build it from the repository:

```shell
pip3 install pycma@git+https://github.com/Mugen-Builders/libcma-binding-python
```

The package is compiled for the RISC-V target, so run the install step inside the Dockerfile that builds your application's machine image.

</TabItem>
<TabItem value="rust" label="Rust">

Add the binding as a git dependency in your `Cargo.toml`:

```toml
[dependencies]
libcma_binding_rust = { git = "https://github.com/Mugen-Builders/libcma_binding_rust", branch = "main" }
```

</TabItem>
<TabItem value="cpp" label="C++">

Download a prebuilt release from the [releases page](https://github.com/Mugen-Builders/machine-asset-tools/releases). Releases contain runtime and development artifacts for musl and glibc systems. In a Dockerfile:

```dockerfile
ARG MACHINE_ASSET_TOOLS_VERSION
ADD https://github.com/Mugen-Builders/machine-asset-tools/releases/download/v${MACHINE_ASSET_TOOLS_VERSION}/machine-asset-tools_glibc_riscv64_v${MACHINE_ASSET_TOOLS_VERSION}.tar.gz /tmp/
RUN tar -xzf /tmp/machine-asset-tools_glibc_riscv64_v${MACHINE_ASSET_TOOLS_VERSION}.tar.gz -C / \
&& rm /tmp/machine-asset-tools_glibc_riscv64_v${MACHINE_ASSET_TOOLS_VERSION}.tar.gz
```

This installs the shared library. Use the `_dev` artifact when you also need the headers and the static library.

</TabItem>
</Tabs>

## Your first wallet

The example below accepts deposits of one ERC20 token, records them in a single asset ledger and answers balance queries. The single asset ledger keeps balances on the accounts flash drive, so they can be recovered on the base layer with the default emergency withdrawal tooling. See [Single-asset ledger](./managing-balances.md#single-asset-ledger) for the details.

<Tabs groupId="language">
<TabItem value="python" label="Python" default>

```python
from pycma import RollupCma, Ledger, decode_erc20_deposit, decode_inspect

ERC20_PORTAL_ADDRESS = "0x22E57511C30CcE6CDaa742E13CE3b774fDC663b1"[2:].lower()
TOKEN = "0x88A2120B7068E78692C8fd12E751d610B6377E4d"

rollup = RollupCma()

# A single asset ledger on the accounts flash drive, fixed to one ERC20 token
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=TOKEN,
)

# The single asset always has id 0
asset = ledger.retrieve_asset(token=TOKEN)
ASSET_ID = asset["asset_id"]

def handle_advance(rollup, ledger):
advance = rollup.read_advance_state()
msg_sender = advance["msg_sender"].hex().lower()

if msg_sender == ERC20_PORTAL_ADDRESS:
deposit = decode_erc20_deposit(advance)
account = ledger.retrieve_account(account=deposit["sender"])
ledger.deposit(ASSET_ID, account["account_id"], deposit["amount"])
return True

return False

def handle_inspect(rollup, ledger):
inspect = rollup.read_inspect_state()
query = decode_inspect(inspect)

if query["type"] == "BALANCE":
account = ledger.retrieve_account(account=query["account"])
balance = ledger.balance(ASSET_ID, account["account_id"])
rollup.emit_report(balance.to_bytes(32, "big"))
return True

return False

handlers = {"advance": handle_advance, "inspect": handle_inspect}

accept = True
while True:
next_request_type = rollup.finish(accept)
accept = handlers[next_request_type](rollup, ledger)
```

</TabItem>
<TabItem value="rust" label="Rust">

The Rust template uses [libcmt-binding-rust](https://github.com/Mugen-Builders/libcmt-binding-rust) to read inputs from the machine and the CMA parser to decode them:

```rust
use libcma_binding_rust::parser::{cma_decode_advance, CmaParserInputType, CmaParserInputData};
use libcma_binding_rust::{Ledger, LedgerAsset, LedgerSingleFileConfig, Address};
use json::object;

const ERC20_PORTAL: &str = "0x22E57511C30CcE6CDaa742E13CE3b774fDC663b1";

// A single asset ledger on the accounts flash drive, fixed to one ERC20 token
fn open_ledger(token: Address) -> Result<Ledger, Box<dyn std::error::Error>> {
let mut ledger = Ledger::new()?;
ledger.init_single_from_file(
"/dev/pmem1",
LedgerSingleFileConfig { offset: 0, memory_length: 4 * 1024 * 1024, max_accounts: 4096 },
LedgerAsset::Erc20(token),
)?;
Ok(ledger)
}

fn handle_erc20_deposit(
ledger: &mut Ledger,
msg_sender: &str,
payload_hex: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// The parser reads the payload from data.payload
// and the sender from data.metadata.msg_sender
let request = object! {
data: {
metadata: { msg_sender: msg_sender },
payload: payload_hex
}
};

let decoded = cma_decode_advance(
CmaParserInputType::CmaParserInputTypeErc20Deposit,
request,
)?;

if let CmaParserInputData::Erc20Deposit(deposit) = decoded.input {
// The single asset always has id 0
let asset_id = ledger.retrieve_erc20_asset_via_address(deposit.token)?;
let account_id = ledger.retrieve_account_via_address(deposit.sender)?;
ledger.deposit(asset_id, account_id, deposit.amount)?;
}
Ok(())
}
```

Compare the sender of each advance request against the ERC20 portal address to pick the right input type, as shown in the [application templates](https://github.com/Mugen-Builders/libcma-app-templates).

</TabItem>
<TabItem value="cpp" label="C++">

In C++ you read inputs with libcmt and pass them straight to the parser:

```cpp
extern "C" {
#include <libcmt/rollup.h>
#include <libcma/parser.h>
#include <libcma/ledger.h>
}

// input is a cmt_rollup_advance_t filled by cmt_rollup_read_advance_state(rollup, &input)
cma_parser_input_t parser_input;

const int err = cma_parser_decode_advance(CMA_PARSER_INPUT_TYPE_ERC20_DEPOSIT, &input, &parser_input);
if (err < 0) {
printf("unable to decode deposit: %d - %s\n", -err, cma_parser_get_last_error_message());
}

// parser_input.erc20_deposit.sender -> who deposited
// parser_input.erc20_deposit.token -> the token contract
// parser_input.erc20_deposit.amount -> how much
```

The [C++ template](https://github.com/Mugen-Builders/libcma-app-templates/blob/main/cpp/app.cpp) shows the complete request loop.

</TabItem>
</Tabs>

## Build and run it

Applications using the CMA library build and run like any other Cartesi application. Follow [Building an application](../../development/building-an-application.md) to produce the machine image, then [Running an application](../../development/running-an-application.md) to start a local node. Use [Sending inputs and assets](../../development/send-inputs-and-assets.md) to make a deposit and watch your wallet handle it.

## Next steps

- [Parsing inputs](./parsing-inputs.md) covers every input the parser can decode.
- [Managing balances](./managing-balances.md) covers the full ledger API.
- [Single-asset ledger](./managing-balances.md#single-asset-ledger) tracks one fixed token and makes balances recoverable on the base layer with the default emergency withdrawal tooling.
- [Vouchers and withdrawals](./vouchers.md) shows how users take their assets back to the base layer.
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
id: ledger-reference
title: Ledger reference
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

This page lists every ledger operation with its signature per language. For a guided walk through, see [Managing balances](./managing-balances.md).

## Lifecycle

Create, persist, reset and release a ledger.

The ledger comes in two flavors. The multi asset ledger tracks many assets at once. The single asset ledger tracks one fixed asset, Ether or one ERC20, chosen when the store is created and immutable after that. It keeps 64 bit balances and stores each account as a 32 byte record, which is the layout the default emergency withdrawal tooling reads. See [Managing balances](./managing-balances.md#single-asset-ledger) for when to use each one.

<Tabs groupId="language">
<TabItem value="python" label="Python" default>

| Method | Description |
| :--- | :--- |
| `Ledger()` | Creates an in memory ledger |
| `Ledger(memory_filename, offset, mem_length, n_accounts, n_assets, n_balances)` | Creates or opens a file backed multi asset ledger. The file is created and sized when missing, and validated when it already exists |
| `Ledger(memory_filename, offset, mem_length, n_accounts, n_assets, n_balances, single_asset_account_drive=True, account_drive_token=None)` | Creates or opens a file backed single asset ledger. Leave `account_drive_token` unset for Ether, or pass an ERC20 address for a token drive. `n_assets` and `n_balances` are unused here but must still be non zero |
| `reset()` | Clears all accounts, assets and balances |

</TabItem>
<TabItem value="rust" label="Rust">

| Method | Returns | Description |
| :--- | :--- | :--- |
| `Ledger::new()` | `Result<Ledger, LedgerError>` | Creates an in memory ledger |
| `init_from_file(file_path, config)` | `Result<(), LedgerError>` | Reinitializes the ledger as a file backed multi asset ledger from a `LedgerFileConfig` |
| `init_from_buffer(buffer, config)` | `Result<(), LedgerError>` | Reinitializes the ledger over a caller provided buffer from a `LedgerBufferConfig` |
| `init_single_from_file(file_path, config, asset)` | `Result<(), LedgerError>` | Reinitializes the ledger as a file backed single asset ledger from a `LedgerSingleFileConfig` and a `LedgerAsset` |
| `init_single_from_buffer(buffer, max_accounts, asset)` | `Result<(), LedgerError>` | Reinitializes the ledger as a single asset ledger over a caller provided buffer |
| `reset()` | `Result<(), LedgerError>` | Clears all records |
| `get_last_error_message()` | `Result<String, LedgerError>` | Returns the text of the last error |

</TabItem>
<TabItem value="cpp" label="C++">

| Function | Description |
| :--- | :--- |
| `cma_ledger_init(ledger)` | Creates an in memory ledger |
| `cma_ledger_init_file(ledger, memory_file_name, offset, mem_length, n_accounts, n_assets, n_balances)` | Opens or creates a file backed multi asset ledger. The file is created when missing and validated when it already exists |
| `cma_ledger_init_buffer(ledger, buffer, mem_length, n_accounts, n_assets, n_balances)` | Creates a multi asset ledger backed by a caller provided buffer |
| `cma_ledger_init_single_file(ledger, memory_file_name, offset, mem_length, n_accounts, asset_type, token_address)` | Opens or creates a file backed single asset ledger. Pass `CMA_LEDGER_ASSET_TYPE_BASE` with a null `token_address` for Ether, or `CMA_LEDGER_ASSET_TYPE_TOKEN_ADDRESS` with a `token_address` for one ERC20 |
| `cma_ledger_init_single_buffer(ledger, buffer, mem_length, n_accounts, asset_type, token_address)` | Creates a single asset ledger backed by a caller provided buffer |
| `cma_ledger_reset(ledger)` | Clears all records |
| `cma_ledger_fini(ledger)` | Releases the ledger |
| `cma_ledger_get_last_error_message()` | Returns the text of the last error |

</TabItem>
</Tabs>

## Retrieving accounts and assets

These calls find an entry and return its internal ID. With the find or create operation they also create missing entries, which is the common case when handling deposits.

In a single asset ledger the asset is fixed, so the asset retrieve calls always return id `0`, and balances are 64 bit values.

<Tabs groupId="language">
<TabItem value="python" label="Python" default>

| Method | Description |
| :--- | :--- |
| `retrieve_account(account_id=None, account=None)` | Finds or creates an account. `account` is a hex string: a 20 byte wallet address or a 32 byte account ID. Returns a dict with `account_id` and `account` |
| `retrieve_asset(asset_id=None, token=None, token_id=None, token_id_with_amount=None, base_token=None, force_find=None)` | Finds or creates an asset. Use `base_token=True` for Ether, `token` for ERC20, `token` plus `token_id` for ERC721, and add `token_id_with_amount=True` for ERC1155. `force_find=True` fails instead of creating. Returns a dict with `asset_id`, `token`, `token_id` and `total_supply` |

</TabItem>
<TabItem value="rust" label="Rust">

| Method | Returns | Description |
| :--- | :--- | :--- |
| `retrieve_account_via_address(address)` | `Result<LedgerAccountId, LedgerError>` | Finds or creates an account from a wallet address |
| `retrieve_account(account_id, account_type, operation, addr_or_id)` | `Result<LedgerAccountId, LedgerError>` | Generic account retrieval with explicit `AccountType` and `RetrieveOperation` |
| `retrieve_ether_assets()` | `Result<LedgerAssetId, LedgerError>` | Finds or creates the Ether asset |
| `retrieve_erc20_asset_via_address(token_address)` | `Result<LedgerAssetId, LedgerError>` | Finds or creates an ERC20 asset from its contract address |
| `retrieve_erc721_assets_via_address(token_address, token_id)` | `Result<LedgerAssetId, LedgerError>` | Finds or creates an ERC721 asset from its contract address and token ID |
| `retrieve_asset(asset_id, token_address, token_id, asset_type, operation)` | `Result<LedgerAssetId, LedgerError>` | Generic asset retrieval with explicit `AssetType` and `RetrieveOperation` |

</TabItem>
<TabItem value="cpp" label="C++">

| Function | Description |
| :--- | :--- |
| `cma_ledger_retrieve_account(ledger, account_id, account, addr_accid, n_balances, account_type, operation)` | Finds or creates an account. When `account_id` is set the call fills the account details, otherwise it fills the ID |
| `cma_ledger_retrieve_asset(ledger, asset_id, token_address, token_id, out_total_supply, asset_type, operation)` | Finds or creates an asset. Also returns the asset's total supply through `out_total_supply` |

</TabItem>
</Tabs>

## State changes

Each operation either completes fully or fails with an error, for example when funds are insufficient.

<Tabs groupId="language">
<TabItem value="python" label="Python" default>

| Method | Description |
| :--- | :--- |
| `deposit(asset_id, account_id, amount)` | Adds `amount` of the asset to the account and raises the total supply |
| `withdraw(asset_id, account_id, amount)` | Removes `amount` from the account and lowers the total supply |
| `transfer(asset_id, from_account_id, to_account_id, amount)` | Moves `amount` between two accounts |

</TabItem>
<TabItem value="rust" label="Rust">

| Method | Returns | Description |
| :--- | :--- | :--- |
| `deposit(asset_id, to_account_id, amount)` | `Result<(), LedgerError>` | Adds `amount` (a `U256`) to the account |
| `withdraw(asset_id, from_account_id, amount)` | `Result<(), LedgerError>` | Removes `amount` from the account |
| `transfer(asset_id, from_account_id, to_account_id, amount)` | `Result<(), LedgerError>` | Moves `amount` between two accounts |

</TabItem>
<TabItem value="cpp" label="C++">

| Function | Description |
| :--- | :--- |
| `cma_ledger_deposit(ledger, asset_id, to_account_id, amount)` | Adds the amount to the account |
| `cma_ledger_withdraw(ledger, asset_id, from_account_id, amount)` | Removes the amount from the account |
| `cma_ledger_transfer(ledger, asset_id, from_account_id, to_account_id, amount)` | Moves the amount between two accounts |

</TabItem>
</Tabs>

## Queries

Read only calls. They never change the ledger, so they are safe to use in inspect handlers.

<Tabs groupId="language">
<TabItem value="python" label="Python" default>

| Method | Description |
| :--- | :--- |
| `balance(asset_id, account_id)` | Returns the account's balance of the asset as an int |
| `supply(asset_id)` | Returns the total supply of the asset as an int |

</TabItem>
<TabItem value="rust" label="Rust">

| Method | Returns | Description |
| :--- | :--- | :--- |
| `get_balance(asset_id, account_id)` | `Result<U256, LedgerError>` | Returns the account's balance of the asset |
| `get_total_supply(asset_id)` | `Result<U256, LedgerError>` | Returns the total supply of the asset |

</TabItem>
<TabItem value="cpp" label="C++">

| Function | Description |
| :--- | :--- |
| `cma_ledger_get_balance(ledger, asset_id, account_id, out_balance, account_balance_info)` | Fills `out_balance` with the account's balance of the asset |
| `cma_ledger_retrieve_asset(...)` | Returns the asset's total supply through its `out_total_supply` parameter |

</TabItem>
</Tabs>

## Errors

Every operation reports failures with a typed error: an exception in Python, a `LedgerError` in Rust, and a negative code in C and C++. The full list, including insufficient funds, missing accounts and storage limits, is in [Types and selectors](./types-and-selectors.md#error-codes).
Loading
Loading