Build Farcaster Frames for Cartesi Rollups: A Simple Guide

Shaheen Ahmed
10 min readSep 16, 2024

--

Before you dive in: The scope of this article guides you into building Frames for Cartesi dApps, a prior familiarity with Farcaster, Cartesi protocol and NextJS framework is required.

Anonymity is a default feature of blockchain protocols, but it poses challenges when building social experiences on-chain. Solutions like ENS domains, attestation systems, and social graphs address this issue. Farcaster is one of the thriving decentralized social protocols which is shaping social identity in Web3 space.

In this tutorial, we’re interested in the mini-apps framework introduced by Farcaster termed Frame. Here are a few questions that we’ll answer in this article:

  • What are Farcaster Frames?
  • How does a Frame interact with a Cartesi dApp?
  • How to build a Frame in NextJS using OnchainKit and Neynar API?
  • Other resources and links

What are Frames?

The Internet is full of hyperlinks, readily sharable with embedded images for preview. A frame is built over a similar idea. It’s an extension to the Open Graph(aka OG) protocol. Instead of serving the user with just an image and a link, a Frame developer can add an input field, buttons and even on-chain transaction functionality to Frames.

The idea is to give the user a minimal set of user interface elements that could be served together with a social post. Below is an example of the initial frame of what we’ll be building later in this tutorial.

A Frame when deployed on Warpcast

An array of ideas could be implemented within the scope of Frames. Some of the frames that I’ve come across had mintable NFTs, quiz games, voting apps, newsletter subscriptions and for some reason a live safari feed right from Savannah. Let me emphasise that all of this is happening without leaving Warpcast. Farcaster is trying to give users reasons to stay and engage with casts/posts. Thanks to the mobile variant of Warpcast, Frames are exposed to a large community of users.

Having said that, bear in mind frames cannot give you all the powers of a full-fledged client application and its ephemeral nature might be a downside in many cases. You can read more on Frames specs here and if you’re curious to play around, /frames is an interesting Warpcast channel to explore.

Remember, at the end of the day, it’s just a Javascript/Typescript web app that lives at a URL on a web server. You could use a React framework and some handy libraries that will help you along the way.

How does a frame talk to a Cartesi dApp?

Let’s understand the architecture from a bird-eye view that will help us grasp the components involved and how the data flows between them.

We broadly have 4 components here:

  1. Frame UI — User interface that is deployed as a cast in a Farcaster client. We’ll use Warpcast in this article.
  2. Frame Server — The middleware that defines the Frame UI and its communication with other components.
  3. Blockchain — The on-chain part of the stack. It could be any EVM-based network Ethereum, Base, Optimism or any other.
  4. Cartesi Rollups Node — The backend of your app packaged with Cartesi Machine, deployed as an app-chain.

To speed things up, we won’t be writing the code from scratch but rather clone a minimal repo that is designed to act as a template for Cartesi Frame developers.

Let’s start building a frame!

We’re creating a simple frame that will take a lowercase string as input, send it to Cartesi backend, get processed into an uppercase string and show it on the Frame. You can watch a quick demo video on the link below.

The frame is live and you can also give it a try here on Warpcast.

From a developer pov, our tech stack will include a NextJS web server that leverages Onchainkit to abstract low-level code for us and Neynar API to validate Farcaster data.

Note: We’re not building Cartesi dApp backend in this tutorial. We’ll play with an existing toUpper example that is already deployed and anchored with Base Sepolia network on this dApp address. You can visit docs here to learn Cartesi development.

Steps-to-run with Code explanations

Open your terminal tab and clone the repo from Mugen-Builders

$ git clone https://github.com/Mugen-Builders/farcaster-frame-cartesi

The project structure inside farcaster-frame-cartesilooks like this:

|--app
|--_contracts
|--InputBoxABI.ts
|--api
|--inspect-cm
|--og
|--tx-success
|--tx
|--config.ts
|--layout.tsx
|--page.tsx
|--public
|--README.md

The directory structure should be easy to grasp, we’ll stitch together the Frame metadata with a few API routes. We define interfacing with Cartesi rollups in three of the routes as below:

  • /tx — JSON-RPC call to add inputs to Cartesi dApp
  • /tx-success — GraphQL query to fetch Cartesi outputs
  • /inspect-cm — HTTP-REST call to read the state of the backend

/app/page.tsx is where we define the Initial frame of your app. We use a helper function from the onchainkit/frame library — getFrameMetadata(...)

Note: The HTML inside the return of thePage()component is NOT rendered on the Frame UI, it’s only reflected on the web server URL.

import { getFrameMetadata } from '@coinbase/onchainkit/frame';
import type { Metadata } from 'next';
import { NEXT_PUBLIC_URL } from './config';

const frameMetadata = getFrameMetadata({
buttons: [
{
action: 'tx',
label: 'Send to Cartesi dApp',
target: `${NEXT_PUBLIC_URL}/api/tx`,
postUrl: `${NEXT_PUBLIC_URL}/api/tx-success`,
},
],
image: {
src: `${NEXT_PUBLIC_URL}/cartesi-machine.png`,
aspectRatio: '1:1',
},
input: {
text: 'What do you want to scream?',
},
});

export const metadata: Metadata = {
title: 'cartesi-frame',
description: 'LFG',
openGraph: {
title: 'cartesi-frame',
description: 'LFG',
images: [`${NEXT_PUBLIC_URL}/cartesi-machine.png`],
},
other: {
...frameMetadata,
},
};

export default function Page() {
return (
<>
<h1>Send inputs to Cartesi dApp</h1>
</>
);
}

The above metadata suggests our landing page will have an image, an input field and a button. Action property of the button is of type tx which is a standard action for transactions with user-connected wallets. Clicking on the button should send a POST request to the web server to process target: `${NEXT_PUBLIC_URL}/api/tx` API route.

Sending Input to the Cartesi node

Let’s see what the tx (transaction) route looks like. Below is the code for the /tx/route.ts file.

import { FrameRequest, getFrameMessage } from '@coinbase/onchainkit/frame';
import { NextRequest, NextResponse } from 'next/server';
import { encodeFunctionData } from 'viem';
import { baseSepolia } from 'viem/chains';
import InputBoxABI from '../../_contracts/InputBoxABI';
import { CARTESI_INPUT_BOX_ADDR, CARTESI_DAPP_ADDRESS, NEYNAR_API_KEY } from '../../config';
import type { FrameTransactionResponse } from '@coinbase/onchainkit/frame';

async function getResponse(req: NextRequest): Promise<NextResponse | Response> {
const body: FrameRequest = await req.json();
const { isValid, message } = await getFrameMessage(body, { neynarApiKey: NEYNAR_API_KEY });

if (!isValid) {
return new NextResponse('Message not valid', { status: 500 });
}

const userInput = message.input || '';

const data = encodeFunctionData({
abi: InputBoxABI,
functionName: 'addInput',
args: [
CARTESI_DAPP_ADDRESS,
`0x${Buffer.from(userInput).toString('hex')}`,
],
});

const txData: FrameTransactionResponse = {
chainId: `eip155:${baseSepolia.id}`,
method: 'eth_sendTransaction',
params: {
abi: [],
data,
to: CARTESI_INPUT_BOX_ADDR,
value: '0',
},
};
return NextResponse.json(txData);
}

export async function POST(req: NextRequest): Promise<Response> {
return getResponse(req);
}

export const dynamic = 'force-dynamic';

The above code primarily handles on-chain transactions. We prepare call data for theaddInput()method of Cartesi rollups InputBox contract. A minimal ABI to interact with the same contract is added inside the /app/_contracts/InputBoxABI.ts file. All Cartesi dApps use the same contract with their unique Application address and app-specific input data as parameters.

If you look back at the page.tsx , we also defined a postUrl: `${NEXT_PUBLIC_URL}/api/tx-success`. After the success of the on-chain transaction, we want the user to check the output returned by the Cartesi dApp.

Note: A successful transaction on the blockchain does not imply the input got processed successfully inside the Cartesi backend. The rollups node reads and processes the transaction only after it’s successfully recorded on-chain.

Reading output from the Cartesi node

Cartesi dApp backend produces outputs in the form of Notices, Vouchers and Reports. We can easily read them by querying the GraphQL server endpoint exposed by the Cartesi node.

In the tx-success route, we define a GraphQL query and poll the node until we fetch the Notice output corresponding to the input we sent earlier.

import { CARTESI_NODE_GRAPHQL_ENDPOINT, CARTESI_INPUT_BOX_ADDR, ... } from '../../config';
// all other imports here

export const runtime = 'edge'

const publicClient = createPublicClient({...
});

interface GraphQLResponse {...
}

// get the input index recorded on-chain
async function getInputIndex(transactionHash: string): Promise<bigint | null> {
const receipt = await publicClient.waitForTransactionReceipt({ hash: transactionHash as `0x${string}` });
const event = receipt.logs.find(log =>
log.address.toLowerCase() === CARTESI_INPUT_BOX_ADDR.toLowerCase()
);

if (event) {
const decodedEvent = decodeEventLog({
abi: InputBoxABI,
data: event.data,
topics: event.topics,
});
return decodedEvent.args.inputIndex as bigint;
}
return null;
}

// fetch the Notice output corresponding to the input index
async function getNotice(inputIndex: number): Promise<string | null> {
const query = `
query {
notice(noticeIndex: 0, inputIndex: ${inputIndex}) {
index
payload
}
}
`;

const response = await fetch(CARTESI_NODE_GRAPHQL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});

const data = await response.json() as GraphQLResponse;

if (!data.data || !data.data.notice) {
console.log('No notice data found');
return null;
}

return data.data.notice.payload ? ethers.toUtf8String(data.data.notice.payload) : null;
}

// poll to fetch Notice output on the Cartesi node
async function pollForNotice(inputIndex: number, maxAttempts = 10, interval = 3000): Promise<string | null> {
for (let i = 0; i < maxAttempts; i++) {
const notice = await getNotice(inputIndex);
if (notice && notice.trim() !== '') {
return notice;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
return null;
}

async function getResponse(req: NextRequest): Promise<NextResponse> {
const body: FrameRequest = await req.json();
const { isValid } = await getFrameMessage(body);

if (!isValid) {
return new NextResponse('Message not valid', { status: 500 });
}

const transactionId = body.untrustedData.transactionId;
if (!transactionId) {
return new NextResponse('No transaction ID provided', { status: 400 });
}

const inputIndexBigInt = await getInputIndex(transactionId);
if (!inputIndexBigInt) {
return new NextResponse('Failed to get input index', { status: 500 });
}

const inputIndex = Number(inputIndexBigInt);
const notice = await pollForNotice(inputIndex);
const message = notice || 'No notice available after polling';
const imageUrl = `${NEXT_PUBLIC_URL}/api/og?message=${encodeURIComponent(message)}`;

return new NextResponse(
getFrameHtmlResponse({
buttons: [
{
label: 'Total scream counter',
action: 'post',
},
],
image: {
src: imageUrl,
},
postUrl: `${NEXT_PUBLIC_URL}/api/inspect-cm`,
}),
);
}

export async function POST(req: NextRequest): Promise<Response> {
return getResponse(req);
}

export const dynamic = 'force-dynamic';

We also have a helper API route api/ogin the directory which is up to you to play around. It defines the image that will display the output we fetched from the above Notice polling.

Inspecting the state with HTTP-REST API

One last API route that also has an interface with Cartesi is api/inspect-cm that handles the Inspect calls directly to the Cartesi Node. You can check the state of your dApp as per the inspect routes defined on the Cartesi backend.

Here we fetch the total Number of Screams that were triggered from the toUpper Cartesi dApp. After the successful call, the metadata of the frame is set to show the fetched number on the ‘og’ image and a new redirect button is added that takes you to the Github repo of the frame.

import { CARTESI_NODE_INSPECT_ENDPOINT, REDIRECT_URL, ... } from '../../config';
// all other imports here

export const runtime = 'edge'

async function getTotalScreams(): Promise<string> {
const response = await fetch(CARTESI_NODE_INSPECT_ENDPOINT + '/total');
const data = await response.json();
if (data.reports && data.reports.length > 0) {
const payload = data.reports[0].payload;
const decodedPayload = ethers.toUtf8String(payload);
const jsonPayload = JSON.parse(decodedPayload);
return jsonPayload.toUpperTotal?.toString() || 'No total available';
}
return 'No data available';
}

async function getResponse(req: NextRequest): Promise<NextResponse> {
const body: FrameRequest = await req.json();
const { isValid } = await getFrameMessage(body);

if (!isValid) {
return new NextResponse('Message not valid', { status: 500 });
}

const totalScreams = await getTotalScreams();
const imageUrl = `${NEXT_PUBLIC_URL}/api/og?message=${encodeURIComponent(totalScreams)}`;

return new NextResponse(
getFrameHtmlResponse({
buttons: [
{
label: 'Github Repo',
action: 'link',
target: REDIRECT_URL,
},
],
image: {
src: imageUrl,
},
}),
);
}

export async function POST(req: NextRequest): Promise<Response> {
return getResponse(req);
}

export const dynamic = 'force-dynamic';

Testing and debugging the Frame

There are tools to test your frames locally like frames.js and framegear by Onchainkit. One thing that might be annoying is the lack of maturity of these tools when it comes to on-chain interactions.

I’d recommend using a secure tunnelling service like ngrok that will let you serve an https URL for your local server. You can install ngrok from here.

Steps-to-test

  • Modify config.ts as per instructions in the file. You’ll need a Neynar API key, please visit the official Neynar website to get one for yourself. It’s required to communicate with Farcaster hubs and validate the data being sent and stored on these hubs. You might need to spend a few bucks here as there is no free tier of the API.
  • Once you’ve got the config setup, we can run the Frame server by following commands:
$ npm install
$ npm run dev
  • Open another terminal tab and hit the below command to generate ngrok live URL.
$ ngrok http http://localhost:3000

Do not forget to update this URL in the /config.ts file and re-run the server.

  • We’re good to test the Frame now. Open Warpcast frames validator. It is a testing ground for live frames before you deploy them to production. Enter the URL provided by ngrok and you’ll see the initial frame of our app in action, ready to be tested.

That’s pretty much it, once you’re happy with the test results, you can deploy the NextJS app in production and get the permanent URL. If you cast the live URL on your Warpcast account, you’ll see your frame live and visible to other Farcaster users.

🎉 You did it!

Hope this article helps you build something meaningful. You can reach out to me at X here in case of any help. Some resources that might help you:

Cartesi docs Video Masterclass
Frames Boilerplate by Onchainkit Farcaster Frames Docs

A big shoutout to Zizzamia for creating the boilerplate repo which helped a lot in creating this tutorial. Huge thanks to Claudio for the initial exploration of integrating Frames with Cartesi. And to all the tools and services used in this guide, helped a lot in keeping the complexity away.

Thank you!

--

--

Shaheen Ahmed
Shaheen Ahmed

Written by Shaheen Ahmed

Exploring all interesting things happening in the world | Developer Advocate

No responses yet