
Solana Frames:在 Farcaster 上铸造 cNFT
本文介绍什么?
去中心化社交媒体标志着一次重大转变:用户对自己的在线数据拥有更强的隐私保护和控制权。与传统社交媒体平台不同,去中心化网络让个人可以直接管理自己的社交图谱和数字身份。Farcaster 等平台正走在前沿,构建抗审查网络,推动不同平台之间开放且可互操作的通信。Farcaster 的一项关键创新是引入 Frames,将普通社交媒体帖子变成功能完整的应用,从而丰富在线社交体验。
本文将介绍 Farcaster 及其近期对 Solana 的支持。我们会说明 Farcaster 是什么、Frames 是什么,以及如何验证 Solana 地址。随后,我们将通过一个简短教程,介绍如何使用 Helius Mint API 创建一个 Frame,将 cNFT 铸造到用户已验证的 Solana 地址。本教程假定你具备 TypeScript 和 Next.js 的使用经验。
Farcaster 是什么?
Farcaster 是一个利用 Ethereum 网络的充分去中心化社交网络。它提供了一个类似 Twitter 和 Reddit 等热门社交网络的公共平台,用户可以创建个人资料、分享帖子(称为 casts)并关注其他用户。Farcaster 的独特之处在于强调用户所有权和可移植性——用户拥有自己的账户以及与他人的关系。他们可以在不同应用之间迁移,同时保留自己的数字身份和社交图谱。
Frames 是什么?
Frames 让开发者能够将任意 cast 变成交互式应用。它通过扩展 OpenGraph 标准,将静态嵌入内容转变为动态交互体验。用户可以在任何 Farcaster 客户端(包括 Warpcaster)中创建投票、实时信息流或交互式图库等功能。Frame 的核心是一组 <meta> 标签,它们随 HTML 页面的 <head> 一起返回。如果页面包含所有必需的 Frame 属性,Farcaster 应用就会将其渲染为 Frame。Frame 规范列出了这些必需属性。例如,一个初始 Frame 必须:
- 在 HTML 的
<head>部分返回有效的 Frame - 在用户通过浏览器点击进入 Frame 时,返回有效的 HTML
<body> - 不在初始 Frame 中包含动态内容,因为 Farcaster 客户端会缓存它
- 不包含
fc:frame:state标签
在 Farcaster 上验证 Solana 地址
Farcaster 最近新增了对 Solana 地址的支持。这意味着开发者可以构建与用户 Solana 钱包交互的 Frames。该功能上线不到 24 小时,已有超过 9328 个不同的 Farcaster FID 验证了 Solana 地址。
用户可以通过以下步骤验证 Solana 地址:
请注意,目前唯一支持的钱包是 Phantom。
教程:创建一个铸造 cNFT 的 Frame
本文将创建一个 Frame,让拥有已验证 Solana 地址的用户可以铸造压缩 NFT(cNFT)。我们将使用 frames.js 初始化项目。这是一个用于构建和调试 Frames 的 Next.js 模板。我们将使用 Coinbase 的 OnchainKit,具体来说是其中的 Frame Kit,以便在 Farcaster 用户与 Frame 交互时获取其信息。然后,我们会将这些数据传给 Helius Mint API,把以下 cNFT 铸造到用户的 Solana 钱包:
设置 Frame
使用 frame.js 创建 Frame 非常简单,只需克隆入门模板。运行以下命令,并将 <name> 替换为你希望用来存放克隆模板的新文件夹名称:
npx degit github:framesjs/frames.js/examples/framesjs-starter#main <name>我决定将目录命名为 helius-frame,因此把 <name> 替换成了 <helius-frame>。接下来,进入新目录(例如 cd helius-frame),并通过 yarn install 命令安装所需依赖项。下载完成后,运行 yarn dev 启动开发服务器。在浏览器中访问 http://localhost:3000,你将看到以下页面:
点击 Debug 链接后,我们会进入 http://localhost:3000/debug:
这个页面非常实用,因为我们无需部署即可在本地测试 Frame。例如,可以登录以测试按钮、模拟其他用户,以及模拟 Hub 状态。Warpcaster 还提供了一款出色的 Frame 验证器工具。接下来,我们将仅出于个人偏好使用 Frame 验证器进行测试。
接下来,运行 yarn install 安装模板所需的相关依赖项。我们还需要运行 yarn install @coinbase/onchainkit 来安装 OnchainKit。
我们将按以下方式重新整理项目文件夹:
- 删除 examples 文件夹——这些示例虽然有助于了解如何构建 Frames,但对我们的目标并非必需。删除此文件夹可以减少项目冗余。不过,我强烈建议你查看其中的内容,以了解构建常见 Frames 的其他方式
- 在 app 目录中创建 api 文件夹。通过明确区分应用的前端和后端,这一步符合 Next.js 的 API 路由约定。然后,将 frames 文件夹及其内容(route.ts)移入新的 api 文件夹。
- 在 app 目录中创建 utils 文件夹。在这个新文件夹中创建 mint.ts 文件——我们将在这里编写 cNFT 铸造逻辑
现在终于可以开始构建 Frame 了!
编辑 Frame
layout.tsx
我们先处理 layout.tsx 文件。删除其中所有内容,并替换为以下代码:
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}RootLayout 组件采用极简方式,以统一的布局结构封装整个应用。它灵活且可复用。以这种极简布局为起点,开发者可以将此文件用作模板,根据需要构建和自定义自己的 Frames。
请注意,修改 layout.tsx 文件后,在本地使用调试器时会遇到问题。编辑 layout.tsx 并非必需。不过,我们这样做是为了在 page.tsx 中引入应用的元数据。作为替代方案,你也可以在此文件中引入元数据。
page.tsx
删除 page.tsx 的内容,并替换为以下代码:
import type { Metadata } from "next";
import { getFrameMetadata } from "@coinbase/onchainkit";
const baseURL = "https://helius-frame.vercel.app/";
const frameMetadata = getFrameMetadata({
image: {
src: `${baseURL}/default.jpg`,
aspectRatio: "1:1",
},
buttons: [
{
label: "Click to mint a cNFT on Solana!",
action: "post",
},
],
postUrl: `${baseURL}api/frames`,
});
export const metadata: Metadata = {
title: "Mint a cNFT on Solana",
description: "Mint a cNFT to your verified Solana address using the Helius Mint API",
openGraph: {
title: "Mint a cNFT on Solana",
description: "Mint a cNFT to your verified Solana address using the Helius Mint API",
images: ["https://helius-frame.vercel.app/default.jpg"],
},
other: {
...frameMetadata,
},
};
export default function Page() {
return (
Mint a cNFT on Farcaster using the Helius Mint API
Built by 0xIchigo
GitHub Repo
);
}以上代码设置了初始 Frame,它将托管在 Web 服务器的某个 URL 上。我的初始 Frame 使用 Vercel 托管,地址为 https://helius-frame.vercel.app/——本教程稍后会介绍如何使用 Vercel 部署。
首先,我们从 Next.js 导入 Metadata 类型,为此文件导出的 metadata 提供类型。我们还从 @coinbase/onchainkit 导入 getFrameMetadata。然后定义应用的基础 URL,以减少重复代码。
接着,我们调用 getFrameMetadata,为初始 Frame 配置元数据。这些元数据包括:
- 一张用作 Frame 视觉元素的图片(即 src),宽高比为 1:1
- 一个标有“点击在 Solana 上铸造 cNFT!”的按钮。点击后,它会向指定的
postUrl(即${baseURL}api/frames)触发 POST 请求,从而启动铸造流程
然后,我们导出一个 metadata 对象,其中包括页面标题和描述、OpenGraph 元数据(即 openGraph),以及展开到 other 属性中的、之前定义的 frameMetadata。
最后,page 定义并导出默认页面组件。我们返回一个简单的 h1,提示用户铸造 cNFT。这是因为 Farcaster 要求我们返回一些 HTML——主要交互由 Frames 管理,而不是直接访问网站本身。不过,如果用户点击进入网站,页面上仍然需要有内容。
mint.ts
将以下代码粘贴到 mint.ts 中:
const url = `Get a URL at dev.helius.xyz`;
export const mintCompressedNFT = async (address: string) => {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: "frames-sol-mint",
method: "mintCompressedNft",
params: {
name: "Solana Soldier",
symbol: "HELIUS",
owner: address,
description: "A decentralized warrior backed by Solana's most loved RPC Nodes, APIs, Webhooks, and Developer Tooling",
attributes: [
{
trait_type: "RPCs",
value: "Fast",
},
{
trait_type: "Company",
value: "Helius"
},
],
imageUrl: "https://shdw-drive.genesysgo.net/HmvVwYAy7cxWECLbu2cjWBSHysVoriTqgdugyePVg3rY/cnftmint.jpg",
externalUrl: "https://www.helius.dev/",
sellerFeeBasisPoints: 6900,
},
}),
});
const data = await response.json();
console.log(`data: ${JSON.stringify(data, null, 2)}`);
if (!response.ok) {
throw new Error(`The response was not okay - ${response.status}`);
}
if (!data.result || !data.result.assetId) {
throw new Error(`The response did not include the expected data.result.assetId field`);
}
return data.result;
};mintCompressedNFT 工具函数对应用至关重要,因为它会将预定义的 cNFT 铸造到指定地址。我们使用 Helius Mint API 创建一个简单、可复用的异步函数,用于封装 cNFT 铸造流程。我们必须提供 Helius General RPC URL,它被定义为 url。如果你还没有,请访问你的 Helius 控制面板,生成一个免费的 RPC URL,用作 url。
请注意,我的代码仓库使用了 SecureRPC URL——请小心避免泄露 API 密钥,并考虑运行代理。关于请求本身,你可以在此处查看请求模式的完整说明。
另请注意,我已经将 cNFT 的图片上传到 ShdwDrive。因为在撰写本文时,Farcaster 尚未添加完整的交易支持——只要提供签名者,Helius Mint API 就会负责将链下元数据上传到 Arweave。
该函数还包含完善的错误处理机制,确保铸造过程中出现的任何问题都能被捕获并报告。它也会记录响应数据,以便调试。
route.ts
在 api/frames/route.ts 中粘贴以下代码:
import { NextRequest, NextResponse} from "next/server";
import { getFrameMessage, getFrameHtmlResponse } from "@coinbase/onchainkit/frame";
import { mintCompressedNFT } from "../../utils/mint";
const baseURL = "https://helius-frame.vercel.app/";
export async function POST(req: NextRequest): Promise {
const body = await req.json();
// Validate the frame message and extract the user ID
const { isValid, message } = await getFrameMessage(body, {
neynarApiKey: "NEYNAR_ONCHAIN_KIT",
});
console.log(JSON.stringify(message, null, 2));
// Make sure they're following us before they can mint the cNFT
if (isValid && !message.following) {
return new NextResponse(getFrameHtmlResponse({
image: {
src: `${baseURL}/error.jpg`,
aspectRatio: "1:1",
},
buttons: [
{
label: "You need to be following to mint! Click to retry",
action: "post",
},
],
postUrl: `${baseURL}api/frames`,
}));
// Make sure they have a verified Solana address
} else if (isValid && message.interactor.verified_addresses.sol_addresses) {
const solanaAddresses = message.interactor.verified_addresses.sol_addresses;
console.log(`SOLANA ADDRESSES: ${solanaAddresses}`);
if (solanaAddresses.length !== 0) {
try {
console.log(`MINTING to ${solanaAddresses[0]}`);
// We take the first address address, in case there's more than one
const mintResult = await mintCompressedNFT(solanaAddresses[0]!);
return new NextResponse(getFrameHtmlResponse({
image: {
src: `${baseURL}/success.jpg`,
aspectRatio: "1:1",
},
buttons: [
{
label: "Success! View your cNFT on XRAY",
action: "link",
target: `https://xray.helius.xyz/token/${mintResult.assetId}?network=mainnet`
},
],
}));
} catch (e: any) {
console.log(`Minting failed: ${e}`);
return new NextResponse(getFrameHtmlResponse({
image: {
src: `${baseURL}/error.jpg`,
aspectRatio: "1:1",
},
buttons: [
{
label: "Minting failed. Click to retry",
action: "post",
},
],
postUrl: `${baseURL}api/frames`,
}));
}
} else {
console.log(`No Solana address found`);
return new NextResponse(getFrameHtmlResponse({
image: {
src: `${baseURL}/error.jpg`,
aspectRatio: "1:1",
},
buttons: [
{
label: "No Solana address found. Click to retry",
action: "post",
},
],
postUrl: `${baseURL}api/frames`
}));
}
} else {
return new NextResponse(getFrameHtmlResponse({
image: {
src: `${baseURL}/error.jpg`,
aspectRatio: "1:1",
},
buttons: [
{
label: "Invalid frame message. Click to retry",
action: "post",
},
],
postUrl: `${baseURL}api/frames`,
}));
}
}此文件是一个 Next.js API 路由,用于处理 Frame 的所有 POST 请求。文件首先从 next/server 导入 NextRequest 和 NextResponse,以处理传入请求并发送响应。我们还从 @coinbase/onchainkit/frame 导入 getFrameMessage 和 getFrameHtmlResponse,用于验证 Frame 消息并为 Frame 交互生成 HTML 响应。此外,我们还导入了 mintCompressedNFT 工具函数。
定义 baseURL 是为了减少重复代码。它用于构建资源和 API 端点的路径。
该文件导出 POST 函数。此函数会解析传入的请求正文,以提取 Frame 消息。我们使用 getFrameMessage 检查传入的 Frame 交互及消息的有效载荷是否有效。为此,我们传入 JSON 格式的请求正文以及默认 Neynar API 密钥(即 “NEYNAR_ONCHAIN_KIT”)。Neynar 是一个旨在简化 Farcaster 开发的开发者平台。getFrameMessage 返回一个 Promise,该对象会解析为 FrameValidationResponse。其定义如下:
export type FrameValidationResponse =
| { isValid: true; message: FrameValidationData }
| { isValid: false; message: undefined };因此,如果 Frame 交互有效,它会返回 FrameValidationData 类型的消息。其定义如下:
export interface FrameValidationData {
button: number; // Number of the button clicked
following: boolean; // Indicates if the viewer clicking the frame follows the cast author
input: string; // Text input from the viewer typing in the frame
interactor: {
fid: number; // Viewer Farcaster ID
custody_address: string; // Viewer custody address
verified_accounts: string[]; // Viewer account addresses
verified_addresses: {
eth_addresses: string[] | null;
sol_addresses: string[] | null;
};
};
liked: boolean; // Indicates if the viewer clicking the frame liked the cast
raw: NeynarFrameValidationInternalModel;
recasted: boolean; // Indicates if the viewer clicking the frame recasted the cast
valid: boolean; // Indicates if the frame is valid
}这些信息非常重要,其中包括用户的 Farcaster ID、用户是否拥有已验证的 Ethereum 或 Solana 地址,以及是否点赞了该 cast。在本教程中,我们要检查尝试铸造的用户是否关注了我的个人资料。如果已关注,我们就允许其铸造 cNFT。我们还要确保用户拥有已验证的 Solana 地址,以便将 cNFT 铸造到该地址。
首先,我们检查交互是否有效,以及用户是否关注了我的账户。如果没有,用户会看到错误消息和一个提示重试的按钮。然后,我们检查用户是否拥有已验证的 Solana 地址。如果存在一个或多个地址,就继续执行铸造流程。我们获取第一个已验证的 Solana 地址,并将其传给 mintCompressedNFT。如果成功,我们会返回表示成功的图片,以及一个用于在 Orb 上查看 cNFT 的链接。如果失败,则记录错误,并返回提示用户重试的消息。如果未找到已验证的 Solana 地址,我们会返回错误消息,说明缺少已验证的 Solana 地址,并提示用户重试。最后,如果 Frame 消息无效,我们会返回有关无效 Frame 的错误消息,并提示用户重试。
代码使用 getFrameHtmlResponse,根据铸造流程或验证检查的具体结果生成相应的 HTML 响应。其中包括为特定用户交互设置响应图片、宽高比和按钮。
推送到生产环境
代码准备就绪后,创建一个新的 GitHub 代码仓库,并将代码推送到其中。在命令行中执行以下命令,将代码推送到 GitHub:
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/<username>/<new repo name>.git
git branch -M main
git push -u origin main将代码推送到 GitHub 后,下一步是部署应用。Vercel 与 GitHub 无缝集成,可以简化整个部署流程,因此我们将使用 Vercel。具体步骤如下:
- 在 Vercel 网站上登录账户。如果你还没有账户,则需要创建一个——其 Hobby 套餐很慷慨(即免费),足以满足我们的需求
- 登录后,前往控制面板并点击 新建项目 按钮。Vercel 会提示你从 GitHub 导入项目。选择你在上一步中创建的代码仓库
- Vercel 会自动检测你使用的框架,并建议构建设置。Vercel 由 Next.js 的创建者打造,并提供一流支持,因此无需担心繁琐的配置过程
- 配置项目设置后,点击 部署 按钮。Vercel 会自动开始部署流程,构建项目并将其部署到唯一的 Vercel URL。
- 部署完成后,Vercel 会提供这个唯一 URL,用于访问上线后的应用。我的项目部署到了 https://helius-frame.vercel.app/——也就是 GitHub 代码仓库名称后接 .vercel.app。Vercel 还支持在项目设置中配置自定义域名
完成了!按照这些步骤,我们已将 Farcaster Frame 推送到 GitHub,并托管在 Vercel 上。你可以使用前面提到的测试工具(例如 Frames 验证器)测试 Frame 的功能。现在只差使用 Frame 的 URL 发布一个 cast!
总结
区块链与社交媒体的结合,显著推动了我们理解数字空间以及在其中互动的方式。Farcaster 和 Frames 的流行,将传统社交媒体平台上曾经静态的帖子变成了交互式体验。本文介绍了 Farcaster 和 Frames,并通过教程展示如何利用 Farcaster 最近新增的 Solana 支持,让用户只需点击一个按钮即可铸造 cNFT。无论你是希望在 Farcaster 上构建应用的开发者,还是想以新颖有趣的方式浏览社交媒体动态,可能性都无穷无尽。
如果你读到了这里,感谢你,anon!如果你想加入 Farcaster,请使用以下邀请链接!
请务必在下方输入你的电子邮件地址,以免错过 Solana 的任何最新动态。准备深入探索了吗?立即阅读 Helius 博客上的最新文章,继续你的 Solana 之旅。
其他资源
相关文章
订阅 Helius
及时了解 Solana 开发的最新动态,并在我们发布新内容时收到更新


