Build a transaction Frame
Follow these steps to build a transaction Open Frame that can be displayed in an app built with XMTP.
To build a transaction Open Frame:- Create a boilerplate Next.js app.
cmd
npx create-next-app my-next-app
- Install
@coinbase/onchainkit
as a dependency.
cmd
npm i @coinbase/onchainkit
- Add the base URL in
.env.local
as aNEXT_PUBLIC_BASE_URL
environment variable. - In
app/page.tsx
, replace the boilerplate with the following code — this is what will be rendered as the initial frame:
import { getFrameMetadata } from "@coinbase/onchainkit/frame";
import { Metadata } from "next";
const frameMetadata = getFrameMetadata({
// Accepts and isOpenFrame keys are required for Open Frame compatibility
accepts: { xmtp: "2024-02-09" },
isOpenFrame: true,
buttons: [
{
// Whatever label you want your first button to have
label: "Make transaction",
// Required 'tx' action for a transaction frame
action: "tx",
// Below buttons are 2 route urls that will be added in the next steps.
// Target will send back info about the transaction
target: `${process.env.NEXT_PUBLIC_BASE_URL}/api/transaction`,
// postUrl will send back a transaction success screen
postUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/api/transaction-success`,
},
],
// This is the image shown on the default screen
// Add whatever path is needed for your starting image
// In this case, using an Open Graph image
image: `${process.env.NEXT_PUBLIC_BASE_URL}/api/og?transaction=null`,
});
export const metadata: Metadata = {
title: "Transaction Frame",
description: "A frame to demonstrate transactions",
other: {
...frameMetadata,
},
};
export default function Home() {
return (
<>
<h1>Open Frames Tx Frame</h1>
</>
);
}
- Add the route to
/api/transaction/route.tsx
. The route is used to get information about the transaction that is sent to the target URL.
import { NextRequest, NextResponse } from "next/server";
import { parseEther, encodeFunctionData } from "viem";
import type { FrameTransactionResponse } from "@coinbase/onchainkit/frame";
import { getXmtpFrameMessage } from "@coinbase/onchainkit/xmtp";
async function getResponse(req: NextRequest): Promise<NextResponse | Response> {
const body = await req.json();
const { isValid } = await getXmtpFrameMessage(body);
if (!isValid) {
return new NextResponse("Message not valid", { status: 500 });
}
// This optional param is needed in scenarios where you're interacting with a smart contract
// The values passed will depend on the implementation details of your contract; this is just an example
const data = encodeFunctionData({
abi: JSON.parse(contractAbi),
functionName: "publicMint",
args: [],
});
const txData: FrameTransactionResponse = {
// Sepolia or whichever chain id; we suggest avoiding mainnet for now
chainId: `eip155:11155111`,
method: "eth_sendTransaction",
params: {
abi: [],
// Address receiving the transaction — in this case, hi.xmtp.eth
to: "0x194c31cAe1418D5256E8c58e0d08Aee1046C6Ed0",
// Transaction value in eth sent back as wei — in this case, ~1 cent.
value: parseEther("0.0000032", "wei").toString(),
data, // If applicable
},
};
return NextResponse.json(txData);
}
export async function POST(req: NextRequest): Promise<Response> {
return getResponse(req);
}
- Get the confirmation frame screen HTML via the
@coinbase/onchainkit
helper to the success image and the success button action — in this case a redirect outside of the frame. (The redirect logic is outside the scope of this tutorial.)
export const confirmationFrameHtml = getFrameHtmlResponse({
accepts: {
xmtp: "2024-02-09",
},
isOpenFrame: true,
buttons: [
{
action: "post_redirect",
label: "Learn more about transaction frames",
},
],
postUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/api/end`,
image: `${process.env.NEXT_PUBLIC_BASE_URL}/api/og?transaction=0.0000032`,
});
- Add the route to return the success frame HTML with the new meta tags at
api/transaction-success/route.ts
.
import { confirmationFrameHtml } from "@/app/page";
import { getXmtpFrameMessage } from "@coinbase/onchainkit/xmtp";
import { NextRequest, NextResponse } from "next/server";
async function getResponse(req: NextRequest): Promise<NextResponse> {
const body = await req.json();
const { isValid } = await getXmtpFrameMessage(body);
if (!isValid) {
return new NextResponse("Message not valid", { status: 500 });
}
return new NextResponse(confirmationFrameHtml);
}
export async function POST(req: NextRequest): Promise<Response> {
return getResponse(req);
}
- Send your transaction Frame in an XMTP message and try interacting with it!
Resources
If you need an XMTP messaging app to use, try one of these: