diff --git a/cartesi-rollups/tutorials/frontend-reactjs-wagmi-application.md b/cartesi-rollups/tutorials/frontend-reactjs-wagmi-application.md new file mode 100644 index 00000000..62391edd --- /dev/null +++ b/cartesi-rollups/tutorials/frontend-reactjs-wagmi-application.md @@ -0,0 +1,1865 @@ +--- +id: frontend-reactjs-wagmi-application +title: "Build a Frontend for Cartesi dApps: A React and Wagmi Tutorial" +resources: + - url: https://github.com/Mugen-Builders/cartesi-frontend-tutorial + title: Source code for the frontend application + - url: https://www.michaelasiedu.dev/posts/deploy-erc20-token-on-localhost/ + title: Deploying an ERC20 Token for Localhost Testing and Adding to MetaMask + - url: https://www.michaelasiedu.dev/posts/deploy-erc721-token-on-localhost/ + title: Deploying an ERC721 Token for Localhost Testing and Adding to MetaMask +--- + +This tutorial will focus on building a frontend for a Cartesi dApp using [**React.js**](https://create-react-app.dev/docs/getting-started) and [**Wagmi**](https://wagmi.sh/react/getting-started). Our primary goal is to create a user-friendly interface, packed with all the significant functionalities, to ensure a seamless interaction with the Cartesi backend. + +## Setting up the environment + +**Wagmi** is an incredibly user-friendly React hooks library for connecting wallets and multiple chains, signing messages and data, sending transactions, and much more! + +We'll use the [`create-wagmi`](https://wagmi.sh/react/getting-started) CLI command to set up a new React project. Then we will add a few necessary dependencies for interacting with Cartesi and the base layer. + +### Creating a new React project + +First, open your terminal and run the following: + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + +

+    npm create wagmi@latest
+    
+
+ + +

+    yarn create wagmi
+    
+
+
+ +This command will prompt you to scaffold a project with all the neccessary configuration. + +```bash +✔ Project name: … cartesi-frontend +✔ Select a framework: › React +✔ Select a variant: › Vite + +Scaffolding project in /Code/cartesi-frontend... + +Done. Now run: + +cd cartesi-frontend +npm install +npm run dev +``` + +This starter template comes with pre-configured supported dependencies: + +- [**Viem**](https://viem.sh/): A TypeScript interface for Ethereum. +- [**Tanstack Query**](https://tanstack.com/query/v5): Wagmi uses Tanstack Query under the hood as an async state manager that handles requests and caching. + +Let’s review the starter template code: + +```bash +cartesi-frontend/ +├── src/ +│ ├── App.tsx +│ ├── main.tsx +│ ├── wagmi.ts +│ ├── index.css +│ +├── package.json +├── tsconfig.json +└── README.md +``` + +- `wagmi.ts`: The main configuration file for multiple chains and an `injected` connector. +- `App.ts`: The main component file where the application’s UI is defined. +- `main.tsx`: Entry point that renders the App component into the DOM. + + + +

+
+```typescript
+import { http, createConfig } from "wagmi";
+import { mainnet, sepolia } from "wagmi/chains";
+import { coinbaseWallet, injected, walletConnect } from "wagmi/connectors";
+
+export const config = createConfig({
+  chains: [mainnet, sepolia],
+  connectors: [
+    injected(),
+    coinbaseWallet(),
+    walletConnect({ projectId: import.meta.env.VITE_WC_PROJECT_ID }),
+  ],
+  transports: {
+    [mainnet.id]: http(),
+    [sepolia.id]: http(),
+  },
+});
+
+declare module "wagmi" {
+  interface Register {
+    config: typeof config;
+  }
+}
+```
+
+
+
+ + +

+
+```javascript
+import { useAccount, useConnect, useDisconnect } from "wagmi";
+
+function App() {
+  const account = useAccount();
+  const { connectors, connect, status, error } = useConnect();
+  const { disconnect } = useDisconnect();
+
+  return (
+    <>
+      
+

Account

+ +
+ status: {account.status} +
+ addresses: {JSON.stringify(account.addresses)} +
+ chainId: {account.chainId} +
+ + {account.status === "connected" && ( + + )} +
+ +
+

Connect

+ {connectors.map((connector) => ( + + ))} +
{status}
+
{error?.message}
+
+ + ); +} + +export default App; +``` + +
+
+ + +

+
+```typescript
+import { Buffer } from "buffer";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { WagmiProvider } from "wagmi";
+
+import App from "./App.tsx";
+import { config } from "./wagmi.ts";
+
+import "./index.css";
+
+globalThis.Buffer = Buffer;
+
+const queryClient = new QueryClient();
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+  
+    
+      
+        
+      
+    
+  
+);
+```
+
+
+
+ +
+ +## Connecting wallet and chains + +The `wagmi.ts` file is the main configuration file for multiple chains and an `injected` connector. + +- Enhance the `wagmi.ts` configuration by adding all the chains supported by Cartesi Rollups. + +- Replace the transports property with a Viem Client integration via the `client` property to have finer control over Wagmi’s internal client creation. + +Edit the `src/wagmi.ts` file: + + + +

+
+```javascript
+import { http, createConfig } from "wagmi";
+import {
+  anvil,
+  arbitrum,
+  arbitrumGoerli,
+  base,
+  baseSepolia,
+  mainnet,
+  optimism,
+  optimismGoerli,
+  sepolia,
+} from "wagmi/chains";
+import { coinbaseWallet, injected, walletConnect } from "wagmi/connectors";
+import { createClient } from "viem";
+
+export const config = createConfig({
+  chains: [
+    anvil,
+    mainnet,
+    sepolia,
+    arbitrum,
+    arbitrumGoerli,
+    optimismGoerli,
+    optimism,
+    base,
+    baseSepolia,
+  ],
+  connectors: [
+    injected(),
+    coinbaseWallet(),
+    walletConnect({ projectId: import.meta.env.VITE_WC_PROJECT_ID }),
+  ],
+  client({ chain }) {
+    return createClient({ chain, transport: http() });
+  },
+});
+
+declare module "wagmi" {
+  interface Register {
+    config: typeof config;
+  }
+}
+
+```
+
+
+
+
+ +:::note supported networks +You can find the list of supported chains and their IDs in the [deployment guide](../deployment/introduction.md/#supported-networks). +::: + +### Building the Account component + +Let's create an implementation for easy network switching and a comprehensive wallet management interface. + +Move the account connection and management logic to a separate component for a cleaner and more organized `App.tsx`. + +We will add [Tailwind CSS classes](https://tailwindcss.com/docs/guides/vite) ensure visual appeal. + +Create a new file `src/components/Account.tsx` and edit the `App.tsx`: + + +

+
+```javascript
+import { useAccount, useConnect, useDisconnect, useSwitchChain } from "wagmi";
+import { useState } from "react";
+
+const Account = () => {
+  const account = useAccount();
+  const { connectors, connect, status, error } = useConnect();
+  const { disconnect } = useDisconnect();
+  const { chains, switchChain } = useSwitchChain();
+  const [isChainDropdownOpen, setIsChainDropdownOpen] = useState(false);
+
+  return (
+    
+
+

Account

+ +
+

+ Status: {account.status.toLocaleUpperCase()} +

+

+ Address:{" "} + {account.addresses?.[0]} +

+

+ Chain ID: {account.chain?.name} | {account.chainId} +

+
+ + {/* Display chain switching and disconnect options when connected */} + {account.status === "connected" && ( +
+ {/* Chain switching dropdown */} +
+ + {/* Dropdown menu for chain options */} + {isChainDropdownOpen && ( +
+ {chains.map((chainOption) => ( + + ))} +
+ )} +
+ {/* Disconnect button */} + +
+ )} +
+ + {/* Connect section */} +
+

Connect

+
+ {connectors.map((connector) => ( + + ))} +
+
Status: {status.toLocaleUpperCase()}
+
{error?.message}
+
+
+ ); +}; + +export default Account; +``` + +
+
+ + +

+
+```javascript
+import Account from "./components/Account";
+
+function App() {
+  return (
+    <>
+      
+    
+  );
+}
+
+export default App;
+
+```
+
+
+
+ +
+ + +- We import the `useSwitchChain` hook from wagmi and add a state for the chains dropdown. +- The `useSwitchChain` hook provides available chains and the `switchChain` function. +- When wallet is connected, we display, a chain-switching dropdow and a disconnect button. + + + +### Define the ABIs and contract addresses + +In a Cartesi dApp, the frontend sends inputs to the backend via the base layer chain as JSON-RPC transactions. + +Pre-deployed smart contracts on supported chains handle generic inputs and assets. We only need their ABIs and addresses to send transactions using Wagmi. + +Cartesi Rollups contracts have consistent ABIs and addresses across all supported chains, including Anvil testnet. + +Supported input types: generic inputs, Ether, ERC20, ERC721, and ERC1155. + +Create a `src/utils` directory to store the following ABIs and contract addresses: + + + + + +

+
+```javascript
+export const contractAddresses = {
+  InputBox: "0x59b22D57D4f067708AB0c00552767405926dc768",
+  EtherPortal: "0xFfdbe43d4c855BF7e0f105c400A50857f53AB044",
+  Erc20Portal: "0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB",
+  Erc721Portal: "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87",
+  Erc1155SinglePortal: "0x7CFB0193Ca87eB6e48056885E026552c3A941FC4",
+  Erc1155BatchPortal: "0xedB53860A6B52bbb7561Ad596416ee9965B055Aa",
+  DAppAddressRelay: "0xF5DE34d6BbC0446E2a45719E718efEbaaE179daE"
+};
+```
+
+
+
+ + +

+
+```typescript
+export const ABIs = {
+  DAppAddressRelayABI: [
+    {
+      inputs: [
+        {
+          internalType: "contract IInputBox",
+          name: "_inputBox",
+          type: "address",
+        },
+      ],
+      stateMutability: "nonpayable",
+      type: "constructor",
+    },
+    {
+      inputs: [],
+      name: "getInputBox",
+      outputs: [
+        { internalType: "contract IInputBox", name: "", type: "address" },
+      ],
+      stateMutability: "view",
+      type: "function",
+    },
+    {
+      inputs: [{ internalType: "address", name: "_dapp", type: "address" }],
+      name: "relayDAppAddress",
+      outputs: [],
+      stateMutability: "nonpayable",
+      type: "function",
+    },
+  ],
+
+  ERC1155BatchPortalABI: [
+    {
+      inputs: [
+        {
+          internalType: "contract IInputBox",
+          name: "_inputBox",
+          type: "address",
+        },
+      ],
+      stateMutability: "nonpayable",
+      type: "constructor",
+    },
+    {
+      inputs: [
+        { internalType: "contract IERC1155", name: "_token", type: "address" },
+        { internalType: "address", name: "_dapp", type: "address" },
+        { internalType: "uint256[]", name: "_tokenIds", type: "uint256[]" },
+        { internalType: "uint256[]", name: "_values", type: "uint256[]" },
+        { internalType: "bytes", name: "_baseLayerData", type: "bytes" },
+        { internalType: "bytes", name: "_execLayerData", type: "bytes" },
+      ],
+      name: "depositBatchERC1155Token",
+      outputs: [],
+      stateMutability: "nonpayable",
+      type: "function",
+    },
+    {
+      inputs: [],
+      name: "getInputBox",
+      outputs: [
+        { internalType: "contract IInputBox", name: "", type: "address" },
+      ],
+      stateMutability: "view",
+      type: "function",
+    },
+  ],
+
+  ERC1155SinglePortalABI: [
+    {
+      inputs: [
+        {
+          internalType: "contract IInputBox",
+          name: "_inputBox",
+          type: "address",
+        },
+      ],
+      stateMutability: "nonpayable",
+      type: "constructor",
+    },
+    {
+      inputs: [
+        { internalType: "contract IERC1155", name: "_token", type: "address" },
+        { internalType: "address", name: "_dapp", type: "address" },
+        { internalType: "uint256", name: "_tokenId", type: "uint256" },
+        { internalType: "uint256", name: "_value", type: "uint256" },
+        { internalType: "bytes", name: "_baseLayerData", type: "bytes" },
+        { internalType: "bytes", name: "_execLayerData", type: "bytes" },
+      ],
+      name: "depositSingleERC1155Token",
+      outputs: [],
+      stateMutability: "nonpayable",
+      type: "function",
+    },
+    {
+      inputs: [],
+      name: "getInputBox",
+      outputs: [
+        { internalType: "contract IInputBox", name: "", type: "address" },
+      ],
+      stateMutability: "view",
+      type: "function",
+    },
+  ],
+
+  ERC20PortalABI: [
+    {
+      inputs: [
+        {
+          internalType: "contract IInputBox",
+          name: "_inputBox",
+          type: "address",
+        },
+      ],
+      stateMutability: "nonpayable",
+      type: "constructor",
+    },
+    {
+      inputs: [
+        { internalType: "contract IERC20", name: "_token", type: "address" },
+        { internalType: "address", name: "_dapp", type: "address" },
+        { internalType: "uint256", name: "_amount", type: "uint256" },
+        { internalType: "bytes", name: "_execLayerData", type: "bytes" },
+      ],
+      name: "depositERC20Tokens",
+      outputs: [],
+      stateMutability: "nonpayable",
+      type: "function",
+    },
+    {
+      inputs: [],
+      name: "getInputBox",
+      outputs: [
+        { internalType: "contract IInputBox", name: "", type: "address" },
+      ],
+      stateMutability: "view",
+      type: "function",
+    },
+  ],
+
+  ERC721PortalABI: [
+    {
+      inputs: [
+        {
+          internalType: "contract IInputBox",
+          name: "_inputBox",
+          type: "address",
+        },
+      ],
+      stateMutability: "nonpayable",
+      type: "constructor",
+    },
+    {
+      inputs: [
+        { internalType: "contract IERC721", name: "_token", type: "address" },
+        { internalType: "address", name: "_dapp", type: "address" },
+        { internalType: "uint256", name: "_tokenId", type: "uint256" },
+        { internalType: "bytes", name: "_baseLayerData", type: "bytes" },
+        { internalType: "bytes", name: "_execLayerData", type: "bytes" },
+      ],
+      name: "depositERC721Token",
+      outputs: [],
+      stateMutability: "nonpayable",
+      type: "function",
+    },
+    {
+      inputs: [],
+      name: "getInputBox",
+      outputs: [
+        { internalType: "contract IInputBox", name: "", type: "address" },
+      ],
+      stateMutability: "view",
+      type: "function",
+    },
+  ],
+
+  EtherPortalABI: [
+    {
+      inputs: [
+        {
+          internalType: "contract IInputBox",
+          name: "_inputBox",
+          type: "address",
+        },
+      ],
+      stateMutability: "nonpayable",
+      type: "constructor",
+    },
+    {
+      inputs: [],
+      name: "EtherTransferFailed",
+      type: "error",
+    },
+    {
+      inputs: [
+        { internalType: "address", name: "_dapp", type: "address" },
+        { internalType: "bytes", name: "_execLayerData", type: "bytes" },
+      ],
+      name: "depositEther",
+      outputs: [],
+      stateMutability: "payable",
+      type: "function",
+    },
+    {
+      inputs: [],
+      name: "getInputBox",
+      outputs: [
+        { internalType: "contract IInputBox", name: "", type: "address" },
+      ],
+      stateMutability: "view",
+      type: "function",
+    },
+  ],
+
+  InputBoxABI: [
+    {
+      inputs: [],
+      name: "InputSizeExceedsLimit",
+      type: "error",
+    },
+    {
+      anonymous: false,
+      inputs: [
+        { indexed: true, internalType: "address", name: "dapp", type: "address" },
+        {
+          indexed: true,
+          internalType: "uint256",
+          name: "inputIndex",
+          type: "uint256",
+        },
+        {
+          indexed: false,
+          internalType: "address",
+          name: "sender",
+          type: "address",
+        },
+        { indexed: false, internalType: "bytes", name: "input", type: "bytes" },
+      ],
+      name: "InputAdded",
+      type: "event",
+    },
+    {
+      inputs: [
+        { internalType: "address", name: "_dapp", type: "address" },
+        { internalType: "bytes", name: "_input", type: "bytes" },
+      ],
+      name: "addInput",
+      outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
+      stateMutability: "nonpayable",
+      type: "function",
+    },
+    {
+      inputs: [
+        { internalType: "address", name: "_dapp", type: "address" },
+        { internalType: "uint256", name: "_index", type: "uint256" },
+      ],
+      name: "getInputHash",
+      outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
+      stateMutability: "view",
+      type: "function",
+    },
+    {
+      inputs: [{ internalType: "address", name: "_dapp", type: "address" }],
+      name: "getNumberOfInputs",
+      outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
+      stateMutability: "view",
+      type: "function",
+    },
+  ],
+};
+```
+
+
+
+ + + +
+ +## Sending a generic input + +The [`InputBox`](../rollups-apis/json-rpc/input-box.md) contract is a trustless and permissionless contract that receives arbitrary blobs (called "inputs") from anyone. + +The `InputBox` contract is deployed on all supported chains. We will use the `InputBox` contract address and ABI to send an input from the frontend. + +Create a new file `src/components/SimpleInput.tsx` and follow the implementation below: + + +### Component setup and imports + +```javascript +import React, { useState } from "react"; +import { BaseError, useWriteContract } from "wagmi"; +import { ABIs } from "../utils/abi"; +import { contractAddresses } from "../utils/addresses"; +import { Hex,stringToHex } from "viem"; +``` +Here, we are importing Wagmi's `useWriteContract` hook, our ABI and address utilities, and Viem's `Hex` type and `stringToHex` function for data conversion. + + +### Component definition and state + +```javascript + +const SimpleInput = () => { + const dAppAddress = `0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e`; + const [inputValue, setInputValue] = useState(""); + + // ... rest of the component +}; +``` + +We define the `dAppAddress` and create a state variable `inputValue` to manage the user's input. + +The `dAppAddress` is the address of the Cartesi backend that will receive the input. In this case, we are using a hardcoded address of a local dApp instance for demonstration purposes. + +### Using the `useWriteContract` Hook + +We'll use the `useWriteContract` hook to interact with the `InputBox` contract: + +```javascript +const { isPending, isSuccess, error, writeContractAsync } = useWriteContract(); +``` + +This hook provides us with state variables and a `writeContractAsync` function to write to the smart contract. + + +### Form submission and component rendering + +```typescript +async function submit(event: React.FormEvent) { + event.preventDefault(); + await writeContractAsync({ + address: contractAddresses.InputBox as Hex, + abi: ABIs.InputBoxABI, + functionName: "addInput", + args: [dAppAddress, stringToHex(inputValue)], + }); + } +``` + +The `submit` function is called when the form is submitted. + +It uses the `writeContractAsync` function to send the input to the [`addInput(address _dapp, bytes _input)`](../rollups-apis/json-rpc/input-box.md/#addinput) function of the `InputBox`. + +The `inputValue` will be received by the particular backend address is `dAppAddress`. + + +Now, let us build our component JSX with an input field and a submit button, styled with Tailwind CSS. It also includes conditional rendering for success and error messages. + +```javascript +return ( +
+

Send Generic Input

+
+
+ setInputValue(e.target.value)} + /> +
+ +
+ + {isSuccess && ( +

Transaction Sent

+ )} + + {error && ( +
+ Error: {(error as BaseError).shortMessage || error.message} +
+ )} +
+); +``` + +### Final Component + +Putting it all together, our complete `` component and `App.tsx` look like this: + + + +

+
+```typescript
+import React, { useState } from "react";
+import { BaseError, useWriteContract } from "wagmi";
+import { ABIs } from "../utils/abi";
+import { contractAddresses } from "../utils/addresses";
+import { Hex, stringToHex } from "viem";
+
+const SimpleInput = () => {
+  const dAppAddress = `0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e`;
+  const [inputValue, setInputValue] = useState("");
+
+  const { isPending, isSuccess, error, writeContractAsync } =
+    useWriteContract();
+
+  async function submit(event: React.FormEvent) {
+    event.preventDefault();
+    await writeContractAsync({
+      address: contractAddresses.InputBox as Hex,
+      abi: ABIs.InputBoxABI,
+      functionName: "addInput",
+      args: [dAppAddress, stringToHex(inputValue)],
+    });
+  }
+
+  return (
+    
+

Send Generic Input

+
+
+ setInputValue(e.target.value)} + /> +
+ +
+ + {isSuccess && ( +

Transaction Sent

+ )} + + {error && ( +
+ Error: {(error as BaseError).shortMessage || error.message} +
+ )} +
+ ); +}; + +export default SimpleInput; + +``` + +
+
+ + +

+
+```typescript
+import Account from "./components/Account";
+import SimpleInput from "./components/SimpleInput";
+
+function App() {
+  return (
+    <>
+      
+      
+    
+  );
+}
+
+export default App;
+
+```
+
+
+
+ +
+ +## Depositing Ether + +The [`EtherPortal`](../rollups-apis/json-rpc/portals/EtherPortal.md) contract is a pre-deployed smart contract that allows users to deposit Ether to the Cartesi backend. + +We will use the `EtherPortal` contract address and ABI to send Ether from the frontend. + +This implementation will be similar to the generic input, but with a few changes to handle Ether transactions. + +The key changes in this compared to [SimpleInput](#sending-a-generic-input) are: + + - The input field now will accept **Ether** values instead of generic text. + + - The `submit` function creates a data string representing the Ether deposit and uses `parseEther` to convert the input value. + + - The `writeContractAsync` call is adjusted to use the [`depositEther(address _dapp, bytes _execLayerData)`](../rollups-apis/json-rpc/portals/EtherPortal.md/#depositether) function of the `EtherPortal` contract, including the Ether value in the transaction. + + ```typescript + + // imports here + + const [etherValue, setEtherValue] = useState(""); + + // rest of the code + async function submit(event: React.FormEvent) { + event.preventDefault(); + const data = stringToHex(`Deposited (${etherValue}) ether.`); + await writeContractAsync({ + address: contractAddresses.EtherPortal as Hex, + abi: ABIs.EtherPortalABI, + functionName: "depositEther", + args: [dAppAddress, data], + value: parseEther(etherValue), + }); + } + + // rest of the code + ``` + +### Final Component + +Create a new file `src/components/SendEther.tsx` and paste the complete code: + + + +

+
+```typescript
+import React, { useState } from "react";
+import { BaseError, useWriteContract } from "wagmi";
+import { ABIs } from "../utils/abi";
+import { contractAddresses } from "../utils/addresses";
+
+import { Hex, parseEther, stringToHex } from "viem";
+
+const SendEther = () => {
+  const dAppAddress = `0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e`;
+  const [etherValue, setEtherValue] = useState("");
+
+  const { isPending, isSuccess, error, writeContractAsync } = useWriteContract();
+
+  async function submit(event: React.FormEvent) {
+    event.preventDefault();
+    const data = stringToHex(`Deposited (${etherValue}) ether.`);
+    await writeContractAsync({
+      address: contractAddresses.EtherPortal as Hex,
+      abi: ABIs.EtherPortalABI,
+      functionName: "depositEther",
+      args: [dAppAddress, data],
+      value: parseEther(etherValue),
+    });
+  }
+
+  return (
+    
+

Deposit Ether

+
+
+ setEtherValue(e.target.value)} + /> +
+ +
+ + {isSuccess && ( +

{etherValue} ETH sent!

+ )} + + {error && ( +
+ Error: {(error as BaseError).shortMessage || error.message} +
+ )} +
+ ); +}; + +export default SendEther; + +``` + +
+
+ + +

+
+```typescript
+import Account from "./components/Account";
+import SimpleInput from "./components/SimpleInput";
+import SendEther from "./components/SendEther";
+
+function App() {
+  return (
+    <>
+      
+      
+      
+    
+  );
+}
+
+export default App;
+
+
+```
+
+
+
+ +
+ + +## Depositing ERC20 Tokens + +The [`ERC20Portal`](../rollups-apis/json-rpc/portals/ERC20Portal.md) contract is a pre-deployed smart contract that allows users to deposit ERC20 tokens to the Cartesi backend. + +We will use the `ERC20Portal` contract address and ABI to send ERC20 tokens from the frontend. + +This implementation will be similar to the [depositing Ether](#depositing-ether), but with a few changes to handle ERC20 token transactions. + +Here are the key differences in depositing ERC20 tokens compared to Ether: + + - ERC20 deposits require both the **ERC20 token address** and **amounts**. + + - The `submit` function first calls [`approve()`](https://docs.openzeppelin.com/contracts/2.x/api/token/erc20#ERC20-approve-address-uint256-) before calling `depositERC20Tokens` on the ERC20Portal contract. + + :::note ERC Token Approval + For [**ERC20, ERC721, and ERC1155 token standards**](https://ethereum.org/en/developers/docs/standards/tokens/), an approval step is need. This ensures you grant explicit permission for a contract (like the Portals) to transfer tokens on your behalf. + + Without this approval, contracts like ERC20Portal cannot move your tokens to the Cartesi backend. + ::: + + - The `writeContractAsync` call is adjusted to use the [`depositERC20Tokens(address _token, uint256 _amount, bytes _execLayerData)`](../rollups-apis/json-rpc/portals/ERC20Portal.md/#depositerc20tokens) function of the `ERC20Portal` contract. + + + ```typescript + import { Address, erc20Abi, parseEther, stringToHex } from "viem"; + + // other imports here + + const [erc20Value, setErc20Value] = useState(""); + const [tokenAddress, setTokenAddress] = useState
(); + + const approveERC20 = async (tokenAddress: Address, amount: string) => { + + try { + await writeContractAsync({ + address: tokenAddress, + abi: erc20Abi, // this type is imported from viem + functionName: "approve", + args: [contractAddresses.Erc20PortalAddress, parseEther(amount)], + }); + console.log("ERC20 Approval successful"); + } catch (error) { + console.error("Error in approving ERC20:", error); + throw error; + } + }; + + async function submit(event: React.FormEvent) { + event.preventDefault(); + + const data = stringToHex(`Deposited (${erc20Value}).`); + + await approveERC20(tokenAddress as Address, erc20Value); + + await writeContractAsync({ + address: contractAddresses.Erc20Portal as Hex, + abi: ABIs.ERC20PortalABI, + functionName: "depositERC20Tokens", + args: [tokenAddress, dAppAddress, parseEther(erc20Value), data], + }); + } + + // rest of the code + + ``` + +For testing purposes, you'll need to deploy a test ERC20 token. Follow [this simple guide to deploy a test ERC20 token and add it to your Metamask wallet](https://www.michaelasiedu.dev/posts/deploy-erc20-token-on-localhost/). + + +### Final Component + +Create a new file `src/components/SendERC20.tsx` and paste the complete code: + + + +

+
+```typescript
+iimport React, { useState } from "react";
+import { BaseError, useWriteContract } from "wagmi";
+import { ABIs } from "../utils/abi";
+import { contractAddresses } from "../utils/addresses";
+import { Address, erc20Abi, parseEther, stringToHex, Hex } from "viem";
+
+const SendERC20 = () => {
+  const dAppAddress = `0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e`;
+  const [erc20Value, setErc20Value] = useState("");
+  const [tokenAddress, setTokenAddress] = useState
(); + + const { isPending, isSuccess, error, writeContractAsync } = + useWriteContract(); + + const approveERC20 = async (tokenAddress: Address, amount: string) => { + try { + await writeContractAsync({ + address: tokenAddress, + abi: erc20Abi, // this type is imported from viem + functionName: "approve", + args: [contractAddresses.Erc20Portal as Hex, parseEther(amount)], + }); + console.log("ERC20 Approval successful"); + } catch (error) { + console.error("Error in approving ERC20:", error); + throw error; + } + }; + + async function submit(event: React.FormEvent) { + event.preventDefault(); + const data = stringToHex(`Deposited (${erc20Value}).`); + await approveERC20(tokenAddress as Address, erc20Value); + await writeContractAsync({ + address: contractAddresses.Erc20Portal as Hex, + abi: ABIs.ERC20PortalABI, + functionName: "depositERC20Tokens", + args: [tokenAddress, dAppAddress, parseEther(erc20Value), data], + }); + } + + return ( +
+

Deposit ERC20

+
+
+ setTokenAddress(e.target.value as Address)} + /> + setErc20Value(e.target.value)} + /> +
+ +
+ + {isSuccess && ( +

+ {erc20Value} tokens sent! +

+ )} + + {error && ( +
+ Error: {(error as BaseError).shortMessage || error.message} +
+ )} +
+ ); +}; + +export default SendERC20; + + +``` + +
+
+ + +

+
+```typescript
+import Account from "./components/Account";
+import SimpleInput from "./components/SimpleInput";
+import SendEther from "./components/SendEther";
+import SendERC20 from "./components/SendERC20";
+
+function App() {
+  return (
+    <>
+      
+      
+      
+      
+    
+  );
+}
+
+export default App;
+
+
+```
+
+
+
+ +
+ + +## Depositing ERC721 Tokens (NFTs) + +The [`ERC721Portal`](../rollups-apis/json-rpc/portals/ERC721Portal.md) contract is a pre-deployed smart contract that allows users to deposit ERC721 tokens to the Cartesi backend. + +We will use the `ERC721Portal` contract address and ABI to send ERC721 tokens from the frontend. + +This implementation will be similar to the [depositing ERC20 tokens](#depositing-erc20-tokens), but with a few changes to handle ERC721 token transactions. + +Here are the key differences in depositing ERC721 tokens compared to ERC20: + + - ERC721 deposits require both the **ERC721 token address** and **token ID**. + + - The `writeContractAsync` call is adjusted to use the [`depositERC721Token(address _token, uint256 _tokenId, bytes _execLayerData)`](../rollups-apis/json-rpc/portals/ERC721Portal.md/#depositerc721token) function of the `ERC721Portal` contract. + + ```typescript + import { Address, erc721Abi, parseEther, stringToHex } from "viem"; + + // other imports here + + const [tokenId, setTokenId] = useState(""); + const [tokenAddress, setTokenAddress] = useState(""); + + const approveERC721 = async (tokenAddress: Address, tokenId: bigint) => { + try { + await writeContractAsync({ + address: tokenAddress, + abi: erc721Abi, // this type is imported from viem + functionName: "approve", + args: [contractAddresses.Erc721Portal as Hex, tokenId], + }); + + console.log("Approval successful"); + } catch (error) { + console.error("Error in approving ERC721:", error); + throw error; // Re-throw the error to be handled by the caller + } + }; + + async function submit(event: React.FormEvent) { + event.preventDefault(); + + const bigIntTokenId = BigInt(tokenId); + const data = stringToHex(`Deposited NFT of token id:(${bigIntTokenId}).`); + + await approveERC721(tokenAddress as Address, bigIntTokenId); + + writeContractAsync({ + address: contractAddresses.Erc721Portal as Hex, + abi: ABIs.ERC721PortalABI, + functionName: "depositERC721Token", + args: [tokenAddress, dAppAddress, bigIntTokenId, "0x", data], + }); + } + + // rest of the code + + ``` + +For testing purposes, you'll need to deploy a test ERC721 token. Follow [this simple guide to deploy and mint a test ERC721 token and add it to your Metamask wallet](https://www.michaelasiedu.dev/posts/deploy-erc721-token-on-localhost/). + + +### Final Component + +Create a new file `src/components/SendERC721.tsx` and paste the complete code: + + + +

+
+```typescript
+import React, { useState } from "react";
+import { BaseError, useWriteContract } from "wagmi";
+import { ABIs } from "../utils/abi";
+import { contractAddresses } from "../utils/addresses";
+import { stringToHex, erc721Abi, Address, Hex } from "viem";
+
+const SendERC721 = () => {
+  const dAppAddress = `0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e`;
+  const [tokenId, setTokenId] = useState("");
+  const [tokenAddress, setTokenAddress] = useState("");
+
+  const { isPending, isSuccess, error, writeContractAsync } = useWriteContract();
+
+  const approveERC721 = async (tokenAddress: Address, tokenId: bigint) => {
+    try {
+      await writeContractAsync({
+        address: tokenAddress,
+        abi: erc721Abi,
+        functionName: "approve",
+        args: [contractAddresses.Erc721Portal as Hex, tokenId],
+      });
+
+      console.log("Approval successful");
+    } catch (error) {
+      console.error("Error in approving ERC721:", error);
+      throw error; // Re-throw the error to be handled by the caller
+    }
+  };
+
+  async function submit(event: React.FormEvent) {
+    event.preventDefault();
+
+    const bigIntTokenId = BigInt(tokenId);
+    const data = stringToHex(`Deposited NFT of token id:(${bigIntTokenId}).`);
+
+    await approveERC721(tokenAddress as Address, bigIntTokenId);
+
+    writeContractAsync({
+      address: contractAddresses.Erc721Portal as Hex,
+      abi: ABIs.ERC721PortalABI,
+      functionName: "depositERC721Token",
+      args: [tokenAddress, dAppAddress, bigIntTokenId, "0x", data],
+    });
+  }
+
+  return (
+    
+

Deposit ERC721 Token

+
+
+ setTokenAddress(e.target.value)} + /> +
+
+ setTokenId(e.target.value)} + /> +
+ +
+ + {isSuccess && ( +

NFT of Token number: {tokenId} sent!

+ )} + + {error && ( +
+ Error: {(error as BaseError).shortMessage || error.message} +
+ )} +
+ ); +}; + +export default SendERC721; + +``` + +
+
+ + +

+
+```typescript
+import Account from "./components/Account";
+import SendERC20 from "./components/SendERC20";
+import SendERC721 from "./components/SendERC721";
+import SendEther from "./components/SendEther";
+import SimpleInput from "./components/SimpleInput";
+
+function App() {
+  return (
+    <>
+      
+      
+      
+      
+      
+    
+  );
+}
+
+export default App;
+
+
+
+```
+
+
+
+ +
+ + +## Listing Notices, Reports, and Vouchers + +All inputs sent to the Cartesi backend are processed by the Cartesi Machine. The Cartesi Machine produces three types of outputs: [Notices](../rollups-apis/backend/notices.md), [Reports](../rollups-apis/backend/reports.md), and [Vouchers](../rollups-apis/backend/vouchers.md). + +These outputs can be queried by the frontend using the [GraphQL API](../rollups-apis/graphql/basics.md) on `http://localhost:8080/graphql`. + +:::note GraphQL API Reference +Refer to the [GraphQL API documentation](../rollups-apis/graphql/basics.md) for all the queries and mutations available. +::: + +Let's move the GraphQL queries to an external file `src/utils/queries.ts` for better organization and reusability. + +Then, we will create a shared function `fetchGraphQLData` created in `src/utils/api.ts` to handle the GraphQL request. + + + + +

+
+```typescript
+// queries.ts
+
+export const NOTICES_QUERY = `
+  query notices {
+    notices {
+      edges {
+        node {
+          index
+          input {
+            index
+          }
+          payload
+        }
+      }
+    }
+  }
+`;
+
+export const REPORTS_QUERY = `
+  query reports {
+    reports {
+      edges {
+        node {
+          index
+          input {
+            index
+          }
+          payload
+        }
+      }
+    }
+  }
+`;
+
+export const VOUCHERS_QUERY = `
+  query vouchers {
+    vouchers {
+      edges {
+        node {
+          index
+          input {
+            index
+          }
+          destination
+          payload
+        }
+      }
+    }
+  }
+`;
+```
+
+
+
+ + +

+
+```typescript
+// types.ts
+
+export type Notice = {
+  index: number;
+  input: {
+    index: number;
+  };
+  payload: string;
+};
+
+export type Report = {
+  index: number;
+  input: {
+    index: number;
+  };
+  payload: string;
+};
+
+export type Voucher = {
+  index: number;
+  input: {
+    index: number;
+  };
+  destination: string;
+  payload: string;
+};
+
+export type GraphQLResponse = {
+  data: T;
+};
+```
+
+
+
+ + +

+
+```typescript
+// api.ts
+
+import axios from 'axios';  // install axios by running `npm i axios`
+import { GraphQLResponse } from './types';
+
+export const fetchGraphQLData = async (query: string) => {
+  const response = await axios.post>('http://localhost:8080/graphql', {
+    query,
+  });
+  return response.data.data;
+};
+```
+
+
+
+
+ +Let's have 3 components for Notices, Reports, and Vouchers that queries from the GraphQL API. + + + + + +

+
+```typescript
+
+import { useEffect, useState } from 'react';
+import { fetchGraphQLData } from '../utils/api';
+import { Notice } from '../utils/types';
+import { NOTICES_QUERY } from '../utils/queries';
+
+const Notices = () => {
+  const [notices, setNotices] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState(null);
+
+  useEffect(() => {
+    const fetchNotices = async () => {
+      try {
+        const data = await fetchGraphQLData<{ notices: { edges: { node: Notice }[] } }>(NOTICES_QUERY);
+        setNotices(data.notices.edges.map(edge => edge.node));
+      } catch (err) {
+        setError('Error fetching notices.');
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    fetchNotices();
+  }, []);
+
+  if (loading) return 
Loading...
; + if (error) return
{error}
; + + return ( +
+

Notices

+ + + + + + + + + + {notices.map((notice, idx) => ( + + + + + + ))} + +
IndexInput IndexPayload
{notice.index}{notice.input.index}{notice.payload}
+
+ ); +}; + +export default Notices; +``` + +
+
+ + +

+
+```typescript
+
+
+import { useEffect, useState } from 'react';
+import { fetchGraphQLData } from '../utils/api';
+import { Report } from '../utils/types';
+import { REPORTS_QUERY } from '../utils/queries';
+
+
+const Reports = () => {
+  const [reports, setReports] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState(null);
+
+  useEffect(() => {
+    const fetchReports = async () => {
+      try {
+        const data = await fetchGraphQLData<{ reports: { edges: { node: Report }[] } }>(REPORTS_QUERY);
+        setReports(data.reports.edges.map(edge => edge.node));
+      } catch (err) {
+        setError('Error fetching reports.');
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    fetchReports();
+  }, []);
+
+  if (loading) return 
Loading...
; + if (error) return
{error}
; + + return ( +
+

Reports

+ + + + + + + + + + {reports.map((report, idx) => ( + + + + + + ))} + +
IndexInput IndexPayload
{report.index}{report.input.index}{report.payload}
+
+ ); +}; + +export default Reports; +``` + +
+
+ + +

+
+```typescript
+
+
+import { useEffect, useState } from 'react';
+import { fetchGraphQLData } from '../utils/api';
+import { Voucher } from '../utils/types';
+import { VOUCHERS_QUERY } from '../utils/queries';
+
+const Vouchers = () => {
+  const [vouchers, setVouchers] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState(null);
+
+  useEffect(() => {
+    const fetchVouchers = async () => {
+      try {
+        const data = await fetchGraphQLData<{ vouchers: { edges: { node: Voucher }[] } }>(VOUCHERS_QUERY);
+        setVouchers(data.vouchers.edges.map(edge => edge.node));
+      } catch (err) {
+        setError('Error fetching vouchers.');
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    fetchVouchers();
+  }, []);
+
+  if (loading) return 
Loading...
; + if (error) return
{error}
; + + return ( +
+

Vouchers

+ + + + + + + + + + + {vouchers.map((voucher, idx) => ( + + + + + + + ))} + +
IndexInput IndexDestinationPayload
{voucher.index}{voucher.input.index}{voucher.destination}{voucher.payload}
+
+ ); +}; + +export default Vouchers; +``` + +
+
+
+ + +### Executing vouchers + +Vouchers in Cartesi dApps authorize specific on-chain actions, such as token swaps or asset transfers, by encapsulating the details of these actions. + +They are validated and executed on the blockchain using the [`executeVoucher(address _destination, bytes _payload, struct Proof _proof)`](../rollups-apis/json-rpc/application.md#executevoucher) function in the [`CartesiDApp`](../rollups-apis/json-rpc/application.md/) contract, ensuring legitimacy and transparency. + +For example, users might generate vouchers to withdraw assets, which are executed on the base later. + +```typescript + + + +// sample code to execute a voucher + +import { useWriteContract } from "wagmi"; + +const executeVoucher = async (voucher: any) => { + const { writeContractAsync } = useWriteContract(); + + if (voucher.proof) { + const newVoucherToExecute = { ...voucher }; + try { + const hash = await writeContractAsync({ + address, // CartesiDApp contract address + abi, // CartesiDApp contract + functionName: "executeVoucher", + args: [voucher.destination, voucher.payload, voucher.proof], + }); + } catch (e) { + console.error("Error executing voucher:", e); + } + } +}; + +``` + +At this stage, you can now interact with the Cartesi backend using the frontend you've built. + diff --git a/docusaurus.config.js b/docusaurus.config.js index fb55268c..299859e2 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -215,7 +215,7 @@ const config = { announcementBar: { id: "mainnet", content: - 'Cartesi Rollups is Mainnet Ready! Over 800K in CTSI is up for grabs... if you can hack Cartesi Rollups.', + 'Cartesi Rollups is Mainnet Ready! Over 950K in CTSI is up for grabs... if you can hack Cartesi Rollups.', backgroundColor: "rgba(0, 0, 0, 0.7)", textColor: "#FFFFFF", diff --git a/sidebarsRollups.js b/sidebarsRollups.js index eb0d777a..7bc1737d 100644 --- a/sidebarsRollups.js +++ b/sidebarsRollups.js @@ -153,7 +153,8 @@ module.exports = { "tutorials/calculator", "tutorials/machine-learning", "tutorials/javascript-wallet", - "tutorials/python-wallet" + "tutorials/python-wallet", + "tutorials/frontend-reactjs-wagmi-application" ], },