Build a React frontend for Cartesi dApps
This tutorial will focus on building a frontend for a Cartesi dApp using React.js. Our primary goal is to create a minimalistic frontend with all the functionalities for seamless interactions with any Cartesi backend.
Setting up the environment
To build a frontend for Cartesi dApps, we'll use React.js along with Wagmi, a library that simplifies blockchain interactions in React applications.
Creating a new React project
To get started quickly with a pre-configured React project that includes Wagmi, you can use the create-wagmi
CLI command. For detailed instructions on setting up a Wagmi project, refer to the official Wagmi documentation.
Install the following dependencies:
Once you've set up your project, you'll have a basic structure that includes:
- A main configuration file for blockchain interactions
- A main App component
- An entry point file
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:
- src/wagmi.ts
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;
}
}
You can find the list of supported chains and their IDs in the deployment guide.
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 to ensure visual appeal.
Create a new file src/components/Account.tsx
and edit the App.tsx
:
- src/components/Account.tsx
- src/App.tsx
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 (
<div className="max-w-2xl mx-auto mt-10 p-6 bg-gradient-to-r
from-purple-500 to-indigo-600 rounded-lg shadow-xl">
<div className="mb-8">
<h2 className="text-3xl font-bold text-white mb-4">Account</h2>
<div className="bg-white bg-opacity-20 rounded-lg p-4 text-white">
<p className="mb-2">
<span className="font-semibold">Status:</span>
<span className="text-white font-semibold">
{account.status.toLocaleUpperCase()} </span>
</p>
<p className="mb-2 font-semibold">
<span>Address:</span>{" "}
{account.addresses?.[0]}
</p>
<p className="font-semibold">
<span>Chain ID:</span> {account.chain?.name} | {account.chainId}
</p>
</div>
{/* Display chain switching and disconnect options when connected */}
{account.status === "connected" && (
<div className="space-y-4 mt-4">
{/* Chain switching dropdown */}
<div className="relative">
<button
type="button"
onClick={() => setIsChainDropdownOpen(!isChainDropdownOpen)}
className="w-full flex justify-between items-center
py-2 px-4 border border-gray-300 rounded-md
shadow-sm text-sm font-medium
text-gray-700 bg-white hover:bg-gray-50
focus:outline-none focus:ring-2 focus:ring-offset-2
focus:ring-indigo-500 transition-colors duration-200"
>
Switch Chain
<svg
className="h-5 w-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414
0L10 10.586l3.293-3.293a1 1 0 111.414
1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
{/* Dropdown menu for chain options */}
{isChainDropdownOpen && (
<div className="absolute z-10 mt-1 w-full
bg-white shadow-lg rounded-md py-1">
{chains.map((chainOption) => (
<button
key={chainOption.id}
onClick={() => {
switchChain({ chainId: chainOption.id });
setIsChainDropdownOpen(false);
}}
className="block w-full text-left
px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
{chainOption.name}
</button>
))}
</div>
)}
</div>
{/* Disconnect button */}
<button
type="button"
onClick={() => disconnect()}
className="w-full flex justify-center py-2
px-4 border border-transparent rounded-md
shadow-sm text-sm font-medium text-white
bg-red-600 hover:bg-red-700 focus:outline-none
focus:ring-2 focus:ring-offset-2
focus:ring-red-500 transition-colors duration-200"
>
Disconnect
</button>
</div>
)}
</div>
{/* Connect section */}
<div>
<h2 className="text-3xl font-bold text-white mb-4">Connect</h2>
<div className="grid grid-cols-2 gap-4">
{connectors.map((connector) => (
<button
key={connector.uid}
onClick={() => connect({ connector })}
type="button"
className="px-4 py-2 bg-white
text-purple-600 rounded-md hover:bg-purple-100
transition-colors duration-300"
>
{connector.name}
</button>
))}
</div>
<div className="mt-4 text-white">
Status: {status.toLocaleUpperCase()}
</div>
<div className="mt-2 text-red-300">
{error?.message}
</div>
</div>
</div>
);
};
export default Account;
import Account from "./components/Account";
function App() {
return (
<>
<Account />
</>
);
}
export default App;
Define the ABIs, contract addresses and hooks
In a Cartesi dApp, the frontend sends inputs to the backend via the base layer chain using 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.
However, manually specifying the ABIs and addresses for all the Cartesi Rollups contracts when making function calls can be a hassle.
Thanks to @wagmi/cli
, we can be more efficient by autogenerating Cartesi-specific hooks.
These hooks come preconfigured with all the ABIs and addresses needed for any function calls to Cartesi. We just need to add the custom arguments for our specific use case.
This will automate manual work so we can build faster! We simply import the hooks, call the functions, and pass in the custom arguments.
We will install the following dependencies to our project:
@wagmi/cli
: The Wagmi CLI tool for ABI-specific hooks.@cartesi/rollups
: The Cartesi Rollups contract implementations.@sunodo/wagmi-plugin-hardhat-deploy
: Wagmi CLI plugin that loads contracts and deployments from the@cartesi/rollups
package.
npm i @wagmi/cli @cartesi/rollups @sunodo/wagmi-plugin-hardhat-deploy
Create a config file in the root of your project: wagmi.config.ts
Then, add contracts and plugins for Cartesi Rollups:
- wagmi.config.ts
import { defineConfig } from "@wagmi/cli";
import { react } from "@wagmi/cli/plugins";
import { erc20Abi, erc721Abi } from "viem";
import hardhatDeploy from "@sunodo/wagmi-plugin-hardhat-deploy";
export default defineConfig({
out: "src/hooks/generated.ts", // Specifies the output file for the hooks
contracts: [
{
abi: erc20Abi,
name: "erc20",
},
{ abi: erc721Abi, name: "erc721" },
],
plugins: [
hardhatDeploy({
directory: "node_modules/@cartesi/rollups/export/abi",
}),
react(),
],
});
The configuration sets up the Wagmi CLI to generate TypeScript hooks for ERC20 and ERC721 contracts, as well as for any contracts in the specified Cartesi Rollups ABI directory, i.e src/hooks/generated
.
To generate, run:
npx wagmi generate
Sending a generic input
The InputBox
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 a React hook to send an input to the backend via the InputBox
contract.
Create a new file src/components/SimpleInput.tsx
and follow the implementation below:
Component setup and imports
import React, { useState } from "react";
import { BaseError } from "wagmi";
import { useWriteInputBoxAddInput } from "../hooks/generated";
import { stringToHex } from "viem";
Here, we are importing the generated useWriteInputBoxAddInput
hook and Viem's stringToHex
function for data conversion.
Component definition and state
const SimpleInput = () => {
const dAppAddress = `0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e`;
const [inputValue, setInputValue] = useState("");
const [hexInput, setHexInput] = useState<boolean>(false);
// ... rest of the component
};
We define the dAppAddress
and create a state variable inputValue
to manage the user's input. The hexInput
state variable is used to toggle between the generic text input and hex values.
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 Hook
We'll use the useWriteInputBoxAddInput
hook to interact with the InputBox
contract:
const { isPending, isSuccess, error, writeContractAsync } = useWriteInputBoxAddInput();
This hook provides us with state variables and a writeContractAsync
function to write to the smart contract.
Form submission and component rendering
async function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
await writeContractAsync({
args: [
dAppAddress,
hexInput ? (inputValue as Hex) : 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)
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.
Final Component
Putting it all together, our complete <SimpleInput/>
component and App.tsx
look like this:
- src/components/SimpleInput.tsx
- src/App.tsx
import React, { useState } from "react";
import { BaseError } from "wagmi";
import { useWriteInputBoxAddInput } from "../hooks/generated";
import { Hex, stringToHex } from "viem";
const SimpleInput = () => {
const dAppAddress = `0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e`;
const [inputValue, setInputValue] = useState("");
const [hexInput, setHexInput] = useState<boolean>(false);
const { isPending, isSuccess, error, writeContractAsync } =
useWriteInputBoxAddInput();
async function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
await writeContractAsync({
args: [
dAppAddress,
hexInput ? (inputValue as Hex) : stringToHex(inputValue),
],
});
}
return (
<div className="max-w-md mx-auto mt-10 p-6
bg-gradient-to-r from-purple-500 to-indigo-600
rounded-lg shadow-xl">
<h2 className="text-3xl font-bold text-white mb-6">Send Generic Input</h2>
<form onSubmit={submit} className="space-y-4">
<div>
<input
type="text"
className="w-full px-4 py-2 rounded-md border
border-gray-300 focus:outline-none
focus:ring-2 focus:ring-purple-500"
placeholder="Enter something"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<input
type="checkbox"
checked={hexInput}
onChange={(e) => setHexInput(!hexInput)}
/>
<span>Raw Hex </span>
</div>
<button
type="submit"
className="w-full px-4 py-2 bg-white
text-purple-600 rounded-md hover:bg-purple-100
transition-colors duration-300 font-medium"
>
{isPending ? "Sending..." : "Send"}
</button>
</form>
{isSuccess && (
<p className="mt-4 text-green-300 font-bold">Transaction Sent</p>
)}
{error && (
<div className="mt-4 text-red-300">
Error: {(error as BaseError).shortMessage || error.message}
</div>
)}
</div>
);
};
export default SimpleInput;
import Account from "./components/Account";
import SimpleInput from "./components/SimpleInput";
function App() {
return (
<>
<Account />
<SimpleInput />
</>
);
}
export default App;
Depositing Ether
The EtherPortal
contract is a pre-deployed smart contract that allows users to deposit Ether to the Cartesi backend.
This implementation will be similar to the generic input, but with a few changes to handle Ether transactions.
The key changes in this 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 usesparseEther
to convert the input value.We will use the
useWriteEtherPortalDepositEther
hook to send Ether.
import { useWriteEtherPortalDepositEther } from "../hooks/generated";
// other imports here
const [etherValue, setEtherValue] = useState("");
// rest of the code
const {isPending,isSuccess,error,writeContractAsync: depositToken} = useWriteEtherPortalDepositEther();
async function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = stringToHex(`Deposited (${etherValue}) ether.`);
await depositToken({
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:
- src/components/SendEther.tsx
import React, { useState } from "react";
import { BaseError } from "wagmi";
import { useWriteEtherPortalDepositEther } from "../hooks/generated";
import { parseEther, stringToHex } from "viem";
const SendEther = () => {
const dAppAddress = `0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e`;
const [etherValue, setEtherValue] = useState("");
const {
isPending,
isSuccess,
error,
writeContractAsync: depositToken,
} = useWriteEtherPortalDepositEther();
async function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = stringToHex(`Deposited (${etherValue}) ether.`);
await depositToken({
args: [dAppAddress, data],
value: parseEther(etherValue),
});
}
return (
<div className="max-w-md mx-auto mt-10 p-6 bg-gradient-to-r
from-purple-500 to-indigo-600 rounded-lg shadow-xl">
<h2 className="text-3xl font-bold text-white mb-6">Deposit Ether</h2>
<form onSubmit={submit} className="space-y-4">
<div>
<input
type="text"
className="w-full px-4 py-2 rounded-md border
border-gray-300 focus:outline-none focus:ring-2
focus:ring-purple-500"
placeholder="Enter Ether amount"
value={etherValue}
onChange={(e) => setEtherValue(e.target.value)}
/>
</div>
<button
type="submit"
className="w-full px-4 py-2 bg-white
text-purple-600 rounded-md
hover:bg-purple-100 transition-colors duration-300 font-medium"
>
{isPending ? "Sending..." : "Send"}
</button>
</form>
{isSuccess && (
<p className="mt-4 text-green-300 font-bold">{etherValue} ETH sent!</p>
)}
{error && (
<div className="mt-4 text-red-300">
Error: {(error as BaseError).shortMessage || error.message}
</div>
)}
</div>
);
};
export default SendEther;
Depositing ERC20 Tokens
The ERC20Portal
contract is a pre-deployed smart contract that allows users to deposit ERC20 tokens to the Cartesi backend.
This implementation will be similar to the 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 callsapprove()
before callingdepositERC20Tokens
on the ERC20Portal contract.ERC Token ApprovalFor ERC20, ERC721, and ERC1155 token standards, 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.
We will use the
useWriteErc20Approve
hook to approve the deposit anduseWriteErc20PortalDepositErc20Tokens
hook to make the deposit.import {
erc20PortalAddress,
useWriteErc20Approve,
useWriteErc20PortalDepositErc20Tokens,
} from "../hooks/generated";
import { Address, parseEther, stringToHex, Hex } from "viem";
// other imports here
const [erc20Value, setErc20Value] = useState("");
const [tokenAddress, setTokenAddress] = useState<Address | null>();
const { writeContractAsync: approveToken } = useWriteErc20Approve();
const { writeContractAsync: depositToken} = useWriteErc20PortalDepositErc20Token();
const approve = async (address: Address, amount: string) => {
try {
await approveToken({
address,
args: [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<HTMLFormElement>) {
event.preventDefault();
const data = stringToHex(`Deposited (${erc20Value}).`);
await approve(tokenAddress as Address, erc20Value);
await depositToken({
args: [tokenAddress as Hex, 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.
Final Component
Create a new file src/components/SendERC20.tsx
and paste the complete code:
- src/components/SendERC20.tsx
- src/App.tsx
import React, { useState } from "react";
import { BaseError } from "wagmi";
import {
erc20PortalAddress,
useWriteErc20Approve,
useWriteErc20PortalDepositErc20Tokens,
} from "../hooks/generated";
import { Address, parseEther, stringToHex, Hex } from "viem";
const SendERC20 = () => {
const dAppAddress = `0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e`;
const [erc20Value, setErc20Value] = useState("");
const [tokenAddress, setTokenAddress] = useState<Address | null>();
const {
isPending,
isSuccess,
error,
writeContractAsync: depositToken,
} = useWriteErc20PortalDepositErc20Tokens();
const { writeContractAsync: approveToken } = useWriteErc20Approve();
const approve = async (address: Address, amount: string) => {
try {
await approveToken({
address,
args: [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<HTMLFormElement>) {
event.preventDefault();
const data = stringToHex(`Deposited (${erc20Value}).`);
await approve(tokenAddress as Address, erc20Value);
await depositToken({
args: [tokenAddress as Hex, dAppAddress, parseEther(erc20Value), data],
});
}
return (
<div className="max-w-md mx-auto mt-10 p-6 bg-gradient-to-r
from-purple-500 to-indigo-600 rounded-lg shadow-xl">
<h2 className="text-3xl font-bold text-white mb-6">Deposit ERC20</h2>
<form onSubmit={submit} className="space-y-4">
<div>
<input
type="text"
className="w-full px-4 py-2 rounded-md border
border-gray-300 focus:outline-none
focus:ring-2 focus:ring-purple-500 mb-4"
placeholder="ERC20 Token Address"
value={tokenAddress as Address}
onChange={(e) => setTokenAddress(e.target.value as Address)}
/>
<input
type="text"
className="w-full px-4 py-2 rounded-md border
border-gray-300 focus:outline-none
focus:ring-2 focus:ring-purple-500"
placeholder="Enter ERC20 amount"
value={erc20Value}
onChange={(e) => setErc20Value(e.target.value)}
/>
</div>
<button
type="submit"
className="w-full px-4 py-2 bg-white
text-purple-600 rounded-md hover:bg-purple-100
transition-colors duration-300 font-medium"
>
{isPending ? "Sending..." : "Send"}
</button>
</form>
{isSuccess && (
<p className="mt-4 text-green-300 font-bold">
{erc20Value} tokens sent!
</p>
)}
{error && (
<div className="mt-4 text-red-300">
Error: {(error as BaseError).shortMessage || error.message}
</div>
)}
</div>
);
};
export default SendERC20;
import Account from "./components/Account";
import SimpleInput from "./components/SimpleInput";
import SendEther from "./components/SendEther";
import SendERC20 from "./components/SendERC20";
function App() {
return (
<>
<Account />
<SimpleInput />
<SendEther />
<SendERC20 />
</>
);
}
export default App;
Depositing ERC721 Tokens (NFTs)
The ERC721Portal
contract is a pre-deployed smart contract that allows users to deposit ERC721 tokens to the Cartesi backend.
This implementation will be similar to the depositing ERC20 tokens, but with a few changes to handle ERC721 token transactions.
Here are the key differences in depositing ERC721 tokens:
ERC721 deposits require both the ERC721 token address and token ID.
We will use the
useWriteErc721Approve
hook to approve the deposit anduseWriteErc721PortalDepositErc721Tokens
hook to make the deposit.import { Address, erc721Abi, parseEther, stringToHex } from "viem";
// other imports here
const [tokenId, setTokenId] = useState<string>("");
const [tokenAddress, setTokenAddress] = useState("");
const {
isPending,
isSuccess,
error,
writeContractAsync: depositToken,
} = useWriteErc721PortalDepositErc721Token();
const { writeContractAsync: approveToken } = useWriteErc721Approve();
const approve = async (address: Address, tokenId: bigint) => {
try {
await approveToken({
address,
args: [erc721PortalAddress, 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<HTMLFormElement>) {
event.preventDefault();
const bigIntTokenId = BigInt(tokenId);
const data = stringToHex(`Deposited NFT of token id:(${bigIntTokenId}).`);
await approve(tokenAddress as Address, bigIntTokenId);
depositToken({
args: [tokenAddress as Hex, 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.
Final Component
Create a new file src/components/SendERC721.tsx
and paste the complete code:
- src/components/SendERC721.tsx
- src/App.tsx
import React, { useState } from "react";
import { BaseError } from "wagmi";
import {
erc721PortalAddress,
useWriteErc721Approve,
useWriteErc721PortalDepositErc721Token,
} from "../hooks/generated";
import { stringToHex, Address, Hex } from "viem";
const SendERC721 = () => {
const dAppAddress = `0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e`;
const [tokenId, setTokenId] = useState<string>("");
const [tokenAddress, setTokenAddress] = useState("");
const {
isPending,
isSuccess,
error,
writeContractAsync: depositToken,
} = useWriteErc721PortalDepositErc721Token();
const { writeContractAsync: approveToken } = useWriteErc721Approve();
const approve = async (address: Address, tokenId: bigint) => {
try {
await approveToken({
address,
args: [erc721PortalAddress, 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<HTMLFormElement>) {
event.preventDefault();
const bigIntTokenId = BigInt(tokenId);
const data = stringToHex(`Deposited NFT of token id:(${bigIntTokenId}).`);
await approve(tokenAddress as Address, bigIntTokenId);
depositToken({
args: [tokenAddress as Hex, dAppAddress, bigIntTokenId, "0x", data],
});
}
return (
<div className="max-w-md mx-auto mt-10 p-6 bg-gradient-to-r
from-purple-500 to-indigo-600 rounded-lg shadow-xl">
<h2 className="text-3xl font-bold text-white mb-6">
Deposit ERC721 Token
</h2>
<form onSubmit={submit} className="space-y-4">
<div>
<input
type="text"
className="w-full px-4 py-2 rounded-md
borderborder-gray-300 focus:outline-none
focus:ring-2 focus:ring-purple-500"
placeholder="ERC721 Token Address"
value={tokenAddress}
onChange={(e) => setTokenAddress(e.target.value)}
/>
</div>
<div>
<input
type="text"
className="w-full px-4 py-2 rounded-md border
border-gray-300 focus:outline-none
focus:ring-2 focus:ring-purple-500"
placeholder="Token ID"
value={tokenId}
onChange={(e) => setTokenId(e.target.value)}
/>
</div>
<button
type="submit"
className="w-full px-4 py-2 bg-white
text-purple-600 rounded-md hover:bg-purple-100
transition-colors duration-300 font-medium"
>
{isPending ? "Sending..." : "Send"}
</button>
</form>
{isSuccess && (
<p className="mt-4 text-green-300 font-bold">
NFT of Token number: {tokenId} sent!
</p>
)}
{error && (
<div className="mt-4 text-red-300">
Error: {(error as BaseError).shortMessage || error.message}
</div>
)}
</div>
);
};
export default SendERC721;
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 (
<>
<Account />
<SimpleInput />
<SendEther />
<SendERC20 />
<SendERC721 />
</>
);
}
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, Reports, and Vouchers.
These outputs can be queried by the frontend using the GraphQL API on http://localhost:8080/graphql
.
Refer to the GraphQL API documentation 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.
- queries.ts
- src/utils/types.ts
- src/utils/api.ts
// 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
}
}
}
}
`;
// 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<T> = {
data: T;
};
// api.ts
import axios from 'axios';
import { GraphQLResponse } from './types';
export const fetchGraphQLData = async <T>(query: string) => {
const response = await
axios.post<GraphQLResponse<T>>('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.
- Notices.tsx
- Reports.tsx
- Vouchers.tsx
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<Notice[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 <div>Loading...</div>;
if (error) return <div>{error}</div>;
return (
<div className="max-w-4xl mx-auto mt-10 p-6
bg-white rounded-lg shadow-xl">
<h2 className="text-3xl font-bold text-center mb-6">Notices</h2>
<table className="min-w-full divide-y
divide-gray-200 bg-gradient-to-r
from-purple-500 to-indigo-600 text-white">
<thead>
<tr>
<th className="px-4 py-2">Index</th>
<th className="px-4 py-2">Input Index</th>
<th className="px-4 py-2">Payload</th>
</tr>
</thead>
<tbody>
{notices.map((notice, idx) => (
<tr key={idx} className="hover:bg-purple-700
transition-colors duration-300">
<td className="px-4 py-2">{notice.index}</td>
<td className="px-4 py-2">{notice.input.index}</td>
<td className="px-4 py-2">{notice.payload}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default Notices;
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<Report[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 <div>Loading...</div>;
if (error) return <div>{error}</div>;
return (
<div className="max-w-4xl mx-auto mt-10 p-6
bg-white rounded-lg shadow-xl">
<h2 className="text-3xl font-bold text-center mb-6">Reports</h2>
<table className="min-w-full divide-y
divide-gray-200 bg-gradient-to-r
from-purple-500 to-indigo-600 text-white">
<thead>
<tr>
<th className="px-4 py-2">Index</th>
<th className="px-4 py-2">Input Index</th>
<th className="px-4 py-2">Payload</th>
</tr>
</thead>
<tbody>
{reports.map((report, idx) => (
<tr key={idx} className="hover:bg-purple-700
transition-colors duration-300">
<td className="px-4 py-2">{report.index}</td>
<td className="px-4 py-2">{report.input.index}</td>
<td className="px-4 py-2">{report.payload}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default Reports;
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<Voucher[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 <div>Loading...</div>;
if (error) return <div>{error}</div>;
return (
<div className="max-w-4xl mx-auto mt-10 p-6
bg-white rounded-lg shadow-xl">
<h2 className="text-3xl font-bold text-center mb-6">Vouchers</h2>
<table className="min-w-full divide-y
divide-gray-200 bg-gradient-to-r
from-purple-500 to-indigo-600 text-white">
<thead>
<tr>
<th className="px-4 py-2">Index</th>
<th className="px-4 py-2">Input Index</th>
<th className="px-4 py-2">Destination</th>
<th className="px-4 py-2">Payload</th>
</tr>
</thead>
<tbody>
{vouchers.map((voucher, idx) => (
<tr key={idx} className="hover:bg-purple-700
transition-colors duration-300">
<td className="px-4 py-2">{voucher.index}</td>
<td className="px-4 py-2">{voucher.input.index}</td>
<td className="px-4 py-2">{voucher.destination}</td>
<td className="px-4 py-2">{voucher.payload}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
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)
function in the CartesiDApp
contract, ensuring legitimacy and transparency.
For example, users might generate vouchers to withdraw assets, which are executed on the base later.
At this stage, you can now interact with the Cartesi backend using the frontend you've built.