diff --git a/cartesi-rollups_versioned_docs/version-2.0/development/asset-handling.md b/cartesi-rollups_versioned_docs/version-2.0/development/asset-handling.md
index 153457b3..4bbf9ff2 100644
--- a/cartesi-rollups_versioned_docs/version-2.0/development/asset-handling.md
+++ b/cartesi-rollups_versioned_docs/version-2.0/development/asset-handling.md
@@ -99,7 +99,7 @@ import AssetWithdrawEtherCPP from './snippets/asset_withdraw_ether_cpp.md';
-For a full guide, see the Tutorials: [ERC-20 Token Wallet](../tutorials/erc-20-token-wallet.md) and [Utilizing test tokens in dev environment](../tutorials/utilizing-the-cli-test-tokens.md).
+For a full guide, see the Tutorials: [ERC-20 Token Wallet](../tutorials/erc-20-token-wallet.md), [ERC-1155 Token Wallet](../tutorials/erc-1155-token-wallet.md), and [Utilizing test tokens in dev environment](../tutorials/utilizing-the-cli-test-tokens.md).
## Withdrawing assets
diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/erc-1155-token-wallet.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/erc-1155-token-wallet.md
new file mode 100644
index 00000000..db012be5
--- /dev/null
+++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/erc-1155-token-wallet.md
@@ -0,0 +1,636 @@
+---
+id: erc-1155-token-wallet
+title: Integrating ERC1155 token wallet functionality
+---
+
+This tutorial guides you through building an ERC-1155 token wallet for a Cartesi backend application using TypeScript. It covers **single** and **batch** deposits from the base layer, internal balance tracking, transfers, and withdrawals for both modes.
+
+## Setting up the project
+
+First, set up your Cartesi project as described in the [Ether wallet tutorial](./ether-wallet.md#setting-up-the-project). Create a new project (for example `erc-1155-token-wallet`) and install [`viem`](https://viem.sh/):
+
+```bash
+cartesi create erc-1155-token-wallet --template typescript
+cd erc-1155-token-wallet
+yarn && yarn run codegen
+yarn add viem
+```
+
+Single deposits use packed fields (`token`, `sender`, `tokenId`, `value`) with an optional standard-ABI tail; batch deposits use packed `token` and `sender` followed by ABI-encoded `tokenIds`, `values`, and optional data fields. See [asset handling](../development/asset-handling.md#abi-encoding-for-deposits). Withdrawals emit vouchers whose `safeTransferFrom` or `safeBatchTransferFrom` sender is your on-chain application address (`metadata.app_contract` on each advance)—see [withdrawing tokens](../development/asset-handling.md#withdrawing-tokens).
+
+## Building the ERC1155 wallet
+
+Create `src/wallet/balance.ts`:
+
+```typescript
+import { Address } from "viem";
+
+/** Per-account balances: ERC-1155 contract → tokenId → amount */
+export class Balance {
+ private holdings: Map
> = new Map();
+
+ constructor(private readonly account: string) {}
+
+ getAmount(erc1155: Address, tokenId: bigint): bigint {
+ return this.holdings.get(erc1155)?.get(tokenId) ?? 0n;
+ }
+
+ increase(erc1155: Address, tokenId: bigint, amount: bigint): void {
+ if (amount < 0n) {
+ throw new Error(`Invalid amount for ${erc1155} id ${tokenId}`);
+ }
+ if (!this.holdings.has(erc1155)) {
+ this.holdings.set(erc1155, new Map());
+ }
+ const byId = this.holdings.get(erc1155)!;
+ byId.set(tokenId, (byId.get(tokenId) ?? 0n) + amount);
+ }
+
+ decrease(erc1155: Address, tokenId: bigint, amount: bigint): void {
+ const current = this.getAmount(erc1155, tokenId);
+ if (current < amount) {
+ throw new Error(
+ `Insufficient balance for ${erc1155} id ${tokenId} on ${this.account}`
+ );
+ }
+ this.holdings.get(erc1155)!.set(tokenId, current - amount);
+ }
+
+ listIds(erc1155: Address): { tokenId: string; amount: string }[] {
+ const byId = this.holdings.get(erc1155);
+ if (!byId) return [];
+ return Array.from(byId.entries()).map(([id, amount]) => ({
+ tokenId: id.toString(),
+ amount: amount.toString(),
+ }));
+ }
+}
+```
+
+Create `src/wallet/wallet.ts`:
+
+```typescript
+import {
+ Address,
+ getAddress,
+ encodeFunctionData,
+ decodeAbiParameters,
+ sliceHex,
+ zeroHash,
+ type Hex,
+} from "viem";
+import { erc1155Abi } from "viem";
+import { Balance } from "./balance";
+import { Voucher } from "..";
+
+export class Wallet {
+ private accounts: Map = new Map();
+
+ private getOrCreate(account: Address): Balance {
+ let balance = this.accounts.get(account);
+ if (!balance) {
+ balance = new Balance(account);
+ this.accounts.set(account, balance);
+ }
+ return balance;
+ }
+
+ getBalance(account: Address, erc1155: Address, tokenId: bigint): bigint {
+ return this.getOrCreate(account).getAmount(erc1155, tokenId);
+ }
+
+ listBalances(account: Address, erc1155: Address) {
+ return {
+ address: account,
+ erc1155,
+ holdings: this.getOrCreate(account).listIds(erc1155),
+ };
+ }
+
+ processSingleDeposit(payload: Hex): string {
+ const [erc1155, account, tokenId, amount] = this.parseSingleDeposit(payload);
+ return this.deposit(account, erc1155, tokenId, amount, "erc1155SingleDeposit");
+ }
+
+ processBatchDeposit(payload: Hex): string {
+ const [erc1155, account, tokenIds, amounts] =
+ this.parseBatchDeposit(payload);
+ const balance = this.getOrCreate(account);
+ for (let i = 0; i < tokenIds.length; i++) {
+ balance.increase(erc1155, tokenIds[i], amounts[i]);
+ }
+ return JSON.stringify({
+ type: "erc1155BatchDeposit",
+ content: {
+ address: account,
+ erc1155,
+ tokenIds: tokenIds.map((id) => id.toString()),
+ amounts: amounts.map((a) => a.toString()),
+ },
+ });
+ }
+
+ private parseSingleDeposit(
+ payload: Hex
+ ): [Address, Address, bigint, bigint] {
+ const erc1155 = getAddress(sliceHex(payload, 0, 20));
+ const account = getAddress(sliceHex(payload, 20, 40));
+ const tokenId = BigInt(sliceHex(payload, 40, 72));
+ const amount = BigInt(sliceHex(payload, 72, 104));
+ return [erc1155, account, tokenId, amount];
+ }
+
+ private parseBatchDeposit(
+ payload: Hex
+ ): [Address, Address, bigint[], bigint[]] {
+ const erc1155 = getAddress(sliceHex(payload, 0, 20));
+ const account = getAddress(sliceHex(payload, 20, 40));
+ const [tokenIds, amounts] = decodeAbiParameters(
+ [
+ { type: "uint256[]" },
+ { type: "uint256[]" },
+ { type: "bytes" },
+ { type: "bytes" },
+ ],
+ sliceHex(payload, 40)
+ );
+ return [erc1155, account, tokenIds, amounts];
+ }
+
+ private deposit(
+ account: Address,
+ erc1155: Address,
+ tokenId: bigint,
+ amount: bigint,
+ noticeType: string
+ ): string {
+ this.getOrCreate(account).increase(erc1155, tokenId, amount);
+ return JSON.stringify({
+ type: noticeType,
+ content: {
+ address: account,
+ erc1155,
+ tokenId: tokenId.toString(),
+ amount: amount.toString(),
+ },
+ });
+ }
+
+ transferSingle(
+ from: Address,
+ to: Address,
+ erc1155: Address,
+ tokenId: bigint,
+ amount: bigint
+ ): string {
+ const fromBal = this.getOrCreate(from);
+ const toBal = this.getOrCreate(to);
+ fromBal.decrease(erc1155, tokenId, amount);
+ toBal.increase(erc1155, tokenId, amount);
+ return JSON.stringify({
+ type: "erc1155SingleTransfer",
+ content: { from, to, erc1155, tokenId: tokenId.toString(), amount: amount.toString() },
+ });
+ }
+
+ transferBatch(
+ from: Address,
+ to: Address,
+ erc1155: Address,
+ tokenIds: bigint[],
+ amounts: bigint[]
+ ): string {
+ if (tokenIds.length !== amounts.length) {
+ throw new Error("tokenIds and amounts length mismatch");
+ }
+ const fromBal = this.getOrCreate(from);
+ const toBal = this.getOrCreate(to);
+ for (let i = 0; i < tokenIds.length; i++) {
+ fromBal.decrease(erc1155, tokenIds[i], amounts[i]);
+ toBal.increase(erc1155, tokenIds[i], amounts[i]);
+ }
+ return JSON.stringify({
+ type: "erc1155BatchTransfer",
+ content: {
+ from,
+ to,
+ erc1155,
+ tokenIds: tokenIds.map((id) => id.toString()),
+ amounts: amounts.map((a) => a.toString()),
+ },
+ });
+ }
+
+ withdrawSingle(
+ application: Address,
+ account: Address,
+ erc1155: Address,
+ tokenId: bigint,
+ amount: bigint
+ ): Voucher {
+ this.getOrCreate(account).decrease(erc1155, tokenId, amount);
+ const call = encodeFunctionData({
+ abi: erc1155Abi,
+ functionName: "safeTransferFrom",
+ args: [application, account, tokenId, amount, "0x"],
+ });
+ return { destination: erc1155, payload: call, value: zeroHash };
+ }
+
+ withdrawBatch(
+ application: Address,
+ account: Address,
+ erc1155: Address,
+ tokenIds: bigint[],
+ amounts: bigint[]
+ ): Voucher {
+ const balance = this.getOrCreate(account);
+ for (let i = 0; i < tokenIds.length; i++) {
+ balance.decrease(erc1155, tokenIds[i], amounts[i]);
+ }
+ const call = encodeFunctionData({
+ abi: erc1155Abi,
+ functionName: "safeBatchTransferFrom",
+ args: [application, account, tokenIds, amounts, "0x"],
+ });
+ return { destination: erc1155, payload: call, value: zeroHash };
+ }
+}
+```
+
+### Voucher creation
+
+- **Single withdraw** encodes `safeTransferFrom(application, recipient, tokenId, amount, "0x")`.
+- **Batch withdraw** encodes `safeBatchTransferFrom(application, recipient, ids[], amounts[], "0x")`.
+
+Tokens deposited through the portals are held by your on-chain `Application` contract; the `from` address in the voucher must match `metadata.app_contract` from the advance. Set `value` to [`zeroHash`](https://viem.sh/docs/glossary/types#zeroHash) when no Ether is sent with the call.
+
+## Using the wallet
+
+Create `src/index.ts` to wire deposits from both portals and user operations sent as JSON inputs.
+
+:::note Portal addresses
+Run [`cartesi address-book`](../development/send-inputs-and-assets.md) and copy the `ERC1155SinglePortal` and `ERC1155BatchPortal` addresses into `index.ts`. Do not hardcode portal addresses—they differ by CLI version and chain.
+:::
+
+```typescript
+import createClient from "openapi-fetch";
+import type { components, paths } from "./schema";
+import { Wallet } from "./wallet/wallet";
+import { stringToHex, getAddress, Address, hexToString, toHex } from "viem";
+
+type AdvanceRequestData = components["schemas"]["Advance"];
+type InspectRequestData = components["schemas"]["Inspect"];
+type RequestHandlerResult = components["schemas"]["Finish"]["status"];
+type RollupRequest = components["schemas"]["RollupRequest"];
+type InspectRequestHandler = (data: InspectRequestData) => Promise;
+type AdvanceRequestHandler = (
+ data: AdvanceRequestData
+) => Promise;
+
+export type Notice = components["schemas"]["Notice"];
+export type Report = components["schemas"]["Report"];
+export type Voucher = components["schemas"]["Voucher"];
+
+const wallet = new Wallet();
+const ERC1155_SINGLE_PORTAL = `0xYOUR_ERC1155_SINGLE_PORTAL_ADDRESS`;
+const ERC1155_BATCH_PORTAL = `0xYOUR_ERC1155_BATCH_PORTAL_ADDRESS`;
+
+const rollupServer = process.env.ROLLUP_HTTP_SERVER_URL;
+
+const parseBigIntArray = (values: string[] | string): bigint[] => {
+ const list = Array.isArray(values) ? values : [values];
+ return list.map((v) => BigInt(v));
+};
+
+const handleAdvance: AdvanceRequestHandler = async (data) => {
+ const application = data.metadata.app_contract;
+ const sender = data.metadata.msg_sender.toLowerCase();
+ const payload = data.payload;
+
+ if (sender === ERC1155_SINGLE_PORTAL.toLowerCase()) {
+ const deposit = wallet.processSingleDeposit(payload);
+ await createNotice({ payload: stringToHex(deposit) });
+ return "accept";
+ }
+
+ if (sender === ERC1155_BATCH_PORTAL.toLowerCase()) {
+ const deposit = wallet.processBatchDeposit(payload);
+ await createNotice({ payload: stringToHex(deposit) });
+ return "accept";
+ }
+
+ try {
+ const body = JSON.parse(hexToString(payload)) as Record;
+ const { operation, mode } = body;
+ const from = getAddress(body.from as Address);
+ const erc1155 = getAddress(body.erc1155 as Address);
+ const app = getAddress(application as Address);
+
+ if (operation === "transfer" && mode === "single") {
+ const notice = wallet.transferSingle(
+ from,
+ getAddress(body.to as Address),
+ erc1155,
+ BigInt(body.tokenId as string),
+ BigInt(body.amount as string)
+ );
+ await createNotice({ payload: stringToHex(notice) });
+ } else if (operation === "transfer" && mode === "batch") {
+ const notice = wallet.transferBatch(
+ from,
+ getAddress(body.to as Address),
+ erc1155,
+ parseBigIntArray(body.tokenIds as string[]),
+ parseBigIntArray(body.amounts as string[])
+ );
+ await createNotice({ payload: stringToHex(notice) });
+ } else if (operation === "withdraw" && mode === "single") {
+ const voucher = wallet.withdrawSingle(
+ app,
+ from,
+ erc1155,
+ BigInt(body.tokenId as string),
+ BigInt(body.amount as string)
+ );
+ await createVoucher(voucher);
+ } else if (operation === "withdraw" && mode === "batch") {
+ const voucher = wallet.withdrawBatch(
+ app,
+ from,
+ erc1155,
+ parseBigIntArray(body.tokenIds as string[]),
+ parseBigIntArray(body.amounts as string[])
+ );
+ await createVoucher(voucher);
+ }
+ } catch (error) {
+ console.error("Error processing payload:", error);
+ }
+
+ return "accept";
+};
+
+const handleInspect: InspectRequestHandler = async (data) => {
+ try {
+ const query = hexToString(data.payload);
+ const [kind, address, erc1155, tokenId] = query.split("/");
+ if (kind !== "erc1155") {
+ throw new Error(`Expected inspect kind erc1155, got ${kind}`);
+ }
+ if (tokenId !== undefined) {
+ const amount = wallet.getBalance(
+ getAddress(address as Address),
+ getAddress(erc1155 as Address),
+ BigInt(tokenId)
+ );
+ await createReport({
+ payload: stringToHex(
+ `Balance of ${erc1155} id ${tokenId} for ${address} is ${amount}`
+ ),
+ });
+ } else {
+ const summary = wallet.listBalances(
+ getAddress(address as Address),
+ getAddress(erc1155 as Address)
+ );
+ await createReport({ payload: toHex(JSON.stringify(summary)) });
+ }
+ } catch (error) {
+ await createReport({
+ payload: stringToHex(`Error processing inspect payload: ${error}`),
+ });
+ }
+};
+
+const createNotice = async (payload: Notice) => {
+ await fetch(`${rollupServer}/notice`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+};
+
+const createVoucher = async (payload: Voucher) => {
+ await fetch(`${rollupServer}/voucher`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+};
+
+const createReport = async (payload: Report) => {
+ await fetch(`${rollupServer}/report`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+};
+
+const main = async () => {
+ const { POST } = createClient({ 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;
+ }
+ } else if (response.status === 202) {
+ console.log(await response.text());
+ }
+ }
+};
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
+```
+
+User input payloads use `mode`: `"single"` or `"batch"`, plus `operation`: `"transfer"` or `"withdraw"`.
+
+**Single transfer:**
+
+```json
+{
+ "operation": "transfer",
+ "mode": "single",
+ "erc1155": "0xTokenAddress",
+ "from": "0xFromAddress",
+ "to": "0xToAddress",
+ "tokenId": "1",
+ "amount": "10"
+}
+```
+
+**Batch transfer:**
+
+```json
+{
+ "operation": "transfer",
+ "mode": "batch",
+ "erc1155": "0xTokenAddress",
+ "from": "0xFromAddress",
+ "to": "0xToAddress",
+ "tokenIds": ["1", "2"],
+ "amounts": ["10", "20"]
+}
+```
+
+**Single withdraw:**
+
+```json
+{
+ "operation": "withdraw",
+ "mode": "single",
+ "erc1155": "0xTokenAddress",
+ "from": "0xFromAddress",
+ "tokenId": "1",
+ "amount": "10"
+}
+```
+
+**Batch withdraw:**
+
+```json
+{
+ "operation": "withdraw",
+ "mode": "batch",
+ "erc1155": "0xTokenAddress",
+ "from": "0xFromAddress",
+ "tokenIds": ["1", "2"],
+ "amounts": ["10", "20"]
+}
+```
+
+## Build and run the application
+
+```shell
+cartesi build
+cartesi run
+```
+
+### Deposits
+
+:::caution token approvals
+ERC-1155 uses `setApprovalForAll` on the token contract for each portal operator. Approve **both** [`ERC1155SinglePortal`](../api-reference/contracts/portals/ERC1155SinglePortal.md) and [`ERC1155BatchPortal`](../api-reference/contracts/portals/ERC1155BatchPortal.md) before depositing through the corresponding portal.
+:::
+
+**Single deposit** — interactively:
+
+```bash
+cartesi deposit erc1155-single
+```
+
+Non-interactive alternative. Run from your **project root** (with `cartesi run` up):
+
+```bash
+MULTI=$(cartesi address-book 2>&1 | grep -i TestMultiToken | awk '{print $NF}')
+SINGLE_PORTAL=$(cartesi address-book 2>&1 | grep -i ERC1155SinglePortal | awk '{print $NF}')
+
+cast send "$MULTI" \
+ "mint(address,uint256,uint256,bytes)" \
+ 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \
+ 1 100 0x \
+ --rpc-url http://127.0.0.1:6751/anvil \
+ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+
+cast send "$MULTI" \
+ "setApprovalForAll(address,bool)" \
+ "$SINGLE_PORTAL" \
+ true \
+ --rpc-url http://127.0.0.1:6751/anvil \
+ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+
+cartesi deposit erc1155-single 1 10 \
+ --token "$MULTI" \
+ --project-name erc-1155-token-wallet \
+ --rpc-url http://127.0.0.1:6751/anvil
+```
+
+Deposits token ID `1` with amount `10`. Skip `mint` if you already hold that ID.
+
+**Batch deposit** — interactively:
+
+```bash
+cartesi deposit erc1155-batch
+```
+
+Non-interactive alternative (from project root):
+
+```bash
+MULTI=$(cartesi address-book 2>&1 | grep -i TestMultiToken | awk '{print $NF}')
+BATCH_PORTAL=$(cartesi address-book 2>&1 | grep -i ERC1155BatchPortal | awk '{print $NF}')
+
+cast send "$MULTI" \
+ "mint(address,uint256,uint256,bytes)" \
+ 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \
+ 2 50 0x \
+ --rpc-url http://127.0.0.1:6751/anvil \
+ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+
+cast send "$MULTI" \
+ "setApprovalForAll(address,bool)" \
+ "$BATCH_PORTAL" \
+ true \
+ --rpc-url http://127.0.0.1:6751/anvil \
+ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+
+cartesi deposit erc1155-batch 1,2 10,20 \
+ --token "$MULTI" \
+ --project-name erc-1155-token-wallet \
+ --rpc-url http://127.0.0.1:6751/anvil
+```
+
+### Balance checks (Inspect)
+
+Inspect payloads use UTF-8 strings (hex-encoded in the JSON body):
+
+- Single ID: `erc1155/0xUser/0xContract/1`
+- All IDs for a contract: `erc1155/0xUser/0xContract`
+
+### Transfers and withdrawals
+
+Interactively:
+
+```bash
+cartesi send
+```
+
+Non-interactive examples (from project root):
+
+```bash
+cartesi send --encoding string \
+ '{"operation":"transfer","mode":"single","erc1155":"0xTokenAddress","from":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","to":"0x70997970C51812dc3A010C7d01b50b0d17e98F2a","tokenId":"1","amount":"5"}' \
+ --project-name erc-1155-token-wallet \
+ --rpc-url http://127.0.0.1:6751/anvil
+```
+
+```bash
+cartesi send --encoding string \
+ '{"operation":"withdraw","mode":"batch","erc1155":"0xTokenAddress","from":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","tokenIds":["1","2"],"amounts":["5","10"]}' \
+ --project-name erc-1155-token-wallet \
+ --rpc-url http://127.0.0.1:6751/anvil
+```
+
+Replace `0xTokenAddress` with `TestMultiToken` from `cartesi address-book`.
+
+### Using the explorer
+
+Start the node with the explorer enabled to execute vouchers after an epoch closes:
+
+```shell
+cartesi run --services explorer
+```
+
+The local explorer is at `http://localhost:6751/explorer` (see [running an application](../development/running-an-application.md)).
diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/erc-20-token-wallet.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/erc-20-token-wallet.md
index 88cd7346..7d6c1c84 100644
--- a/cartesi-rollups_versioned_docs/version-2.0/tutorials/erc-20-token-wallet.md
+++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/erc-20-token-wallet.md
@@ -1,17 +1,10 @@
---
id: erc-20-token-wallet
title: Integrating ERC20 token wallet functionality
-resources:
- - url: https://github.com/masiedu4/erc20-wallet-tutorial
- title: Source code for ERC20 wallet tutorial
---
This tutorial will guide you through creating a basic ERC20 token wallet for a Cartesi backend application using TypeScript.
-:::note community tools
-This tutorial is for educational purposes. For production dApps, we recommend using [Deroll](https://deroll.dev/), a TypeScript package that simplifies app and wallet functionality across all token standards for Cartesi applications.
-:::
-
## Setting up the project
First, create a new TypeScript project using the [Cartesi CLI](../development/installation.md/#cartesi-cli).
@@ -26,92 +19,13 @@ Run the following to generate the types for your project:
yarn && yarn run codegen
```
-Now, navigate to the project directory and install [`ethers`](https://docs.ethers.org/v5/), [`viem`](https://viem.sh/) and [`@cartesi/rollups`](https://www.npmjs.com/package/@cartesi/rollups) package:
-
-```bash
-yarn add ethers viem
-yarn add -D @cartesi/rollups
-```
-
-## Define the ABIs
-
-Let's write a configuration to generate the ABIs of the Cartesi Rollups Contracts.
-
-We will need the Solidity compiler and the contract code from the `@cartesi/rollups` package to generate the ABIs as constants.
-
-1. [Install the Solidity compiler](https://docs.soliditylang.org/en/latest/installing-solidity.html).
-
-2. Create a new file named `generate_abis.sh` in the root of your project and add the following code:
-
-```bash
-#!/bin/bash
-
-set -e # Exit immediately if a command exits with a non-zero status.
-
-# Output directory for TypeScript files
-TS_DIR="src/wallet/abi"
-
-# Temporary directory for compilation output
-TEMP_DIR="temp_solc_output"
-
-# Create output and temporary directories
-mkdir -p "$TS_DIR"
-mkdir -p "$TEMP_DIR"
-
-# Function to generate ABI and export as a TypeScript variable
-generate_abi() {
- local sol_file="$1"
- local contract_name="$2"
- local output_file="$TS_DIR/${contract_name}Abi.ts"
-
- echo "Compiling $sol_file..."
-
- # Compile the contract in the temporary directory
- npx solcjs --abi "$sol_file" --base-path . --include-path node_modules/ --output-dir "$TEMP_DIR"
-
- # Find the generated ABI file
- abi_file=$(find "$TEMP_DIR" -name "*_${contract_name}.abi")
-
- if [ ! -f "$abi_file" ]; then
- echo "Error: ABI file not found for $contract_name"
- return 1
- fi
-
- # Read the ABI content
- abi=$(cat "$abi_file")
-
- echo "Extracted ABI for $contract_name"
-
- # Create a TypeScript file with exported ABI
- echo "export const ${contract_name}Abi = $abi as const;" > "$output_file"
-
- echo "Generated ABI for $contract_name"
- echo "----------------------"
-}
-
-# Generate ABIs
-generate_abi "node_modules/@cartesi/rollups/contracts/dapp/CartesiDApp.sol" "CartesiDApp"
-generate_abi "node_modules/@cartesi/rollups/contracts/portals/EtherPortal.sol" "EtherPortal"
-
-# Clean up the temporary directory
-rm -rf "$TEMP_DIR"
-
-echo "ABI generation complete"
-```
-
-This script will look for all specified `.sol` files and create a TypeScript file with the ABIs in the `src/wallet/abi` directory.
-
-Now, let's make the script executable:
+Now, navigate to the project directory and install [`viem`](https://viem.sh/):
```bash
-chmod +x generate_abis.sh
+yarn add viem
```
-And run it:
-
-```bash
-./generate_abis.sh
-```
+Deposit payloads from portals are packed ABI-encoded fields; see [asset handling](../development/asset-handling.md#abi-encoding-for-deposits) for the ERC-20 layout (`token`, `sender`, `amount`). Withdrawals use vouchers executed by the on-chain [`Application`](../api-reference/contracts/application.md) contract—see [withdrawing tokens](../development/asset-handling.md#withdrawing-tokens).
## Building the ERC20 wallet
@@ -187,8 +101,14 @@ The `Balance` class represents an individual account's balance. It includes meth
Now, create a file named `wallet.ts` in the `src/wallet` directory and add the following code:
```typescript
-import { Address, getAddress, encodeFunctionData } from "viem";
-import { ethers } from "ethers";
+import {
+ Address,
+ getAddress,
+ encodeFunctionData,
+ sliceHex,
+ zeroHash,
+ type Hex,
+} from "viem";
import { Balance } from "./balance";
import { erc20Abi } from "viem";
import { Voucher } from "..";
@@ -231,18 +151,12 @@ export class Wallet {
}
};
- private parseErc20Deposit = (payload: string): [Address, Address, bigint] => {
+ private parseErc20Deposit = (payload: Hex): [Address, Address, bigint] => {
try {
- let inputData = [];
- inputData[0] = ethers.dataSlice(payload, 0, 20);
- inputData[1] = ethers.dataSlice(payload, 20, 40);
- inputData[2] = ethers.dataSlice(payload, 40, 72);
-
- return [
- getAddress(inputData[0]),
- getAddress(inputData[1]),
- BigInt(inputData[2]),
- ];
+ const erc20 = getAddress(sliceHex(payload, 0, 20));
+ const account = getAddress(sliceHex(payload, 20, 40));
+ const amount = BigInt(sliceHex(payload, 40, 72));
+ return [erc20, account, amount];
} catch (e) {
throw new Error(`Error parsing ERC20 deposit: ${e}`);
}
@@ -267,6 +181,7 @@ export class Wallet {
};
withdrawErc20 = (
+ application: Address,
account: Address,
erc20: Address,
amount: bigint
@@ -276,8 +191,8 @@ export class Wallet {
balance.decreaseErc20Balance(erc20, amount);
const call = encodeFunctionData({
abi: erc20Abi,
- functionName: "transfer",
- args: [account, amount],
+ functionName: "transferFrom",
+ args: [application, account, amount],
});
console.log(`Voucher creation success`, {
@@ -288,7 +203,7 @@ export class Wallet {
return {
destination: erc20,
payload: call,
- value: "0x0",
+ value: zeroHash,
};
} catch (e) {
throw Error(`Error withdrawing ERC20 tokens: ${e}`);
@@ -326,19 +241,25 @@ export class Wallet {
}
```
+### Voucher creation
+
+The `withdrawErc20` method encodes `transferFrom(application, recipient, amount)` and returns a voucher. Tokens deposited through the portal sit in your on-chain `Application` contract; the application address in the voucher must match `metadata.app_contract` from the advance. Set `value` to [`zeroHash`](https://viem.sh/docs/glossary/types#zeroHash) when no Ether is sent with the call. See [withdrawing tokens](../development/asset-handling.md#withdrawing-tokens).
+
## Using the ERC20 wallet
-Now, let's create a simple wallet app at the entry point `src/index.ts` to test the wallet’s functionality.
+Now, let's create a simple application at the entry point `src/index.ts` to test the wallet’s functionality.
+
+The [`ERC20Portal`](../api-reference/contracts/portals/ERC20Portal.md) contract moves ERC-20 tokens from the base layer into your application. Deposits arrive as advances whose `metadata.msg_sender` is the portal address.
-:::note
-Run `cartesi address-book` to get the contract address of the `ERC20Portal` contract. Save this as a const in the `index.ts` file.
+:::note ERC20Portal address
+Run [`cartesi address-book`](../development/send-inputs-and-assets.md) and copy the `ERC20Portal` address for your network into `index.ts`. Do not hardcode portal addresses—they differ by CLI version and chain.
:::
```typescript
import createClient from "openapi-fetch";
import type { components, paths } from "./schema";
import { Wallet } from "./wallet/wallet";
-import { stringToHex, getAddress, Address, hexToString, toHex } from "viem";
+import { stringToHex, getAddress, Address, hexToString } from "viem";
type AdvanceRequestData = components["schemas"]["Advance"];
type InspectRequestData = components["schemas"]["Inspect"];
@@ -355,9 +276,8 @@ export type Report = components["schemas"]["Report"];
export type Voucher = components["schemas"]["Voucher"];
const wallet = new Wallet();
-
-const ERC20Portal = `0x05355c2F9bA566c06199DEb17212c3B78C1A3C31`;
-
+// Replace with the ERC20Portal address from `cartesi address-book`
+const ERC20Portal = `0xYOUR_ERC20_PORTAL_ADDRESS`;
const rollupServer = process.env.ROLLUP_HTTP_SERVER_URL;
console.log(`HTTP rollup_server url is ${rollupServer}`);
@@ -365,6 +285,7 @@ console.log(`HTTP rollup_server url is ${rollupServer}`);
const handleAdvance: AdvanceRequestHandler = async (data) => {
console.log("Received advance request data " + JSON.stringify(data));
+ const application = data["metadata"]["app_contract"];
const sender = data["metadata"]["msg_sender"];
const payload = data.payload;
@@ -390,6 +311,7 @@ const handleAdvance: AdvanceRequestHandler = async (data) => {
await createNotice({ payload: stringToHex(transfer) });
} else if (operation === "withdraw") {
const voucher = wallet.withdrawErc20(
+ getAddress(application as Address),
getAddress(from as Address),
getAddress(erc20 as Address),
BigInt(amount)
@@ -428,7 +350,7 @@ const handleInspect: InspectRequestHandler = async (data) => {
await createReport({ payload: stringToHex(balmsg) });
} catch (error) {
- const error_message = `Error processing inspect payload:", ${error}`;
+ const error_message = `Error processing inspect payload: ${error}`;
await createReport({ payload: stringToHex(error_message) });
}
@@ -507,9 +429,9 @@ Here is a breakdown of the wallet functionality:
- For `transfers`, we call `wallet.transferErc20` and create a notice with the parsed parameters.
-- For `withdrawals`, we call `wallet.withdrawErc20` and create voucher using the dApp dress and the parsed parameters.
+- For `withdrawals`, we call `wallet.withdrawErc20` with the on-chain application address (`metadata.app_contract`) as the `transferFrom` sender, then emit a voucher to the token contract.
-- We created helper functions to `createNotice` for deposits and transfers, `createReport` for balance checks and `createVoucher` for withdrawals.
+- We created helper functions to `createNotice` for deposits and transfers, `createReport` for balance checks, and `createVoucher` for withdrawals.
## Build and run the application
@@ -537,21 +459,59 @@ You will encounter this error if you don't approve the `ERC20Portal` address bef
`ContractFunctionExecutionError: The contract function "depositERC20Tokens" reverted with the following reason: ERC20: insufficient allowance`
:::
-To deposit ERC20 tokens, use the `cartesi deposit erc20` command and follow the prompts.
+To deposit ERC20 tokens interactively:
-### Balance checks(used in Inspect requests)
+```bash
+cartesi deposit erc20
+```
+
+Non-interactive alternative. Run from your **project root** (with `cartesi run` up). Resolve addresses from [`cartesi address-book`](../development/send-inputs-and-assets.md):
+
+```bash
+TOKEN=$(cartesi address-book 2>&1 | grep -i TestToken | awk '{print $NF}')
+ERC20_PORTAL=$(cartesi address-book 2>&1 | grep -i ERC20Portal | awk '{print $NF}')
+
+cast send "$TOKEN" \
+ "approve(address,uint256)" \
+ "$ERC20_PORTAL" \
+ 1000000000000000000 \
+ --rpc-url http://127.0.0.1:6751/anvil \
+ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+
+cartesi deposit erc20 100000000000000000 \
+ --token "$TOKEN" \
+ --project-name erc-20-token-wallet \
+ --rpc-url http://127.0.0.1:6751/anvil
+```
-To inspect balance, make an HTTP (post) call to:
+### Balance checks (used in Inspect requests)
+
+Inspect payloads use the form `userAddress/erc20TokenAddress` as a UTF-8 string (hex-encoded in the JSON body). Example for user `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` and token `0x5FbDB2315678afecb367f032d93F642f64180aa3`:
```bash
curl -X POST http://127.0.0.1:6751/inspect/erc-20-token-wallet \
-H "Content-Type: application/json" \
- -d '{0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266}'
+ -d '{"payload": "0x3078663339466436653531616164383846364634636536614238383237323739636666466239323236362f307835466244423233313536373861666563623336376630333264393346363432663634313830616133"}'
```
+Replace the inspect path if your project name is not `erc-20-token-wallet` (see the URL printed by `cartesi run`). Build the payload with `stringToHex` from viem if you use different addresses.
+
### Transfers and Withdrawals
-Use the `cartesi send` command then follow the prompts to select `String encoding`, finally enter any of the sample payloads:
+To process transfers and withdrawals interactively, run the command below, select `String encoding`, then enter one of the sample payloads:
+
+```bash
+cartesi send
+```
+
+Non-interactive alternative (from your project root):
+
+```bash
+cartesi send --encoding string \
+ '{"operation":"withdraw","erc20":"0xTokenAddress","from":"0xFromAddress","amount":"1000000000000000000"}' \
+ --project-name erc-20-token-wallet \
+ --rpc-url http://127.0.0.1:6751/anvil
+```
1. For transfers:
@@ -565,6 +525,20 @@ Use the `cartesi send` command then follow the prompts to select `String encodin
{"operation":"withdraw","erc20":"0xTokenAddress","from":"0xFromAddress","amount":"1000000000000000000"}
```
+### Using the explorer
+
+[CartesiScan](https://cartesiscan.io/) is a web application that offers a comprehensive overview of your application. It provides expandable data regarding notices, vouchers, and reports.
+
+Start the node with the explorer service enabled:
+
+```shell
+cartesi run --services explorer
+```
+
+The local explorer is then available at `http://localhost:6751/explorer` (same port as the application node—see [running an application](../development/running-an-application.md)). The explorer is not started by plain `cartesi run`; you must pass `--services explorer`.
+
+You can execute your vouchers via the explorer, which completes the withdrawal process at the end of [an epoch](../api-reference/backend/vouchers.md/#epoch-configuration).
+
:::info Repo Link
You can access the complete project implementation [here](https://github.com/Mugen-Builders/docs_examples/tree/main/erc-20-token-wallet)!
:::
\ No newline at end of file
diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/erc-721-token-wallet.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/erc-721-token-wallet.md
index f4a850e4..0c0c8530 100644
--- a/cartesi-rollups_versioned_docs/version-2.0/tutorials/erc-721-token-wallet.md
+++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/erc-721-token-wallet.md
@@ -1,24 +1,15 @@
---
id: erc-721-token-wallet
title: Integrating ERC721 token wallet functionality
-resources:
- - url: https://github.com/masiedu4/erc721-wallet-tutorials
- title: Source code for ERC721 wallet tutorial
---
This tutorial will guide you through creating a basic ERC721(NFT) token wallet using TypeScript for a Cartesi backend application.
-:::note community tools
-This tutorial is for educational purposes. For production dApps, we recommend using [Deroll](https://deroll.dev/), a TypeScript package that simplifies app and wallet functionality across all token standards for Cartesi applications.
-:::
-
## Setting up the project
-First, set up your Cartesi project as described in the [Erc20 token wallet tutorial](./erc-20-token-wallet.md#setting-up-the-project). Make sure you have the necessary dependencies installed.
+First, set up your Cartesi project as described in the [Ether wallet tutorial](./ether-wallet.md#setting-up-the-project). Create a new project (for example `erc-721-token-wallet`) and install [`viem`](https://viem.sh/) only.
-## Define the ABIs
-
-Next, Define the necessary Abi's as described in the [Erc20 token wallet tutorial](./erc-20-token-wallet.md#define-the-abis).
+ERC-721 deposit payloads use packed fields (`token`, `sender`, `tokenId`) with an optional standard-ABI tail; see [asset handling](../development/asset-handling.md#abi-encoding-for-deposits). Withdrawals emit vouchers whose `safeTransferFrom` sender is your on-chain application address (`metadata.app_contract` on each advance)—see [withdrawing tokens](../development/asset-handling.md#withdrawing-tokens).
## Building the ERC721 wallet
@@ -79,10 +70,15 @@ The `Balance` class represents an account's balance. It contains a map of ERC721
Now, create a file named `wallet.ts` in the `src/wallet` directory and add the following code:
```typescript
-import { Address, getAddress, hexToBytes, encodeFunctionData } from "viem";
-import { ethers } from "ethers";
+import {
+ Address,
+ getAddress,
+ encodeFunctionData,
+ sliceHex,
+ zeroHash,
+ type Hex,
+} from "viem";
import { Balance } from "./balance";
-
import { erc721Abi } from "viem";
import { Voucher } from "..";
@@ -138,10 +134,10 @@ export class Wallet {
}
}
- private parseErc721Deposit(payload: string): [Address, Address, number] {
- const erc721 = getAddress(ethers.dataSlice(payload, 0, 20));
- const account = getAddress(ethers.dataSlice(payload, 20, 40));
- const tokenId = parseInt(ethers.dataSlice(payload, 40, 72));
+ private parseErc721Deposit(payload: Hex): [Address, Address, number] {
+ const erc721 = getAddress(sliceHex(payload, 0, 20));
+ const account = getAddress(sliceHex(payload, 20, 40));
+ const tokenId = Number(BigInt(sliceHex(payload, 40, 72)));
return [erc721, account, tokenId];
}
@@ -164,7 +160,7 @@ export class Wallet {
}
withdrawErc721(
- rollupAddress: Address,
+ application: Address,
account: Address,
erc721: Address,
tokenId: number
@@ -175,7 +171,7 @@ export class Wallet {
const call = encodeFunctionData({
abi: erc721Abi,
functionName: "safeTransferFrom",
- args: [rollupAddress, account, BigInt(tokenId)],
+ args: [application, account, BigInt(tokenId)],
});
console.log("Voucher creator success", {
destination: erc721,
@@ -185,7 +181,7 @@ export class Wallet {
return {
destination: erc721,
payload: call,
- value: "0x0"
+ value: zeroHash,
};
} catch (e) {
throw Error(`Error withdrawing ERC721 token: ${e}`);
@@ -223,12 +219,18 @@ export class Wallet {
}
```
+### Voucher creation
+
+The `withdrawErc721` method encodes `safeTransferFrom(application, recipient, tokenId)` and returns a voucher. NFTs deposited through the portal are held by your on-chain `Application` contract; pass `metadata.app_contract` from the advance as the `from` address. Set `value` to [`zeroHash`](https://viem.sh/docs/glossary/types#zeroHash) when no Ether is sent with the call. See [withdrawing tokens](../development/asset-handling.md#withdrawing-tokens).
+
## Using the wallet
-Now, let's create a simple wallet app at the entry point `src/index.ts` to test the wallet’s functionality.
+Now, let's create a simple application at the entry point `src/index.ts` to test the wallet’s functionality.
-:::note
-Run `cartesi address-book` to get the addresses of the `ERC721Portal` contract. Save these as constants in the `index.ts` file.
+The [`ERC721Portal`](../api-reference/contracts/portals/ERC721Portal.md) contract moves ERC-721 tokens from the base layer into your application. Deposits arrive as advances whose `metadata.msg_sender` is the portal address.
+
+:::note ERC721Portal address
+Run [`cartesi address-book`](../development/send-inputs-and-assets.md) and copy the `ERC721Portal` address for your network into `index.ts`. Do not hardcode portal addresses—they differ by CLI version and chain.
:::
```typescript
@@ -252,8 +254,8 @@ export type Report = components["schemas"]["Report"];
export type Voucher = components["schemas"]["Voucher"];
const wallet = new Wallet();
-
-const ERC721Portal = `0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87`;
+// Replace with the ERC721Portal address from `cartesi address-book`
+const ERC721Portal = `0xYOUR_ERC721_PORTAL_ADDRESS`;
const rollupServer = process.env.ROLLUP_HTTP_SERVER_URL;
console.log(`HTTP rollup_server url is ${rollupServer}`);
@@ -261,7 +263,7 @@ console.log(`HTTP rollup_server url is ${rollupServer}`);
const handleAdvance: AdvanceRequestHandler = async (data) => {
console.log("Received advance request data " + JSON.stringify(data));
- const dAppAddress = data["metadata"]["app_contract"];
+ const application = data["metadata"]["app_contract"];
const sender = data["metadata"]["msg_sender"];
const payload = data.payload;
@@ -287,7 +289,7 @@ const handleAdvance: AdvanceRequestHandler = async (data) => {
await createNotice({ payload: stringToHex(transfer) });
} else if (operation === "withdraw") {
const voucher = wallet.withdrawErc721(
- getAddress(dAppAddress as Address),
+ getAddress(application as Address),
getAddress(from as Address),
getAddress(erc721 as Address),
parseInt(tokenId)
@@ -398,9 +400,9 @@ Here is a breakdown of the wallet functionality:
- For `transfers`, we call `wallet.transferErc721` and create a notice with the parsed parameters.
-- For `withdrawals`, we call `wallet.withdrawErc721` and create voucher using the dApp dress and the parsed parameters.
+- For `withdrawals`, we call `wallet.withdrawErc721` with the on-chain application address (`metadata.app_contract`) as the `safeTransferFrom` sender, then emit a voucher to the token contract.
-- We created helper functions to `createNotice` for deposits and transfers, `createReport` for balance checks and `createVoucher` for withdrawals.
+- We created helper functions to `createNotice` for deposits and transfers, `createReport` for balance checks, and `createVoucher` for withdrawals.
## Build and run the application
@@ -423,26 +425,74 @@ An approval step is needed for the [**ERC721 token standard**](https://ethereum.
Without this approval, the `ERC721Portal` cannot deposit your tokens to the Cartesi backend.
-You will encounter this error if you don't approve the `ERC20Portal` address before deposits:
+You will encounter this error if you don't approve the `ERC721Portal` address before deposits:
`ContractFunctionExecutionError: The contract function "depositERC721Tokens" reverted with the following reason: ERC721: insufficient allowance`
:::
-To deposit ERC721 tokens, use the `cartesi deposit erc721` command and follow the prompts.
+To deposit ERC721 tokens interactively:
+
+```bash
+cartesi deposit erc721
+```
+
+Non-interactive alternative. Run from your **project root** (with `cartesi run` up). Resolve addresses from [`cartesi address-book`](../development/send-inputs-and-assets.md):
+
+```bash
+NFT=$(cartesi address-book 2>&1 | grep -i TestNFT | awk '{print $NF}')
+ERC721_PORTAL=$(cartesi address-book 2>&1 | grep -i ERC721Portal | awk '{print $NF}')
+
+cast send "$NFT" \
+ "safeMint(address,uint256,string)" \
+ 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \
+ 1 \
+ "https://example.com/metadata/1.json" \
+ --rpc-url http://127.0.0.1:6751/anvil \
+ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+
+cast send "$NFT" \
+ "setApprovalForAll(address,bool)" \
+ "$ERC721_PORTAL" \
+ true \
+ --rpc-url http://127.0.0.1:6751/anvil \
+ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+
+cartesi deposit erc721 1 \
+ --token "$NFT" \
+ --project-name erc-721-token-wallet \
+ --rpc-url http://127.0.0.1:6751/anvil
+```
+
+Skip `safeMint` if token ID `1` is already minted to your address.
-### Balance checks(used in Inspect requests)
+### Balance checks (used in Inspect requests)
-To inspect the balance, make an HTTP call to:
+Inspect payloads use the form `userAddress/erc721TokenAddress` as a UTF-8 string (hex-encoded in the JSON body). Example for user `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` and collection `0x5FbDB2315678afecb367f032d93F642f64180aa3`:
```bash
curl -X POST http://127.0.0.1:6751/inspect/erc-721-token-wallet \
-H "Content-Type: application/json" \
- -d '{0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266}'
+ -d '{"payload": "0x3078663339466436653531616164383846364634636536614238383237323739636666466239323236362f307835466244423233313536373861666563623336376630333264393346363432663634313830616133"}'
```
+Replace the inspect path if your project name is not `erc-721-token-wallet` (see the URL printed by `cartesi run`). Build the payload with `stringToHex` from viem if you use different addresses.
+
### Transfers and Withdrawals
-Use the `cartesi send` command then follow the prompts to select `String encoding`, finally enter any of the sample payloads:
+To process transfers and withdrawals interactively, run the command below, select `String encoding`, then enter one of the sample payloads:
+
+```bash
+cartesi send
+```
+
+Non-interactive alternative (from your project root):
+
+```bash
+cartesi send --encoding string \
+ '{"operation":"withdraw","erc721":"0xTokenAddress","from":"0xFromAddress","tokenId":"1"}' \
+ --project-name erc-721-token-wallet \
+ --rpc-url http://127.0.0.1:6751/anvil
+```
1. For transfers:
@@ -456,6 +506,20 @@ Use the `cartesi send` command then follow the prompts to select `String encodin
{"operation":"withdraw","erc721":"0xTokenAddress","from":"0xFromAddress","tokenId":"1"}
```
+### Using the explorer
+
+[CartesiScan](https://cartesiscan.io/) is a web application that offers a comprehensive overview of your application. It provides expandable data regarding notices, vouchers, and reports.
+
+Start the node with the explorer service enabled:
+
+```shell
+cartesi run --services explorer
+```
+
+The local explorer is then available at `http://localhost:6751/explorer` (same port as the application node—see [running an application](../development/running-an-application.md)). The explorer is not started by plain `cartesi run`; you must pass `--services explorer`.
+
+You can execute your vouchers via the explorer, which completes the withdrawal process at the end of [an epoch](../api-reference/backend/vouchers.md/#epoch-configuration).
+
:::info Repo Link
You can access the complete project implementation [here](https://github.com/Mugen-Builders/docs_examples/tree/main/erc-721-token-wallet)!
:::
\ No newline at end of file
diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/ether-wallet.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/ether-wallet.md
index b040b06d..6df01a44 100644
--- a/cartesi-rollups_versioned_docs/version-2.0/tutorials/ether-wallet.md
+++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/ether-wallet.md
@@ -1,25 +1,18 @@
---
id: ether-wallet
title: Integrating Ether wallet functionality
-resources:
- - url: https://github.com/masiedu4/ether-wallet-tutorial
- title: Source code for Ether wallet tutorial
---
This tutorial will build a basic Ether wallet inside a Cartesi backend application using TypeScript.
The goal is to have a backend application to track balances and receive, transfer, and withdraw Ether.
-:::note community tools
-This tutorial is for educational purposes. For production dApps, we recommend using [Deroll](https://deroll.dev/), a TypeScript package that simplifies app and wallet functionality across all token standards for Cartesi applications.
-:::
-
## Setting up the project
First, create a new TypeScript project using the [Cartesi CLI](../development/installation.md/#cartesi-cli).
```bash
-cartesi create ether-wallet-dapp --template typescript
+cartesi create ether-wallet --template typescript
```
Run the following to generate the types for your project:
@@ -28,91 +21,10 @@ Run the following to generate the types for your project:
yarn && yarn run codegen
```
-Now, navigate to the project directory and install [`ethers`](https://docs.ethers.org/v5/), [`viem`](https://viem.sh/) and [`@cartesi/rollups`](https://www.npmjs.com/package/@cartesi/rollups) package:
-
-```bash
-yarn add ethers viem
-yarn add -D @cartesi/rollups@1.4.3
-```
-
-## Define the ABIs
-
-Let's write a configuration to generate the ABIs of the Cartesi Rollups Contracts.
-
-We will need the Solidity compiler and the contract code from the `@cartesi/rollups` package to generate the ABIs as constants.
-
-1. [Install the Solidity compiler](https://docs.soliditylang.org/en/latest/installing-solidity.html).
-
-2. Create a new file named `generate_abis.sh` in the root of your project and add the following code:
+Now, navigate to the project directory and install [`viem`](https://viem.sh/):
```bash
-#!/bin/bash
-
-set -e # Exit immediately if a command exits with a non-zero status.
-
-# Output directory for TypeScript files
-TS_DIR="src/wallet/abi"
-
-# Temporary directory for compilation output
-TEMP_DIR="temp_solc_output"
-
-# Create output and temporary directories
-mkdir -p "$TS_DIR"
-mkdir -p "$TEMP_DIR"
-
-# Function to generate ABI and export as a TypeScript variable
-generate_abi() {
- local sol_file="$1"
- local contract_name="$2"
- local output_file="$TS_DIR/${contract_name}Abi.ts"
-
- echo "Compiling $sol_file..."
-
- # Compile the contract in the temporary directory
- npx solcjs --abi "$sol_file" --base-path . --include-path node_modules/ --output-dir "$TEMP_DIR"
-
- # Find the generated ABI file
- abi_file=$(find "$TEMP_DIR" -name "*_${contract_name}.abi")
-
- if [ ! -f "$abi_file" ]; then
- echo "Error: ABI file not found for $contract_name"
- return 1
- fi
-
- # Read the ABI content
- abi=$(cat "$abi_file")
-
- echo "Extracted ABI for $contract_name"
-
- # Create a TypeScript file with exported ABI
- echo "export const ${contract_name}Abi = $abi as const;" > "$output_file"
-
- echo "Generated ABI for $contract_name"
- echo "----------------------"
-}
-
-# Generate ABIs
-generate_abi "node_modules/@cartesi/rollups/contracts/dapp/CartesiDApp.sol" "CartesiDApp"
-generate_abi "node_modules/@cartesi/rollups/contracts/portals/EtherPortal.sol" "EtherPortal"
-
-# Clean up the temporary directory
-rm -rf "$TEMP_DIR"
-
-echo "ABI generation complete"
-```
-
-This script will look for all specified `.sol` files and create a TypeScript file with the ABIs in the `src/wallet/abi` directory.
-
-Now, let's make the script executable:
-
-```bash
- chmod +x generate_abis.sh
-```
-
-And run it:
-
-```bash
- ./generate_abis.sh
+yarn add viem
```
## Building the Ether wallet
@@ -158,15 +70,12 @@ Now, create a file named `wallet.ts` in the `src/wallet` directory and add the f
import {
Address,
getAddress,
- hexToBytes,
- stringToHex,
- encodeFunctionData,
+ Hex,
numberToHex,
- parseEther
+ sliceHex,
+ zeroHash,
} from "viem";
-import { ethers } from "ethers";
import { Balance } from "./balance";
-import { CartesiDAppAbi } from "./abi/CartesiDAppAbi";
import { Voucher } from "..";
export class Wallet {
@@ -198,16 +107,12 @@ export class Wallet {
});
}
- withdrawEther(
- application: Address,
- address: Address,
- amount: bigint
- ): Voucher {
+ withdrawEther(address: Address, amount: bigint): Voucher {
const balance = this.getOrCreateBalance(address);
if (balance.getEther() >= amount) {
balance.decreaseEther(amount);
- const voucher = this.encodeWithdrawCall(application, address, amount);
+ const voucher = this.encodeWithdrawCall(address, amount);
console.log("Voucher created successfully", voucher);
@@ -241,26 +146,17 @@ export class Wallet {
}
}
- private parseDepositPayload(payload: string): [Address, bigint] {
- const addressData = ethers.dataSlice(payload, 0, 20);
- const amountData = ethers.dataSlice(payload, 20, 52);
- if (!addressData) {
- throw new Error("Invalid deposit payload");
- }
+ private parseDepositPayload(payload: Hex): [Address, bigint] {
+ const addressData = sliceHex(payload, 0, 20);
+ const amountData = sliceHex(payload, 20, 52);
return [getAddress(addressData), BigInt(amountData)];
}
- private encodeWithdrawCall(
- application: Address,
- receiver: Address,
- amount: bigint
- ): Voucher {
- const call = "0x"
-
+ private encodeWithdrawCall(receiver: Address, amount: bigint): Voucher {
return {
destination: receiver,
- payload: call,
- value: `${amount.toString(16).padStart(64, '0')}` as `0x${string}`,
+ payload: zeroHash,
+ value: numberToHex(amount).slice(2),
};
}
}
@@ -272,24 +168,24 @@ The `Wallet` class manages multiple accounts and provides methods for everyday w
### Voucher creation
-The `encodeWithdrawCall` method returns a voucher. Creating vouchers is a crucial concept in Cartesi rollups for executing withdrawal operations on the base layer chain.
+The `encodeWithdrawCall` method returns a voucher. Creating vouchers is a crucial concept in Cartesi Rollups for executing withdrawal operations on the base layer chain.
-The Ether’s withdrawal voucher contains an empty payload (`0x`), this is because the [`function _executeVoucher(bytes calldata arguments)`](../api-reference/contracts/application.md/#_executevoucher) of the CartesiDapp makes a safecall to the destination (receiver) address passing the ether value (amount) and an empty payload, this call triggers the destination address `receive function` which collects the specified amount of Eth.
+In Rollups v2, the on-chain [`Application`](../api-reference/contracts/application.md) contract executes vouchers through [`executeOutput()`](../api-reference/contracts/application.md#executeoutput), which decodes a voucher as `(destination, value, payload)` and performs a [`safeCall`](https://github.com/cartesi/rollups-contracts/blob/v2.0.1/src/library/LibAddress.sol) to `destination` with the specified wei `value`. Ether held by your application (see `metadata.app_contract` on each advance) is sent to the recipient this way.
-It returns a Voucher object with two properties:
+For a plain Ether transfer, leave `payload` as [`zeroHash`](https://viem.sh/docs/glossary/types#zeroHash) (no calldata). The voucher fields are:
-- `destination`: The address to receive the withdrawn Ether.
-- `payload`: An empty function calldata
-- `value`: A bytes encoding of the amount of Ether to withdraw.
+- `destination`: The address that receives the withdrawn Ether.
+- `payload`: `zeroHash` when you are not calling a function on the recipient.
+- `value`: The withdrawal amount in wei, as a hex string **without** the `0x` prefix (see [asset handling](../development/asset-handling.md#withdrawing-ether)).
## Using the Ether wallet
Now, let's create a simple application at the entry point, `src/index.ts,` to test the wallet functionality.
-The [`EtherPortal`](../api-reference/contracts/portals/EtherPortal.md) contract allows anyone to perform transfers of Ether to a dApp. All deposits to a dApp are made via the `EtherPortal` contract.
+The [`EtherPortal`](../api-reference/contracts/portals/EtherPortal.md) contract allows anyone to transfer Ether into your application. All deposits are made via the `EtherPortal` contract.
-:::note
-Run `cartesi address-book` to get the addresses of the `EtherPortal` contract. Save these as constants in the `index.ts` file.
+:::note EtherPortal address
+Run [`cartesi address-book`](../development/send-inputs-and-assets.md) and copy the `EtherPortal` address for your network into `index.ts`. Do not hardcode portal addresses—they differ by CLI version and chain.
:::
```typescript
@@ -313,7 +209,8 @@ export type Report = components["schemas"]["Report"];
export type Voucher = components["schemas"]["Voucher"];
const wallet = new Wallet();
-const EtherPortal = `0xd31aD6613bDaA139E7D12B2428C0Dd00fdBF8aDa`;
+// Replace with the EtherPortal address from `cartesi address-book`
+const EtherPortal = `0xYOUR_ETHER_PORTAL_ADDRESS`;
const rollupServer = process.env.ROLLUP_HTTP_SERVER_URL;
console.log(`HTTP rollup_server url is ${rollupServer}`);
@@ -321,7 +218,7 @@ console.log(`HTTP rollup_server url is ${rollupServer}`);
const handleAdvance: AdvanceRequestHandler = async (data) => {
console.log(`Received advance request data ${JSON.stringify(data)}`);
- const dAppAddress = data["metadata"]["app_contract"];
+ const application = data["metadata"]["app_contract"];
const sender = data["metadata"]["msg_sender"];
const payload = data.payload;
@@ -342,8 +239,9 @@ const handleAdvance: AdvanceRequestHandler = async (data) => {
);
await createNotice({ payload: stringToHex(transfer) });
} else if (operation === "withdraw") {
+ // `application` is the on-chain Application that holds deposited ETH
+ console.log(`Withdrawing from application ${application}`);
const voucher = wallet.withdrawEther(
- getAddress(dAppAddress as Address),
getAddress(from as Address),
BigInt(amount)
);
@@ -368,7 +266,7 @@ const handleInspect: InspectRequestHandler = async (data) => {
console.log(address);
const balance = wallet.getBalance(address as Address);
- const reportPayload = `Balance for ${address} is ${balance} wei}`;
+ const reportPayload = `Balance for ${address} is ${balance} wei`;
await createReport({ payload: stringToHex(reportPayload) });
} catch (error) {
console.error("Error processing inspect payload:", error);
@@ -443,7 +341,7 @@ This code sets up a simple application that listens for requests from the Cartes
Here is a breakdown of the wallet functionality:
-- The `handle_advance` handles three main scenarios: dApp address relay, Ether deposits, and user operations (transfers/withdrawals).
+- `handleAdvance` handles Ether deposits (from `EtherPortal`) and user operations (transfers/withdrawals).
- We handle deposits and create a notice when the sender is the `EtherPortal`.
@@ -451,15 +349,9 @@ Here is a breakdown of the wallet functionality:
- For `transfers`, we call `wallet.transferEther` and create a notice with the parsed parameters.
-For `withdrawals,` we call `wallet.withdrawEther` and create a voucher using the dApp dress and the parsed parameters.
-
-- We created helper functions to `createNotice` for deposits and transfers, `createReport` for balance checks and `createVoucher` for withdrawals.
+For withdrawals, we call `wallet.withdrawEther` and create a voucher that sends wei from the on-chain Application (`metadata.app_contract`) to the user.
-:::caution important
-The dApp address needs to be relayed strictly before withdrawal requests.
-
-To relay the dApp address, run: `cartesi send dapp-address`
-:::
+- We created helper functions to `createNotice` for deposits and transfers, `createReport` for balance checks, and `createVoucher` for withdrawals.
## Build and run the application
@@ -477,32 +369,53 @@ cartesi run
### Deposits
-To deposit ether, run the command below and follow the prompts:
+To deposit ether interactively:
```bash
cartesi deposit ether
```
+Non-interactive alternative (with `cartesi run` up). Run from your **project root** directory so the CLI can resolve the application address:
+
+```bash
+cartesi deposit ether 1 \
+ --project-name ether-wallet \
+ --rpc-url http://127.0.0.1:6751/anvil
+```
+
+Change the amount (`1` = 1 ETH) as needed. Use your project name in `--project-name` if it differs from `ether-wallet`.
+
### Balance checks(used in Inspect requests)
To inspect balance, make an HTTP (post) call to:
```bash
-curl -X POST http://127.0.0.1:6751/inspect/ether-wallet-dapp \
+curl -X POST http://127.0.0.1:6751/inspect/ether-wallet \
-H "Content-Type: application/json" \
- -d '{0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266}'
+ -d '{"payload": "0x307866333946643665353161616438384636463463653661423838323732373963666646623932323636"}'
```
+The `payload` field must be a hex-encoded string. The example above is the UTF-8 address `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` encoded with `stringToHex` from viem. Replace the inspect path if your project name is not `ether-wallet` (see the URL printed by `cartesi run`).
+
### Transfer and Withdrawals
Transfers and withdrawal requests will be sent as generic json strings that will be parsed and processed.
-To process transfers and withdrawals, run the command below, select `String encoding` then follow the prompts:
+To process transfers and withdrawals interactively, run the command below, select `String encoding`, then follow the prompts:
```bash
cartesi send
```
+Non-interactive alternative (from your project root):
+
+```bash
+cartesi send --encoding string \
+ '{"operation":"transfer","from":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","to":"0x3f2bd12ea0b8604c2af5bf241f6a606e892a403a","amount":"1000000000000000000"}' \
+ --project-name ether-wallet \
+ --rpc-url http://127.0.0.1:6751/anvil
+```
+
Here are the sample payloads as one-liners, ready to be used in your code:
1. For transfers:
@@ -523,7 +436,13 @@ For end-to-end functionality, developers will likely build their [custom user-fa
[CartesiScan](https://cartesiscan.io/) is a web application that offers a comprehensive overview of your application. It provides expandable data regarding notices, vouchers, and reports.
-When you run your application with `cartesi run`, there is a local instance of CartesiScan on `http://localhost:8080/explorer`.
+Start the node with the explorer service enabled:
+
+```shell
+cartesi run --services explorer
+```
+
+The local explorer is then available at `http://localhost:6751/explorer` (same port as the application node—see [running an application](../development/running-an-application.md)). The explorer is not started by plain `cartesi run`; you must pass `--services explorer`.
You can execute your vouchers via the explorer, which completes the withdrawal process at the end of [an epoch](../api-reference/backend/vouchers.md/#epoch-configuration).
diff --git a/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json b/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json
index 22056881..e0da3dff 100644
--- a/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json
+++ b/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json
@@ -193,6 +193,7 @@
"tutorials/ether-wallet",
"tutorials/erc-20-token-wallet",
"tutorials/erc-721-token-wallet",
+ "tutorials/erc-1155-token-wallet",
"tutorials/react-frontend-application",
"tutorials/cli-account-abstraction-feature",
"tutorials/utilizing-the-cli-test-tokens",
diff --git a/sidebarsRollups.js b/sidebarsRollups.js
index 88ee4aa0..e184b9d2 100644
--- a/sidebarsRollups.js
+++ b/sidebarsRollups.js
@@ -154,6 +154,7 @@ module.exports = {
"tutorials/ether-wallet",
"tutorials/erc-20-token-wallet",
"tutorials/erc-721-token-wallet",
+ "tutorials/erc-1155-token-wallet",
"tutorials/react-frontend-application",
"tutorials/cli-account-abstraction-feauture"
],