| name | cartesi-backend-js-ts |
| version | 0.2.0 |
| description | Build Cartesi Rollups v2 backend applications in JavaScript and TypeScript with shared fundamentals: finish loop, typed payload parsing, advance/inspect handler separation, output emitters, asset deposits/withdrawals (Ether, ERC20, ERC721), and tests. Use when backend runtime is Node.js and the stack is JS or TS. |
Cartesi Rollups JS/TS Backend
Objective
Use this skill for JavaScript and TypeScript Cartesi backend codebases.
cartesi-backend-core is the source of truth for backend fundamental design principles (determinism, state transition model, payload contracts, output semantics, and finish-loop correctness). Use this skill for JS/TS-specific implementation choices only.
In scope:
- Node.js app loop design around
/finish;
- Advance/inspect handler structure;
- Typed payload decoding/validation;
- Notice/report/voucher emission helpers;
- Asset handling (Ether/ERC20/ERC721) via portal decoding and voucher encoding;
- JS/TS testing and runtime checks.
Out of scope:
- Deployment infrastructure (use
cartesi-deploy);
- Frontend wallet/UI workflows;
- Solidity integration contract implementation details.
Authoritative references
Scaffold from Cartesi CLI template
Start new projects with Cartesi CLI before applying architecture rules in this skill.
cartesi create <app-name> --template javascript
cartesi create <app-name> --template typescript
Replace <app-name> with your target project name.
Use the directory and file structure generated by the Cartesi CLI template as the default. Only recommend structural refactors (new files/folders/module splits) when the developer explicitly asks for them.
After scaffolding a TypeScript project, install deps and generate rollups schema types:
yarn && yarn run codegen
Finish-loop rules
- Dispatch by
request_type.
- Set returned status from handler result; do not force accept unconditionally.
- Keep error paths visible (logs + optional report payloads).
- Preserve deterministic behavior on repeated identical inputs.
TypeScript bootstrap pattern
The Cartesi TypeScript template uses openapi-fetch with the generated paths/components schema for a typed /finish poll loop. Keep handleAdvance and handleInspect in separate functions and let /finish carry the handler result back:
import createClient from "openapi-fetch";
import type { components, paths } from "./schema";
type AdvanceRequestData = components["schemas"]["Advance"];
type InspectRequestData = components["schemas"]["Inspect"];
type RequestHandlerResult = components["schemas"]["Finish"]["status"];
type RollupRequest = components["schemas"]["RollupRequest"];
export type Notice = components["schemas"]["Notice"];
export type Report = components["schemas"]["Report"];
export type Voucher = components["schemas"]["Voucher"];
const rollupServer = process.env.ROLLUP_HTTP_SERVER_URL!;
const main = async () => {
const { POST } = createClient<paths>({ baseUrl: rollupServer });
let status: RequestHandlerResult = "accept";
while (true) {
const { data, response } = await POST("/finish", {
body: { status },
parseAs: "text",
});
if (response.status === 200 && data) {
const request = JSON.parse(data) as RollupRequest;
switch (request.request_type) {
case "advance_state":
status = await handleAdvance(request.data as AdvanceRequestData);
break;
case "inspect_state":
await handleInspect(request.data as InspectRequestData);
break;
}
}
}
};
main().catch((e) => {
console.log(e);
process.exit(1);
});
Output emitter helpers
Wrap each output type (/notice, /voucher, /report) in a small helper keyed on the generated schema types. Encode string payloads via stringToHex (from viem) so the backend codec stays symmetric with senders:
import { stringToHex } from "viem";
const createNotice = (payload: Notice) =>
fetch(`${rollupServer}/notice`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const createVoucher = (payload: Voucher) =>
fetch(`${rollupServer}/voucher`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const createReport = (payload: Report) =>
fetch(`${rollupServer}/report`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
await createNotice({ payload: stringToHex(JSON.stringify({ type: "transfer", amount })) });
Payload contract rules
- Define and enforce versioned payload schema.
- Keep sender/backend codec symmetry (hex encoding expectations).
- Validate before mutation; reject malformed payloads early.
User-driven operations sent through cartesi send (or InputBox) arrive as hex-encoded payloads. For string/JSON encodings, decode with viem and normalize any addresses with getAddress before passing into domain logic:
import { hexToString, getAddress, Address } from "viem";
const { operation, ...args } = JSON.parse(hexToString(payload));
switch (operation) {
case "transfer":
break;
case "withdraw":
break;
default:
return "reject";
}
Inspect contract rules
- Inspect endpoints are read-only.
- Use stable route names and response envelopes.
- Include lightweight diagnostics (
health, counters, recent errors).
- Avoid expensive full-state dumps; support pagination for large sets.
Asset handling
Primary reference: Asset handling. The wallet tutorials linked above remain useful as end-to-end walkthroughs but are slightly outdated; treat the asset-handling page as the source of truth and the tutorials as background context.
Cartesi exposes Portal contracts on L1 that move assets into the application. Each deposit arrives as an advance_state input whose metadata.msg_sender equals the Portal contract address. Withdrawals run the other direction: only the application can initiate them by emitting a voucher; the voucher is executed on L1 once its epoch settles.
Resolve Portal addresses for your environment with:
cartesi address-book
Pin them in src/index.ts and dispatch on data.metadata.msg_sender (case-insensitive).
For production dApps prefer the community library Deroll, which wraps these patterns across Ether, ERC-20, ERC-721, and ERC-1155. The snippets below show the underlying primitives.
Deposit payload encoding
Portals deliver deposit payloads as packed ABI-encoded fields, optionally followed by a standard ABI-encoded tail for variable-length data such as baseLayerData and execLayerData.
| Asset | Packed fields | Standard-ABI tail |
|---|
| Ether | address sender, uint256 value | bytes execLayerData |
| ERC-20 | address token, address sender, uint256 amount | bytes execLayerData |
| ERC-721 | address token, address sender, uint256 tokenId | bytes baseLayerData, bytes execLayerData |
| ERC-1155 single | address token, address sender, uint256 tokenId, uint256 value | bytes baseLayerData, bytes execLayerData |
| ERC-1155 batch | address token, address sender | uint256[] tokenIds, uint256[] values, bytes baseLayerData, bytes execLayerData |
Decoding a deposit
The advance handler receives the deposit payload as a hex string. Read fixed-width fields at known byte offsets; the tail (after the packed prefix) is the standard-ABI region. Example for ERC-20 (token(20) + sender(20) + amount(32) = 72 bytes, then execLayerData):
function decodeErc20Deposit(payloadHex) {
if (typeof payloadHex !== "string") {
throw new TypeError("payload must be a hex string");
}
const payload = payloadHex.startsWith("0x") ? payloadHex.slice(2) : payloadHex;
const raw = Buffer.from(payload, "hex");
if (raw.length < 72) {
throw new Error("invalid ERC-20 deposit payload");
}
const token = `0x${raw.subarray(0, 20).toString("hex")}`.toLowerCase().trim();
const sender = `0x${raw.subarray(20, 40).toString("hex")}`.toLowerCase().trim();
const amount = BigInt(`0x${raw.subarray(40, 72).toString("hex")}`);
const exec_layer_data = raw.subarray(72);
return { token, sender, amount, exec_layer_data };
}
Apply the same offset pattern for Ether (sender(20) + value(32)) and ERC-721 (token(20) + sender(20) + tokenId(32)); switch to a real ABI decoder when the tail carries arrays (ERC-1155 batch) or non-empty baseLayerData/execLayerData.
Withdrawing ERC-20 tokens
Encode transfer(recipient, amount) calldata with viem and emit a voucher whose destination is the token contract and value is zeroHash (no Ether attached):
import { encodeFunctionData, erc20Abi, hexToString, zeroHash } from "viem";
const rollup_server = process.env.ROLLUP_HTTP_SERVER_URL;
async function handle_advance(data) {
const sender = data["metadata"]["msg_sender"];
const payload = hexToString(data.payload);
const erc20Token = "0x784f0c076CC55EAD0a585a9A13e57c467c91Dc3a";
const call = encodeFunctionData({
abi: erc20Abi,
functionName: "transfer",
args: [sender, BigInt(10)],
});
const voucher = {
destination: erc20Token,
payload: call,
value: zeroHash,
};
await emitVoucher(voucher);
return "accept";
}
const emitVoucher = async (voucher) => {
await fetch(rollup_server + "/voucher", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(voucher),
});
};
The application contract is the msg.sender during voucher execution, so the token contract sees transfer as coming from the app's own balance.
Withdrawing Ether
The Application contract executes vouchers via a safeCall to destination, forwarding value (wei) and payload (calldata). For a plain Ether transfer leave payload empty (zeroHash); to call a payable function on the receiver, set payload to the encoded function call.
import { hexToString, numberToHex, parseEther, zeroHash } from "viem";
const rollup_server = process.env.ROLLUP_HTTP_SERVER_URL;
async function handle_advance(data) {
const sender = data["metadata"]["msg_sender"];
const payload = hexToString(data.payload);
const voucher = {
destination: sender,
payload: zeroHash,
value: numberToHex(BigInt(parseEther("1"))).slice(2),
};
await emitVoucher(voucher);
return "accept";
}
Voucher destinations and function signatures
| Asset | Destination | Function signature |
|---|
| Ether | dApp contract | withdrawEther(address,uint256) |
| ERC-20 | Token contract | transfer(address,uint256) / transferFrom(address,address,uint256) |
| ERC-721 | Token contract | safeTransferFrom(address,address,uint256) / safeTransferFrom(address,address,uint256,bytes) |
| ERC-1155 single | Token contract | safeTransferFrom(address,address,uint256,uint256,data) |
| ERC-1155 batch | Token contract | safeBatchTransferFrom(address,address,uint256[],uint256[],data) |
For withdrawals that need the application's own address as a from/sender argument (for example transferFrom or safeTransferFrom), read it from advance.metadata.app_contract inside the handler.
Epoch length and voucher execution
Cartesi nodes close one epoch every 7200 blocks by default. Vouchers can only be executed after the epoch they were emitted in has settled, so withdrawals do not appear on L1 immediately. For dev/test workflows you can manually shorten the epoch length to speed up voucher execution.
Determinism and accounting rules
- Keep per-account state in typed maps:
Map<Address, bigint> for fungible balances, Map<Address, Set<number>> for NFTs keyed by contract.
- Validate amounts (no negative deltas, no underflow) before mutating state.
- Emit a notice on deposit and on transfer; emit a voucher only on withdrawal.
- Decrease balance before emitting the withdrawal voucher so replay produces identical state and outputs.
- Vouchers MUST be deterministic functions of input and current state (no time, randomness, or external calls).
- Inspect handlers MUST NOT mutate balances; they only read and emit reports.
Testing baseline
Use vitest or project-standard test runner.
Minimum coverage:
- codec roundtrip (hex/string, JSON);
- advance success and reject behavior;
- inspect route schema and deterministic output;
- finish-loop status propagation correctness;
- if asset-handling is in scope:
- deposit payload decoding for each supported asset type (packed prefix + ABI tail);
- voucher encoding round-trip (decode the emitted calldata back to the expected function and args);
- insufficient-balance and unknown-operation reject paths.
Completion contract
Return with:
- whether template structure was kept as-is or changed on request;
- payload + inspect route contract;
- portal addresses used (per environment) when asset handling is in scope;
- test commands and outcomes;
- explicit unresolved risks.