diff --git a/.changeset/cmio-libcmt-v2.md b/.changeset/cmio-libcmt-v2.md
index d1823a6a..bfe93daa 100644
--- a/.changeset/cmio-libcmt-v2.md
+++ b/.changeset/cmio-libcmt-v2.md
@@ -2,29 +2,34 @@
"@deroll/cmio": minor
---
-Port the binding to the libcmt v2 API overhaul (machine-guest-tools `feature/libcmt-v2`).
+Port the binding to the libcmt v2 API overhaul (machine-guest-tools
+`7ffb2da`, "output indexing" era).
This is a **breaking** change. libcmt v2 made the rollup layer raw I/O only and moved
-all EVM-ABI encoding/decoding into a separate `codec` module, so the binding now mirrors
+all EVM-ABI encoding/decoding into a separate `codec` module, so the binding mirrors
that split:
-- **`Rollup` is now a thin, raw wrapper.** `finish()` is replaced by
+- **`Rollup` is a thin, raw wrapper.** `finish()` is replaced by
`waitForInput({ accept })`, which returns `{ type, payload }` with the **raw, undecoded**
input bytes. Outputs are emitted with `emitOutput(bytes)` (returns the output index);
`emitReport`/`emitException`/`progress`/`close`/`run` are unchanged in spirit.
-- **ABI encoding/decoding moved to JS helpers.** New functions
- `decodeAdvance(input)`, `encodeNotice(payload)`, `encodeVoucher({ destination, value, payload })`
- and `encodeDelegateCallVoucher({ destination, payload })` mirror libcmt's codec module,
- implemented with [`ox`](https://oxlib.sh) (a new runtime dependency) for the EVM-ABI work.
+- **ABI encoding/decoding lives in JS helpers**, one per libcmt `codec.h` entry,
+ implemented with [`ox`](https://oxlib.sh) (a new runtime dependency) and verified
+ byte-for-byte against the C encoders: `decodeAdvance`/`encodeAdvance`
+ (`EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes)`),
+ `encodeNotice` (`Notice(bytes)`), `encodeCallVoucher` (`CallVoucher(address,uint256,bytes)`),
+ and the new asset-transfer outputs `encodeERC20Transfer`, `encodeERC721Transfer`,
+ `encodeERC1155SingleTransfer` and `encodeERC1155BatchTransfer`.
Compose them with the raw API, e.g. `rollup.emitOutput(encodeNotice(payload))` and
`decodeAdvance(request.payload)`.
-- **The on-chain output format changed.** Notices and vouchers are now encoded as the
- `Output1..Output4(bytes32[N],bytes)` envelope with a 32-byte type tag, not the old
- `Notice(bytes)` / `Voucher(address,uint256,bytes)` selectors. This tracks a newer
- rollups-contracts version.
-- **Removed:** `gio()` (libcmt v2 dropped generic IO support entirely) and the
- `loadMerkle`/`saveMerkle`/`resetMerkle` methods (no longer part of the rollup API;
- `cmt_merkle_reset` is also absent from the v2 sources).
+- **The wire formats track the new rollups contracts.** `EvmAdvance` narrows
+ `chainId`/`blockNumber`/`blockTimestamp`/`index` to `uint64` (new selector), and
+ outputs use plain per-type selectors (`Notice`, `CallVoucher`, `ERC*Transfer`)
+ instead of the previous `Voucher(address,uint256,bytes)` format.
+- **Removed:** delegate-call vouchers (dropped by libcmt v2 along with the
+ `Output1..Output4` envelope design), `gio()` (libcmt v2 dropped generic IO support),
+ and the `loadMerkle`/`saveMerkle`/`resetMerkle` methods (no longer part of the
+ rollup API).
- The high-level `emitVoucher`/`emitNotice`/`emitDelegateCallVoucher` methods and the
decoded `AdvanceRequest` fields on `finish()` are gone; use the `encode*`/`decodeAdvance`
helpers instead.
diff --git a/apps/docs/pages/cmio/guide/emitting-outputs.mdx b/apps/docs/pages/cmio/guide/emitting-outputs.mdx
index 4a52beed..137c7105 100644
--- a/apps/docs/pages/cmio/guide/emitting-outputs.mdx
+++ b/apps/docs/pages/cmio/guide/emitting-outputs.mdx
@@ -4,7 +4,8 @@ While handling a request your application can emit four kinds of outputs. They d
| Output | Provable | Purpose |
| --- | --- | --- |
-| [Voucher](/cmio/reference/encode-voucher) | yes | Execute a transaction on-chain (transfer assets, call a contract) |
+| [Call voucher](/cmio/reference/encode-call-voucher) | yes | Execute a transaction on-chain (transfer ether, call a contract) |
+| [Asset transfers](/cmio/reference/encode-asset-transfers) | yes | Typed ERC-20/721/1155 transfers out of the application |
| [Notice](/cmio/reference/encode-notice) | yes | Attest a fact about the application state |
| [Report](/cmio/reference/emit-report) | no | Logs, diagnostics, inspect responses |
| [Exception](/cmio/reference/emit-exception) | no | Signal that the request could not be processed |
@@ -16,12 +17,12 @@ Provable outputs are built in two steps: an `encode*` helper produces the EVM-AB
A voucher is a one-shot transaction the application authorizes: when the epoch containing it settles, anyone can execute it through the application contract. Use it to transfer assets out, call DeFi protocols, or trigger any on-chain effect:
```ts twoslash
-import { Rollup, encodeVoucher } from '@deroll/cmio';
+import { Rollup, encodeCallVoucher } from '@deroll/cmio';
const rollup = new Rollup();
// ---cut---
// transfer 1 ETH from the application contract to a user
const index = rollup.emitOutput(
- encodeVoucher({
+ encodeCallVoucher({
destination: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
value: 1_000_000_000_000_000_000n, // wei
payload: '0x', // no calldata — plain value transfer
@@ -29,24 +30,26 @@ const index = rollup.emitOutput(
);
```
-To call a contract, put the EVM calldata in `payload` — for example an ERC-20 `transfer(address,uint256)`:
+To call a contract, put the EVM calldata in `payload`. For plain token withdrawals, prefer the typed asset-transfer outputs below.
+
+## Asset transfers: withdraw tokens
+
+The new rollups contracts understand first-class asset-transfer outputs, so an application transfers ERC-20/721/1155 tokens out without hand-crafting calldata:
```ts twoslash
-import { Rollup, encodeVoucher } from '@deroll/cmio';
+import { Rollup, encodeERC20Transfer } from '@deroll/cmio';
const rollup = new Rollup();
// ---cut---
-const recipient = 'f39Fd6e51aad88F6F4ce6aB8827279cffFb92266'.padStart(64, '0');
-const amount = (10n ** 18n).toString(16).padStart(64, '0');
-
rollup.emitOutput(
- encodeVoucher({
- destination: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // token contract
- payload: `0xa9059cbb${recipient}${amount}`, // transfer(address,uint256) selector + args
+ encodeERC20Transfer({
+ recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
+ token: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ value: 10n ** 18n,
}),
);
```
-There is also a [delegate-call variant](/cmio/reference/encode-delegate-call-voucher) that executes the target's code in the application contract's own storage context — an advanced tool for upgrade-style patterns.
+See the [asset transfer encoders](/cmio/reference/encode-asset-transfers) for the ERC-721 and ERC-1155 (single and batch) variants.
## Notices: attest facts
@@ -102,8 +105,8 @@ rollup.emitReport(new Uint8Array([1, 2, 3])); // Uint8Array
The `encode*` helpers validate their structured arguments:
-- **Addresses** (`destination`) must be exactly 20 bytes; as strings they are 0x-prefixed hex.
-- **256-bit values** (`value`) accept `bigint`, `number`, or 32 bytes.
+- **Addresses** (`destination`, `recipient`, `token`) must be exactly 20 bytes; as strings they are 0x-prefixed hex.
+- **256-bit values** (`value`, `tokenId`) accept `bigint`, `number`, or 32 bytes.
- Outgoing data is always a `Buffer`; decoded advance numbers are `bigint` and addresses are 0x-hex strings.
Hex strings are typed as `` `0x${string}` `` ([`Hex`](/cmio/reference/types#input-types)), so values from [viem](https://viem.sh) — `Hex` from `encodeFunctionData`, `Address`, `bigint` from `parseEther` — pass in directly, and decoded request addresses pass back into viem without casts.
diff --git a/apps/docs/pages/cmio/guide/handling-requests.mdx b/apps/docs/pages/cmio/guide/handling-requests.mdx
index 01280224..ea4a0224 100644
--- a/apps/docs/pages/cmio/guide/handling-requests.mdx
+++ b/apps/docs/pages/cmio/guide/handling-requests.mdx
@@ -2,7 +2,7 @@
A Cartesi application processes two kinds of requests:
-- **Advance** — an input submitted on-chain. It can change application state and produce provable outputs ([vouchers](/cmio/reference/encode-voucher) and [notices](/cmio/reference/encode-notice)).
+- **Advance** — an input submitted on-chain. It can change application state and produce provable outputs ([vouchers](/cmio/reference/encode-call-voucher), [asset transfers](/cmio/reference/encode-asset-transfers) and [notices](/cmio/reference/encode-notice)).
- **Inspect** — an off-chain, read-only query. It can only answer with [reports](/cmio/reference/emit-report).
This page shows the request lifecycle; [Emitting Outputs](/cmio/guide/emitting-outputs) covers what you can produce while handling them.
diff --git a/apps/docs/pages/cmio/guide/testing.mdx b/apps/docs/pages/cmio/guide/testing.mdx
index 4db40bb8..5d5772f9 100644
--- a/apps/docs/pages/cmio/guide/testing.mdx
+++ b/apps/docs/pages/cmio/guide/testing.mdx
@@ -31,52 +31,29 @@ advance.report-0.bin # first report
advance.exception-0.bin # first exception
```
-Provable outputs are EVM-ABI encoded in the `Output1..Output4(bytes32[N],bytes)` envelope: a 4-byte function selector, a 32-byte type tag identifying the output kind (notice, call voucher, …), then the ABI-encoded fields. Reports and exceptions are raw bytes.
+Provable outputs are EVM-ABI encoded calls with a per-type function selector — `Notice(bytes)`, `CallVoucher(address,uint256,bytes)`, `ERC20Transfer(address,address,uint256)`, … — exactly the bytes the `encode*` helpers produce. Reports and exceptions are raw bytes.
## Generating an advance input
-An advance input file is the ABI encoding of the `EvmAdvance(uint256,address,address,uint256,uint256,uint256,uint256,bytes)` call that the on-chain input box would produce. You can craft one with foundry's `cast calldata`, or in a few lines of TypeScript:
+An advance input file is the ABI encoding of the `EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes)` call that the on-chain input box would produce. [`encodeAdvance`](/cmio/reference/decode-advance) builds one:
```ts twoslash
-// @filename: encode.ts
-const word = (value: bigint) => {
- const bytes = Buffer.alloc(32);
- let v = value;
- for (let i = 31; i >= 0 && v > 0n; i--) {
- bytes[i] = Number(v & 0xffn);
- v >>= 8n;
- }
- return bytes;
-};
-const addressWord = (hex: string) =>
- Buffer.concat([Buffer.alloc(12), Buffer.from(hex.slice(2), 'hex')]);
-const pad32 = (bytes: Buffer) =>
- Buffer.concat([bytes, Buffer.alloc((32 - (bytes.length % 32)) % 32)]);
-
-export function encodeEvmAdvance(input: {
- chainId: bigint;
- appContract: string;
- msgSender: string;
- blockNumber: bigint;
- blockTimestamp: bigint;
- prevRandao: bigint;
- index: bigint;
- payload: Buffer;
-}) {
- return Buffer.concat([
- Buffer.from('415bf363', 'hex'), // EvmAdvance selector
- word(input.chainId),
- addressWord(input.appContract),
- addressWord(input.msgSender),
- word(input.blockNumber),
- word(input.blockTimestamp),
- word(input.prevRandao),
- word(input.index),
- word(8n * 32n), // offset of the payload `bytes` field
- word(BigInt(input.payload.length)),
- pad32(input.payload),
- ]);
-}
+import fs from 'node:fs';
+import { encodeAdvance } from '@deroll/cmio';
+
+fs.writeFileSync(
+ 'input.bin',
+ encodeAdvance({
+ chainId: 31337n,
+ appContract: `0x${'02'.repeat(20)}`,
+ msgSender: `0x${'03'.repeat(20)}`,
+ blockNumber: 1n,
+ blockTimestamp: 1700000000n,
+ prevRandao: 0n,
+ index: 0n,
+ payload: Buffer.from('hello'),
+ }),
+);
```
## A complete test
@@ -84,26 +61,13 @@ export function encodeEvmAdvance(input: {
Putting it together with `node:test` — encode an input, point `CMT_INPUTS` at it, run the application, and assert on the output files:
```ts twoslash
-// @filename: encode.ts
-export declare function encodeEvmAdvance(input: {
- chainId: bigint;
- appContract: string;
- msgSender: string;
- blockNumber: bigint;
- blockTimestamp: bigint;
- prevRandao: bigint;
- index: bigint;
- payload: Buffer;
-}): Buffer;
-
// @filename: app.test.ts
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
-import { decodeAdvance, encodeNotice } from '@deroll/cmio';
-import { encodeEvmAdvance } from './encode.js';
+import { decodeAdvance, encodeAdvance, encodeNotice } from '@deroll/cmio';
test('echoes the payload as a notice', async () => {
// run in a temp dir: the mock writes output files next to the inputs
@@ -113,7 +77,7 @@ test('echoes the payload as a notice', async () => {
const input = path.join(dir, 'input.bin');
fs.writeFileSync(
input,
- encodeEvmAdvance({
+ encodeAdvance({
chainId: 31337n,
appContract: `0x${'02'.repeat(20)}`,
msgSender: `0x${'03'.repeat(20)}`,
@@ -140,15 +104,11 @@ test('echoes the payload as a notice', async () => {
})
.catch(() => {}); // inputs exhausted — session over
- // Output1 envelope: funsel (4) + notice type tag (32) + offset (32) +
- // length (32) + padded payload — so the payload starts at byte 100.
+ // Notice(bytes): funsel (4) + offset (32) + length (32) + padded
+ // payload — so the payload starts at byte 68.
const notice = fs.readFileSync(path.join(dir, 'input.output-0.bin'));
- assert.equal(notice.subarray(0, 4).toString('hex'), 'aed682a1');
- assert.equal(
- notice.subarray(4, 36).toString('hex'),
- 'e4f5829fb698a59fba2cf6128b6bf1e8ce1dc09d271c55b787781bd415db8eed',
- );
- assert.equal(notice.subarray(100, 100 + 5).toString(), 'hello');
+ assert.equal(notice.subarray(0, 4).toString('hex'), 'c258d6e5');
+ assert.equal(notice.subarray(68, 68 + 5).toString(), 'hello');
});
```
diff --git a/apps/docs/pages/cmio/reference/decode-advance.mdx b/apps/docs/pages/cmio/reference/decode-advance.mdx
index 2b61444a..a7b0ccbb 100644
--- a/apps/docs/pages/cmio/reference/decode-advance.mdx
+++ b/apps/docs/pages/cmio/reference/decode-advance.mdx
@@ -1,6 +1,8 @@
# decodeAdvance
-Decodes the raw payload of an [advance request](/cmio/reference/wait-for-input) — an EVM-ABI encoded `EvmAdvance` input — into its structured fields. This is a pure-JS helper that mirrors libcmt's `cmt_decode_advance_state`; it touches no device state.
+Decodes the raw payload of an [advance request](/cmio/reference/wait-for-input) — an EVM-ABI encoded `EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes)` input — into its structured fields. This is a pure-JS helper that mirrors libcmt's `cmt_evmadvance_decode`; it touches no device state.
+
+The inverse, `encodeAdvance`, builds the input bytes from the same fields (mirroring `cmt_evmadvance_encode`) — useful for crafting mock inputs (`CMT_INPUTS`) when [testing on the host](/cmio/guide/testing).
## Usage
diff --git a/apps/docs/pages/cmio/reference/emit-output.mdx b/apps/docs/pages/cmio/reference/emit-output.mdx
index f01ee8b1..72e65a4d 100644
--- a/apps/docs/pages/cmio/reference/emit-output.mdx
+++ b/apps/docs/pages/cmio/reference/emit-output.mdx
@@ -5,8 +5,8 @@ Emits a **provable output** — a raw, already EVM-ABI encoded output that is ad
`emitOutput` takes the encoded bytes directly. Build them with one of the standalone encoders:
- [`encodeNotice`](/cmio/reference/encode-notice) — attest a fact about the application state.
-- [`encodeVoucher`](/cmio/reference/encode-voucher) — authorize an on-chain transaction (transfer assets, call a contract).
-- [`encodeDelegateCallVoucher`](/cmio/reference/encode-delegate-call-voucher) — a `DELEGATECALL` voucher.
+- [`encodeCallVoucher`](/cmio/reference/encode-call-voucher) — authorize an on-chain transaction (transfer ether, call a contract).
+- [asset transfer encoders](/cmio/reference/encode-asset-transfers) — typed ERC-20/721/1155 transfer outputs.
## Usage
@@ -22,11 +22,11 @@ const index = rollup.emitOutput(
A voucher is emitted the same way:
```ts twoslash
-import { Rollup, encodeVoucher } from '@deroll/cmio';
+import { Rollup, encodeCallVoucher } from '@deroll/cmio';
const rollup = new Rollup();
// ---cut---
const index = rollup.emitOutput(
- encodeVoucher({
+ encodeCallVoucher({
destination: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
value: 1_000_000_000_000_000_000n, // 1 ETH in wei
}),
@@ -44,7 +44,7 @@ const index = rollup.emitOutput(
- Type: `BytesLike` — 0x-prefixed hex string, `Buffer` or `Uint8Array`
- Required
-The already-encoded output bytes. Produce them with [`encodeNotice`](/cmio/reference/encode-notice), [`encodeVoucher`](/cmio/reference/encode-voucher) or [`encodeDelegateCallVoucher`](/cmio/reference/encode-delegate-call-voucher).
+The already-encoded output bytes. Produce them with [`encodeNotice`](/cmio/reference/encode-notice), [`encodeCallVoucher`](/cmio/reference/encode-call-voucher) or the [asset transfer encoders](/cmio/reference/encode-asset-transfers).
## Errors
diff --git a/apps/docs/pages/cmio/reference/encode-asset-transfers.mdx b/apps/docs/pages/cmio/reference/encode-asset-transfers.mdx
new file mode 100644
index 00000000..c34c74db
--- /dev/null
+++ b/apps/docs/pages/cmio/reference/encode-asset-transfers.mdx
@@ -0,0 +1,86 @@
+# Asset transfer encoders
+
+Encoders for the **typed asset-transfer outputs** introduced by the new rollups contracts: instead of hand-crafting a [call voucher](/cmio/reference/encode-call-voucher) with ERC-20/721/1155 calldata, the application emits a first-class transfer output that the contracts know how to execute. Each helper mirrors one entry of libcmt's codec module and produces the exact same bytes.
+
+| Function | Wire format |
+| --- | --- |
+| `encodeERC20Transfer` | `ERC20Transfer(address,address,uint256)` |
+| `encodeERC721Transfer` | `ERC721Transfer(address,address,uint256,bytes)` |
+| `encodeERC1155SingleTransfer` | `ERC1155SingleTransfer(address,address,uint256,uint256,bytes)` |
+| `encodeERC1155BatchTransfer` | `ERC1155BatchTransfer(address,address,uint256[2][],bytes)` |
+
+All of them return a `Buffer`, ready to pass to [`emitOutput`](/cmio/reference/emit-output).
+
+## Usage
+
+```ts twoslash
+import {
+ Rollup,
+ encodeERC20Transfer,
+ encodeERC721Transfer,
+ encodeERC1155SingleTransfer,
+ encodeERC1155BatchTransfer,
+} from '@deroll/cmio';
+const rollup = new Rollup();
+// ---cut---
+const recipient = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
+
+// 100 units of an ERC-20 token
+rollup.emitOutput(
+ encodeERC20Transfer({
+ recipient,
+ token: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ value: 100n,
+ }),
+);
+
+// one ERC-721 token
+rollup.emitOutput(
+ encodeERC721Transfer({
+ recipient,
+ token: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
+ tokenId: 7n,
+ }),
+);
+
+// ERC-1155: a single (id, value) or a batch of [id, value] pairs
+rollup.emitOutput(
+ encodeERC1155SingleTransfer({
+ recipient,
+ token: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
+ tokenId: 7n,
+ value: 3n,
+ }),
+);
+rollup.emitOutput(
+ encodeERC1155BatchTransfer({
+ recipient,
+ token: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
+ tokenIdsAndValues: [
+ [1n, 2n],
+ [3n, 4n],
+ ],
+ }),
+);
+```
+
+## Parameters
+
+All four take a named options object:
+
+| Field | Type | Applies to | Description |
+| --- | --- | --- | --- |
+| `recipient` | `AddressLike` | all | Address receiving the assets (20 bytes) |
+| `token` | `AddressLike` | all | Token contract address (20 bytes) |
+| `value` | `U256Like` | ERC-20, ERC-1155 single | Amount of tokens to transfer |
+| `tokenId` | `U256Like` | ERC-721, ERC-1155 single | Token id to transfer |
+| `tokenIdsAndValues` | `[U256Like, U256Like][]` | ERC-1155 batch | `[tokenId, value]` pairs |
+| `data` | `BytesLike`, default empty | ERC-721, ERC-1155 | Extra data forwarded to the recipient |
+
+## Errors
+
+| Condition | Error |
+| --- | --- |
+| `recipient`/`token` not 20 bytes / malformed hex | `TypeError` |
+| `value`/`tokenId` negative or ≥ 2²⁵⁶ | `RangeError` |
+| `tokenIdsAndValues` entry not a `[tokenId, value]` pair | `TypeError` |
diff --git a/apps/docs/pages/cmio/reference/encode-voucher.mdx b/apps/docs/pages/cmio/reference/encode-call-voucher.mdx
similarity index 69%
rename from apps/docs/pages/cmio/reference/encode-voucher.mdx
rename to apps/docs/pages/cmio/reference/encode-call-voucher.mdx
index b22d98c0..4c35c910 100644
--- a/apps/docs/pages/cmio/reference/encode-voucher.mdx
+++ b/apps/docs/pages/cmio/reference/encode-call-voucher.mdx
@@ -1,19 +1,19 @@
-# encodeVoucher
+# encodeCallVoucher
-Encodes a **voucher** — a one-shot on-chain transaction authorized by the application: a destination, an ether value, and calldata. Once the epoch containing it settles, the voucher can be executed through the application contract, exactly once.
+Encodes a **call voucher** — a one-shot on-chain transaction authorized by the application: a destination, an ether value, and calldata. Once the epoch containing it settles, the voucher can be executed through the application contract, exactly once.
-`encodeVoucher` is a pure-JS helper that returns the EVM-ABI encoded bytes (a CALL voucher in an `Output2(bytes32[2],bytes)` envelope). Emit them with [`emitOutput`](/cmio/reference/emit-output) to add the voucher to the outputs merkle tree (provable).
+`encodeCallVoucher` is a pure-JS helper that returns the EVM-ABI encoded bytes (a `CallVoucher(address,uint256,bytes)` call, mirroring libcmt's `cmt_callvoucher_encode`). Emit them with [`emitOutput`](/cmio/reference/emit-output) to add the voucher to the outputs merkle tree (provable).
## Usage
Plain value transfer:
```ts twoslash
-import { Rollup, encodeVoucher } from '@deroll/cmio';
+import { Rollup, encodeCallVoucher } from '@deroll/cmio';
const rollup = new Rollup();
// ---cut---
const index = rollup.emitOutput(
- encodeVoucher({
+ encodeCallVoucher({
destination: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
value: 1_000_000_000_000_000_000n, // 1 ETH in wei
}),
@@ -23,12 +23,12 @@ const index = rollup.emitOutput(
Contract call (calldata in `payload`):
```ts twoslash
-import { Rollup, encodeVoucher } from '@deroll/cmio';
+import { Rollup, encodeCallVoucher } from '@deroll/cmio';
const rollup = new Rollup();
declare const calldata: Uint8Array; // e.g. ERC-20 transfer(address,uint256)
// ---cut---
rollup.emitOutput(
- encodeVoucher({
+ encodeCallVoucher({
destination: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
payload: calldata,
}),
diff --git a/apps/docs/pages/cmio/reference/encode-delegate-call-voucher.mdx b/apps/docs/pages/cmio/reference/encode-delegate-call-voucher.mdx
deleted file mode 100644
index df6acbee..00000000
--- a/apps/docs/pages/cmio/reference/encode-delegate-call-voucher.mdx
+++ /dev/null
@@ -1,48 +0,0 @@
-# encodeDelegateCallVoucher
-
-Encodes a **delegate-call voucher** — like a [voucher](/cmio/reference/encode-voucher), but executed with `DELEGATECALL`: the destination's code runs in the application contract's own storage context, address and balance. An advanced primitive for library-style execution and upgrade patterns; with great power comes great responsibility over the application contract's state.
-
-`encodeDelegateCallVoucher` is a pure-JS helper that returns the EVM-ABI encoded bytes (a DELEGATECALL voucher in an `Output2(bytes32[2],bytes)` envelope). Emit them with [`emitOutput`](/cmio/reference/emit-output) to add the voucher to the outputs merkle tree (provable).
-
-## Usage
-
-```ts twoslash
-import { Rollup, encodeDelegateCallVoucher } from '@deroll/cmio';
-const rollup = new Rollup();
-declare const calldata: Uint8Array;
-// ---cut---
-const index = rollup.emitOutput(
- encodeDelegateCallVoucher({
- destination: '0x5FbDB2315678afecb367f032d93F642f64180aa3', // library contract
- payload: calldata,
- }),
-);
-```
-
-The output index comes from [`emitOutput`](/cmio/reference/emit-output)'s return value.
-
-## Returns
-
-`Buffer` — the encoded delegate-call voucher, ready to pass to [`emitOutput`](/cmio/reference/emit-output).
-
-## Parameters
-
-### destination
-
-- Type: `AddressLike` — 0x-prefixed hex string, `Buffer` or `Uint8Array`
-- Required; must be exactly 20 bytes
-
-The contract whose code will run in the application contract's context.
-
-### payload
-
-- Type: `BytesLike` — 0x-prefixed hex string, `Buffer` or `Uint8Array`
-- Default: empty
-
-Calldata for the delegate call. There is no `value` parameter — `DELEGATECALL` cannot transfer ether.
-
-## Errors
-
-| Condition | Error |
-| --- | --- |
-| `destination` not 20 bytes / malformed hex | `TypeError` |
diff --git a/apps/docs/pages/cmio/reference/encode-notice.mdx b/apps/docs/pages/cmio/reference/encode-notice.mdx
index 008fabb5..0696ffb5 100644
--- a/apps/docs/pages/cmio/reference/encode-notice.mdx
+++ b/apps/docs/pages/cmio/reference/encode-notice.mdx
@@ -2,7 +2,7 @@
Encodes a **notice** — a verifiable statement of fact about the application state, with no on-chain action attached. Off-chain consumers can validate a notice against the application's settled output root instead of re-executing the machine.
-`encodeNotice` is a pure-JS helper that returns the EVM-ABI encoded bytes (an `Output1(bytes32[1],bytes)` envelope). Emit them with [`emitOutput`](/cmio/reference/emit-output) to add the notice to the outputs merkle tree (provable).
+`encodeNotice` is a pure-JS helper that returns the EVM-ABI encoded bytes (a `Notice(bytes)` call, mirroring libcmt's `cmt_notice_encode`). Emit them with [`emitOutput`](/cmio/reference/emit-output) to add the notice to the outputs merkle tree (provable).
## Usage
diff --git a/apps/docs/pages/cmio/reference/run.mdx b/apps/docs/pages/cmio/reference/run.mdx
index 943db934..212f0bc8 100644
--- a/apps/docs/pages/cmio/reference/run.mdx
+++ b/apps/docs/pages/cmio/reference/run.mdx
@@ -21,7 +21,7 @@ await rollup.run({
});
```
-Handlers receive the request with its **raw, undecoded payload**. For an advance, call [`decodeAdvance`](/cmio/reference/decode-advance) to read the input fields, and build provable outputs with [`encodeNotice`](/cmio/reference/encode-notice) / [`encodeVoucher`](/cmio/reference/encode-voucher) before passing them to [`emitOutput`](/cmio/reference/emit-output).
+Handlers receive the request with its **raw, undecoded payload**. For an advance, call [`decodeAdvance`](/cmio/reference/decode-advance) to read the input fields, and build provable outputs with [`encodeNotice`](/cmio/reference/encode-notice) / [`encodeCallVoucher`](/cmio/reference/encode-call-voucher) before passing them to [`emitOutput`](/cmio/reference/emit-output).
## Returns
diff --git a/apps/docs/pages/cmio/reference/types.mdx b/apps/docs/pages/cmio/reference/types.mdx
index e279c437..f70c7560 100644
--- a/apps/docs/pages/cmio/reference/types.mdx
+++ b/apps/docs/pages/cmio/reference/types.mdx
@@ -5,16 +5,21 @@ Everything the package exports besides the [`Rollup`](/cmio/reference/rollup) cl
```ts twoslash
import type {
Advance,
+ AdvanceArgs,
AddressLike,
AdvanceRequest,
BytesLike,
- DelegateCallVoucher,
+ CallVoucher,
+ ERC20Transfer,
+ ERC721Transfer,
+ ERC1155SingleTransfer,
+ ERC1155BatchTransfer,
Hex,
InspectRequest,
RollupRequest,
RunHandlers,
+ U64Like,
U256Like,
- Voucher,
} from '@deroll/cmio';
```
@@ -25,26 +30,30 @@ Three "like" types describe what the binding accepts and converts automatically
| Type | Accepts | Used for |
| --- | --- | --- |
| `BytesLike` | `Hex`, `Buffer`, `Uint8Array` | output and report payloads |
-| `AddressLike` | `Hex`, `Buffer`, `Uint8Array` (20 bytes) | voucher destinations |
-| `U256Like` | `bigint`, `number`, `Hex`, `Uint8Array` (32 bytes) | voucher values |
+| `AddressLike` | `Hex`, `Buffer`, `Uint8Array` (20 bytes) | voucher destinations, transfer recipients and tokens |
+| `U256Like` | `bigint`, `number`, `Hex`, `Uint8Array` (32 bytes) | voucher values, token amounts and ids |
+| `U64Like` | `bigint`, `number` | `encodeAdvance` numeric fields |
## Encoder argument types
-The two voucher encoders take a named options object, importable for typing helpers that build outputs:
+The output encoders take a named options object, importable for typing helpers that build outputs:
-- `Voucher` — argument of [`encodeVoucher`](/cmio/reference/encode-voucher): `destination` (`AddressLike`), optional `value` (`U256Like`, default `0n`) and `payload` (`BytesLike`, default empty).
-- `DelegateCallVoucher` — argument of [`encodeDelegateCallVoucher`](/cmio/reference/encode-delegate-call-voucher): `destination` (`AddressLike`) and optional `payload` (`BytesLike`). No `value` — `DELEGATECALL` cannot transfer ether.
+- `CallVoucher` — argument of [`encodeCallVoucher`](/cmio/reference/encode-call-voucher): `destination` (`AddressLike`), optional `value` (`U256Like`, default `0n`) and `payload` (`BytesLike`, default empty).
+- `ERC20Transfer` / `ERC721Transfer` / `ERC1155SingleTransfer` / `ERC1155BatchTransfer` — arguments of the [asset transfer encoders](/cmio/reference/encode-asset-transfers).
+- `AdvanceArgs` — argument of `encodeAdvance` (see [`decodeAdvance`](/cmio/reference/decode-advance)): the same fields as `Advance` with "like" input types.
```ts twoslash
-import type { Voucher, DelegateCallVoucher } from '@deroll/cmio';
+import type { CallVoucher, ERC20Transfer } from '@deroll/cmio';
-const voucher: Voucher = {
+const voucher: CallVoucher = {
destination: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
value: 1_000_000_000_000_000_000n,
};
-const delegateCall: DelegateCallVoucher = {
- destination: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
+const transfer: ERC20Transfer = {
+ recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
+ token: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ value: 100n,
};
```
diff --git a/apps/docs/vocs.config.ts b/apps/docs/vocs.config.ts
index e8ec0ac1..04f87a2d 100644
--- a/apps/docs/vocs.config.ts
+++ b/apps/docs/vocs.config.ts
@@ -270,10 +270,10 @@ export default defineConfig({
{ text: "emitException", link: "/cmio/reference/emit-exception" },
{ text: "progress", link: "/cmio/reference/progress" },
{ text: "close", link: "/cmio/reference/close" },
- { text: "decodeAdvance", link: "/cmio/reference/decode-advance" },
+ { text: "decodeAdvance / encodeAdvance", link: "/cmio/reference/decode-advance" },
{ text: "encodeNotice", link: "/cmio/reference/encode-notice" },
- { text: "encodeVoucher", link: "/cmio/reference/encode-voucher" },
- { text: "encodeDelegateCallVoucher", link: "/cmio/reference/encode-delegate-call-voucher" },
+ { text: "encodeCallVoucher", link: "/cmio/reference/encode-call-voucher" },
+ { text: "Asset transfer encoders", link: "/cmio/reference/encode-asset-transfers" },
{ text: "Types & Constants", link: "/cmio/reference/types" },
],
},
diff --git a/packages/bindings/cmio/README.md b/packages/bindings/cmio/README.md
index ae687a75..c69e8c70 100644
--- a/packages/bindings/cmio/README.md
+++ b/packages/bindings/cmio/README.md
@@ -9,24 +9,28 @@ The package is a Node-API native addon:
The right flavor is selected automatically by target architecture, so the same dapp code runs unchanged on the host and in the machine.
-The API is **fully synchronous** on purpose: calls that wait on the emulator (`finish`, `gio`) yield the machine, which pauses the entire guest — including the Node.js event loop — so there is nothing to run concurrently while they wait. On the host mock they return immediately.
+The API is **fully synchronous** on purpose: calls that wait on the emulator (`waitForInput`) yield the machine, which pauses the entire guest — including the Node.js event loop — so there is nothing to run concurrently while they wait. On the host mock they return immediately.
+
+The binding mirrors libcmt's own split between its **rollup** module (raw I/O) and its **codec** module (EVM-ABI wire formats): the `Rollup` class only moves raw bytes, and standalone `encode*`/`decodeAdvance` helpers translate between structured values and bytes.
## Usage
```js
-import { Rollup } from '@deroll/cmio';
+import { Rollup, decodeAdvance, encodeCallVoucher, encodeNotice } from '@deroll/cmio';
const rollup = new Rollup();
await rollup.run({
advance(request, rollup) {
- // request: { chainId, appContract, msgSender, blockNumber,
+ // request.payload is the raw EvmAdvance input; decode it
+ const advance = decodeAdvance(request.payload);
+ // advance: { chainId, appContract, msgSender, blockNumber,
// blockTimestamp, prevRandao, index, payload }
- rollup.emitNotice(request.payload);
- rollup.emitVoucher({
- destination: request.msgSender,
+ rollup.emitOutput(encodeNotice(advance.payload));
+ rollup.emitOutput(encodeCallVoucher({
+ destination: advance.msgSender,
value: 0n,
payload: '0xdeadbeef',
- });
+ }));
return true; // accept (default); return false to reject
},
inspect(request, rollup) {
@@ -41,7 +45,7 @@ Or drive the loop yourself:
const rollup = new Rollup();
let accept = true;
for (;;) {
- const request = rollup.finish({ accept });
+ const request = rollup.waitForInput({ accept });
accept = handle(request); // your logic
}
```
@@ -50,30 +54,40 @@ Byte arguments accept `Buffer`, `Uint8Array` or 0x-prefixed hex strings. Address
The package is dual ESM + CommonJS — `const { Rollup } = require('@deroll/cmio')` works too, and both entry points share the same native addon instance.
-### API
+### Rollup API (raw I/O)
| Method | Description |
| --- | --- |
| `new Rollup()` | Opens the rollup device. Only **one** instance may be open at a time (`-EBUSY` otherwise); `close()` the previous one first. |
-| `finish({ accept })` | Accepts/rejects the previous request, yields, and returns the next `{ type: 'advance' \| 'inspect', payload, ... }`. |
-| `emitVoucher({ destination, value, payload })` | Emits `Voucher(address,uint256,bytes)`. Returns the output index. |
-| `emitDelegateCallVoucher({ destination, payload })` | Emits `DelegateCallVoucher(address,bytes)`. Returns the output index. |
-| `emitNotice(payload)` | Emits `Notice(bytes)`. Returns the output index. |
+| `waitForInput({ accept })` | Accepts/rejects the previous request, yields, and returns the next `{ type: 'advance' \| 'inspect', payload }` with the raw, undecoded payload. |
+| `emitOutput(bytes)` | Emits a raw, already EVM-ABI encoded output; adds it to the outputs merkle tree and returns its index. |
| `emitReport(payload)` | Emits a report (raw bytes, not in the outputs merkle tree). |
| `emitException(payload)` | Signals that the request could not be processed. |
| `progress(value)` | Reports progress (raw uint32). |
-| `gio({ domain, id })` | Generic IO request; returns `{ responseCode, responseData }`. |
-| `saveMerkle(file)` / `loadMerkle(file)` / `resetMerkle()` | Persist/restore/reset the outputs merkle tree. |
| `close()` | Releases the device. |
-| `run({ advance, inspect })` | Convenience loop over `finish`; handlers may be async. Handler exceptions reject the input and are emitted as reports. |
+| `run({ advance, inspect })` | Convenience loop over `waitForInput`; handlers may be async. Handler exceptions reject the input and are emitted as reports. |
Failed libcmt calls throw a `RollupError` with the negative errno in `error.errno` and the failed call in `error.syscall`.
-## Documentation
+### Codec helpers (EVM-ABI wire formats)
-Published at **** (deployed by the [Docs workflow](.github/workflows/docs.yml) on every push to `main`).
+Each helper mirrors one entry of libcmt's `codec.h` and produces/consumes the exact same bytes:
+
+| Function | Wire format |
+| --- | --- |
+| `decodeAdvance(input)` / `encodeAdvance(fields)` | `EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes)` |
+| `encodeNotice(payload)` | `Notice(bytes)` |
+| `encodeCallVoucher({ destination, value, payload })` | `CallVoucher(address,uint256,bytes)` |
+| `encodeERC20Transfer({ recipient, token, value })` | `ERC20Transfer(address,address,uint256)` |
+| `encodeERC721Transfer({ recipient, token, tokenId, data })` | `ERC721Transfer(address,address,uint256,bytes)` |
+| `encodeERC1155SingleTransfer({ recipient, token, tokenId, value, data })` | `ERC1155SingleTransfer(address,address,uint256,uint256,bytes)` |
+| `encodeERC1155BatchTransfer({ recipient, token, tokenIdsAndValues, data })` | `ERC1155BatchTransfer(address,address,uint256[2][],bytes)` |
+
+Compose them with the raw API: `rollup.emitOutput(encodeNotice(payload))`. `encodeAdvance` is the inverse of `decodeAdvance`, useful for crafting mock inputs when testing on the host.
+
+## Documentation
-The site ([Vocs](https://vocs.dev)) has its pages in [`docs/pages/`](docs/pages/) and is configured by [`vocs.config.ts`](vocs.config.ts): `npm run docs:dev` / `docs:build` / `docs:preview`.
+Published at **** (the `apps/docs` Vocs site in this monorepo, under *cmio*).
## Testing on the host (mock)
@@ -84,7 +98,7 @@ CMT_INPUTS="0:advance.bin,1:inspect.bin" node my-dapp.js
# -> advance.output-0.bin, advance.report-0.bin, ...
```
-Reason `0` is advance (EVM-ABI encoded `EvmAdvance`), `1` is inspect (raw payload); any other reason is a gio reply with that response code. Set `CMT_DEBUG=yes` for verbose logging. See the [libcmt README](https://github.com/cartesi/machine-guest-tools/tree/main/sys-utils/libcmt#testing) for how to generate inputs with foundry's `cast`, or `test/rollup.test.mjs` here for a pure-JS encoder.
+Reason `0` is advance (EVM-ABI encoded `EvmAdvance` — build one with `encodeAdvance`), `1` is inspect (raw payload). Set `CMT_DEBUG=yes` for verbose logging. See the [libcmt README](https://github.com/cartesi/machine-guest-tools/tree/main/sys-utils/libcmt#testing) for how to generate inputs with foundry's `cast`, or `test/rollup.test.mjs` here for a pure-JS encoder.
## Testing inside a Cartesi Machine
@@ -117,4 +131,4 @@ Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishe
## License
-The libcmt-node repository and all contributions are licensed under [APACHE 2.0](https://www.apache.org/licenses/LICENSE-2.0). Please review our [LICENSE](LICENSE) file.
+This package and all contributions are licensed under [APACHE 2.0](https://www.apache.org/licenses/LICENSE-2.0). Please review our [LICENSE](LICENSE) file.
diff --git a/packages/bindings/cmio/deps/machine-guest-tools b/packages/bindings/cmio/deps/machine-guest-tools
index 99339046..7ffb2dae 160000
--- a/packages/bindings/cmio/deps/machine-guest-tools
+++ b/packages/bindings/cmio/deps/machine-guest-tools
@@ -1 +1 @@
-Subproject commit 993390463d1cda53ad86ed9c8d88e2cdffa43016
+Subproject commit 7ffb2daea30e94440d0f1a9b9b2de1b4ee64a467
diff --git a/packages/bindings/cmio/lib/index.d.ts b/packages/bindings/cmio/lib/index.d.ts
index 7f6521cc..9f7fad5e 100644
--- a/packages/bindings/cmio/lib/index.d.ts
+++ b/packages/bindings/cmio/lib/index.d.ts
@@ -28,6 +28,9 @@ export type AddressLike = Hex | Uint8Array;
/** Unsigned 256-bit value: bigint, number, or 32 bytes (hex string/Uint8Array). */
export type U256Like = bigint | number | Hex | Uint8Array;
+/** Unsigned 64-bit value: bigint or number. */
+export type U64Like = bigint | number;
+
export const ADDRESS_LENGTH: 20;
export const U256_LENGTH: 32;
@@ -65,8 +68,28 @@ export interface Advance {
payload: Buffer;
}
-/** Arguments for {@link encodeVoucher}. Encoded on-chain as a CALL voucher. */
-export interface Voucher {
+/** Arguments for {@link encodeAdvance}, the inverse of {@link decodeAdvance}. */
+export interface AdvanceArgs {
+ /** Network chain id. */
+ chainId: U64Like;
+ /** Application contract address (20 bytes). */
+ appContract: AddressLike;
+ /** Input sender address (20 bytes). */
+ msgSender: AddressLike;
+ /** Block number of this input. */
+ blockNumber: U64Like;
+ /** Block timestamp of this input (UNIX epoch seconds). */
+ blockTimestamp: U64Like;
+ /** RANDAO mix of the post beacon state of the previous block. */
+ prevRandao: U256Like;
+ /** Input index relative to all inputs ever sent to the application. */
+ index: U64Like;
+ /** Input payload. */
+ payload: BytesLike;
+}
+
+/** Arguments for {@link encodeCallVoucher}. Executed on-chain as a CALL. */
+export interface CallVoucher {
/** Address the voucher executes against (20 bytes): an EOA for transfers, a contract for calls. */
destination: AddressLike;
/** Amount of wei sent with the execution. Default: `0n`. */
@@ -75,28 +98,84 @@ export interface Voucher {
payload?: BytesLike;
}
-/** Arguments for {@link encodeDelegateCallVoucher}. Encoded on-chain as a DELEGATECALL voucher. */
-export interface DelegateCallVoucher {
- /** Contract whose code runs in the application contract's storage context (20 bytes). */
- destination: AddressLike;
- /** Calldata for the delegate call. Default: empty. There is no `value` — `DELEGATECALL` cannot transfer ether. */
- payload?: BytesLike;
+/** Arguments for {@link encodeERC20Transfer}. */
+export interface ERC20Transfer {
+ /** Address receiving the tokens (20 bytes). */
+ recipient: AddressLike;
+ /** ERC-20 token contract address (20 bytes). */
+ token: AddressLike;
+ /** Amount of tokens to transfer. */
+ value: U256Like;
+}
+
+/** Arguments for {@link encodeERC721Transfer}. */
+export interface ERC721Transfer {
+ /** Address receiving the token (20 bytes). */
+ recipient: AddressLike;
+ /** ERC-721 token contract address (20 bytes). */
+ token: AddressLike;
+ /** Token id to transfer. */
+ tokenId: U256Like;
+ /** Extra data forwarded to the recipient. Default: empty. */
+ data?: BytesLike;
+}
+
+/** Arguments for {@link encodeERC1155SingleTransfer}. */
+export interface ERC1155SingleTransfer {
+ /** Address receiving the tokens (20 bytes). */
+ recipient: AddressLike;
+ /** ERC-1155 token contract address (20 bytes). */
+ token: AddressLike;
+ /** Token id to transfer. */
+ tokenId: U256Like;
+ /** Amount of tokens to transfer. */
+ value: U256Like;
+ /** Extra data forwarded to the recipient. Default: empty. */
+ data?: BytesLike;
+}
+
+/** Arguments for {@link encodeERC1155BatchTransfer}. */
+export interface ERC1155BatchTransfer {
+ /** Address receiving the tokens (20 bytes). */
+ recipient: AddressLike;
+ /** ERC-1155 token contract address (20 bytes). */
+ token: AddressLike;
+ /** `[tokenId, value]` pairs to transfer. */
+ tokenIdsAndValues: readonly (readonly [U256Like, U256Like])[];
+ /** Extra data forwarded to the recipient. Default: empty. */
+ data?: BytesLike;
}
/**
* Decode an `EvmAdvance` input (the raw payload of an advance request) into its
- * structured fields. Mirrors libcmt's `cmt_decode_advance_state`.
+ * structured fields. Mirrors libcmt's `cmt_evmadvance_decode`.
*/
export function decodeAdvance(input: BytesLike): Advance;
-/** Encode a notice into an `Output1(bytes32[1],bytes)` envelope. */
+/**
+ * Encode an `EvmAdvance` input from its structured fields, the inverse of
+ * {@link decodeAdvance}. Mirrors libcmt's `cmt_evmadvance_encode`; useful for
+ * crafting mock inputs (`CMT_INPUTS`) when testing on the host.
+ */
+export function encodeAdvance(advance: AdvanceArgs): Buffer;
+
+/** Encode a `Notice(bytes)` output. Mirrors libcmt's `cmt_notice_encode`. */
export function encodeNotice(payload: BytesLike): Buffer;
-/** Encode a CALL voucher into an `Output2(bytes32[2],bytes)` envelope. */
-export function encodeVoucher(voucher: Voucher): Buffer;
+/** Encode a `CallVoucher(address,uint256,bytes)` output. Mirrors libcmt's `cmt_callvoucher_encode`. */
+export function encodeCallVoucher(voucher: CallVoucher): Buffer;
+
+/** Encode an `ERC20Transfer(address,address,uint256)` output. Mirrors libcmt's `cmt_erc20transfer_encode`. */
+export function encodeERC20Transfer(transfer: ERC20Transfer): Buffer;
+
+/** Encode an `ERC721Transfer(address,address,uint256,bytes)` output. Mirrors libcmt's `cmt_erc721transfer_encode`. */
+export function encodeERC721Transfer(transfer: ERC721Transfer): Buffer;
+
+/** Encode an `ERC1155SingleTransfer(address,address,uint256,uint256,bytes)` output. Mirrors libcmt's `cmt_erc1155singletransfer_encode`. */
+export function encodeERC1155SingleTransfer(transfer: ERC1155SingleTransfer): Buffer;
-/** Encode a DELEGATECALL voucher into an `Output2(bytes32[2],bytes)` envelope. */
-export function encodeDelegateCallVoucher(voucher: DelegateCallVoucher): Buffer;
+/** Encode an `ERC1155BatchTransfer(address,address,uint256[2][],bytes)` output. Mirrors libcmt's `cmt_erc1155batchtransfer_encode`. */
+export function encodeERC1155BatchTransfer(transfer: ERC1155BatchTransfer): Buffer;
/**
* Error thrown when a libcmt binding call fails (e.g. a too-large output, or
diff --git a/packages/bindings/cmio/lib/index.js b/packages/bindings/cmio/lib/index.js
index 4abc397f..89b414de 100644
--- a/packages/bindings/cmio/lib/index.js
+++ b/packages/bindings/cmio/lib/index.js
@@ -17,35 +17,35 @@
'use strict';
const path = require('node:path');
-const { AbiFunction, AbiParameters, Bytes, Hash } = require('ox');
+const { AbiFunction, AbiParameters } = require('ox');
const binding = require('node-gyp-build')(path.join(__dirname, '..'));
const ADDRESS_LENGTH = 20;
const U256_LENGTH = 32;
const EMPTY = Buffer.alloc(0);
-// EVM-ABI encoders/decoders for libcmt's output envelopes, expressed with ox so
-// the low-level word packing/padding stays battle-tested. The Output1/Output2
-// selectors ox derives from these signatures match libcmt's hardcoded funsels
-// (0xaed682a1 / 0x50b41f12).
-const OUTPUT1 = AbiFunction.from('function Output1(bytes32[1], bytes)');
-const OUTPUT2 = AbiFunction.from('function Output2(bytes32[2], bytes)');
+// EVM-ABI codecs for libcmt's wire formats, expressed with ox so the low-level
+// word packing/padding stays battle-tested. One AbiFunction per codec.h entry;
+// the selectors ox derives from these signatures match libcmt's hardcoded
+// funsels (codec.h stores them as little-endian uint32, so e.g. Notice's
+// 0xe5d658c2 there is wire bytes c2 58 d6 e5 = ox's 0xc258d6e5).
+const NOTICE = AbiFunction.from('function Notice(bytes payload)'); // 0xc258d6e5
+const CALL_VOUCHER = AbiFunction.from('function CallVoucher(address destination, uint256 value, bytes payload)'); // 0x4691c2bc
+const ERC20_TRANSFER = AbiFunction.from('function ERC20Transfer(address recipient, address token, uint256 value)'); // 0xe59fdd36
+const ERC721_TRANSFER = AbiFunction.from(
+ 'function ERC721Transfer(address recipient, address token, uint256 tokenId, bytes data)',
+); // 0x2b0e47a0
+const ERC1155_SINGLE_TRANSFER = AbiFunction.from(
+ 'function ERC1155SingleTransfer(address recipient, address token, uint256 tokenId, uint256 value, bytes data)',
+); // 0x8c76b598
+const ERC1155_BATCH_TRANSFER = AbiFunction.from(
+ 'function ERC1155BatchTransfer(address recipient, address token, uint256[2][] tokenIdsAndValues, bytes data)',
+); // 0x2c38972f
const EVM_ADVANCE = AbiFunction.from(
- 'function EvmAdvance(uint256 chainId, address appContract, address msgSender, ' +
- 'uint256 blockNumber, uint256 blockTimestamp, uint256 prevRandao, uint256 index, bytes payload)',
-);
+ 'function EvmAdvance(uint64 chainId, address appContract, address msgSender, ' +
+ 'uint64 blockNumber, uint64 blockTimestamp, uint256 prevRandao, uint64 index, bytes payload)',
+); // 0x233a0ebf
const EVM_ADVANCE_SELECTOR = EVM_ADVANCE.hash.slice(0, 10); // 0x + 4 bytes
-// CALL voucher dynamic content: abi.encode(uint256 value, bytes payload)
-const CALL_VOUCHER_DATA = AbiParameters.from('uint256 value, bytes payload');
-
-// Each output kind is tagged with a domain-separated identifier:
-// keccak256("cartesi.output.v1."). These are the same 32-byte constants
-// libcmt hardcodes in codec.c as CARTESI_OUTPUT_V1_*; we derive them so the
-// source-of-truth string is visible rather than an opaque hash.
-const outputTag = (kind) => Hash.keccak256(Bytes.fromString(`cartesi.output.v1.${kind}`), { as: 'Hex' });
-const TAG_NOTICE = outputTag('notice');
-const TAG_CALL_VOUCHER = outputTag('call-voucher');
-const TAG_DELEGATECALL_VOUCHER = outputTag('delegatecall-voucher');
/**
* Error thrown when a libcmt binding call fails. Carries the negative errno
@@ -112,36 +112,47 @@ function toAddress(value, name) {
return bytes;
}
-// Validate an unsigned 256-bit value and return it as a bigint for ox.
-function toU256(value, name) {
+// Validate an unsigned integer of `bits` width and return it as a bigint for
+// ox. 256-bit values also accept a 32-byte big-endian buffer/hex string.
+function toUint(value, name, bits) {
let v;
if (typeof value === 'bigint' || typeof value === 'number') {
v = BigInt(value);
- } else {
+ } else if (bits === 256) {
const bytes = toBytes(value, name);
if (bytes.length !== U256_LENGTH) {
throw new TypeError(`${name} must be ${U256_LENGTH} bytes long`);
}
v = bytes.length === 0 ? 0n : BigInt(toHex(bytes));
+ } else {
+ throw new TypeError(`${name} must be a bigint or number`);
}
- if (v < 0n || v >= 1n << 256n) {
- throw new RangeError(`${name} must fit in an unsigned 256-bit integer`);
+ if (v < 0n || v >= 1n << BigInt(bits)) {
+ throw new RangeError(`${name} must fit in an unsigned ${bits}-bit integer`);
}
return v;
}
+const toU256 = (value, name) => toUint(value, name, 256);
+const toU64 = (value, name) => toUint(value, name, 64);
+
function toHex(bytes) {
return `0x${bytes.toString('hex')}`;
}
-// Left-pad a validated 20-byte address into a 32-byte ABI word (0x-hex).
-function addressToWord(bytes) {
- return `0x${bytes.toString('hex').padStart(2 * U256_LENGTH, '0')}`;
+// Validate a 20-byte address argument and return it as 0x-hex for ox.
+function toAddressHex(value, name) {
+ return toHex(toAddress(value, name));
+}
+
+// Encode a call to `fn` with `args` and return the calldata as a Buffer.
+function encodeData(fn, args) {
+ return Buffer.from(AbiFunction.encodeData(fn, args).slice(2), 'hex');
}
/**
* Decode an `EvmAdvance` input (the raw payload of an advance request) into its
- * structured fields. Mirrors libcmt's `cmt_decode_advance_state`.
+ * structured fields. Mirrors libcmt's `cmt_evmadvance_decode`.
*/
function decodeAdvance(input) {
const bytes = toBytes(input, 'input');
@@ -165,32 +176,103 @@ function decodeAdvance(input) {
};
}
-/** Encode a notice into an `Output1(bytes32[1],bytes)` envelope. */
+/**
+ * Encode an `EvmAdvance` input from its structured fields, the inverse of
+ * {@link decodeAdvance}. Mirrors libcmt's `cmt_evmadvance_encode`; useful for
+ * crafting mock inputs (`CMT_INPUTS`) when testing on the host.
+ */
+function encodeAdvance({ chainId, appContract, msgSender, blockNumber, blockTimestamp, prevRandao, index, payload }) {
+ return encodeData(EVM_ADVANCE, [
+ toU64(chainId, 'chainId'),
+ toAddressHex(appContract, 'appContract'),
+ toAddressHex(msgSender, 'msgSender'),
+ toU64(blockNumber, 'blockNumber'),
+ toU64(blockTimestamp, 'blockTimestamp'),
+ toU256(prevRandao, 'prevRandao'),
+ toU64(index, 'index'),
+ toHex(toBytes(payload, 'payload')),
+ ]);
+}
+
+/** Encode a `Notice(bytes)` output. Mirrors libcmt's `cmt_notice_encode`. */
function encodeNotice(payload) {
- const data = toHex(toBytes(payload, 'payload'));
- return Buffer.from(AbiFunction.encodeData(OUTPUT1, [[TAG_NOTICE], data]).slice(2), 'hex');
+ return encodeData(NOTICE, [toHex(toBytes(payload, 'payload'))]);
}
/**
- * Encode a CALL voucher into an `Output2(bytes32[2],bytes)` envelope, whose
- * dynamic content is `abi.encode(uint256 value, bytes payload)`.
+ * Encode a `CallVoucher(address,uint256,bytes)` output: an on-chain CALL to
+ * `destination` with `value` wei and `payload` calldata. Mirrors libcmt's
+ * `cmt_callvoucher_encode`.
*/
-function encodeVoucher({ destination, value = 0n, payload = EMPTY }) {
- const dest = addressToWord(toAddress(destination, 'destination'));
- const val = toU256(value, 'value');
- const data = toHex(toBytes(payload, 'payload'));
- const inner = AbiParameters.encode(CALL_VOUCHER_DATA, [val, data]);
- return Buffer.from(AbiFunction.encodeData(OUTPUT2, [[TAG_CALL_VOUCHER, dest], inner]).slice(2), 'hex');
+function encodeCallVoucher({ destination, value = 0n, payload = EMPTY }) {
+ return encodeData(CALL_VOUCHER, [
+ toAddressHex(destination, 'destination'),
+ toU256(value, 'value'),
+ toHex(toBytes(payload, 'payload')),
+ ]);
}
/**
- * Encode a DELEGATECALL voucher into an `Output2(bytes32[2],bytes)` envelope.
- * There is no `value` — `DELEGATECALL` cannot transfer ether.
+ * Encode an `ERC20Transfer(address,address,uint256)` output: transfer `value`
+ * of `token` to `recipient`. Mirrors libcmt's `cmt_erc20transfer_encode`.
*/
-function encodeDelegateCallVoucher({ destination, payload = EMPTY }) {
- const dest = addressToWord(toAddress(destination, 'destination'));
- const data = toHex(toBytes(payload, 'payload'));
- return Buffer.from(AbiFunction.encodeData(OUTPUT2, [[TAG_DELEGATECALL_VOUCHER, dest], data]).slice(2), 'hex');
+function encodeERC20Transfer({ recipient, token, value }) {
+ return encodeData(ERC20_TRANSFER, [
+ toAddressHex(recipient, 'recipient'),
+ toAddressHex(token, 'token'),
+ toU256(value, 'value'),
+ ]);
+}
+
+/**
+ * Encode an `ERC721Transfer(address,address,uint256,bytes)` output: transfer
+ * `tokenId` of `token` to `recipient`. Mirrors libcmt's `cmt_erc721transfer_encode`.
+ */
+function encodeERC721Transfer({ recipient, token, tokenId, data = EMPTY }) {
+ return encodeData(ERC721_TRANSFER, [
+ toAddressHex(recipient, 'recipient'),
+ toAddressHex(token, 'token'),
+ toU256(tokenId, 'tokenId'),
+ toHex(toBytes(data, 'data')),
+ ]);
+}
+
+/**
+ * Encode an `ERC1155SingleTransfer(address,address,uint256,uint256,bytes)`
+ * output: transfer `value` of `tokenId` of `token` to `recipient`. Mirrors
+ * libcmt's `cmt_erc1155singletransfer_encode`.
+ */
+function encodeERC1155SingleTransfer({ recipient, token, tokenId, value, data = EMPTY }) {
+ return encodeData(ERC1155_SINGLE_TRANSFER, [
+ toAddressHex(recipient, 'recipient'),
+ toAddressHex(token, 'token'),
+ toU256(tokenId, 'tokenId'),
+ toU256(value, 'value'),
+ toHex(toBytes(data, 'data')),
+ ]);
+}
+
+/**
+ * Encode an `ERC1155BatchTransfer(address,address,uint256[2][],bytes)` output:
+ * transfer several `[tokenId, value]` pairs of `token` to `recipient`. Mirrors
+ * libcmt's `cmt_erc1155batchtransfer_encode`.
+ */
+function encodeERC1155BatchTransfer({ recipient, token, tokenIdsAndValues, data = EMPTY }) {
+ if (!Array.isArray(tokenIdsAndValues)) {
+ throw new TypeError('tokenIdsAndValues must be an array of [tokenId, value] pairs');
+ }
+ const pairs = tokenIdsAndValues.map((pair, i) => {
+ if (!Array.isArray(pair) || pair.length !== 2) {
+ throw new TypeError(`tokenIdsAndValues[${i}] must be a [tokenId, value] pair`);
+ }
+ return [toU256(pair[0], `tokenIdsAndValues[${i}][0]`), toU256(pair[1], `tokenIdsAndValues[${i}][1]`)];
+ });
+ return encodeData(ERC1155_BATCH_TRANSFER, [
+ toAddressHex(recipient, 'recipient'),
+ toAddressHex(token, 'token'),
+ pairs,
+ toHex(toBytes(data, 'data')),
+ ]);
}
class Rollup {
@@ -261,9 +343,13 @@ module.exports = {
Rollup,
RollupError,
decodeAdvance,
+ encodeAdvance,
encodeNotice,
- encodeVoucher,
- encodeDelegateCallVoucher,
+ encodeCallVoucher,
+ encodeERC20Transfer,
+ encodeERC721Transfer,
+ encodeERC1155SingleTransfer,
+ encodeERC1155BatchTransfer,
ADDRESS_LENGTH,
U256_LENGTH,
};
diff --git a/packages/bindings/cmio/lib/index.mjs b/packages/bindings/cmio/lib/index.mjs
index 2f3cdefb..3a83770d 100644
--- a/packages/bindings/cmio/lib/index.mjs
+++ b/packages/bindings/cmio/lib/index.mjs
@@ -20,9 +20,13 @@ export {
Rollup,
RollupError,
decodeAdvance,
+ encodeAdvance,
encodeNotice,
- encodeVoucher,
- encodeDelegateCallVoucher,
+ encodeCallVoucher,
+ encodeERC20Transfer,
+ encodeERC721Transfer,
+ encodeERC1155SingleTransfer,
+ encodeERC1155BatchTransfer,
ADDRESS_LENGTH,
U256_LENGTH,
} from './index.js';
diff --git a/packages/bindings/cmio/test/machine/abi.mjs b/packages/bindings/cmio/test/machine/abi.mjs
index 59b65e72..48c17457 100644
--- a/packages/bindings/cmio/test/machine/abi.mjs
+++ b/packages/bindings/cmio/test/machine/abi.mjs
@@ -15,12 +15,12 @@
//
// Minimal EVM-ABI helpers shared by encode-inputs.mjs and verify-outputs.mjs,
-// enough to encode EvmAdvance inputs and decode Voucher/Notice outputs.
+// enough to encode EvmAdvance inputs and CallVoucher/Notice outputs.
// Mirrors the encoder in test/rollup.test.mjs.
export const SELECTOR = {
- evmAdvance: '415bf363', // EvmAdvance(uint256,address,address,uint256,uint256,uint256,uint256,bytes)
- voucher: '237a816f', // Voucher(address,uint256,bytes)
+ evmAdvance: '233a0ebf', // EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes)
+ callVoucher: '4691c2bc', // CallVoucher(address,uint256,bytes)
notice: 'c258d6e5', // Notice(bytes)
};
@@ -62,9 +62,9 @@ export function encodeNotice(payload) {
]);
}
-export function encodeVoucher({ destination, value, payload }) {
+export function encodeCallVoucher({ destination, value, payload }) {
return Buffer.concat([
- Buffer.from(SELECTOR.voucher, 'hex'),
+ Buffer.from(SELECTOR.callVoucher, 'hex'),
addressWord(destination),
word(value),
word(3 * 32), // offset of the payload `bytes` field
diff --git a/packages/bindings/cmio/test/machine/app.mjs b/packages/bindings/cmio/test/machine/app.mjs
index 4cc556c4..df049e0c 100644
--- a/packages/bindings/cmio/test/machine/app.mjs
+++ b/packages/bindings/cmio/test/machine/app.mjs
@@ -15,21 +15,24 @@
//
// Echo dapp run inside the Cartesi Machine by test/machine/run.sh. For each
-// advance it emits a notice and a voucher echoing the payload plus a report;
-// for each inspect it reports the query payload back.
+// advance it emits a notice and a call voucher echoing the payload plus a
+// report; for each inspect it reports the query payload back.
-import { Rollup } from '@deroll/cmio';
+import { Rollup, decodeAdvance, encodeCallVoucher, encodeNotice } from '@deroll/cmio';
const rollup = new Rollup();
await rollup.run({
advance(request, rollup) {
- rollup.emitNotice(request.payload);
- rollup.emitVoucher({
- destination: request.msgSender,
- value: request.index,
- payload: request.payload,
- });
- rollup.emitReport(Buffer.from(`advance index=${request.index} chainId=${request.chainId}`));
+ const advance = decodeAdvance(request.payload);
+ rollup.emitOutput(encodeNotice(advance.payload));
+ rollup.emitOutput(
+ encodeCallVoucher({
+ destination: advance.msgSender,
+ value: advance.index,
+ payload: advance.payload,
+ }),
+ );
+ rollup.emitReport(Buffer.from(`advance index=${advance.index} chainId=${advance.chainId}`));
return true;
},
inspect(request, rollup) {
diff --git a/packages/bindings/cmio/test/machine/verify-outputs.mjs b/packages/bindings/cmio/test/machine/verify-outputs.mjs
index d4753b8b..b0a701a3 100644
--- a/packages/bindings/cmio/test/machine/verify-outputs.mjs
+++ b/packages/bindings/cmio/test/machine/verify-outputs.mjs
@@ -23,7 +23,7 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
-import { ADVANCES, QUERY, encodeNotice, encodeVoucher } from './abi.mjs';
+import { ADVANCES, QUERY, encodeCallVoucher, encodeNotice } from './abi.mjs';
const dir = process.argv[2];
if (!dir) {
@@ -38,7 +38,7 @@ ADVANCES.forEach((advance, i) => {
assert.deepEqual(read(`input-${i}-output-0.bin`), encodeNotice(advance.payload), `input ${i}: notice mismatch`);
assert.deepEqual(
read(`input-${i}-output-1.bin`),
- encodeVoucher({ destination: advance.msgSender, value: advance.index, payload: advance.payload }),
+ encodeCallVoucher({ destination: advance.msgSender, value: advance.index, payload: advance.payload }),
`input ${i}: voucher mismatch`,
);
assert.equal(
diff --git a/packages/bindings/cmio/test/rollup.test.mjs b/packages/bindings/cmio/test/rollup.test.mjs
index 149bdf95..5a68d292 100644
--- a/packages/bindings/cmio/test/rollup.test.mjs
+++ b/packages/bindings/cmio/test/rollup.test.mjs
@@ -24,22 +24,29 @@ import {
Rollup,
RollupError,
decodeAdvance,
+ encodeAdvance,
encodeNotice,
- encodeVoucher,
- encodeDelegateCallVoucher,
+ encodeCallVoucher,
+ encodeERC20Transfer,
+ encodeERC721Transfer,
+ encodeERC1155SingleTransfer,
+ encodeERC1155BatchTransfer,
} from '../lib/index.mjs';
// keep the mock by-product files (advance.output-0.bin etc.) out of the repo
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'libcmt-node-'));
process.chdir(tmp);
-// EVM-ABI function selectors / output type tags, mirroring libcmt's codec.c.
-const EVM_ADVANCE = '415bf363'; // EvmAdvance(uint256,address,address,...,bytes)
-const FUNSEL = { output1: 'aed682a1', output2: '50b41f12' };
-const TAG = {
- notice: 'e4f5829fb698a59fba2cf6128b6bf1e8ce1dc09d271c55b787781bd415db8eed',
- callVoucher: 'd515b20044ba3bb84cfce3004f8b64ee11fb8ca22f936e4bc25a65e4b2133120',
- delegateCallVoucher: 'e166d466bf2d7d71d7f3f69e4c68f516330d8073b6ddb408c507548b64a1f3bb',
+// EVM-ABI function selectors, mirroring libcmt's codec.h funsels (which are
+// stored there as little-endian uint32, so the byte order here is reversed).
+const FUNSEL = {
+ evmAdvance: '233a0ebf', // EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes)
+ notice: 'c258d6e5', // Notice(bytes)
+ callVoucher: '4691c2bc', // CallVoucher(address,uint256,bytes)
+ erc20Transfer: 'e59fdd36', // ERC20Transfer(address,address,uint256)
+ erc721Transfer: '2b0e47a0', // ERC721Transfer(address,address,uint256,bytes)
+ erc1155SingleTransfer: '8c76b598', // ERC1155SingleTransfer(address,address,uint256,uint256,bytes)
+ erc1155BatchTransfer: '2c38972f', // ERC1155BatchTransfer(address,address,uint256[2][],bytes)
};
function word(value) {
@@ -55,9 +62,12 @@ function word(value) {
const addressWord = (hex) => Buffer.concat([Buffer.alloc(12), Buffer.from(hex.slice(2), 'hex')]);
const pad32 = (bytes) => Buffer.concat([bytes, Buffer.alloc((32 - (bytes.length % 32)) % 32)]);
+// Hand-rolled EvmAdvance encoder, independent of the library's encodeAdvance
+// so the two implementations cross-check each other. All heads are 32-byte
+// words regardless of the field's declared width (uint64 fields included).
function encodeEvmAdvance({ chainId, appContract, msgSender, blockNumber, blockTimestamp, prevRandao, index, payload }) {
return Buffer.concat([
- Buffer.from(EVM_ADVANCE, 'hex'),
+ Buffer.from(FUNSEL.evmAdvance, 'hex'),
word(chainId),
addressWord(appContract),
addressWord(msgSender),
@@ -105,6 +115,9 @@ test('advance request, outputs and reports', async () => {
const request = rollup.waitForInput();
assert.equal(request.type, 'advance');
+ // the library encoder and the hand-rolled one above must agree
+ assert.deepEqual(encodeAdvance(ADVANCE), encodeEvmAdvance(ADVANCE));
+
const advance = decodeAdvance(request.payload);
assert.equal(advance.chainId, ADVANCE.chainId);
assert.equal(advance.appContract, ADVANCE.appContract);
@@ -123,30 +136,26 @@ test('advance request, outputs and reports', async () => {
// outputs are EVM-ABI encoded in JS and emitted as raw bytes; the index is
// the position in the outputs merkle tree.
assert.equal(rollup.emitOutput(encodeNotice(noticePayload)), 0);
- assert.equal(rollup.emitOutput(encodeVoucher({ destination, value: 1000n, payload: voucherPayload })), 1);
+ assert.equal(rollup.emitOutput(encodeCallVoucher({ destination, value: 1000n, payload: voucherPayload })), 1);
rollup.emitReport(reportPayload);
rollup.progress(500);
- // notice: Output1 envelope (funsel | NOTICE tag | offset | length | payload)
+ // notice: Notice(bytes) = funsel | offset | length | payload
const notice = fs.readFileSync(path.join(dir, 'advance.output-0.bin'));
- assert.equal(hex(notice.subarray(0, 4)), FUNSEL.output1);
- assert.equal(hex(notice.subarray(4, 36)), TAG.notice);
- assert.deepEqual(notice.subarray(36, 68), word(0x40));
- assert.deepEqual(notice.subarray(68, 100), word(noticePayload.length));
- assert.deepEqual(notice.subarray(100, 100 + noticePayload.length), noticePayload);
+ assert.equal(hex(notice.subarray(0, 4)), FUNSEL.notice);
+ assert.deepEqual(notice.subarray(4, 36), word(0x20));
+ assert.deepEqual(notice.subarray(36, 68), word(noticePayload.length));
+ assert.deepEqual(notice.subarray(68, 68 + noticePayload.length), noticePayload);
- // call voucher: Output2 envelope; dynamic content is abi.encode(value, payload)
+ // call voucher: CallVoucher(address,uint256,bytes) =
+ // funsel | destination | value | offset | length | payload
const voucher = fs.readFileSync(path.join(dir, 'advance.output-1.bin'));
- assert.equal(hex(voucher.subarray(0, 4)), FUNSEL.output2);
- assert.equal(hex(voucher.subarray(4, 36)), TAG.callVoucher);
- assert.deepEqual(voucher.subarray(36, 68), addressWord(destination));
+ assert.equal(hex(voucher.subarray(0, 4)), FUNSEL.callVoucher);
+ assert.deepEqual(voucher.subarray(4, 36), addressWord(destination));
+ assert.deepEqual(voucher.subarray(36, 68), word(1000n)); // value
assert.deepEqual(voucher.subarray(68, 100), word(0x60));
- const used = 96 + pad32(voucherPayload).length;
- assert.deepEqual(voucher.subarray(100, 132), word(used));
- assert.deepEqual(voucher.subarray(132, 164), word(1000n)); // value
- assert.deepEqual(voucher.subarray(164, 196), word(0x40)); // inner offset
- assert.deepEqual(voucher.subarray(196, 228), word(voucherPayload.length));
- assert.deepEqual(voucher.subarray(228, 228 + voucherPayload.length), voucherPayload);
+ assert.deepEqual(voucher.subarray(100, 132), word(voucherPayload.length));
+ assert.deepEqual(voucher.subarray(132, 132 + voucherPayload.length), voucherPayload);
// reports are raw
assert.deepEqual(fs.readFileSync(path.join(dir, 'advance.report-0.bin')), reportPayload);
@@ -180,23 +189,68 @@ test('inspect request', async () => {
rollup.close();
});
-test('delegate call voucher', async () => {
- const dir = writeInputs('dcv', [[0, 'advance.bin', encodeEvmAdvance(ADVANCE)]]);
+test('asset transfer outputs', async () => {
+ const dir = writeInputs('transfers', [[0, 'advance.bin', encodeEvmAdvance(ADVANCE)]]);
const rollup = new Rollup();
rollup.waitForInput();
- const destination = `0x${'bb'.repeat(20)}`;
- const payload = Buffer.from('delegate-payload');
- assert.equal(rollup.emitOutput(encodeDelegateCallVoucher({ destination, payload })), 0);
-
- // Output2 envelope: funsel | DELEGATECALL tag | destination | offset | length | payload
- const output = fs.readFileSync(path.join(dir, 'advance.output-0.bin'));
- assert.equal(hex(output.subarray(0, 4)), FUNSEL.output2);
- assert.equal(hex(output.subarray(4, 36)), TAG.delegateCallVoucher);
- assert.deepEqual(output.subarray(36, 68), addressWord(destination));
- assert.deepEqual(output.subarray(68, 100), word(0x60));
- assert.deepEqual(output.subarray(100, 132), word(payload.length));
- assert.deepEqual(output.subarray(132, 132 + payload.length), payload);
+ const recipient = `0x${'bb'.repeat(20)}`;
+ const token = `0x${'cc'.repeat(20)}`;
+ const data = Buffer.from('transfer-data');
+
+ assert.equal(rollup.emitOutput(encodeERC20Transfer({ recipient, token, value: 1000n })), 0);
+ assert.equal(rollup.emitOutput(encodeERC721Transfer({ recipient, token, tokenId: 7n, data })), 1);
+ assert.equal(rollup.emitOutput(encodeERC1155SingleTransfer({ recipient, token, tokenId: 7n, value: 3n })), 2);
+ assert.equal(
+ rollup.emitOutput(
+ encodeERC1155BatchTransfer({
+ recipient,
+ token,
+ tokenIdsAndValues: [
+ [1n, 2n],
+ [3n, 4n],
+ ],
+ }),
+ ),
+ 3,
+ );
+
+ // ERC20Transfer(address,address,uint256): static, funsel + 3 words
+ const erc20 = fs.readFileSync(path.join(dir, 'advance.output-0.bin'));
+ assert.equal(hex(erc20.subarray(0, 4)), FUNSEL.erc20Transfer);
+ assert.deepEqual(erc20.subarray(4, 36), addressWord(recipient));
+ assert.deepEqual(erc20.subarray(36, 68), addressWord(token));
+ assert.deepEqual(erc20.subarray(68, 100), word(1000n));
+ assert.equal(erc20.length, 100);
+
+ // ERC721Transfer(address,address,uint256,bytes) =
+ // funsel | recipient | token | tokenId | offset | length | data
+ const erc721 = fs.readFileSync(path.join(dir, 'advance.output-1.bin'));
+ assert.equal(hex(erc721.subarray(0, 4)), FUNSEL.erc721Transfer);
+ assert.deepEqual(erc721.subarray(68, 100), word(7n));
+ assert.deepEqual(erc721.subarray(100, 132), word(0x80));
+ assert.deepEqual(erc721.subarray(132, 164), word(data.length));
+ assert.deepEqual(erc721.subarray(164, 164 + data.length), data);
+
+ // ERC1155SingleTransfer(address,address,uint256,uint256,bytes): empty data
+ const single = fs.readFileSync(path.join(dir, 'advance.output-2.bin'));
+ assert.equal(hex(single.subarray(0, 4)), FUNSEL.erc1155SingleTransfer);
+ assert.deepEqual(single.subarray(68, 100), word(7n)); // tokenId
+ assert.deepEqual(single.subarray(100, 132), word(3n)); // value
+ assert.deepEqual(single.subarray(132, 164), word(0xa0)); // data offset
+ assert.deepEqual(single.subarray(164, 196), word(0)); // data length
+
+ // ERC1155BatchTransfer(address,address,uint256[2][],bytes): pairs are
+ // encoded as a dynamic array of static [tokenId, value] tuples
+ const batch = fs.readFileSync(path.join(dir, 'advance.output-3.bin'));
+ assert.equal(hex(batch.subarray(0, 4)), FUNSEL.erc1155BatchTransfer);
+ assert.deepEqual(batch.subarray(68, 100), word(0x80)); // pairs offset
+ assert.deepEqual(batch.subarray(100, 132), word(0x120)); // data offset
+ assert.deepEqual(batch.subarray(132, 164), word(2)); // pair count
+ assert.deepEqual(batch.subarray(164, 196), word(1n));
+ assert.deepEqual(batch.subarray(196, 228), word(2n));
+ assert.deepEqual(batch.subarray(228, 260), word(3n));
+ assert.deepEqual(batch.subarray(260, 292), word(4n));
rollup.close();
});
@@ -253,9 +307,14 @@ test('input validation', async () => {
const rollup = new Rollup();
rollup.waitForInput();
- assert.throws(() => encodeVoucher({ destination: '0x1234' }), /destination must be 20 bytes/);
- assert.throws(() => encodeVoucher({ destination: 'not-hex' }), TypeError);
- assert.throws(() => encodeVoucher({ destination: `0x${'aa'.repeat(20)}`, value: -1n }), RangeError);
+ assert.throws(() => encodeCallVoucher({ destination: '0x1234' }), /destination must be 20 bytes/);
+ assert.throws(() => encodeCallVoucher({ destination: 'not-hex' }), TypeError);
+ assert.throws(() => encodeCallVoucher({ destination: `0x${'aa'.repeat(20)}`, value: -1n }), RangeError);
+ assert.throws(() => encodeAdvance({ ...ADVANCE, chainId: 1n << 64n }), RangeError); // uint64 overflow
+ assert.throws(
+ () => encodeERC1155BatchTransfer({ recipient: `0x${'aa'.repeat(20)}`, token: `0x${'bb'.repeat(20)}`, tokenIdsAndValues: [[1n]] }),
+ TypeError,
+ );
assert.throws(() => rollup.emitOutput(42), TypeError);
assert.throws(() => decodeAdvance(Buffer.alloc(3)), RangeError); // shorter than a selector
assert.throws(() => decodeAdvance(Buffer.alloc(300)), TypeError); // long enough, wrong selector