diff --git a/cartesi-rollups/tutorials/erc-20-token-wallet.md b/cartesi-rollups/tutorials/erc-20-token-wallet.md
index 98ecfa7c..206ea9ab 100644
--- a/cartesi-rollups/tutorials/erc-20-token-wallet.md
+++ b/cartesi-rollups/tutorials/erc-20-token-wallet.md
@@ -364,7 +364,7 @@ To deposit ERC20 tokens, use the `cartesi send erc20` command and follow the pro
To inspect balance, make an HTTP call to:
```
-http://localhost:8080/inspect/{address}
+http://localhost:8080/inspect/{address}/{tokenAddress}
```
diff --git a/cartesi-rollups/tutorials/erc-721-token-wallet.md b/cartesi-rollups/tutorials/erc-721-token-wallet.md
new file mode 100644
index 00000000..dd7b8468
--- /dev/null
+++ b/cartesi-rollups/tutorials/erc-721-token-wallet.md
@@ -0,0 +1,363 @@
+---
+id: erc-721-token-wallet
+title: Integrating ERC721 token wallet functionality
+---
+
+This tutorial will guide you through creating a basic ERC721(NFT) token wallet for a Cartesi backend application using TypeScript.
+
+## Setting up the project
+First, set up your Cartesi project as described in the [Ether wallet tutorial](./ether-wallet.md/#setting-up-the-project). Make sure you have the necessary dependencies installed.
+
+
+## Building the ERC20 wallet
+
+Create a file named `balance.ts` in the `src/wallet` directory and add the following code:
+
+```typescript
+import { Address } from "viem";
+
+export class Balance {
+ private account: string;
+ private erc721Tokens: Map
>;
+
+ constructor(account: string, erc721Tokens: Map>) {
+ this.account = account;
+ this.erc721Tokens = erc721Tokens;
+ }
+
+ listErc721(): Map> {
+ return this.erc721Tokens;
+ }
+
+ getErc721Tokens(erc721: Address): Set | undefined {
+ return this.erc721Tokens.get(erc721);
+ }
+
+ addErc721Token(erc721: Address, tokenId: number): void {
+ if (!this.erc721Tokens.has(erc721)) {
+ this.erc721Tokens.set(erc721, new Set());
+ }
+ const tokens = this.erc721Tokens.get(erc721);
+ if (tokens) {
+ tokens.add(tokenId);
+ } else {
+ throw new Error(`Failed to add token ${erc721}, id:${tokenId} for ${this.account}`);
+ }
+ }
+
+ removeErc721Token(erc721: Address, tokenId: number): void {
+ if (!this.erc721Tokens.has(erc721)) {
+ throw new Error(`Failed to remove token ${erc721}, id:${tokenId} from ${this.account}: Collection not found`);
+ }
+ const tokens = this.erc721Tokens.get(erc721);
+ if (!tokens?.delete(tokenId)) {
+ throw new Error(`Failed to remove token ${erc721}, id:${tokenId} from ${this.account}: Token not found`);
+ }
+ }
+}
+```
+
+The `Balance` class represents an account's balance. It contains a map of ERC721 tokens and their corresponding token IDs.
+
+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 { Balance } from "./balance";
+
+import { erc721Abi } from "viem";
+
+export class Wallet {
+ private accounts: Map = new Map();
+
+ private getOrCreateBalance(address: Address): Balance {
+ let balance = this.accounts.get(address);
+ if (!balance) {
+ balance = new Balance(address, new Map());
+ this.accounts.set(address, balance);
+ }
+ return balance;
+ }
+
+ getBalance(address: Address): Balance {
+ return this.getOrCreateBalance(address);
+ }
+
+ processErc721Deposit(payload: string): string {
+ try {
+ const [erc721, account, tokenId] = this.parseErc721Deposit(payload);
+ console.info(`Token ERC-721 ${erc721} id: ${tokenId} deposited in ${account}`);
+ return this.depositErc721(account, erc721, tokenId);
+ } catch (e) {
+ return `Error depositing ERC721 token: ${e}`;
+ }
+ }
+
+ 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));
+ return [erc721, account, tokenId];
+ }
+
+ private depositErc721(account: Address, erc721: Address, tokenId: number): string {
+ const balance = this.getOrCreateBalance(account);
+ balance.addErc721Token(erc721, tokenId);
+ const noticePayload = {
+ type: "erc721deposit",
+ content: {
+ address: account,
+ erc721: erc721,
+ tokenId: tokenId.toString(),
+ },
+ };
+ return JSON.stringify(noticePayload);
+ }
+
+ withdrawErc721(rollupAddress: Address, account: Address, erc721: Address, tokenId: number): string {
+ try {
+ const balance = this.getOrCreateBalance(account);
+ balance.removeErc721Token(erc721, tokenId);
+ const call = encodeFunctionData({
+ abi: erc721Abi,
+ functionName: "safeTransferFrom",
+ args: [rollupAddress, account, BigInt(tokenId)],
+ });
+ console.info(`Token ERC-721:${erc721}, id:${tokenId} withdrawn from ${account}`);
+ return JSON.stringify(hexToBytes(call))
+ } catch (e) {
+ return `Error withdrawing ERC721 token: ${e}`;
+ }
+ }
+
+ transferErc721(from: Address, to: Address, erc721: Address, tokenId: number): string {
+ try {
+ const balanceFrom = this.getOrCreateBalance(from);
+ const balanceTo = this.getOrCreateBalance(to);
+ balanceFrom.removeErc721Token(erc721, tokenId);
+ balanceTo.addErc721Token(erc721, tokenId);
+ const noticePayload = {
+ type: "erc721transfer",
+ content: {
+ from: from,
+ to: to,
+ erc721: erc721,
+ tokenId: tokenId.toString(),
+ },
+ };
+ console.info(`Token ERC-721 ${erc721} id:${tokenId} transferred from ${from} to ${to}`);
+ return JSON.stringify(noticePayload);
+ } catch (e) {
+ return `Error transferring ERC721 token: ${e}`;
+ }
+ }
+}
+```
+
+## Using the wallet
+
+Now, let's create a simple wallet app in the entrypoint, `src/index.ts` to test the wallet functionality.
+
+Run `cartesi address-book` to get the contract address of the `ERC20Portal` contract. Save this as a const in the `index.ts` file.
+
+```typescript
+import createClient from "openapi-fetch";
+import { 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 RollupsRequest = components["schemas"]["RollupRequest"];
+export type Notice = components["schemas"]["Notice"];
+export type Payload = components["schemas"]["Payload"];
+export type Report = components["schemas"]["Report"];
+export type Voucher = components["schemas"]["Voucher"];
+
+type InspectRequestHandler = (data: InspectRequestData) => Promise;
+type AdvanceRequestHandler = (
+ data: AdvanceRequestData
+) => Promise;
+
+const wallet = new Wallet();
+
+const ERC721Portal = `0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87`;
+
+const rollupServer = process.env.ROLLUP_HTTP_SERVER_URL;
+console.log("HTTP rollup_server url is " + rollupServer);
+
+const handleAdvance: AdvanceRequestHandler = async (data) => {
+ console.log("Received advance request data " + JSON.stringify(data));
+
+ const sender = data["metadata"]["msg_sender"];
+ const payload = data.payload;
+
+ if (sender.toLowerCase() === ERC721Portal.toLowerCase()) {
+ // Handle deposit
+ const deposit = wallet.processErc721Deposit(payload);
+ await sendNotice(stringToHex(deposit));
+ } else {
+ // Handle transfer or withdrawal
+ try {
+ const { operation, erc721, from, to, tokenId } = JSON.parse(hexToString(payload));
+
+ if (operation === "transfer") {
+ const transfer = wallet.transferErc721(
+ getAddress(from as Address),
+ getAddress(to as Address),
+ getAddress(erc721 as Address),
+ parseInt(tokenId)
+ );
+ console.log(transfer);
+ await sendNotice(stringToHex(transfer));
+ } else if (operation === "withdraw") {
+ const withdraw = wallet.withdrawErc721(
+ getAddress(ERC721Portal as Address),
+ getAddress(from as Address),
+ getAddress(erc721 as Address),
+ parseInt(tokenId)
+ );
+ console.log(withdraw);
+ await sendVoucher(JSON.parse(withdraw));
+ } else {
+ console.log("Unknown operation");
+ }
+ } catch (error) {
+ console.error("Error processing payload:", error);
+ }
+ }
+
+ return "accept";
+};
+
+const handleInspect: InspectRequestHandler = async (data) => {
+ console.log("Received inspect request data " + JSON.stringify(data));
+
+ try {
+ const payloadString = hexToString(data.payload);
+ const address = '0x' + payloadString.slice(0, 40);
+ const erc721 = '0x' + payloadString.slice(40, 80);
+
+ const balance = wallet.getBalance(getAddress(address as Address));
+ let erc721balance = balance.getErc721Tokens(erc721 as Address);
+
+ if (erc721balance === undefined) {
+ throw new Error("ERC721 balance is undefined");
+ }
+
+ // Convert Set to Uint8Array
+ const erc721balanceArray = new Uint8Array(Array.from(erc721balance));
+
+ await sendReport({ payload: toHex(erc721balanceArray) });
+ } catch (error) {
+ console.error("Error processing inspect payload:", error);
+ }
+};
+
+const sendNotice = async (payload: Payload) => {
+ await fetch(`${rollupServer}/notice`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(payload),
+ });
+};
+
+const sendVoucher = async (payload: Voucher) => {
+ await fetch(`${rollupServer}/voucher`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ payload }),
+ });
+};
+
+const sendReport = 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 { response } = await POST("/finish", {
+ body: { status },
+ parseAs: "text",
+ });
+
+ if (response.status === 200) {
+ const data = (await response.json()) as RollupsRequest;
+ switch (data.request_type) {
+ case "advance_state":
+ status = await handleAdvance(data.data as AdvanceRequestData);
+ break;
+ case "inspect_state":
+ await handleInspect(data.data as InspectRequestData);
+ break;
+ }
+ } else if (response.status === 202) {
+ console.log(await response.text());
+ }
+ }
+};
+
+main().catch((e) => {
+ console.log(e);
+ process.exit(1);
+});
+
+```
+
+
+
+Here is a breakdown of the wallet functionality:
+- We handle deposits when the sender is the `ERC721Portal`.
+- For other senders, we parse the payload to determine the operation (`transfer` or `withdraw`).
+- For `transfers`, we call `wallet.transferERC721` with the parsed parameters.
+- For `withdrawals`, we call `wallet.withdrawERC721` with the parsed parameters.
+- We created helper functions to `sendNotice` for deposits and transfers, `sendReport` for balance checks and `sendVoucher` for withdrawals.
+
+
+#### Deposits
+To deposit ERC721 tokens, use the `cartesi send erc721` command and follow the prompts.
+
+#### Balance checks(used in Inspect requests)
+
+To inspect balance, make an HTTP call to:
+
+```
+http://localhost:8080/inspect/{address}/{tokenAddress}
+```
+
+
+#### Transfers and Withdrawals
+
+Use the `cartesi send generic` command and follow the prompts. Here are sample payloads:
+
+1. For transfers:
+
+ ```js
+ {"operation":"transfer","erc721":"0xTokenAddress","from":"0xFromAddress","to":"0xToAddress","tokenId":"1"}
+ ```
+
+2. For withdrawals:
+
+ ```js
+ {"operation":"withdraw","erc721":"0xTokenAddress","from":"0xFromAddress","tokenId":"1"}
+ ```
+
+:::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.
+:::
+
+
diff --git a/sidebarsRollups.js b/sidebarsRollups.js
index f89187c5..0fd54314 100644
--- a/sidebarsRollups.js
+++ b/sidebarsRollups.js
@@ -153,6 +153,7 @@ module.exports = {
"tutorials/calculator",
"tutorials/ether-wallet",
"tutorials/erc-20-token-wallet",
+ "tutorials/erc-721-token-wallet",
"tutorials/react-frontend-application"
],
},