From eaaa13353e861071499f0140751cfd9a7e1540cd Mon Sep 17 00:00:00 2001 From: Idogwu Chinonso Date: Thu, 18 Jun 2026 16:51:11 +0100 Subject: [PATCH 1/2] Mod: General modifications to align rust implementation with python and c++ --- .github/workflows/test.yml | 61 ++ README.md | 97 +-- src/error.rs | 14 + src/helpers.rs | 6 +- src/ledger.rs | 18 +- src/lib.rs | 2 +- src/mocks.rs | 66 +- src/parser.rs | 1527 ++++++++++++++++++++---------------- src/types.rs | 150 +--- tests/ledger_tests.rs | 9 + tests/parser_tests.rs | 106 ++- tests/parser_vectors.rs | 164 ++++ 12 files changed, 1283 insertions(+), 937 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 tests/parser_vectors.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e1df1e1 --- /dev/null +++ b/.github/workflows/test.yml @@ -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" diff --git a/README.md b/README.md index 347d19b..ee0caab 100644 --- a/README.md +++ b/README.md @@ -6,96 +6,72 @@
Cartesi Rollups LIBCMA Binding for Rust
-
- -

-

- license - last-commit -

# 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` - 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` - 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` - 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` +- `cma_decode_inspect(input) -> Result` +- `cma_encode_voucher(req_type, app_address, voucher_request) -> Result` -- `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 @@ -103,16 +79,11 @@ This crate is useful for Cartesi dApp developers who need to: 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 diff --git a/src/error.rs b/src/error.rs index d80352d..cd4a152 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,10 +8,17 @@ pub enum LedgerError { InsufficientFunds, AccountNotFound, AssetNotFound, + BalanceNotFound, SupplyOverflow, BalanceOverflow, InvalidAccount, InsertionError, + MaxAssetsReached, + MaxAccountsReached, + MaxBalancesReached, + AssetSupply, + AccountBalance, + Remove, Other(i32), } @@ -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), } } diff --git a/src/helpers.rs b/src/helpers.rs index fc7c77d..13ff85e 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -97,10 +97,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; + } } } \ No newline at end of file diff --git a/src/ledger.rs b/src/ledger.rs index e2e0c25..c37cbca 100644 --- a/src/ledger.rs +++ b/src/ledger.rs @@ -201,17 +201,13 @@ impl Ledger { } pub fn retrieve_ether_assets(&mut self) -> Result { - 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 diff --git a/src/lib.rs b/src/lib.rs index 996bc5f..d6b313c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::*; \ No newline at end of file diff --git a/src/mocks.rs b/src/mocks.rs index a9b2dd8..5b20440 100644 --- a/src/mocks.rs +++ b/src/mocks.rs @@ -31,7 +31,9 @@ struct AccountInfo { // Use thread-local storage so each test thread has its own state thread_local! { static MOCK_STATE: RefCell = RefCell::new(MockLedgerState { - next_asset_id: 1, + // Asset id 1 is reserved for the Base (Ether) asset (see BASE_ASSET_ID), + // so generated token asset ids must start at 2 to avoid colliding with it. + next_asset_id: 2, next_account_id: 1, assets: HashMap::new(), accounts: HashMap::new(), @@ -49,7 +51,7 @@ pub unsafe extern "C" fn cma_ledger_init(ledger: *mut bindings::cma_ledger_t) -> // Reset the mock state MOCK_STATE.with(|state| { let mut s = state.borrow_mut(); - s.next_asset_id = 1; + s.next_asset_id = 2; // reserve id 1 for Base (Ether) asset s.next_account_id = 1; s.assets.clear(); s.accounts.clear(); @@ -103,7 +105,7 @@ pub unsafe extern "C" fn cma_ledger_fini(_ledger: *mut bindings::cma_ledger_t) - pub unsafe extern "C" fn cma_ledger_reset(_ledger: *mut bindings::cma_ledger_t) -> i32 { MOCK_STATE.with(|state| { let mut s = state.borrow_mut(); - s.next_asset_id = 1; + s.next_asset_id = 2; // reserve id 1 for Base (Ether) asset s.next_account_id = 1; s.assets.clear(); s.accounts.clear(); @@ -152,32 +154,51 @@ pub unsafe extern "C" fn cma_ledger_retrieve_asset( t if t == bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_ID => { let id_val = *asset_id; if id_val != 0 && s.assets.contains_key(&id_val) { + if operation == bindings::cma_ledger_retrieve_operation_t_CMA_LEDGER_OP_FIND_AND_REMOVE + { + s.balances.retain(|(a_id, _), _| *a_id != id_val); + s.assets.remove(&id_val); + s.asset_lookup.retain(|_, v| *v != id_val); + return bindings::CMA_LEDGER_SUCCESS as i32; + } if !out_total_supply.is_null() { (*out_total_supply).data = mock_total_supply_for_asset(&s, id_val); } return bindings::CMA_LEDGER_SUCCESS as i32; } - if !token_address.is_null() && !token_id.is_null() { - let addr = (*token_address).data; - let id = (*token_id).data; - let lookup_key = (addr, id); - - if let Some(&found_id) = s.asset_lookup.get(&lookup_key) { - *asset_id = found_id; - if !out_total_supply.is_null() { - (*out_total_supply).data = mock_total_supply_for_asset(&s, found_id); - } - return bindings::CMA_LEDGER_SUCCESS as i32; + } + t if t == bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_BASE => { + const BASE_ASSET_ID: u64 = 1; + if s.assets.contains_key(&BASE_ASSET_ID) { + *asset_id = BASE_ASSET_ID; + if !out_total_supply.is_null() { + (*out_total_supply).data = + mock_total_supply_for_asset(&s, BASE_ASSET_ID); } + return bindings::CMA_LEDGER_SUCCESS as i32; } + if operation == bindings::cma_ledger_retrieve_operation_t_CMA_LEDGER_OP_FIND { + return bindings::CMA_LEDGER_ERROR_ASSET_NOT_FOUND as i32; + } + s.assets.insert( + BASE_ASSET_ID, + AssetInfo { + token_address: None, + token_id: None, + }, + ); + *asset_id = BASE_ASSET_ID; + if !out_total_supply.is_null() { + (*out_total_supply).data = mock_total_supply_for_asset(&s, BASE_ASSET_ID); + } + return bindings::CMA_LEDGER_SUCCESS as i32; } t if t == bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_TOKEN_ADDRESS => { if !token_address.is_null() { let addr = (*token_address).data; - // Simple lookup by address only for (&id, info) in &s.assets { if let Some(ref stored_addr) = info.token_address { - if stored_addr == &addr { + if stored_addr == &addr && info.token_id.is_none() { *asset_id = id; if !out_total_supply.is_null() { let sum = mock_total_supply_for_asset(&s, id); @@ -189,12 +210,15 @@ pub unsafe extern "C" fn cma_ledger_retrieve_asset( } } } - t if t == bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_TOKEN_ADDRESS_ID => { + t if t == bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_TOKEN_ADDRESS_ID + || t + == bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_TOKEN_ADDRESS_ID_AMOUNT => + { if !token_address.is_null() && !token_id.is_null() { let addr = (*token_address).data; let id = (*token_id).data; let lookup_key = (addr, id); - + if let Some(&found_id) = s.asset_lookup.get(&lookup_key) { *asset_id = found_id; if !out_total_supply.is_null() { @@ -209,8 +233,10 @@ pub unsafe extern "C" fn cma_ledger_retrieve_asset( } match operation { - op if op == bindings::cma_ledger_retrieve_operation_t_CMA_LEDGER_OP_FIND => { - return bindings::CMA_LEDGER_ERROR_ASSET_NOT_FOUND as i32; + op if op == bindings::cma_ledger_retrieve_operation_t_CMA_LEDGER_OP_FIND + || op == bindings::cma_ledger_retrieve_operation_t_CMA_LEDGER_OP_FIND_AND_REMOVE => + { + bindings::CMA_LEDGER_ERROR_ASSET_NOT_FOUND as i32 } op if op == bindings::cma_ledger_retrieve_operation_t_CMA_LEDGER_OP_CREATE || op == bindings::cma_ledger_retrieve_operation_t_CMA_LEDGER_OP_FIND_OR_CREATE => diff --git a/src/parser.rs b/src/parser.rs index 4881141..2352e27 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,10 +1,176 @@ -use crate::helpers::{hex_to_string}; +use crate::helpers::hex_to_string; use ethers_core::abi::{ParamType, Token, decode, encode}; use ethers_core::types::{Address, Bytes, U256}; use ethers_core::utils::{id, to_checksum}; use hex; -use json::{JsonValue}; +use json::JsonValue; +use std::cell::RefCell; + +thread_local! { + static LAST_PARSER_ERROR: RefCell = RefCell::new(String::new()); +} + +fn set_last_parser_error(message: impl Into) { + LAST_PARSER_ERROR.with(|msg| *msg.borrow_mut() = message.into()); +} + +fn payload_bytes(input: &JsonValue) -> Result, CmaParserError> { + let payload_hex = input["data"]["payload"] + .as_str() + .ok_or(CmaParserError::MalformedInput)?; + hex::decode(payload_hex.trim_start_matches("0x")) + .map_err(|_| CmaParserError::MalformedInput) +} + +fn require_len(bytes: &[u8], min: usize) -> Result<(), CmaParserError> { + if bytes.len() < min { + Err(CmaParserError::MalformedInput) + } else { + Ok(()) + } +} + +fn decode_abi_tail_two_bytes(tail: &[u8]) -> Result<(Bytes, Bytes), CmaParserError> { + if tail.is_empty() { + return Ok((Bytes::from(vec![]), Bytes::from(vec![]))); + } + let decoded = decode( + &[ParamType::Bytes, ParamType::Bytes], + tail, + ) + .map_err(|_| CmaParserError::MalformedInput)?; + let base = match &decoded[0] { + Token::Bytes(b) => Bytes::from(b.clone()), + _ => return Err(CmaParserError::MalformedInput), + }; + let exec = match &decoded[1] { + Token::Bytes(b) => Bytes::from(b.clone()), + _ => return Err(CmaParserError::MalformedInput), + }; + Ok((base, exec)) +} + +fn decode_abi_tail_batch( + tail: &[u8], +) -> Result<(Vec, Vec, Bytes, Bytes), CmaParserError> { + if tail.is_empty() { + return Err(CmaParserError::MalformedInput); + } + let decoded = decode( + &[ + ParamType::Array(Box::new(ParamType::Uint(256))), + ParamType::Array(Box::new(ParamType::Uint(256))), + ParamType::Bytes, + ParamType::Bytes, + ], + tail, + ) + .map_err(|_| CmaParserError::MalformedInput)?; + + let token_ids = match &decoded[0] { + Token::Array(items) => items + .iter() + .map(|item| match item { + Token::Uint(v) => Ok(*v), + _ => Err(CmaParserError::MalformedInput), + }) + .collect::, _>>()?, + _ => return Err(CmaParserError::MalformedInput), + }; + let amounts = match &decoded[1] { + Token::Array(items) => items + .iter() + .map(|item| match item { + Token::Uint(v) => Ok(*v), + _ => Err(CmaParserError::MalformedInput), + }) + .collect::, _>>()?, + _ => return Err(CmaParserError::MalformedInput), + }; + let base = match &decoded[2] { + Token::Bytes(b) => Bytes::from(b.clone()), + _ => return Err(CmaParserError::MalformedInput), + }; + let exec = match &decoded[3] { + Token::Bytes(b) => Bytes::from(b.clone()), + _ => return Err(CmaParserError::MalformedInput), + }; + + if token_ids.is_empty() || amounts.is_empty() || token_ids.len() != amounts.len() { + return Err(CmaParserError::MalformedInput); + } + + Ok((token_ids, amounts, base, exec)) +} + +fn parse_hex_account_id(value: &str) -> Result { + let value = value.trim(); + if !value.starts_with("0x") { + return Err(CmaParserError::MalformedInput); + } + let hex_body = &value[2..]; + if hex_body.len() % 2 != 0 { + return Err(CmaParserError::MalformedInput); + } + + let mut bytes = [0u8; 32]; + let offset = if value.len() == 42 { + 12 + } else if value.len() <= 66 { + (64usize.saturating_sub(hex_body.len())) / 2 + } else { + return Err(CmaParserError::MalformedInput); + }; + + let decoded = hex::decode(hex_body).map_err(|_| CmaParserError::MalformedInput)?; + if offset + decoded.len() > 32 { + return Err(CmaParserError::MalformedInput); + } + bytes[offset..offset + decoded.len()].copy_from_slice(&decoded); + Ok(U256::from_big_endian(&bytes)) +} + +fn parse_hex_token_id(value: &str) -> Result { + let value = value.trim(); + if !value.starts_with("0x") { + return Err(CmaParserError::MalformedInput); + } + let mut hex_body = value[2..].to_string(); + if hex_body.len() % 2 != 0 { + hex_body.insert(0, '0'); + } + if hex_body.len() > 64 { + return Err(CmaParserError::MalformedInput); + } + let mut bytes = [0u8; 32]; + let offset = (64usize.saturating_sub(hex_body.len())) / 2; + let decoded = hex::decode(hex_body).map_err(|_| CmaParserError::MalformedInput)?; + if offset + decoded.len() > 32 { + return Err(CmaParserError::MalformedInput); + } + bytes[offset..offset + decoded.len()].copy_from_slice(&decoded); + Ok(U256::from_big_endian(&bytes)) +} + +fn parse_token_address(value: &str) -> Result { + if value.len() != 42 || !value.starts_with("0x") { + return Err(CmaParserError::MalformedInput); + } + value + .parse::
() + .map_err(|_| CmaParserError::MalformedInput) +} + +fn tokenize_u256_list(tokens: &[Token]) -> Result, CmaParserError> { + tokens + .iter() + .map(|item| match item { + Token::Uint(v) => Ok(*v), + _ => Err(CmaParserError::MalformedInput), + }) + .collect() +} #[repr(u32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -20,8 +186,8 @@ pub enum TxHexCodes { // Bytecode for solidity WithdrawErc1155Batch(address,uint256[],uint256[],bytes) = 50c80019 WithdrawErc1155Batch = 0x50c80019, - // Bytecode for solidity TransferEther(uint256,bytes32,bytes) = 428c9c4d - TransferEther = 0x428c9c4d, + // Bytecode for solidity TransferEther(bytes32,uint256,bytes) = ff67c903 + TransferEther = 0xff67c903, // Bytecode for solidity TransferErc20(address,bytes32,uint256,bytes) = 03d61dcd TransferErc20 = 0x03d61dcd, // Bytecode for solidity TransferErc721(address,bytes32,uint256,bytes) = af615a5a @@ -41,7 +207,7 @@ impl TxHexCodes { Self::WithdrawErc721 => "0x33acf293", Self::WithdrawErc1155Single => "0x8bb0a811", Self::WithdrawErc1155Batch => "0x50c80019", - Self::TransferEther => "0x428c9c4d", + Self::TransferEther => "0xff67c903", Self::TransferErc20 => "0x03d61dcd", Self::TransferErc721 => "0xaf615a5a", Self::TransferErc1155Single => "0xe1c913ed", @@ -57,7 +223,7 @@ impl TxHexCodes { "0x33acf293" => Self::WithdrawErc721, "0x8bb0a811" => Self::WithdrawErc1155Single, "0x50c80019" => Self::WithdrawErc1155Batch, - "0x428c9c4d" => Self::TransferEther, + "0xff67c903" => Self::TransferEther, "0x03d61dcd" => Self::TransferErc20, "0xaf615a5a" => Self::TransferErc721, "0xe1c913ed" => Self::TransferErc1155Single, @@ -92,12 +258,6 @@ pub enum CmaVoucherFieldType { Erc1155BatchVoucherFields(CmaParserErc1155BatchVoucherFields), } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum TokenType { - Erc20, - Erc721, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CmaParserInputType { CmaParserInputTypeNone, @@ -144,8 +304,10 @@ impl CmaParserInputType { "Erc721Transfer" => CmaParserInputType::CmaParserInputTypeErc721Transfer, "Erc1155SingleTransfer" => CmaParserInputType::CmaParserInputTypeErc1155SingleTransfer, "Erc1155BatchTransfer" => CmaParserInputType::CmaParserInputTypeErc1155BatchTransfer, - "ledgerGetBalance" => CmaParserInputType::CmaParserInputTypeBalance, - "ledgerGetTotalSupply" => CmaParserInputType::CmaParserInputTypeSupply, + "ledger_getBalance" | "ledgerGetBalance" => CmaParserInputType::CmaParserInputTypeBalance, + "ledger_getTotalSupply" | "ledgerGetTotalSupply" => { + CmaParserInputType::CmaParserInputTypeSupply + } _ => CmaParserInputType::CmaParserInputTypeUnidentified, } } @@ -164,9 +326,11 @@ pub enum CmaParserVoucherType { #[derive(Debug, Clone, PartialEq, Eq)] pub enum CmaParserError { Success, + Unknown, + Exception, IncompatibleInput, MalformedInput, - Unknown, + InvalidAmount, Message(String), } @@ -174,10 +338,11 @@ impl CmaParserError { pub fn from_code(code: i32) -> Self { match code { 0 => Self::Success, - -2001 => Self::IncompatibleInput, - -2002 => Self::MalformedInput, - -2003 => Self::Unknown, - -2004 => Self::Message("Unknown error".to_string()), + -2001 => Self::Unknown, + -2002 => Self::Exception, + -2003 => Self::IncompatibleInput, + -2004 => Self::MalformedInput, + -2005 => Self::InvalidAmount, _ => Self::Unknown, } } @@ -185,18 +350,38 @@ impl CmaParserError { pub fn to_code(self) -> i32 { match self { Self::Success => 0, - Self::IncompatibleInput => -2001, - Self::MalformedInput => -2002, - Self::Unknown => -2003, - Self::Message(_) => -2004, + Self::Unknown => -2001, + Self::Exception => -2002, + Self::IncompatibleInput => -2003, + Self::MalformedInput => -2004, + Self::InvalidAmount => -2005, + Self::Message(_) => -2002, } } + + pub fn message(&self) -> String { + match self { + Self::Message(msg) => msg.clone(), + other => format!("{:?}", other), + } + } +} + +pub fn cma_parser_get_last_error_message() -> Option { + LAST_PARSER_ERROR.with(|msg| { + let value = msg.borrow().clone(); + if value.is_empty() { + None + } else { + Some(value) + } + }) } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CmaVoucher { pub destination: String, - // pub value: String, + pub value: String, pub payload: String, } @@ -232,20 +417,19 @@ pub struct CmaParserErc721VoucherFields { #[derive(Debug, Clone, PartialEq, Eq)] pub struct CmaParserErc1155SingleVoucherFields { pub token: Address, - pub token_id: U256, pub receiver: Address, - pub value: U256, + pub token_id: U256, pub amount: U256, + pub exec_layer_data: Bytes, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CmaParserErc1155BatchVoucherFields { pub token: Address, pub receiver: Address, - pub count: usize, pub token_ids: Vec, - pub value: U256, pub amounts: Vec, + pub exec_layer_data: Bytes, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -268,6 +452,7 @@ pub struct CmaParserErc721Deposit { pub sender: Address, pub token: Address, pub token_id: U256, + pub base_layer_data: Bytes, pub exec_layer_data: Bytes, } @@ -277,6 +462,7 @@ pub struct CmaParserErc1155SingleDeposit { pub token: Address, pub token_id: U256, pub amount: U256, + pub base_layer_data: Bytes, pub exec_layer_data: Bytes, } @@ -330,7 +516,6 @@ pub struct CmaParserErc1155BatchWithdrawal { pub count: usize, pub token_ids: Vec, pub amounts: Vec, - pub base_layer_data: String, pub exec_layer_data: String, } @@ -359,8 +544,7 @@ pub struct CmaParserErc721Transfer { #[derive(Debug, Clone, PartialEq, Eq)] pub struct CmaParserErc1155SingleTransfer { - pub sender: Address, - pub receiver: Address, + pub receiver: U256, pub token: Address, pub token_id: U256, pub amount: U256, @@ -369,27 +553,27 @@ pub struct CmaParserErc1155SingleTransfer { #[derive(Debug, Clone, PartialEq, Eq)] pub struct CmaParserErc1155BatchTransfer { - pub sender: Address, - pub receiver: Address, + pub receiver: U256, pub token: Address, pub count: usize, pub token_ids: Vec, pub amounts: Vec, - pub base_layer_data: String, pub exec_layer_data: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CmaParserBalance { - pub account: Address, + pub account: U256, pub token: Address, - pub token_ids: Option>, + pub token_id: U256, + pub exec_layer_data: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CmaParserSupply { pub token: Address, - pub token_ids: Vec, + pub token_id: U256, + pub exec_layer_data: String, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -421,49 +605,27 @@ pub struct CmaParserInput { } fn handle_unidentified_method(input: JsonValue) -> Result { - let payload_hex = input["data"]["payload"] + let bytes = payload_bytes(&input)?; + let msg_sender = input["data"]["metadata"]["msg_sender"] .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - let msg_sender = - input["data"]["metadata"]["msg_sender"] - .as_str() - .ok_or(CmaParserError::Message(String::from( - "Invalid msg_sender address", - )))?; + .ok_or(CmaParserError::MalformedInput)?; let sender = Address::from_slice( &hex::decode(msg_sender.trim_start_matches("0x")) - .map_err(|e| CmaParserError::Message(format!("hex decode error: {}", e)))?, + .map_err(|_| CmaParserError::MalformedInput)?, ); - let bytes = hex::decode(payload_hex.trim_start_matches("0x")).expect("invalid hex"); - - return Ok(CmaParserInputData::Unidentified(CmaParserUnidentifiedInput { + Ok(CmaParserInputData::Unidentified(CmaParserUnidentifiedInput { abi_encoded_bytes: bytes, msg_sender: sender, - })); + })) } fn handle_parse_ether_deposit(input: JsonValue) -> Result { - let payload_hex = input["data"]["payload"] - .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - let payload = payload_hex.trim_start_matches("0x"); - - let bytes = hex::decode(payload) - .map_err(|e| CmaParserError::Message(format!("hex decode error: {}", e)))?; - - if bytes.len() < 20 + 32 { - return Err(CmaParserError::Message( - "Invalid payload length".to_string(), - )); - } - - let sender_bytes = &bytes[0..20]; - let sender = Address::from_slice(sender_bytes); - - let value_bytes = &bytes[20..52]; - let value = U256::from_big_endian(value_bytes); + let bytes = payload_bytes(&input)?; + require_len(&bytes, 52)?; + let sender = Address::from_slice(&bytes[0..20]); + let value = U256::from_big_endian(&bytes[20..52]); let exec_layer_data = Bytes::from(bytes[52..].to_vec()); Ok(CmaParserInputData::EtherDeposit(CmaParserEtherDeposit { @@ -473,119 +635,61 @@ fn handle_parse_ether_deposit(input: JsonValue) -> Result Result { - let payload_hex = input["data"]["payload"] - .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - let payload = payload_hex.trim_start_matches("0x"); - - let bytes = hex::decode(payload) - .map_err(|e| CmaParserError::Message(format!("hex decode error: {}", e)))?; +fn handle_parse_erc20_deposit(input: JsonValue) -> Result { + let bytes = payload_bytes(&input)?; + require_len(&bytes, 72)?; - if bytes.len() < 20 + 20 + 32 { - return Err(CmaParserError::Message( - "Invalid payload length".to_string(), - )); - } - - let token = &bytes[1..21]; - let sender = &bytes[21..41]; - let amount_bytes = &bytes[41..73]; - let amount = U256::from_big_endian(amount_bytes); - let exec_layer_data = Bytes::from(bytes[73..].to_vec()); + let token = Address::from_slice(&bytes[0..20]); + let sender = Address::from_slice(&bytes[20..40]); + let amount = U256::from_big_endian(&bytes[40..72]); + let exec_layer_data = Bytes::from(bytes[72..].to_vec()); - match t_type { - TokenType::Erc20 => Ok(CmaParserInputData::Erc20Deposit(CmaParserErc20Deposit { - sender: Address::from_slice(sender), - token: Address::from_slice(token), - amount, - exec_layer_data, - })), - TokenType::Erc721 => Ok(CmaParserInputData::Erc721Deposit(CmaParserErc721Deposit { - sender: Address::from_slice(sender), - token: Address::from_slice(token), - token_id: amount, - exec_layer_data, - })), - } + Ok(CmaParserInputData::Erc20Deposit(CmaParserErc20Deposit { + sender, + token, + amount, + exec_layer_data, + })) } -fn handle_parse_erc721_deposit( - input: JsonValue, - t_type: TokenType, -) -> Result { - let payload_hex = input["data"]["payload"] - .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - let payload = payload_hex.trim_start_matches("0x"); +fn handle_parse_erc721_deposit(input: JsonValue) -> Result { + let bytes = payload_bytes(&input)?; + require_len(&bytes, 72)?; - let bytes = hex::decode(payload) - .map_err(|e| CmaParserError::Message(format!("hex decode error: {}", e)))?; + let token = Address::from_slice(&bytes[0..20]); + let sender = Address::from_slice(&bytes[20..40]); + let token_id = U256::from_big_endian(&bytes[40..72]); + let (base_layer_data, exec_layer_data) = decode_abi_tail_two_bytes(&bytes[72..])?; - if bytes.len() < 20 + 20 + 32 { - return Err(CmaParserError::Message( - "Invalid payload length".to_string(), - )); - } - - let token = &bytes[0..20]; - let sender = &bytes[20..40]; - let amount_bytes = &bytes[40..72]; - let amount = U256::from_big_endian(amount_bytes); - let exec_layer_data = Bytes::from(bytes[72..].to_vec()); - - match t_type { - TokenType::Erc20 => Ok(CmaParserInputData::Erc20Deposit(CmaParserErc20Deposit { - sender: Address::from_slice(sender), - token: Address::from_slice(token), - amount, - exec_layer_data, - })), - TokenType::Erc721 => Ok(CmaParserInputData::Erc721Deposit(CmaParserErc721Deposit { - sender: Address::from_slice(sender), - token: Address::from_slice(token), - token_id: amount, - exec_layer_data, - })), - } + Ok(CmaParserInputData::Erc721Deposit(CmaParserErc721Deposit { + sender, + token, + token_id, + base_layer_data, + exec_layer_data, + })) } fn handle_parse_erc1155_single_deposit( input: JsonValue, ) -> Result { - let payload_hex = input["data"]["payload"] - .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - let payload = payload_hex.trim_start_matches("0x"); - - let bytes = hex::decode(payload) - .map_err(|e| CmaParserError::Message(format!("hex decode error: {}", e)))?; + let bytes = payload_bytes(&input)?; + require_len(&bytes, 104)?; - if bytes.len() < 20 + 20 + 32 + 32 { - return Err(CmaParserError::Message( - "Invalid payload length".to_string(), - )); - } - - let token = &bytes[0..20]; - let sender = &bytes[20..40]; - let token_id_bytes = &bytes[40..72]; - let token_id = U256::from_big_endian(token_id_bytes); - let amount_bytes = &bytes[72..104]; - let amount = U256::from_big_endian(amount_bytes); - let _base_layer_data = Bytes::from(bytes[104..136].to_vec()); - let exec_layer_data = Bytes::from(bytes[136..].to_vec()); + let token = Address::from_slice(&bytes[0..20]); + let sender = Address::from_slice(&bytes[20..40]); + let token_id = U256::from_big_endian(&bytes[40..72]); + let amount = U256::from_big_endian(&bytes[72..104]); + let (base_layer_data, exec_layer_data) = decode_abi_tail_two_bytes(&bytes[104..])?; Ok(CmaParserInputData::Erc1155SingleDeposit( CmaParserErc1155SingleDeposit { - sender: Address::from_slice(sender), - token: Address::from_slice(token), + sender, + token, token_id, amount, + base_layer_data, exec_layer_data, }, )) @@ -594,387 +698,463 @@ fn handle_parse_erc1155_single_deposit( fn handle_parse_erc1155_batch_deposit( input: JsonValue, ) -> Result { - let payload_hex = input["data"]["payload"] - .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - let payload = payload_hex.trim_start_matches("0x"); - - let bytes = hex::decode(payload) - .map_err(|e| CmaParserError::Message(format!("hex decode error: {}", e)))?; - - if bytes.len() < 20 + 20 + 32 + 32 + 32 { - return Err(CmaParserError::Message( - "Invalid payload length".to_string(), - )); - } - - let u256_from = |b: &[u8]| U256::from_big_endian(b); - let as_addr = |b: &[u8]| Address::from_slice(&b[12..32]); - - let token = as_addr(&bytes[0..32]); - let sender = as_addr(&bytes[32..64]); - - let token_ids_offset = u256_from(&bytes[64..96]).as_usize(); - let values_offset = u256_from(&bytes[96..128]).as_usize(); - let base_offset = u256_from(&bytes[128..160]).as_usize(); - let exec_offset = u256_from(&bytes[160..192]).as_usize(); - - let token_ids_len = u256_from(&bytes[token_ids_offset..token_ids_offset + 32]).as_usize(); - let mut token_ids = Vec::with_capacity(token_ids_len); - - let mut cursor = token_ids_offset + 32; - for _ in 0..token_ids_len { - token_ids.push(u256_from(&bytes[cursor..cursor + 32])); - cursor += 32; - } - - let values_len = u256_from(&bytes[values_offset..values_offset + 32]).as_usize(); - let mut values = Vec::with_capacity(values_len); - - let mut cursor2 = values_offset + 32; - for _ in 0..values_len { - values.push(u256_from(&bytes[cursor2..cursor2 + 32])); - cursor2 += 32; - } - - let base_len = u256_from(&bytes[base_offset..base_offset + 32]).as_usize(); - let base_start = base_offset + 32; - let base_end = base_start + base_len; - let base_layer_data = Bytes::from(bytes[base_start..base_end].to_vec()); - - let exec_len = u256_from(&bytes[exec_offset..exec_offset + 32]).as_usize(); - let exec_start = exec_offset + 32; - let exec_end = exec_start + exec_len; - let exec_layer_data = Bytes::from(bytes[exec_start..exec_end].to_vec()); + let bytes = payload_bytes(&input)?; + require_len(&bytes, 40)?; - if token_ids_len == 0 || values_len == 0 || token_ids_len != values_len { - return Err(CmaParserError::Message("Invalid payload data".to_string())); - } + let token = Address::from_slice(&bytes[0..20]); + let sender = Address::from_slice(&bytes[20..40]); + let (token_ids, amounts, base_layer_data, exec_layer_data) = + decode_abi_tail_batch(&bytes[40..])?; Ok(CmaParserInputData::Erc1155BatchDeposit( CmaParserErc1155BatchDeposit { sender, token, - count: token_ids_len, + count: token_ids.len(), token_ids, - amounts: values, + amounts, base_layer_data, exec_layer_data, }, )) } -fn handle_ether_withdrawal(input: JsonValue) -> Result { - let payload_hex = input["data"]["payload"] +fn withdrawal_receiver(input: &JsonValue) -> Result { + let msg_sender = input["data"]["metadata"]["msg_sender"] .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - let msg_sender = - input["data"]["metadata"]["msg_sender"] - .as_str() - .ok_or(CmaParserError::Message(String::from( - "Invalid msg_sender address", - )))?; - - let bytes = hex::decode(payload_hex.trim_start_matches("0x")).expect("invalid hex"); - let (_first_4_bytes, encoded_args) = bytes.split_at(4); - - let param_types = vec![ - ParamType::Uint(256), - ParamType::Bytes, - ]; - - let receiver = msg_sender + .ok_or(CmaParserError::MalformedInput)?; + msg_sender .parse::
() - .map_err(|e| CmaParserError::Message(format!("msg_sender address error: {}", e)))?; - + .map_err(|_| CmaParserError::MalformedInput) +} - let decoded = decode(¶m_types, encoded_args).map_err(|e| CmaParserError::Message(format!("ABI decode failed: {}", e)))?; +fn decode_after_selector(bytes: &[u8]) -> Result<&[u8], CmaParserError> { + require_len(bytes, 4)?; + Ok(&bytes[4..]) +} + +fn exec_layer_hex(exec_layer_byte: &[u8]) -> String { + format!("0x{}", hex::encode(exec_layer_byte)) +} + +fn handle_ether_withdrawal(input: JsonValue) -> Result { + let bytes = payload_bytes(&input)?; + let encoded_args = decode_after_selector(&bytes)?; + let receiver = withdrawal_receiver(&input)?; + + let decoded = decode( + &[ParamType::Uint(256), ParamType::Bytes], + encoded_args, + ) + .map_err(|_| CmaParserError::MalformedInput)?; let amount = match &decoded[0] { - Token::Uint(v) => v, - _ => return Err(CmaParserError::Message(format!("Error decoding uint256 amount"))), + Token::Uint(v) => *v, + _ => return Err(CmaParserError::MalformedInput), }; - let exec_layer_byte = match &decoded[1] { - Token::Bytes(b) => b, - _ => return Err(CmaParserError::Message(format!("Error decoding bytes execution layer data"))), + Token::Bytes(b) => b.as_slice(), + _ => return Err(CmaParserError::MalformedInput), }; - let exec_layer_data = format!("0x{}", hex::encode(exec_layer_byte)); - - return Ok(CmaParserInputData::EtherWithdrawal( - CmaParserEtherWithdrawal { - receiver, - amount: amount.clone(), - exec_layer_data, - }, - )); - + Ok(CmaParserInputData::EtherWithdrawal(CmaParserEtherWithdrawal { + receiver, + amount, + exec_layer_data: exec_layer_hex(exec_layer_byte), + })) } fn handle_erc20_withdrawal(input: JsonValue) -> Result { - let payload_hex = input["data"]["payload"] - .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - let msg_sender = - input["data"]["metadata"]["msg_sender"] - .as_str() - .ok_or(CmaParserError::Message(String::from( - "Invalid msg_sender address", - )))?; - - let bytes = hex::decode(payload_hex.trim_start_matches("0x")).expect("invalid hex"); - let (_first_4_bytes, encoded_args) = bytes.split_at(4); - - let param_types = vec![ - ParamType::Address, - ParamType::Uint(256), - ParamType::Bytes, - ]; - - let receiver = msg_sender - .parse::
() - .map_err(|e| CmaParserError::Message(format!("msg_sender address error: {}", e)))?; - + let bytes = payload_bytes(&input)?; + let encoded_args = decode_after_selector(&bytes)?; + let receiver = withdrawal_receiver(&input)?; - let decoded = decode(¶m_types, encoded_args).map_err(|e| CmaParserError::Message(format!("ABI decode failed: {}", e)))?; + let decoded = decode( + &[ParamType::Address, ParamType::Uint(256), ParamType::Bytes], + encoded_args, + ) + .map_err(|_| CmaParserError::MalformedInput)?; - let token = match &decoded[0] { + let token = match &decoded[0] { Token::Address(v) => *v, - _ => return Err(CmaParserError::Message(format!("Error decoding receiver address"))), + _ => return Err(CmaParserError::MalformedInput), }; - let amount = match &decoded[1] { Token::Uint(v) => *v, - _ => return Err(CmaParserError::Message(format!("Error decoding uint256 amount"))), + _ => return Err(CmaParserError::MalformedInput), }; - let exec_layer_byte = match &decoded[2] { - Token::Bytes(b) => b, - _ => return Err(CmaParserError::Message(format!("Error decoding bytes execution layer data"))), + Token::Bytes(b) => b.as_slice(), + _ => return Err(CmaParserError::MalformedInput), }; - let exec_layer_data = format!("0x{}", hex::encode(exec_layer_byte)); - - return Ok(CmaParserInputData::Erc20Withdrawal( - CmaParserErc20Withdrawal { - receiver, - token, - amount, - exec_layer_data, - }, - )) + Ok(CmaParserInputData::Erc20Withdrawal(CmaParserErc20Withdrawal { + receiver, + token, + amount, + exec_layer_data: exec_layer_hex(exec_layer_byte), + })) } fn handle_erc721_withdrawal(input: JsonValue) -> Result { - let payload_hex = input["data"]["payload"] - .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - let msg_sender = - input["data"]["metadata"]["msg_sender"] - .as_str() - .ok_or(CmaParserError::Message(String::from( - "Invalid msg_sender address", - )))?; - - let bytes = hex::decode(payload_hex.trim_start_matches("0x")).expect("invalid hex"); - let (_first_4_bytes, encoded_args) = bytes.split_at(4); - - let param_types = vec![ - ParamType::Address, - ParamType::Uint(256), - ParamType::Bytes, - ]; - - let receiver = msg_sender - .parse::
() - .map_err(|e| CmaParserError::Message(format!("msg_sender address error: {}", e)))?; - + let bytes = payload_bytes(&input)?; + let encoded_args = decode_after_selector(&bytes)?; + let receiver = withdrawal_receiver(&input)?; - let decoded = decode(¶m_types, encoded_args).map_err(|e| CmaParserError::Message(format!("ABI decode failed: {}", e)))?; + let decoded = decode( + &[ParamType::Address, ParamType::Uint(256), ParamType::Bytes], + encoded_args, + ) + .map_err(|_| CmaParserError::MalformedInput)?; - let token = match &decoded[0] { + let token = match &decoded[0] { Token::Address(v) => *v, - _ => return Err(CmaParserError::Message(format!("Error decoding receiver address"))), + _ => return Err(CmaParserError::MalformedInput), }; - let token_id = match &decoded[1] { Token::Uint(v) => *v, - _ => return Err(CmaParserError::Message(format!("Error decoding uint256 token id"))), + _ => return Err(CmaParserError::MalformedInput), }; - let exec_layer_byte = match &decoded[2] { - Token::Bytes(b) => b, - _ => return Err(CmaParserError::Message(format!("Error decoding bytes execution layer data"))), + Token::Bytes(b) => b.as_slice(), + _ => return Err(CmaParserError::MalformedInput), }; - let exec_layer_data = format!("0x{}", hex::encode(exec_layer_byte)); + Ok(CmaParserInputData::Erc721Withdrawal(CmaParserErc721Withdrawal { + receiver, + token, + token_id, + exec_layer_data: exec_layer_hex(exec_layer_byte), + })) +} + +fn handle_erc1155_single_withdrawal( + input: JsonValue, +) -> Result { + let bytes = payload_bytes(&input)?; + let encoded_args = decode_after_selector(&bytes)?; + let receiver = withdrawal_receiver(&input)?; + + let decoded = decode( + &[ + ParamType::Address, + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Bytes, + ], + encoded_args, + ) + .map_err(|_| CmaParserError::MalformedInput)?; + + let token = match &decoded[0] { + Token::Address(v) => *v, + _ => return Err(CmaParserError::MalformedInput), + }; + let token_id = match &decoded[1] { + Token::Uint(v) => *v, + _ => return Err(CmaParserError::MalformedInput), + }; + let amount = match &decoded[2] { + Token::Uint(v) => *v, + _ => return Err(CmaParserError::MalformedInput), + }; + let exec_layer_byte = match &decoded[3] { + Token::Bytes(b) => b.as_slice(), + _ => return Err(CmaParserError::MalformedInput), + }; - return Ok(CmaParserInputData::Erc721Withdrawal( - CmaParserErc721Withdrawal { + Ok(CmaParserInputData::Erc1155SingleWithdrawal( + CmaParserErc1155SingleWithdrawal { receiver, token, token_id, - exec_layer_data, + amount, + exec_layer_data: exec_layer_hex(exec_layer_byte), }, )) } -fn handle_ether_transfer(input: JsonValue) -> Result { - let payload_hex = input["data"]["payload"] - .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; +fn handle_erc1155_batch_withdrawal( + input: JsonValue, +) -> Result { + let bytes = payload_bytes(&input)?; + let encoded_args = decode_after_selector(&bytes)?; + let receiver = withdrawal_receiver(&input)?; + + let decoded = decode( + &[ + ParamType::Address, + ParamType::Array(Box::new(ParamType::Uint(256))), + ParamType::Array(Box::new(ParamType::Uint(256))), + ParamType::Bytes, + ], + encoded_args, + ) + .map_err(|_| CmaParserError::MalformedInput)?; + + let token = match &decoded[0] { + Token::Address(v) => *v, + _ => return Err(CmaParserError::MalformedInput), + }; + let token_ids = match &decoded[1] { + Token::Array(items) => tokenize_u256_list(items)?, + _ => return Err(CmaParserError::MalformedInput), + }; + let amounts = match &decoded[2] { + Token::Array(items) => tokenize_u256_list(items)?, + _ => return Err(CmaParserError::MalformedInput), + }; + let exec_layer_byte = match &decoded[3] { + Token::Bytes(b) => b.as_slice(), + _ => return Err(CmaParserError::MalformedInput), + }; + + if token_ids.len() != amounts.len() || token_ids.is_empty() { + return Err(CmaParserError::MalformedInput); + } - let bytes = hex::decode(payload_hex.trim_start_matches("0x")).expect("invalid hex"); - let (_first_4_bytes, encoded_args) = bytes.split_at(4); + Ok(CmaParserInputData::Erc1155BatchWithdrawal( + CmaParserErc1155BatchWithdrawal { + receiver, + token, + count: token_ids.len(), + token_ids, + amounts, + exec_layer_data: exec_layer_hex(exec_layer_byte), + }, + )) +} - let param_types = vec![ - ParamType::Uint(256), - ParamType::FixedBytes(32), - ParamType::Bytes - ]; +fn handle_ether_transfer(input: JsonValue) -> Result { + let bytes = payload_bytes(&input)?; + let encoded_args = decode_after_selector(&bytes)?; - let decoded = decode(¶m_types, encoded_args).map_err(|e| CmaParserError::Message(format!("ABI decode failed: {}", e)))?; + let decoded = decode( + &[ParamType::Uint(256), ParamType::Uint(256), ParamType::Bytes], + encoded_args, + ) + .map_err(|_| CmaParserError::MalformedInput)?; - let amount = match &decoded[0] { + let receiver = match &decoded[0] { Token::Uint(v) => *v, - _ => return Err(CmaParserError::Message(format!("Error decoding u256 amount"))), + _ => return Err(CmaParserError::MalformedInput), }; - - let receiver = match &decoded[1] { - Token::FixedBytes(v) if v.len() == 32 => { - U256::from_big_endian(v) - } - _ => { - return Err(CmaParserError::Message( - "Error decodeing bytes32 receiver id".to_string(), - )) - } + let amount = match &decoded[1] { + Token::Uint(v) => *v, + _ => return Err(CmaParserError::MalformedInput), }; - let exec_layer_byte = match &decoded[2] { - Token::Bytes(b) => b, - _ => return Err(CmaParserError::Message(format!("Error decoding bytes execution layer data"))), + Token::Bytes(b) => b.as_slice(), + _ => return Err(CmaParserError::MalformedInput), }; - let exec_layer_data = format!("0x{}", hex::encode(exec_layer_byte)); - Ok(CmaParserInputData::EtherTransfer(CmaParserEtherTransfer { receiver, amount, - exec_layer_data, + exec_layer_data: exec_layer_hex(exec_layer_byte), })) } fn handle_erc20_transfer(input: JsonValue) -> Result { - let payload_hex = input["data"]["payload"] - .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - - let bytes = hex::decode(payload_hex.trim_start_matches("0x")).expect("invalid hex"); - let (_first_4_bytes, encoded_args) = bytes.split_at(4); - - let param_types = vec![ - ParamType::Address, - ParamType::FixedBytes(32), - ParamType::Uint(256), - ParamType::Bytes - ]; - - let decoded = decode(¶m_types, encoded_args).map_err(|e| CmaParserError::Message(format!("ABI decode failed: {}", e)))?; - let token = match &decoded[0] { + let bytes = payload_bytes(&input)?; + let encoded_args = decode_after_selector(&bytes)?; + + let decoded = decode( + &[ + ParamType::Address, + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Bytes, + ], + encoded_args, + ) + .map_err(|_| CmaParserError::MalformedInput)?; + + let token = match &decoded[0] { Token::Address(v) => *v, - _ => return Err(CmaParserError::Message(format!("Error decoding token address"))), + _ => return Err(CmaParserError::MalformedInput), }; - let receiver = match &decoded[1] { - Token::FixedBytes(v) if v.len() == 32 => { - U256::from_big_endian(v) - } - _ => { - return Err(CmaParserError::Message( - "Error decodeing bytes32 receiver id".to_string(), - )) - } + Token::Uint(v) => *v, + _ => return Err(CmaParserError::MalformedInput), }; - - let amount = match &decoded[2] { + let amount = match &decoded[2] { Token::Uint(v) => *v, - _ => return Err(CmaParserError::Message(format!("Error decoding u256 amount"))), + _ => return Err(CmaParserError::MalformedInput), }; - - let exec_layer_byte = match &decoded[3] { - Token::Bytes(b) => b, - _ => return Err(CmaParserError::Message(format!("Error decoding bytes execution layer data"))), + Token::Bytes(b) => b.as_slice(), + _ => return Err(CmaParserError::MalformedInput), }; - let exec_layer_data = format!("0x{}", hex::encode(exec_layer_byte)); - - return Ok(CmaParserInputData::Erc20Transfer(CmaParserErc20Transfer { + Ok(CmaParserInputData::Erc20Transfer(CmaParserErc20Transfer { receiver, token, amount, - exec_layer_data, + exec_layer_data: exec_layer_hex(exec_layer_byte), })) } fn handle_erc721_transfer(input: JsonValue) -> Result { - let payload_hex = input["data"]["payload"] - .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - - let bytes = hex::decode(payload_hex.trim_start_matches("0x")).expect("invalid hex"); - let (_first_4_bytes, encoded_args) = bytes.split_at(4); + let bytes = payload_bytes(&input)?; + let encoded_args = decode_after_selector(&bytes)?; + + let decoded = decode( + &[ + ParamType::Address, + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Bytes, + ], + encoded_args, + ) + .map_err(|_| CmaParserError::MalformedInput)?; + + let token = match &decoded[0] { + Token::Address(v) => *v, + _ => return Err(CmaParserError::MalformedInput), + }; + let receiver = match &decoded[1] { + Token::Uint(v) => *v, + _ => return Err(CmaParserError::MalformedInput), + }; + let token_id = match &decoded[2] { + Token::Uint(v) => *v, + _ => return Err(CmaParserError::MalformedInput), + }; + let exec_layer_byte = match &decoded[3] { + Token::Bytes(b) => b.as_slice(), + _ => return Err(CmaParserError::MalformedInput), + }; - let param_types = vec![ - ParamType::Address, - ParamType::FixedBytes(32), - ParamType::Uint(256), - ParamType::Bytes - ]; + Ok(CmaParserInputData::Erc721Transfer(CmaParserErc721Transfer { + receiver, + token, + token_id, + exec_layer_data: exec_layer_hex(exec_layer_byte), + })) +} - let decoded = decode(¶m_types, encoded_args).map_err(|e| CmaParserError::Message(format!("ABI decode failed: {}", e)))?; - let token = match &decoded[0] { +fn handle_erc1155_single_transfer( + input: JsonValue, +) -> Result { + let bytes = payload_bytes(&input)?; + let encoded_args = decode_after_selector(&bytes)?; + + let decoded = decode( + &[ + ParamType::Address, + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Bytes, + ], + encoded_args, + ) + .map_err(|_| CmaParserError::MalformedInput)?; + + let token = match &decoded[0] { Token::Address(v) => *v, - _ => return Err(CmaParserError::Message(format!("Error decoding token address"))), + _ => return Err(CmaParserError::MalformedInput), }; - let receiver = match &decoded[1] { - Token::FixedBytes(v) if v.len() == 32 => { - U256::from_big_endian(v) - } - _ => { - return Err(CmaParserError::Message( - "Error decodeing bytes32 receiver id".to_string(), - )) - } + Token::Uint(v) => *v, + _ => return Err(CmaParserError::MalformedInput), }; - - let token_id = match &decoded[2] { + let token_id = match &decoded[2] { + Token::Uint(v) => *v, + _ => return Err(CmaParserError::MalformedInput), + }; + let amount = match &decoded[3] { Token::Uint(v) => *v, - _ => return Err(CmaParserError::Message(format!("Error decoding u256 amount"))), + _ => return Err(CmaParserError::MalformedInput), + }; + let exec_layer_byte = match &decoded[4] { + Token::Bytes(b) => b.as_slice(), + _ => return Err(CmaParserError::MalformedInput), }; + Ok(CmaParserInputData::Erc1155SingleTransfer( + CmaParserErc1155SingleTransfer { + receiver, + token, + token_id, + amount, + exec_layer_data: exec_layer_hex(exec_layer_byte), + }, + )) +} - let exec_layer_byte = match &decoded[3] { - Token::Bytes(b) => b, - _ => return Err(CmaParserError::Message(format!("Error decoding bytes execution layer data"))), +fn handle_erc1155_batch_transfer( + input: JsonValue, +) -> Result { + let bytes = payload_bytes(&input)?; + let encoded_args = decode_after_selector(&bytes)?; + + let decoded = decode( + &[ + ParamType::Address, + ParamType::Uint(256), + ParamType::Array(Box::new(ParamType::Uint(256))), + ParamType::Array(Box::new(ParamType::Uint(256))), + ParamType::Bytes, + ], + encoded_args, + ) + .map_err(|_| CmaParserError::MalformedInput)?; + + let token = match &decoded[0] { + Token::Address(v) => *v, + _ => return Err(CmaParserError::MalformedInput), + }; + let receiver = match &decoded[1] { + Token::Uint(v) => *v, + _ => return Err(CmaParserError::MalformedInput), + }; + let token_ids = match &decoded[2] { + Token::Array(items) => tokenize_u256_list(items)?, + _ => return Err(CmaParserError::MalformedInput), + }; + let amounts = match &decoded[3] { + Token::Array(items) => tokenize_u256_list(items)?, + _ => return Err(CmaParserError::MalformedInput), + }; + let exec_layer_byte = match &decoded[4] { + Token::Bytes(b) => b.as_slice(), + _ => return Err(CmaParserError::MalformedInput), }; - let exec_layer_data = format!("0x{}", hex::encode(exec_layer_byte)); + if token_ids.len() != amounts.len() || token_ids.is_empty() { + return Err(CmaParserError::MalformedInput); + } - return Ok(CmaParserInputData::Erc721Transfer( - CmaParserErc721Transfer { + Ok(CmaParserInputData::Erc1155BatchTransfer( + CmaParserErc1155BatchTransfer { receiver, token, - token_id, - exec_layer_data, + count: token_ids.len(), + token_ids, + amounts, + exec_layer_data: exec_layer_hex(exec_layer_byte), }, )) } -pub fn cma_decode_advance(req_type: CmaParserInputType, input: JsonValue) -> Result { +pub fn cma_decode_advance( + req_type: CmaParserInputType, + input: JsonValue, +) -> Result { + let result = cma_decode_advance_inner(req_type, input); + if let Err(ref err) = result { + set_last_parser_error(err.message()); + } + result +} + +fn cma_decode_advance_inner( + req_type: CmaParserInputType, + input: JsonValue, +) -> Result { // Determine the portal type based on msg_sender match req_type { CmaParserInputType::CmaParserInputTypeErc1155BatchDeposit => { @@ -990,19 +1170,15 @@ pub fn cma_decode_advance(req_type: CmaParserInputType, input: JsonValue) -> Re }); } CmaParserInputType::CmaParserInputTypeErc721Deposit => { - return handle_parse_erc721_deposit(input, TokenType::Erc721).map(|data| { - CmaParserInput { - req_type, - input: data, - } + return handle_parse_erc721_deposit(input).map(|data| CmaParserInput { + req_type, + input: data, }); } CmaParserInputType::CmaParserInputTypeErc20Deposit => { - return handle_parse_erc20_deposit(input, TokenType::Erc20).map(|data| { - CmaParserInput { - req_type, - input: data, - } + return handle_parse_erc20_deposit(input).map(|data| CmaParserInput { + req_type, + input: data, }); } CmaParserInputType::CmaParserInputTypeEtherDeposit => { @@ -1013,352 +1189,329 @@ pub fn cma_decode_advance(req_type: CmaParserInputType, input: JsonValue) -> Re } // IF CALLER IS NOT ANY OF THE ABOVE PORTALS, WE TRY TO PARSE THE PAYLOAD FOR WITHDRAWALS/TRANSFERS CmaParserInputType::CmaParserInputTypeAuto => { - let payload_hex = input["data"]["payload"] - .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; - - let bytes = hex::decode(payload_hex.trim_start_matches("0x")).expect("invalid hex"); - - let (first_4_bytes,_encoded_args) = bytes.split_at(4); - let selector_str = format!("0x{}", hex::encode(first_4_bytes)); + let bytes = payload_bytes(&input)?; + require_len(&bytes, 4)?; + let selector_str = format!("0x{}", hex::encode(&bytes[..4])); let req_type = TxHexCodes::from_string(&selector_str).to_input_type(); - // Handle the request based on the determined type - match req_type { + let result = match req_type { CmaParserInputType::CmaParserInputTypeEtherWithdrawal => { - return handle_ether_withdrawal(input).map(|data| CmaParserInput { - req_type, - input: data, - }); + handle_ether_withdrawal(input) } CmaParserInputType::CmaParserInputTypeErc20Withdrawal => { - return handle_erc20_withdrawal(input).map(|data| CmaParserInput { - req_type, - input: data, - }); + handle_erc20_withdrawal(input) } CmaParserInputType::CmaParserInputTypeErc721Withdrawal => { - return handle_erc721_withdrawal(input).map(|data| CmaParserInput { - req_type, - input: data, - }); + handle_erc721_withdrawal(input) + } + CmaParserInputType::CmaParserInputTypeErc1155SingleWithdrawal => { + handle_erc1155_single_withdrawal(input) + } + CmaParserInputType::CmaParserInputTypeErc1155BatchWithdrawal => { + handle_erc1155_batch_withdrawal(input) } CmaParserInputType::CmaParserInputTypeEtherTransfer => { - return handle_ether_transfer(input).map(|data| CmaParserInput { - req_type, - input: data, - }); + handle_ether_transfer(input) } CmaParserInputType::CmaParserInputTypeErc20Transfer => { - return handle_erc20_transfer(input).map(|data| CmaParserInput { - req_type, - input: data, - }); + handle_erc20_transfer(input) } CmaParserInputType::CmaParserInputTypeErc721Transfer => { - return handle_erc721_transfer(input).map(|data| CmaParserInput { - req_type, - input: data, - }); + handle_erc721_transfer(input) + } + CmaParserInputType::CmaParserInputTypeErc1155SingleTransfer => { + handle_erc1155_single_transfer(input) + } + CmaParserInputType::CmaParserInputTypeErc1155BatchTransfer => { + handle_erc1155_batch_transfer(input) } CmaParserInputType::CmaParserInputTypeUnidentified => { - return handle_unidentified_method(input).map(|data| CmaParserInput { - req_type, - input: data, - }); + handle_unidentified_method(input) } _ => Err(CmaParserError::IncompatibleInput), - } - } - _ => { - return Err(CmaParserError::Unknown) + }; + + result.map(|data| CmaParserInput { req_type, input: data }) } + _ => Err(CmaParserError::Unknown), } } -fn handle_ledger_get_balance(parsed_json: JsonValue) -> Result { - // Extract params array +fn inspect_param_strings(parsed_json: &JsonValue) -> Result, CmaParserError> { let params_val = &parsed_json["params"]; if !params_val.is_array() { - return Err(CmaParserError::Message( - "Invalid params: not an array".into(), - )); + return Err(CmaParserError::MalformedInput); } + params_val + .members() + .map(|member| { + member + .as_str() + .map(str::to_string) + .ok_or(CmaParserError::MalformedInput) + }) + .collect() +} - // params[0] = account - let account_str = params_val[0] - .as_str() - .ok_or_else(|| CmaParserError::Message("Invalid account param".into()))?; - - let account = account_str - .parse::
() - .map_err(|e| CmaParserError::Message(format!("account address error: {}", e)))?; +fn handle_ledger_get_balance(parsed_json: JsonValue) -> Result { + let params = inspect_param_strings(&parsed_json)?; + if params.is_empty() || params.len() > 4 { + return Err(CmaParserError::MalformedInput); + } - // params[1] = token - let token_str = params_val[1] - .as_str() - .ok_or_else(|| CmaParserError::Message("Invalid token param".into()))?; + let account = parse_hex_account_id(¶ms[0])?; + let mut balance = CmaParserBalance { + account, + token: Address::zero(), + token_id: U256::zero(), + exec_layer_data: String::new(), + }; - let token = token_str - .parse::
() - .map_err(|e| CmaParserError::Message(format!("token address error: {}", e)))?; - - // params[2] = optional array containing token id's - let mut token_id: Vec = Vec::new(); - if params_val.len() > 2 && params_val[2].is_array() { - // params_val[2][0] might itself be an array of ids or params_val[2] might be the array - if params_val[2][0].is_array() { - for v in params_val[2][0].members() { - let s = v - .as_str() - .ok_or_else(|| CmaParserError::Message("Invalid token id".into()))?; - let id = U256::from_dec_str(s) - .map_err(|e| CmaParserError::Message(format!("token id parse error: {}", e)))?; - token_id.push(id); - } - } else { - // handle array of strings/numbers or comma-separated string(s) - for v in params_val[2].members() { - if let Some(s) = v.as_str() { - if s.contains(',') { - for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) { - let id = U256::from_dec_str(part).map_err(|e| { - CmaParserError::Message(format!("token id parse error: {}", e)) - })?; - token_id.push(id); - } - } else { - let id = U256::from_dec_str(s).map_err(|e| { - CmaParserError::Message(format!("token id parse error: {}", e)) - })?; - token_id.push(id); - } - } else if v.is_number() { - let id = U256::from_dec_str(&v.to_string()).map_err(|e| { - CmaParserError::Message(format!("token id parse error: {}", e)) - })?; - token_id.push(id); - } else { - return Err(CmaParserError::Message("Invalid token id format".into())); - } - } - } + if params.len() >= 2 { + balance.token = parse_token_address(¶ms[1])?; + } + if params.len() >= 3 { + balance.token_id = parse_hex_token_id(¶ms[2])?; + } + if params.len() == 4 { + balance.exec_layer_data = params[3].clone(); } - Ok(CmaParserBalance { - account, - token, - token_ids: Some(token_id), - }) + Ok(balance) } fn handle_ledger_get_supply(parsed_json: JsonValue) -> Result { - // Ensure params is an array - let params_val = &parsed_json["params"]; - if !params_val.is_array() { - return Err(CmaParserError::Message( - "Invalid params: not an array".into(), - )); + let params = if parsed_json["params"].is_array() { + inspect_param_strings(&parsed_json)? + } else { + Vec::new() + }; + + if params.len() > 3 { + return Err(CmaParserError::MalformedInput); } - // params[0] = token address string - let token_str = params_val[0] - .as_str() - .ok_or_else(|| CmaParserError::Message("Invalid token param".into()))?; + let mut supply = CmaParserSupply { + token: Address::zero(), + token_id: U256::zero(), + exec_layer_data: String::new(), + }; - let token = token_str - .parse::
() - .map_err(|e| CmaParserError::Message(format!("token address error: {}", e)))?; - - // params[1] = optional array containing token id's - let mut token_id: Vec = Vec::new(); - if params_val.len() > 1 && params_val[1].is_array() { - // params_val[1][0] might itself be an array of ids or params_val[1] might be the array - if params_val[1][0].is_array() { - for v in params_val[1][0].members() { - let s = v - .as_str() - .ok_or_else(|| CmaParserError::Message("Invalid token id".into()))?; - let id = U256::from_dec_str(s) - .map_err(|e| CmaParserError::Message(format!("token id parse error: {}", e)))?; - token_id.push(id); - } - } else { - // handle array of strings/numbers or comma-separated string(s) - for v in params_val[1].members() { - if let Some(s) = v.as_str() { - if s.contains(',') { - for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) { - let id = U256::from_dec_str(part).map_err(|e| { - CmaParserError::Message(format!("token id parse error: {}", e)) - })?; - token_id.push(id); - } - } else { - let id = U256::from_dec_str(s).map_err(|e| { - CmaParserError::Message(format!("token id parse error: {}", e)) - })?; - token_id.push(id); - } - } else if v.is_number() { - let id = U256::from_dec_str(&v.to_string()).map_err(|e| { - CmaParserError::Message(format!("token id parse error: {}", e)) - })?; - token_id.push(id); - } else { - return Err(CmaParserError::Message("Invalid token id format".into())); - } - } - } + if !params.is_empty() { + supply.token = parse_token_address(¶ms[0])?; + } + if params.len() >= 2 { + supply.token_id = parse_hex_token_id(¶ms[1])?; + } + if params.len() == 3 { + supply.exec_layer_data = params[2].clone(); } - Ok(CmaParserInputData::Supply(CmaParserSupply { - token, - token_ids: token_id, - })) + Ok(CmaParserInputData::Supply(supply)) } pub fn cma_decode_inspect(input: JsonValue) -> Result { + let result = cma_decode_inspect_inner(input); + if let Err(ref err) = result { + set_last_parser_error(err.message()); + } + result +} + +fn cma_decode_inspect_inner(input: JsonValue) -> Result { let payload_hex = input["data"]["payload"] .as_str() - .ok_or(CmaParserError::Message(String::from("Invalid payload hex")))?; + .ok_or(CmaParserError::MalformedInput)?; let payload_str = hex_to_string(payload_hex) - .map_err(|e| CmaParserError::Message(format!("hex to string conversion error: {}", e)))?; + .map_err(|_| CmaParserError::MalformedInput)?; let payload_json = json::parse(&payload_str) - .map_err(|e| CmaParserError::Message(format!("Error parsing string to JSON: {}", e)))?; + .map_err(|_| CmaParserError::MalformedInput)?; - let req_type: CmaParserInputType = - CmaParserInputType::from_string(payload_json["method"].as_str().ok_or( - CmaParserError::Message(String::from("Invalid inspection type")), - )?); + let method = payload_json["method"] + .as_str() + .ok_or(CmaParserError::MalformedInput)?; + let req_type = CmaParserInputType::from_string(method); + if req_type == CmaParserInputType::CmaParserInputTypeUnidentified { + return Err(CmaParserError::IncompatibleInput); + } match req_type { CmaParserInputType::CmaParserInputTypeBalance => { - return handle_ledger_get_balance(payload_json).map(|data| CmaParserInput { + handle_ledger_get_balance(payload_json).map(|data| CmaParserInput { req_type, input: CmaParserInputData::Balance(data), - }); + }) } CmaParserInputType::CmaParserInputTypeSupply => { - return handle_ledger_get_supply(payload_json).map(|data| CmaParserInput { + handle_ledger_get_supply(payload_json).map(|data| CmaParserInput { req_type, input: data, - }); - } - _ => { - // Unsupported inspection type - return Err(CmaParserError::IncompatibleInput); + }) } + _ => Err(CmaParserError::IncompatibleInput), } } fn handle_ether_voucher_encoding( voucher_request: &CmaVoucherFieldType, ) -> Result { - if let CmaVoucherFieldType::EtherVoucherFields(fields) = &voucher_request { - let payload = "0x".to_string(); - + if let CmaVoucherFieldType::EtherVoucherFields(fields) = voucher_request { let mut value_bytes = [0u8; 32]; fields.amount.to_big_endian(&mut value_bytes); - let voucher = CmaVoucher { + Ok(CmaVoucher { destination: to_checksum(&fields.receiver, None), - payload, - }; - - Ok(voucher) + value: format!("0x{}", hex::encode(value_bytes)), + payload: "0x".to_string(), + }) } else { - Err(CmaParserError::Message(String::from( - "Invalid voucher fields for Ether", - ))) + Err(CmaParserError::IncompatibleInput) } } fn handle_erc20_voucher_encoding( voucher_request: &CmaVoucherFieldType, ) -> Result { - if let CmaVoucherFieldType::Erc20VoucherFields(fields) = &voucher_request { - let token = fields.token; + if let CmaVoucherFieldType::Erc20VoucherFields(fields) = voucher_request { + let args = vec![Token::Address(fields.receiver), Token::Uint(fields.amount)]; + let selector = &id("transfer(address,uint256)")[..4]; + let mut payload_bytes = Vec::new(); + payload_bytes.extend_from_slice(selector); + payload_bytes.extend_from_slice(&encode(&args)); - let args: Vec = vec![Token::Address(fields.receiver), Token::Uint(fields.amount)]; + Ok(CmaVoucher { + destination: to_checksum(&fields.token, None), + value: "0x".to_string(), + payload: format!("0x{}", hex::encode(payload_bytes)), + }) + } else { + Err(CmaParserError::IncompatibleInput) + } +} + +fn handle_erc721_voucher_encoding( + app_address: Address, + voucher_request: &CmaVoucherFieldType, +) -> Result { + if let CmaVoucherFieldType::Erc721VoucherFields(fields) = voucher_request { + let args = vec![ + Token::Address(app_address), + Token::Address(fields.receiver), + Token::Uint(fields.token_id), + ]; + let selector = &id("safeTransferFrom(address,address,uint256)")[..4]; + let mut payload_bytes = Vec::new(); + payload_bytes.extend_from_slice(selector); + payload_bytes.extend_from_slice(&encode(&args)); - let function_sig = "transfer(address,uint256)"; - let selector = &id(function_sig)[..4]; + Ok(CmaVoucher { + destination: to_checksum(&fields.token, None), + value: "0x".to_string(), + payload: format!("0x{}", hex::encode(payload_bytes)), + }) + } else { + Err(CmaParserError::IncompatibleInput) + } +} - let encoded_args = encode(&args); +fn handle_erc1155_single_voucher_encoding( + app_address: Address, + voucher_request: &CmaVoucherFieldType, +) -> Result { + if let CmaVoucherFieldType::Erc1155SingleVoucherFields(fields) = voucher_request { + let args = vec![ + Token::Address(app_address), + Token::Address(fields.receiver), + Token::Uint(fields.token_id), + Token::Uint(fields.amount), + Token::Bytes(fields.exec_layer_data.to_vec()), + ]; + let selector = &id("safeTransferFrom(address,address,uint256,uint256,bytes)")[..4]; let mut payload_bytes = Vec::new(); payload_bytes.extend_from_slice(selector); - payload_bytes.extend_from_slice(&encoded_args); - let payload = format!("0x{}", hex::encode(payload_bytes)); - - let voucher = CmaVoucher { - destination: format!("{:?}", token), - payload: format!("{}", payload), - }; - return Ok(voucher); + payload_bytes.extend_from_slice(&encode(&args)); + + Ok(CmaVoucher { + destination: to_checksum(&fields.token, None), + value: "0x".to_string(), + payload: format!("0x{}", hex::encode(payload_bytes)), + }) } else { - Err(CmaParserError::Message(String::from( - "Invalid voucher fields for ERC20", - ))) + Err(CmaParserError::IncompatibleInput) } } -fn handle_erc721_voucher_encoding( +fn handle_erc1155_batch_voucher_encoding( + app_address: Address, voucher_request: &CmaVoucherFieldType, ) -> Result { - if let CmaVoucherFieldType::Erc721VoucherFields(fields) = &voucher_request { - let token = fields.token; + if let CmaVoucherFieldType::Erc1155BatchVoucherFields(fields) = voucher_request { + if fields.token_ids.len() != fields.amounts.len() || fields.token_ids.is_empty() { + return Err(CmaParserError::MalformedInput); + } - let args: Vec = vec![ - Token::Address(fields.application_address), + let token_id_tokens: Vec = fields + .token_ids + .iter() + .map(|id| Token::Uint(*id)) + .collect(); + let amount_tokens: Vec = fields + .amounts + .iter() + .map(|amount| Token::Uint(*amount)) + .collect(); + + let args = vec![ + Token::Address(app_address), Token::Address(fields.receiver), - Token::Uint(fields.token_id.into()), + Token::Array(token_id_tokens), + Token::Array(amount_tokens), + Token::Bytes(fields.exec_layer_data.to_vec()), ]; - let function_sig = "transferFrom(address,address,uint256)"; - let selector = &id(function_sig)[..4]; - let encoded_args = encode(&args); + let selector = &id("safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)")[..4]; let mut payload_bytes = Vec::new(); payload_bytes.extend_from_slice(selector); - payload_bytes.extend_from_slice(&encoded_args); - let payload = format!("0x{}", hex::encode(payload_bytes)); - - let voucher = CmaVoucher { - destination: format!("{:?}", token), - payload: format!("{}", payload), - }; + payload_bytes.extend_from_slice(&encode(&args)); - return Ok(voucher); + Ok(CmaVoucher { + destination: to_checksum(&fields.token, None), + value: "0x".to_string(), + payload: format!("0x{}", hex::encode(payload_bytes)), + }) } else { - Err(CmaParserError::Message(String::from( - "Invalid voucher fields for ERC721", - ))) + Err(CmaParserError::IncompatibleInput) } } pub fn cma_encode_voucher( req_type: CmaParserVoucherType, + app_address: Option
, voucher_request: CmaVoucherFieldType, ) -> Result { - match req_type { + let result = match req_type { CmaParserVoucherType::CmaParserVoucherTypeEther => { - return handle_ether_voucher_encoding(&voucher_request); + handle_ether_voucher_encoding(&voucher_request) } CmaParserVoucherType::CmaParserVoucherTypeErc20 => { - return handle_erc20_voucher_encoding(&voucher_request); + handle_erc20_voucher_encoding(&voucher_request) } CmaParserVoucherType::CmaParserVoucherTypeErc721 => { - return handle_erc721_voucher_encoding(&voucher_request); + let app_address = app_address.ok_or(CmaParserError::MalformedInput)?; + handle_erc721_voucher_encoding(app_address, &voucher_request) } CmaParserVoucherType::CmaParserVoucherTypeErc1155Single => { - // TODO Implement encoding logic for ERC1155 Single voucher - return Err(CmaParserError::Message(String::from("Not Implemented yet"))); + let app_address = app_address.ok_or(CmaParserError::MalformedInput)?; + handle_erc1155_single_voucher_encoding(app_address, &voucher_request) } CmaParserVoucherType::CmaParserVoucherTypeErc1155Batch => { - // TODO Implement encoding logic for ERC1155 Batch voucher - return Err(CmaParserError::Message(String::from("Not Implemented yet"))); - } - CmaParserVoucherType::CmaParserVoucherTypeNone => { - return Err(CmaParserError::Message(String::from("Not Implemented yet"))) + let app_address = app_address.ok_or(CmaParserError::MalformedInput)?; + handle_erc1155_batch_voucher_encoding(app_address, &voucher_request) } + CmaParserVoucherType::CmaParserVoucherTypeNone => Err(CmaParserError::IncompatibleInput), + }; + + if let Err(ref err) = result { + set_last_parser_error(err.message()); } + result } diff --git a/src/types.rs b/src/types.rs index aadde74..3fa995a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -126,114 +126,6 @@ impl U256CBindingsExt for U256 { } } -// /// Ethereum address (20 bytes) -// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -// pub struct Address([u8; 20]); - -// impl Address { -// pub const LENGTH: usize = 20; - -// pub fn from_str(s: &str) -> Result { -// let s = s.strip_prefix("0x").unwrap_or(s); -// if s.len() != 40 { -// return Err(format!("Address hex string must be 40 characters, got {}", s.len())); -// } -// let bytes = hex::decode(s).map_err(|e| format!("Invalid hex string: {}", e))?; -// Self::from_slice(&bytes) -// } - -// pub fn new(bytes: [u8; 20]) -> Self { -// Address(bytes) -// } - -// pub fn as_bytes(&self) -> &[u8; 20] { -// &self.0 -// } - -// pub fn from_slice(slice: &[u8]) -> Result { -// if slice.len() != 20 { -// return Err(format!("Address must be 20 bytes, got {}", slice.len())); -// } -// let mut bytes = [0u8; 20]; -// bytes.copy_from_slice(slice); -// Ok(Address(bytes)) -// } - -// pub fn to_c(&self) -> bindings::cmt_abi_address_t { -// bindings::cmt_abi_address_t { data: self.0 } -// } - -// pub fn from_c(c_addr: &bindings::cmt_abi_address_t) -> Self { -// Address(c_addr.data) -// } -// } - -// impl fmt::Display for Address { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// write!(f, "0x{}", hex::encode(self.0)) -// } -// } - -// /// U256 value (32 bytes, big-endian) -// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -// pub struct U256([u8; 32]); - -// impl U256 { -// pub const LENGTH: usize = 32; - -// pub fn new(bytes: [u8; 32]) -> Self { -// U256(bytes) -// } - -// pub fn zero() -> Self { -// U256([0u8; 32]) -// } - -// pub fn from_u64(value: u64) -> Self { -// let mut bytes = [0u8; 32]; -// // Big-endian encoding -// bytes[24..32].copy_from_slice(&value.to_be_bytes()); -// U256(bytes) -// } - -// pub fn to_u128(&self) -> Option { -// // Check if the value fits in u128 (lower 16 bytes must be zero) -// if self.0[..16].iter().any(|&b| b != 0) { -// return None; -// } -// let mut bytes = [0u8; 16]; -// bytes.copy_from_slice(&self.0[16..32]); -// Some(u128::from_be_bytes(bytes)) -// } - -// pub fn as_bytes(&self) -> &[u8; 32] { -// &self.0 -// } - -// pub fn from_slice(slice: &[u8]) -> Result { -// if slice.len() != 32 { -// return Err(format!("U256 must be 32 bytes, got {}", slice.len())); -// } -// let mut bytes = [0u8; 32]; -// bytes.copy_from_slice(slice); -// Ok(U256(bytes)) -// } - -// pub fn to_c(&self) -> bindings::cmt_abi_u256_t { -// bindings::cmt_abi_u256_t { data: self.0 } -// } - -// pub fn from_c(c_u256: &bindings::cmt_abi_u256_t) -> Self { -// U256(c_u256.data) -// } -// } - -// impl fmt::Display for U256 { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// write!(f, "0x{}", hex::encode(self.0)) -// } -// } - /// Account ID (32 bytes) pub type AccountId = U256; @@ -257,6 +149,7 @@ pub enum RetrieveOperation { Find, Create, FindOrCreate, + FindAndRemove, } impl RetrieveOperation { @@ -269,6 +162,9 @@ impl RetrieveOperation { RetrieveOperation::FindOrCreate => { bindings::cma_ledger_retrieve_operation_t_CMA_LEDGER_OP_FIND_OR_CREATE } + RetrieveOperation::FindAndRemove => { + bindings::cma_ledger_retrieve_operation_t_CMA_LEDGER_OP_FIND_AND_REMOVE + } } } } @@ -277,20 +173,26 @@ impl RetrieveOperation { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AssetType { Id, + Base, TokenAddress, TokenAddressId, + TokenAddressIdAmount, } impl AssetType { pub fn to_c(&self) -> bindings::cma_ledger_asset_type_t { match self { AssetType::Id => bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_ID, + AssetType::Base => bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_BASE, AssetType::TokenAddress => { bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_TOKEN_ADDRESS } AssetType::TokenAddressId => { bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_TOKEN_ADDRESS_ID } + AssetType::TokenAddressIdAmount => { + bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_TOKEN_ADDRESS_ID_AMOUNT + } } } } @@ -317,12 +219,34 @@ impl AccountType { } } -/// Helper to convert bytes to C bytes struct -pub fn bytes_to_c_bytes(data: &[u8]) -> bindings::cmt_abi_bytes_t { - bindings::cmt_abi_bytes_t { - data: data.as_ptr() as *mut std::ffi::c_void, - length: data.len(), +/// Owned byte buffer that can be safely passed to C as `cmt_abi_bytes_t`. +#[derive(Debug, Clone)] +pub struct OwnedCBytes { + inner: Vec, +} + +impl OwnedCBytes { + pub fn new(data: impl Into>) -> Self { + Self { + inner: data.into(), + } } + + pub fn as_c_bytes(&self) -> bindings::cmt_abi_bytes_t { + bindings::cmt_abi_bytes_t { + data: self.inner.as_ptr() as *mut std::ffi::c_void, + length: self.inner.len(), + } + } + + pub fn as_slice(&self) -> &[u8] { + &self.inner + } +} + +/// Helper to convert bytes into an owned C-compatible buffer wrapper. +pub fn bytes_to_c_bytes(data: &[u8]) -> OwnedCBytes { + OwnedCBytes::new(data.to_vec()) } /// Helper to safely extract bytes from C bytes struct diff --git a/tests/ledger_tests.rs b/tests/ledger_tests.rs index 43a8ac1..c68c4c4 100644 --- a/tests/ledger_tests.rs +++ b/tests/ledger_tests.rs @@ -645,6 +645,15 @@ fn test_find_nonexistent_account() { } } +#[test] +fn test_retrieve_ether_asset() { + let mut ledger = Ledger::new().expect("Failed to initialize ledger"); + let asset_id = ledger + .retrieve_ether_assets() + .expect("Should create base ether asset"); + assert!(asset_id.0 > 0, "Ether asset ID should be non-zero"); +} + #[test] fn test_large_amounts() { let mut ledger = Ledger::new().expect("Failed to initialize ledger"); diff --git a/tests/parser_tests.rs b/tests/parser_tests.rs index f58913e..05b8e62 100644 --- a/tests/parser_tests.rs +++ b/tests/parser_tests.rs @@ -72,47 +72,75 @@ fn test_ether_deposit_success() { // let sender: Address = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" // .parse() // .unwrap(); -// let amount = U256::from_dec_str("300000000000000000000").unwrap(); // 300 ERC20 tokens in wei +// let amount = U256::from_dec_str("300000000000000000000").unwrap(); // let token_address: Address = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C" // .parse() -// .unwrap(); // TEST token address -// let payload = "0xfbdb734ef6a23ad76863cba6f10d0c5cbbd8342cf39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000001043561a8829300000".to_string(); // Sample payload from the ERC20 portal - +// .unwrap(); +// let payload = "0xfbdb734ef6a23ad76863cba6f10d0c5cbbd8342cf39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000001043561a8829300000".to_string(); +// // let input = create_test_input( // CARTESI_ADDRESSES // .get_portal_address(Portals::ERC20Portal) // .unwrap(), // &payload, // ); - +// // match cma_decode_advance(CmaParserInputType::CmaParserInputTypeErc20Deposit, input) { // Ok(result) => { // let is_correct_method = // result.req_type == CmaParserInputType::CmaParserInputTypeErc20Deposit; // let is_correct_sender = if let CmaParserInputData::Erc20Deposit(deposit) = result.input // { -// if deposit.amount == amount +// deposit.amount == amount // && deposit.sender == sender // && deposit.token == token_address -// { -// true -// } else { -// false -// } // } else { // false // }; - -// assert_eq!(true, is_correct_method, "Expected ERC20 Deposit method"); -// assert_eq!( -// true, is_correct_sender, -// "Expected correct sender, token address and amount" -// ); +// +// assert!(is_correct_method, "Expected ERC20 Deposit method"); +// assert!(is_correct_sender, "Expected correct sender, token address and amount"); // } // Err(e) => panic!("Expected success, got error: {:?}", e), // } // } +#[test] +fn test_erc20_deposit_success() { + let sender: Address = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + .parse() + .unwrap(); + let amount = U256::from_dec_str("300000000000000000000").unwrap(); + let token_address: Address = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C" + .parse() + .unwrap(); + let payload = "0xfbdb734ef6a23ad76863cba6f10d0c5cbbd8342cf39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000001043561a8829300000".to_string(); + + let input = create_test_input( + CARTESI_ADDRESSES + .get_portal_address(Portals::ERC20Portal) + .unwrap(), + &payload, + ); + + match cma_decode_advance(CmaParserInputType::CmaParserInputTypeErc20Deposit, input) { + Ok(result) => { + assert_eq!( + result.req_type, + CmaParserInputType::CmaParserInputTypeErc20Deposit + ); + if let CmaParserInputData::Erc20Deposit(deposit) = result.input { + assert_eq!(deposit.amount, amount); + assert_eq!(deposit.sender, sender); + assert_eq!(deposit.token, token_address); + } else { + panic!("Expected ERC20 deposit payload"); + } + } + Err(e) => panic!("Expected success, got error: {:?}", e), + } +} + #[test] fn test_erc721_deposit_success() { let sender: Address = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" @@ -312,10 +340,10 @@ fn test_ether_transfer_success() { let amount = U256::from_dec_str("1500000000000000000").unwrap(); // 1.5 Ether in wei let abi_encoded_input = abi_encode_call( - "TransferEther(uint256,bytes32,bytes)", + "TransferEther(bytes32,uint256,bytes)", vec![ - Token::Uint(amount), Token::FixedBytes(recipient), + Token::Uint(amount), Token::Bytes(vec![0x0]), ], ) @@ -472,13 +500,17 @@ fn test_ether_voucher_encoding_success() { amount, }); - match cma_encode_voucher(CmaParserVoucherType::CmaParserVoucherTypeEther, request) { + match cma_encode_voucher(CmaParserVoucherType::CmaParserVoucherTypeEther, None, request) { Ok(voucher) => { - // Basic checks on the voucher structure assert!( voucher.destination.to_lowercase() == receipient_string, "Incorrect destination address in voucher" ); + assert_eq!( + voucher.value, + format!("0x{}", hex::encode(expected_value_bytes)), + "Incorrect value in voucher" + ); assert!( voucher.payload == "0x", "Payload should be empty for Ether transfer" @@ -516,7 +548,7 @@ fn test_erc20_voucher_encoding_success() { amount, receiver: recipient, }); - match cma_encode_voucher(CmaParserVoucherType::CmaParserVoucherTypeErc20, request) { + match cma_encode_voucher(CmaParserVoucherType::CmaParserVoucherTypeErc20, None, request) { Ok(voucher) => { // Basic checks on the voucher structure assert!( @@ -553,7 +585,7 @@ fn test_erc721_voucher_encoding_success() { Token::Uint(token_id), ]; - let function_sig = "transferFrom(address,address,uint256)"; + let function_sig = "safeTransferFrom(address,address,uint256)"; let selector = &id(function_sig)[..4]; let encoded_args = encode(&args); @@ -568,7 +600,11 @@ fn test_erc721_voucher_encoding_success() { receiver: recipient, application_address, }); - match cma_encode_voucher(CmaParserVoucherType::CmaParserVoucherTypeErc721, request) { + match cma_encode_voucher( + CmaParserVoucherType::CmaParserVoucherTypeErc721, + Some(application_address), + request, + ) { Ok(voucher) => { // Basic checks on the voucher structure assert!( @@ -592,7 +628,7 @@ fn test_ledger_get_balance_success() { let erc20_token: Address = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C" .parse() .unwrap(); // TEST token address - let payload = r#"{"method": "ledgerGetBalance", "params": ["0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C", [1,2]]}"#.to_string(); + let payload = r#"{"method": "ledger_getBalance", "params": ["0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C", "0x1"]}"#.to_string(); // let payload = r#"{"method": "ledgerGetBalance", "params": ["0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C", []]}"#.to_string(); let payload_hex = hex::encode(payload.as_bytes()); @@ -603,14 +639,11 @@ fn test_ledger_get_balance_success() { let is_correct_method = result.req_type == CmaParserInputType::CmaParserInputTypeBalance; let is_correct_address = if let CmaParserInputData::Balance(data) = result.input { - if data.account == address + let mut expected_account = [0u8; 32]; + expected_account[12..].copy_from_slice(address.as_bytes()); + data.account == U256::from_big_endian(&expected_account) && data.token == erc20_token - && data.token_ids == Some(vec![U256::from(1), U256::from(2)]) - { - true - } else { - false - } + && data.token_id == U256::from(1) } else { false }; @@ -633,7 +666,7 @@ fn test_ledger_get_total_supply_success() { let erc20_token: Address = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C" .parse() .unwrap(); // TEST token address - let payload = r#"{"method": "ledgerGetTotalSupply", "params": ["0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C", [1,2]]}"#.to_string(); + let payload = r#"{"method": "ledger_getTotalSupply", "params": ["0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C", "0x1"]}"#.to_string(); let payload_hex = hex::encode(payload.as_bytes()); let input = create_test_input(&address.to_string(), &payload_hex); @@ -641,12 +674,7 @@ fn test_ledger_get_total_supply_success() { Ok(result) => { let is_correct_method = result.req_type == CmaParserInputType::CmaParserInputTypeSupply; let is_correct_address = if let CmaParserInputData::Supply(data) = result.input { - if data.token == erc20_token && data.token_ids == vec![U256::from(1), U256::from(2)] - { - true - } else { - false - } + data.token == erc20_token && data.token_id == U256::from(1) } else { false }; diff --git a/tests/parser_vectors.rs b/tests/parser_vectors.rs new file mode 100644 index 0000000..4e33948 --- /dev/null +++ b/tests/parser_vectors.rs @@ -0,0 +1,164 @@ +//! Canonical parser vectors ported from `third_party/machine-asset-tools/tests/parser.c`. + +use ethers_core::types::{Address, U256}; +use json::JsonValue; +use libcma_binding_rust::parser::{ + cma_decode_advance, cma_decode_inspect, cma_encode_voucher, CmaParserErc721VoucherFields, + CmaParserEtherVoucherFields, CmaParserInputData, CmaParserInputType, CmaParserVoucherType, + CmaVoucherFieldType, +}; + +fn advance_input(payload_hex: &str) -> JsonValue { + let mut input = JsonValue::new_object(); + input["data"]["metadata"]["msg_sender"] = "0x0000000000000000000000000000000000000001".into(); + input["data"]["payload"] = payload_hex.into(); + input +} + +fn inspect_input(payload_json: &str) -> JsonValue { + let mut input = JsonValue::new_object(); + input["data"]["metadata"]["msg_sender"] = "0x0000000000000000000000000000000000000001".into(); + input["data"]["payload"] = hex::encode(payload_json.as_bytes()).into(); + input +} + +#[test] +fn vector_ether_deposit_from_parser_c() { + let payload = concat!( + "0x", + "0000000000000000000000000000000000000001", + "0000000000000000000000000000000000000000000000000000000000000004" + ); + let result = cma_decode_advance( + CmaParserInputType::CmaParserInputTypeEtherDeposit, + advance_input(payload), + ) + .expect("ether deposit should decode"); + + if let CmaParserInputData::EtherDeposit(deposit) = result.input { + let mut expected_sender = [0u8; 20]; + expected_sender[19] = 1; + assert_eq!(deposit.sender, Address::from(expected_sender)); + assert_eq!(deposit.amount, U256::from(4u64)); + assert!(deposit.exec_layer_data.is_empty()); + } else { + panic!("expected ether deposit"); + } +} + +#[test] +fn vector_erc20_deposit_from_parser_c() { + let payload = concat!( + "0x", + "00000000000000000000000000000000000000ff", + "0000000000000000000000000000000000000001", + "0000000000000000000000000000000000000000000000000000000000000004" + ); + let result = cma_decode_advance( + CmaParserInputType::CmaParserInputTypeErc20Deposit, + advance_input(payload), + ) + .expect("erc20 deposit should decode"); + + if let CmaParserInputData::Erc20Deposit(deposit) = result.input { + let mut expected_token = [0u8; 20]; + expected_token[19] = 0xff; + let mut expected_sender = [0u8; 20]; + expected_sender[19] = 1; + assert_eq!(deposit.token, Address::from(expected_token)); + assert_eq!(deposit.sender, Address::from(expected_sender)); + assert_eq!(deposit.amount, U256::from(4u64)); + } else { + panic!("expected erc20 deposit"); + } +} + +#[test] +fn vector_ether_transfer_from_parser_c() { + let payload = concat!( + "0x", + "ff67c903", + "0000000000000000000000000000000000000000000000000000000000000002", + "0000000000000000000000000000000000000000000000000000000000000011", + "0000000000000000000000000000000000000000000000000000000000000060", + "0000000000000000000000000000000000000000000000000000000000000000" + ); + let result = cma_decode_advance( + CmaParserInputType::CmaParserInputTypeAuto, + advance_input(payload), + ) + .expect("ether transfer should decode"); + + if let CmaParserInputData::EtherTransfer(transfer) = result.input { + assert_eq!(transfer.receiver, U256::from(2u64)); + assert_eq!(transfer.amount, U256::from(17u64)); + } else { + panic!("expected ether transfer"); + } +} + +#[test] +fn vector_ledger_get_balance_from_parser_c() { + let payload = r#"{"method":"ledger_getBalance","params":["0x0000000000000000000000000000000000000001"]}"#; + let result = + cma_decode_inspect(inspect_input(payload)).expect("ledger_getBalance should decode"); + + if let CmaParserInputData::Balance(balance) = result.input { + assert_eq!(balance.account, U256::from(1u64)); + } else { + panic!("expected balance inspect"); + } +} + +#[test] +fn vector_erc721_voucher_from_parser_c() { + let app_address = Address::from_slice(&[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xbe, 0xef, + ]); + let receiver = Address::from_slice(&[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + ]); + let token = Address::from_slice(&[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, + ]); + + let request = CmaVoucherFieldType::Erc721VoucherFields(CmaParserErc721VoucherFields { + token, + token_id: U256::from(1u64), + receiver, + application_address: app_address, + }); + + let voucher = cma_encode_voucher( + CmaParserVoucherType::CmaParserVoucherTypeErc721, + Some(app_address), + request, + ) + .expect("erc721 voucher should encode"); + + assert!(voucher.payload.starts_with("0x42842e0e")); + assert_eq!(voucher.value, "0x"); +} + +#[test] +fn vector_ether_voucher_value_from_parser_c() { + let receiver: Address = "0x3e157927fb178490941bb18adcdc4144e442e32a" + .parse() + .unwrap(); + let amount = U256::from_dec_str("1500000000000000000").unwrap(); + let request = CmaVoucherFieldType::EtherVoucherFields(CmaParserEtherVoucherFields { + receiver, + amount, + }); + + let voucher = cma_encode_voucher(CmaParserVoucherType::CmaParserVoucherTypeEther, None, request) + .expect("ether voucher should encode"); + + let mut expected_value = [0u8; 32]; + amount.to_big_endian(&mut expected_value); + assert_eq!( + voucher.value, + format!("0x{}", hex::encode(expected_value)) + ); + assert_eq!(voucher.payload, "0x"); +} From d5ee54da7669611c2d6303ccf6c6a53c05bea782 Mon Sep 17 00:00:00 2001 From: Idogwu Chinonso Date: Fri, 19 Jun 2026 03:31:00 +0100 Subject: [PATCH 2/2] Mod(helpers): removed helpers reference to static cartesi addresses --- Cargo.lock | 1 - Cargo.toml | 1 - src/helpers.rs | 63 ------------------------------------------- tests/parser_tests.rs | 30 ++++++++------------- 4 files changed, 11 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 413dad8..952dd0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -603,7 +603,6 @@ dependencies = [ "hex", "json", "lazy_static", - "once_cell", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2d17267..f6d96cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/src/helpers.rs b/src/helpers.rs index 13ff85e..0a87a3b 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,5 +1,4 @@ use hex; -use once_cell::sync::Lazy; use ethers_core::types::{Address}; use json::{JsonValue, object}; use crate::parser::CmaVoucher; @@ -11,68 +10,6 @@ pub fn hex_to_string(hex: &str) -> Result> { 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 = 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; } diff --git a/tests/parser_tests.rs b/tests/parser_tests.rs index 05b8e62..cc9279f 100644 --- a/tests/parser_tests.rs +++ b/tests/parser_tests.rs @@ -2,13 +2,20 @@ use ethers_core::abi::{encode, AbiParser, FixedBytes, Token}; use ethers_core::types::{Address, U256}; use ethers_core::utils::id; use json::JsonValue; -use libcma_binding_rust::helpers::{PortalMatcher, Portals, CARTESI_ADDRESSES}; use libcma_binding_rust::parser::{ cma_decode_advance, cma_decode_inspect, cma_encode_voucher, CmaParserErc20VoucherFields, CmaParserErc721VoucherFields, CmaParserEtherVoucherFields, CmaParserInputData, CmaParserInputType, CmaParserVoucherType, CmaVoucherFieldType, }; +// Portal addresses used purely as the `msg_sender` of test inputs. Portal +// matching is a consumer concern (the library no longer ships addresses); +// `cma_decode_advance` selects the decoder from the explicit `req_type`, so the +// exact value here does not affect decoding. +const ETHER_PORTAL: &str = "0xc70076a466789B595b50959cdc261227F0D70051"; +const ERC20_PORTAL: &str = "0xc700D6aDd016eECd59d989C028214Eaa0fCC0051"; +const ERC721_PORTAL: &str = "0xc700d52F5290e978e9CAe7D1E092935263b60051"; + // Helper to create a basic JsonValue input structure fn create_test_input(msg_sender: &str, payload: &str) -> JsonValue { let mut input = JsonValue::new_object(); @@ -34,12 +41,7 @@ fn test_ether_deposit_success() { let amount = U256::from_dec_str("2000000000000000000").unwrap(); // 2 Ether in wei let payload = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000001bc16d674ec80000".to_string(); // Sample payload from the ether portal - let input = create_test_input( - CARTESI_ADDRESSES - .get_portal_address(Portals::EtherPortal) - .unwrap(), - &payload, - ); + let input = create_test_input(ETHER_PORTAL, &payload); match cma_decode_advance(CmaParserInputType::CmaParserInputTypeEtherDeposit, input) { Ok(result) => { @@ -116,12 +118,7 @@ fn test_erc20_deposit_success() { .unwrap(); let payload = "0xfbdb734ef6a23ad76863cba6f10d0c5cbbd8342cf39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000001043561a8829300000".to_string(); - let input = create_test_input( - CARTESI_ADDRESSES - .get_portal_address(Portals::ERC20Portal) - .unwrap(), - &payload, - ); + let input = create_test_input(ERC20_PORTAL, &payload); match cma_decode_advance(CmaParserInputType::CmaParserInputTypeErc20Deposit, input) { Ok(result) => { @@ -152,12 +149,7 @@ fn test_erc721_deposit_success() { .unwrap(); // Sample ERC721 token address let payload = "0xba46623ad94ab45850c4ecba9555d26328917c3bf39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".to_string(); // Sample payload from the ERC721 portal - let input = create_test_input( - CARTESI_ADDRESSES - .get_portal_address(Portals::ERC721Portal) - .unwrap(), - &payload, - ); + let input = create_test_input(ERC721_PORTAL, &payload); match cma_decode_advance(CmaParserInputType::CmaParserInputTypeErc721Deposit, input) { Ok(result) => { let is_correct_method =