Skip to content
Merged
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
61 changes: 61 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: test

on:
push:
branches: ["**"]
pull_request:

jobs:
native-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: dtolnay/rust-toolchain@stable

- name: Install libclang for bindgen
run: sudo apt-get update && sudo apt-get install -y clang libclang-dev

- name: Run mocked native tests
run: cargo test --features native

riscv-link-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: dtolnay/rust-toolchain@stable

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Install build dependencies
run: sudo apt-get update && sudo apt-get install -y clang libclang-dev build-essential cmake

- name: Build libcma for riscv64
working-directory: third_party/machine-asset-tools
run: |
make third-party-boost
make docker ARGS="build/riscv64/libcma.a"

- name: Verify riscv64 libcma artifact
run: |
set -euo pipefail
lib=third_party/machine-asset-tools/build/riscv64/libcma.a
if [[ ! -f "$lib" ]]; then
echo "Missing $lib"
ls -laR third_party/machine-asset-tools/build/riscv64 || true
exit 1
fi
member=$(ar t "$lib" | head -1)
obj=$(mktemp)
ar p "$lib" "$member" > "$obj"
file "$obj" | grep -F 'RISC-V'
rm -f "$obj"
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ path = "src/lib.rs"
hex = "0.4"
lazy_static = { version = "1.4", optional = true }
json = "0.12"
once_cell = "1.18"
ethers-core = "1.0.0"

[build-dependencies]
Expand Down
97 changes: 34 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,113 +6,84 @@
<div align="center">
<i>Cartesi Rollups LIBCMA Binding for Rust</i>
</div>
<div align="center">
<!-- <b>Any Code. Ethereum's Security.</b> -->
</div>
<br>
<p align="center">
<img src="https://img.shields.io/github/license/Mugen-Builders/libcma_binding_rust?style=default&logo=opensourceinitiative&logoColor=white&color=008DA5" alt="license">
<img src="https://img.shields.io/github/last-commit/Mugen-Builders/libcma_binding_rustt?style=default&logo=git&logoColor=white&color=000000" alt="last-commit">
</p>

# libcma-binding-rust

LIBCMA is a Lightweight Rust utilities to parse Cartesi Machine Application (CMA) inputs, build on-chain voucher payloads to be executed on the base layer and handle core asset operations such as deposits, withdrawals, transfers, and balance checks. It acts as a plugin wallet for applicaitons and is particularly useful when you want scalable, predictable, and secure asset management in Cartesi applications that support assets like ETH, ERC20, ERC721, and ERC1155.
LIBCMA is a lightweight Rust binding for Cartesi Machine Application (CMA) tooling: parse rollup inputs, build on-chain voucher payloads, and manage application assets (deposits, withdrawals, transfers, balances) for ETH, ERC-20, ERC-721, and ERC-1155.

## Clone with submodules

Headers come from two git submodules under `third_party/`:
Headers come from git submodules under `third_party/`:

- **`third_party/machine-asset-tools`** — `libcma` (parser, types, ledger).
- **`third_party/machine-guest-tools`** — `libcmt` headers used by `libcma` (e.g. `libcmt/abi.h`); required for `bindgen` when generating bindings.

Clone with submodules in one step:
- **`third_party/machine-asset-tools`** — `libcma` (parser, types, ledger)
- **`third_party/machine-guest-tools`** — `libcmt` headers used by `libcma`

```bash
git clone --recurse-submodules https://github.com/Mugen-Builders/libcma_binding_rust
```

If you already cloned without submodules:

```bash
# or, if already cloned:
git submodule update --init --recursive
```

## Build requirements

- **Clang / libclang** — `bindgen` needs a working libclang installation (same as any typical `bindgen` setup).
- **Submodules** — `build.rs` expects both `third_party` trees to exist (after the step above).
- **Linking the real C library** — With default features disabled, the build links the static library from `third_party/machine-asset-tools/build/riscv64` (see **Feature flags**). For local development on the host, the default `native` feature uses Rust mocks instead of linking that binary.
- **Clang / libclang** for `bindgen`
- **Submodules** present before building
- **Real C library** — disable default features to link the static `libcma` archive from `third_party/machine-asset-tools/build/riscv64`

## Feature flags

| Feature | Default | Purpose |
| --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `native` | yes | Compiles `src/mocks.rs` shims for ledger/parser C symbols so you can build and run tests on the host (e.g. macOS) without the RISC-V `libcma` archive. |
| `riscv64` | no | Reserved for cross-builds that link the vendored static `cma` library (see `build.rs`). |

Build without default features when you have the static library path and target set up:
| Feature | Default | Purpose |
| --------- | ------- | ------- |
| `native` | yes | Compiles `src/mocks.rs` shims so host tests run without the RISC-V `libcma` archive |
| `riscv64` | no | Cross-build path that links the vendored static `cma` library |

```bash
cargo build --no-default-features
cargo build --no-default-features --features riscv64
```

## Ledger wrapper

The `Ledger` type wraps the C `cma_ledger_*` API. Besides `Ledger::new()` (in-memory basic ledger), you can reinitialize storage using the same functions as the C library:
`Ledger` wraps `cma_ledger_*` with helpers for file/buffer initialization, asset/account retrieval, deposit/withdraw/transfer, balance, and total supply.

- `Ledger::init_from_file(path, LedgerFileConfig { ... })` → `cma_ledger_init_file`
- `Ledger::init_from_buffer(&mut [u8], LedgerBufferConfig { ... })` → `cma_ledger_init_buffer`

Exported helpers: `LedgerMemoryMode`, `LedgerFileConfig`, `LedgerBufferConfig`.
- `retrieve_ether_assets()` uses `AssetType::Base`
- `AssetType` also supports `TokenAddress`, `TokenAddressId`, and `TokenAddressIdAmount`
- `RetrieveOperation::FindAndRemove` is supported

## Parser and vouchers

This crate is useful for Cartesi dApp developers who need to:
Pure-Rust parser aligned with `machine-asset-tools` / Cartesi Rollups v2.0:

- Decode deposit payloads coming from Cartesi portals (ETH, ERC20, ERC721, ERC1155).
- Decode withdrawal / transfer / ledger inspection requests encoded as JSON and hex.
- Encode voucher payloads for on-chain token transfers (Ether, ERC20, ERC721 — ERC1155 encoding TODO).
- Portal deposit decoding (packed + ABI tails for ERC-721/1155)
- Auto-decode withdrawals/transfers by function selector
- Inspect decoding for `ledger_getBalance` and `ledger_getTotalSupply`
- Voucher encoding for Ether, ERC-20, ERC-721 (`safeTransferFrom`), ERC-1155 single/batch (`safeTransferFrom` / `safeBatchTransferFrom`)

### Core public functions

- `cma_decode_advance(input: JsonValue) -> Result<CmaParserInput, CmaParserError>`
Detects whether an input is a portal deposit (based on the caller address) or a user instruction (withdrawal/transfer). Returns a typed `CmaParserInput` (enum + associated structs) with parsed fields (addresses, amounts, token ids, exec_layer data).
- `cma_decode_inspect(input: JsonValue) -> Result<CmaParserInput, CmaParserError>`
Parses inspection (ledger) requests like `ledgerGetBalance` and `ledgerGetTotalSupply` encoded as JSON-in-hex and returns structured arguments.
- `cma_encode_voucher(req_type: CmaParserVoucherType, voucher_request: CmaParserVoucherData) -> Result<CmaVoucher, CmaParserError>`
Builds a `CmaVoucher` (destination, value, payload) for sending to the chain. Implemented for Ether, ERC20 and ERC721 vouchers.

### Important types
- `cma_decode_advance(req_type, input) -> Result<CmaParserInput, CmaParserError>`
- `cma_decode_inspect(input) -> Result<CmaParserInput, CmaParserError>`
- `cma_encode_voucher(req_type, app_address, voucher_request) -> Result<CmaVoucher, CmaParserError>`

- `CmaParserInputType` — recognized request types (deposits, withdrawals, transfers, ledger queries).
- `CmaParserInputData` — enum wrapping parsed payload structs, e.g. `EtherDeposit`, `Erc20Transfer`, `EtherWithdrawal`, etc.
- `CmaParserError` — parsing error variants and helper conversions.
- `CmaVoucher` — voucher structure (destination, value, payload) ready for submission.
`CmaVoucher` fields: `destination`, `value` (wei for ether vouchers), `payload`.

## Dependencies
Inspect params are flat JSON strings, e.g.:

- ethers-core = "1.0.0"
- hex = "0.4"
- json = "0.12"
- once_cell = "1.18"
```json
{"method":"ledger_getBalance","params":["0x...account...","0x...token...","0x1"]}
```

## Tests

```bash
cargo test
```

Ledger integration tests exercise the `native` mock path by default.

## Notes and limitations

- ERC1155 voucher encoding is not implemented (placeholders return not implemented).
- The parser expects JSON input as received by the application; it inspects the caller and method to choose a decoding path.

## Contributing
- `tests/parser_tests.rs` — integration tests against the pure-Rust parser
- `tests/parser_vectors.rs` — vectors ported from `third_party/machine-asset-tools/tests/parser.c`
- `tests/ledger_tests.rs` — ledger tests via native mocks

- Open PRs for improvements, add missing voucher encodings, add more exhaustive parsing tests for edge cases.
CI runs native tests on every push/PR and attempts an riscv64 link check when `libcma` can be built.

## License

Expand Down
14 changes: 14 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ pub enum LedgerError {
InsufficientFunds,
AccountNotFound,
AssetNotFound,
BalanceNotFound,
SupplyOverflow,
BalanceOverflow,
InvalidAccount,
InsertionError,
MaxAssetsReached,
MaxAccountsReached,
MaxBalancesReached,
AssetSupply,
AccountBalance,
Remove,
Other(i32),
}

Expand All @@ -24,10 +31,17 @@ impl LedgerError {
x if x == bindings::CMA_LEDGER_ERROR_INSUFFICIENT_FUNDS as i32 => LedgerError::InsufficientFunds,
x if x == bindings::CMA_LEDGER_ERROR_ACCOUNT_NOT_FOUND as i32 => LedgerError::AccountNotFound,
x if x == bindings::CMA_LEDGER_ERROR_ASSET_NOT_FOUND as i32 => LedgerError::AssetNotFound,
x if x == bindings::CMA_LEDGER_ERROR_BALANCE_NOT_FOUND as i32 => LedgerError::BalanceNotFound,
x if x == bindings::CMA_LEDGER_ERROR_SUPPLY_OVERFLOW as i32 => LedgerError::SupplyOverflow,
x if x == bindings::CMA_LEDGER_ERROR_BALANCE_OVERFLOW as i32 => LedgerError::BalanceOverflow,
x if x == bindings::CMA_LEDGER_ERROR_INVALID_ACCOUNT as i32 => LedgerError::InvalidAccount,
x if x == bindings::CMA_LEDGER_ERROR_INSERTION_ERROR as i32 => LedgerError::InsertionError,
x if x == bindings::CMA_LEDGER_ERROR_MAX_ASSETS_REACHED as i32 => LedgerError::MaxAssetsReached,
x if x == bindings::CMA_LEDGER_ERROR_MAX_ACCOUNTS_REACHED as i32 => LedgerError::MaxAccountsReached,
x if x == bindings::CMA_LEDGER_ERROR_MAX_BALANCES_REACHED as i32 => LedgerError::MaxBalancesReached,
x if x == bindings::CMA_LEDGER_ERROR_ASSET_SUPPLY as i32 => LedgerError::AssetSupply,
x if x == bindings::CMA_LEDGER_ERROR_ACCOUNT_BALANCE as i32 => LedgerError::AccountBalance,
x if x == bindings::CMA_LEDGER_ERROR_REMOVE as i32 => LedgerError::Remove,
_ => LedgerError::Other(code),
}
}
Expand Down
69 changes: 3 additions & 66 deletions src/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use hex;
use once_cell::sync::Lazy;
use ethers_core::types::{Address};
use json::{JsonValue, object};
use crate::parser::CmaVoucher;
Expand All @@ -11,68 +10,6 @@ pub fn hex_to_string(hex: &str) -> Result<String, Box<dyn std::error::Error>> {
Ok(s)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Portals {
ERC1155BatchPortal,
ERC1155SinglePortal,
ERC20Portal,
ERC721Portal,
EtherPortal,
None,
}

pub struct CartesiAddresses {
pub erc1155_batch_portal: String,
pub erc1155_single_portal: String,
pub erc20_portal: String,
pub erc721_portal: String,
pub ether_portal: String,
}

pub static CARTESI_ADDRESSES: Lazy<CartesiAddresses> = Lazy::new(|| CartesiAddresses {
erc1155_batch_portal: "0xc700A2e5531E720a2434433b6ccf4c0eA2400051".to_string(),
erc1155_single_portal: "0xc700A261279aFC6F755A3a67D86ae43E2eBD0051".to_string(),
erc20_portal: "0xc700D6aDd016eECd59d989C028214Eaa0fCC0051".to_string(),
erc721_portal: "0xc700d52F5290e978e9CAe7D1E092935263b60051".to_string(),
ether_portal: "0xc70076a466789B595b50959cdc261227F0D70051".to_string(),
});

pub trait PortalMatcher {
fn match_portal(&self, addr: &str) -> Portals;
fn get_portal_address(&self, portal: Portals) -> Option<&str>;
}

impl PortalMatcher for CartesiAddresses {
fn match_portal(&self, addr: &str) -> Portals {
let caller_address = addr.trim().to_lowercase();

if caller_address == self.erc1155_batch_portal.to_lowercase() {
Portals::ERC1155BatchPortal
} else if caller_address == self.erc1155_single_portal.to_lowercase() {
Portals::ERC1155SinglePortal
} else if caller_address == self.erc20_portal.to_lowercase() {
Portals::ERC20Portal
} else if caller_address == self.erc721_portal.to_lowercase() {
Portals::ERC721Portal
} else if caller_address == self.ether_portal.to_lowercase() {
Portals::EtherPortal
} else {
Portals::None
}
}

fn get_portal_address(&self, portal: Portals) -> Option<&str> {
match portal {
Portals::ERC1155BatchPortal => Some(&self.erc1155_batch_portal),
Portals::ERC1155SinglePortal => Some(&self.erc1155_single_portal),
Portals::ERC20Portal => Some(&self.erc20_portal),
Portals::ERC721Portal => Some(&self.erc721_portal),
Portals::EtherPortal => Some(&self.ether_portal),
Portals::None => None,
}
}
}

pub trait ToAddress {
fn to_address(&self) -> Result<Address, String>;
}
Expand All @@ -97,10 +34,10 @@ pub trait ToJson {

impl ToJson for CmaVoucher {
fn to_json(&self) -> JsonValue {
let voucher = object! {
object! {
"destination" => format!("{}", self.destination),
"value" => format!("{}", self.value),
"payload" => format!("{}", self.payload),
};
return voucher;
}
}
}
18 changes: 7 additions & 11 deletions src/ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,17 +201,13 @@ impl Ledger {
}

pub fn retrieve_ether_assets(&mut self) -> Result<LedgerAssetId, LedgerError> {
let asset_id = self
.retrieve_asset(
None,
None,
None,
AssetType::TokenAddressId,
RetrieveOperation::FindOrCreate,
)
.map_err(|e| e)?;

return Ok(asset_id);
self.retrieve_asset(
None,
None,
None,
AssetType::Base,
RetrieveOperation::FindOrCreate,
)
}

/// Retrieve or create an account
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ pub mod parser;

pub use error::{LedgerError, ParserError};
pub use ledger::{Ledger, LedgerBufferConfig, LedgerFileConfig, LedgerMemoryMode};
pub use parser::{CmaParserInputType, CmaParserVoucherType, CmaParserError, CmaVoucher, CmaParserUnidentifiedInput, CmaParserEtherDeposit, CmaParserErc20Deposit, CmaParserErc721Deposit, CmaParserErc1155SingleDeposit, CmaParserErc1155BatchDeposit, CmaParserEtherWithdrawal, CmaParserErc20Withdrawal, CmaParserErc721Withdrawal, CmaParserErc1155SingleWithdrawal, CmaParserErc1155BatchWithdrawal, CmaParserEtherTransfer, CmaParserErc20Transfer, CmaParserErc721Transfer, CmaParserErc1155SingleTransfer, CmaParserErc1155BatchTransfer, CmaParserBalance, CmaParserSupply, CmaParserInput, CmaParserInputData};
pub use parser::{CmaParserInputType, CmaParserVoucherType, CmaParserError, CmaVoucher, CmaParserUnidentifiedInput, CmaParserEtherDeposit, CmaParserErc20Deposit, CmaParserErc721Deposit, CmaParserErc1155SingleDeposit, CmaParserErc1155BatchDeposit, CmaParserEtherWithdrawal, CmaParserErc20Withdrawal, CmaParserErc721Withdrawal, CmaParserErc1155SingleWithdrawal, CmaParserErc1155BatchWithdrawal, CmaParserEtherTransfer, CmaParserErc20Transfer, CmaParserErc721Transfer, CmaParserErc1155SingleTransfer, CmaParserErc1155BatchTransfer, CmaParserBalance, CmaParserSupply, CmaParserInput, CmaParserInputData, cma_parser_get_last_error_message};
pub use types::*;
Loading
Loading