这篇文章详细介绍了如何使用 Solana Pay 与自定义 Solana 程序集成,采用 Next.js 框架构建应用。用户将学习如何生成二维码以用作与后端交互,并运行自定义交易指令,通过使用 Solana 的 WebSocket 订阅机制实现实时数据更新。文章提供了清晰的结构和丰富的技术细节,适合具有相关经验的开发者阅读。
Solana Pay 在 Solana 区块链上启用快速、安全的支付通道。然而,一个鲜为人知的事实是,Solana Pay 背后的技术可以用于的不仅仅是支付。本指南将向你展示如何使用 Solana Pay 调用自定义的 Solana 程序。
创建一个 Next.js 13 应用,生成一个用于通过你的后端调用自定义 Solana 程序的二维码:
来源: Solana Pay 文档
具体来说,你将:
本高级指南将涉及构建 Solana 所需的一些概念。在继续之前,请先审核以下要求。
要开始,打开终端并运行以下命令以创建一个新的 Next.js 项目:
npx create-next-app@latest solana-pay-beyond
### 或
yarn create next-app solana-pay-beyond
系统会询问你大约 5 个问题,关于如何配置你的项目。对于本指南,你可以接受默认值。这将为你的项目创建一个名为 solana-pay-beyond
的新目录,并用最新版本的 Next.js 初始化它。导航到你的新项目目录:
cd solana-pay-beyond
运行 yarn dev
启动开发服务器,并确保安装成功。这将会在默认浏览器(通常是 localhost:3000)中打开项目。你应该看到默认的 Next.js 登陆页面:
干得不错。关闭浏览器窗口并通过在终端中按 Ctrl + C
(或 Mac 上的 Cmd + C
)停止开发服务器。
现在我们需要安装 Solana-web3.js 和 Solana Pay 包。运行以下命令:
npm install @solana/web3.js@1 @solana/pay
### 或
yarn add @solana/web3.js@1 @solana/pay
最后,你需要一个连接到 Solana devnet 的 Solana 端点,以组装交易。
要在 Solana 上构建,你需要一个 API 端点以连接到网络。你可以使用公共节点或部署并管理自己的基础设施;但是,如果你想要 8 倍的响应速度,可以将重任留给我们。
查看为什么超过 50% 的 Solana 项目选择 QuickNode,并在 这里 注册一个免费账户。我们将使用 Solana Devnet 端点。
复制 HTTP 提供者链接:
干得不错。你已准备好开始构建应用程序。如果你在设置过程中需要帮助或遇到任何问题,请在 Discord 上与我们联系。
在这个演示中,我们将使用一个简单的程序来递增计数器。我们最终会通过调用带有我们的二维码的 increment
指令来调用这个程序。我们已经为你创建了一个程序可供本指南使用( Devnet yV5T4jugYYqkPfA2REktXugfJ3HvmvRLEw7JxuB2TUf
)。该程序包括一个名为 increment
的函数,每次调用时将计数器增加 1。
关于我们的程序,需要知道的重要内容是它创建了一个 PDA,存储 count
状态。有关 PDA 的更多信息,请查看我们的 指南:如何使用 PDA。 这是我们的账户结构:
##[account]
pub struct Counter {
pub count: u64,
}
如果你想查看此程序的源代码或创建自己的版本,请在 Solana Playground 上查看。
在构建我们的后端之前,先看一下使用 Solana Pay 发送自定义交易所需的步骤。以下是 Solana Pay 规范和发送自定义交易的流程总结:
GET
请求。label
和 icon
URL,以向用户的钱包显示。POST
请求,带有用户的 account
id(公钥作为字符串)*increment
指令的 Solana 交易。简而言之,我们的后端必须对 GET
请求返回带有 label
和 icon
URL 的响应,并对 POST
请求返回序列化的交易。
为此演示,我们将使用 Next.js API 路由。API 路由是创建应用程序后端的绝佳方式,而无需设置独立的服务器。“在 pages/api
文件夹中的任何文件映射到 /api/*
,并将被视为 API 端点,而不是页面。”你可以在 这里 阅读有关 Next.js API 路由的更多信息。
导航到 pages/api
并删除 hello.ts
。我们将用自己的 API 路由替换此文件。创建一个名为 pay.ts
的新文件,并添加以下代码:
import { NextApiRequest, NextApiResponse } from 'next';
import { Connection, PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js';
import crypto from 'crypto';
// 常量
const programId = new PublicKey('yV5T4jugYYqkPfA2REktXugfJ3HvmvRLEw7JxuB2TUf'); // 👈 你可以使用此程序或创建/使用自己的程序
const counterSeed = 'counter'; // 这是用于生成计数器账户的种子(如果你使用其他程序,则可能不同)
const functionName = 'increment'; // 这是我们 Anchor 指令的名称(如果你使用不同的程序,则可能不同)
const message = `QuickNode 演示 - 递增计数器`;
const quickNodeEndpoint = 'https://example.solana-devnet.quiknode.pro/0123456/'; // 👈 使用你的 devnet 端点替换
const connection = new Connection(quickNodeEndpoint, 'confirmed');
const label = 'QuickCount +1';
const icon = 'https://www.arweave.net/wtjT0OwnRfwRuUhe9WXzSzGMUCDlmIX7rh8zqbapzno?ext=png';
// 生成特定 Anchor 指令数据的实用程序函数
function getInstructionData(instructionName: string) {
return Buffer.from(
crypto.createHash('sha256').update(`global:${instructionName}`).digest().subarray(0, 8)
);
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
// POST 代码将在这里
} else if (req.method === 'GET') {
res.status(200).json({ label, icon });
} else {
res.status(405).json({ error: '方法不被允许' });
}
}
我们在这里做的是:
message
和 label
)。确保用你的 QuickNode 端点更新 quicknodeEndpoint
。如果你使用与我们提供的程序不同的程序,则可能需要更改 counterSeed
和 functionName
常量。GET
和 POST
请求。我们将使用 req.method
属性来确定采取何种操作。如果请求方法是 GET
,则以 label
和 icon
URL 响应——由于我们在常量中定义了这些,因此我们可以直接返回,调用 res.status(200).json({ label, icon })
。如果请求方法是 POST
,我们将生成交易。如果请求方法是其他任何方法,我们将返回错误。你可以为每个动作使用单独的处理程序,但为了简单起见,我们将使用一个处理程序。getInstructionData
函数。该函数将为我们的 increment
指令生成数据。我们使用哈希函数生成可以传入我们的 Transaction 的序列化数据。这是 Anchor 序列化账户指令的方式——你可以在 这里 查看源代码。当钱包向我们的后端发送一个 POST
请求时,我们需要生成一个交易。我们将使用钱包发给我们的 account
id(公钥)来创建交易。首先,我们需要确保钱包实际传入了 account
。将以下代码添加到 POST
处理程序中:
if (req.method === 'POST') {
try {
const account: string = req.body?.account;
if (!account) res.status(400).json({ error: '缺少账户字段' });
const transaction = await generateTx(account);
res.status(200).send({ transaction, message });
} catch (error) {
console.error('错误:', error);
res.status(500).json({ error: '内部服务器错误' });
}
}
我们在这里做的是:
account
字段。如果没有,我们将返回 400
错误。account
字段,我们将调用一个新函数 generateTx
,并将 account
作为参数传递。该函数将在下一个步骤中生成一个交易,递增计数器(我们将接着构建它)。transaction
和一条 message
(在我们的常量中定义)到钱包。钱包将会向用户显示该消息并要求他们确认交易。现在让我们创建 generateTx
函数。将以下代码添加到 pay.ts
,在处理程序下方:
async function generateTx(account: string) {
// 1. 获取计数器 PDA
const [counterPda] = PublicKey.findProgramAddressSync([Buffer.from(counterSeed)], programId);
// 2. 使用函数选择器创建数据缓冲区
const data = getInstructionData(functionName);
// 3. 构建调用增加函数的交易
const tx = new Transaction();
const incrementIx = new TransactionInstruction({
keys: [\
{ pubkey: counterPda, isWritable: true, isSigner: false },\
],
programId: programId,
data
});
// 4. 设置最新的区块哈希并设置手续费付款人
const latestBlockhash = await connection.getLatestBlockhash();
tx.feePayer = new PublicKey(account);
tx.recentBlockhash = latestBlockhash.blockhash;
tx.add(incrementIx);
// 5. 序列化交易
const serializedTransaction = tx.serialize({
verifySignatures: false,
requireAllSignatures: false,
});
// 6. 将交易数据编码为 base64
const base64Transaction = serializedTransaction.toString('base64');
return base64Transaction;
}
让我们走过我们在这里做的事情:
counterSeed
和 programId
生成了计数器 PDA。我们必须将此账户传入我们 increment
函数的交易指令。increment
函数生成数据缓冲区。我们使用之前定义的 getInstructionData
函数。counterPda
(作为可写的非付款人账户)和步骤 1 和 2 中生成的 data
。我们还传入在常量中定义的 programId
。注意:如果你使用自己的程序,你需要根据你的程序定义的上下文更新这些值。verifySignatures
和 requireAllSignatures
设为 false
,因为我们不对交易进行签名。我们将让钱包处理该操作。base64
(Base64 是一种常见的二进制数据编码格式),并将其返回到钱包。干得不错!你刚刚创建了一个生成将递增计数器的交易的函数。你的后端现在已经准备好接受来自钱包的请求。让我们测试一下!
运行以下命令启动服务器:
npm run dev
## 或
yarn dev
然后在一个单独的终端窗口中,运行以下 cURL 脚本以向 /api/pay
端点发出 GET
请求:
curl -X GET http://localhost:3000/api/pay
这应返回你在常量中定义的 label
和 icon
。现在让我们测试 POST
请求。运行以下 cURL 脚本向 /api/pay
端点发出 POST
请求:
curl -X POST "http://localhost:3000/api/pay" \
-H "Content-Type: application/json" \
-d '{"account": "YOUR_WALLET_ADDRESS"}'
你应该收到的响应类似于以下内容:
{
"transaction":"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDWvArSV39ujMKeO06xNO5Sx4ql4HJrmyWxnQjubHQp0iDxxzKuMff4tsV5PtxlzfcnR+CW+QUuiF+PqTIV/uDQ54RxxfTuGSHXAe+/I1AVzHOi5+zqX/ntgsd/DMy3V0VsyJ9ZUQHHexample/ZfFplpKKLcl3bpmiHJ0DTRUBAgexample",
"message":"QuickNode 演示 - 递增计数器"
}
干得不错!你刚刚创建了一个 API 端点,该端点生成对我们客户程序的交易并返回给钱包。现在让我们构建前端。
现在我们的后端已经设置好,让我们创建一个前端。前端将是一个简单的 React 应用:
GET
请求打开 /pages/index.tsx
并用以下内容替换默认内容:
import Head from 'next/head';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { createQR, encodeURL } from '@solana/pay';
import { Connection, PublicKey } from '@solana/web3.js';
import { u64 } from '@solana/buffer-layout-utils';
import { struct } from '@solana/buffer-layout';
const quickNodeEndpoint = 'https://example.solana-devnet.quiknode.pro/0123456/'; // 👈 使用你的 devnet 端点替换
const connection = new Connection(quickNodeEndpoint, 'confirmed');
const programId = new PublicKey('yV5T4jugYYqkPfA2REktXugfJ3HvmvRLEw7JxuB2TUf');
const counterSeed = 'counter';
const [counterPda] = PublicKey.findProgramAddressSync([Buffer.from(counterSeed)], programId);
// TODO: 添加计数器接口
export default function Home() {
const [qrCode, setQrCode] = useState<string>();
const [count, setCount] = useState<string>('');
useEffect(() => {
// TODO: 调用二维码生成
}, []);
const generateQr = async () => {
// TODO: 添加二维码生成
}
return (
<>
<Head>
<title>QuickNode Solana Pay 演示:Quick Count</title>
<meta name="description" content="QuickNode 指南:Solana Pay" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
<h1 className='text-2xl font-semibold'>Solana Pay 演示:QuickCount</h1>
<h1 className='text-xl font-semibold'>计数:{count}</h1>
</div>
{qrCode && (
<Image
src={qrCode}
style={{ position: "relative", background: "white" }}
alt="二维码"
width={200}
height={200}
priority
/>
)}
</main>
</>
);
}
这为我们工作提供了一个很好的起点。让我们走过这里的内容:
quickNodeEndpoint
、connection
、programId
和 counterPda
。我们将这些用于连接到我们的程序并获取账户数据。使用 .env 文件
请注意,我们在这里哈德编码了端点以简化操作。在生产应用中,你应该使用环境变量来存储你的端点。查看 Next.js 文档 以了解有关使用环境变量的更多信息。
Home
组件,用于渲染我们的 UI。UI 将显示计数和二维码(尽管我们尚未定义它们)。我们还创建了一个 useEffect 钩子,它将在组件挂载时运行。我们将使用此钩子来获取账户数据并生成二维码。我们将生成一个二维码作为 base64 字符串,并将其存储在 qrCode
状态变量中,以便将其传递给我们的 Image
组件。首先,让我们构建我们的 generateQr
函数。在 generateQr
函数中添加以下代码:
const generateQr = async () => {
const apiUrl = `${window.location.protocol}/${window.location.host}/api/pay`;
const label = 'label';
const message = 'message';
const url = encodeURL({ link: new URL(apiUrl), label, message });
const qr = createQR(url);
const qrBlob = await qr.getRawData('png');
if (!qrBlob) return;
const reader = new FileReader();
reader.onload = (event) => {
if (typeof event.target?.result === 'string') {
setQrCode(event.target.result);
}
};
reader.readAsDataURL(qrBlob);
}
让我们深入了解此函数所做的事情:
apiUrl
,这是我们后端 API 端点的 URL。我们使用 window.location
对象获取当前页面的协议和主机。然后将 /api/pay
附加到 URL 的末尾。这将允许我们的 API 在 localhost
和已部署的应用上有效(而不会硬编码 URL)。label
和 message
,实际上是此演示的占位符。@solana/pay
的 encodeURL
函数创建一个将触发扫描钱包对我们的后端发出 GET
请求的 URL。我们将 apiUrl
作为 new URL
传递。qrCode
状态变量中。现在我们有了生成二维码的函数,让我们在组件挂载时调用它。在 useEffect
钩子中添加以下代码:
useEffect(() => {
generateQr();
}, []);
这应该会在页面加载时渲染我们的二维码。如果你现在运行应用,你应该能看到二维码!以下是它应看起来的例子:
让我们抓取并显示程序的计数,以确保我们的对程序的调用正常工作。我们的前端已经在 Home
组件中包含了 <h1>Count: {count}</h1>
,所以我们只需要获取计数数据并反序列化账户即可。首先,让我们定义账户结构。为了反序列化我们的数据,我们需要知道我们链上程序结构的账户模式——如果你记得,使用的是一个 u64 计数和一个 8 字节的鉴别标志(用于所有 Anchor 账户)。我们可以使用 @solana/buffer-layout
库定义我们的账户结构。在 Home
组件上方添加以下代码:
interface Counter {
discriminator: bigint;
count: bigint;
}
const CountLayout = struct<Counter>([\
u64('discriminator'),\
u64('count'),\
]);
如果你需要有关如何反序列化 Solana 账户数据的复习,请查看 这份指南。简而言之,我们所做的就是定义我们的数据模式,并告知我们期望看到两个不同的 8 字节值,使用 u64 布局。
现在我们已经定义了账户结构,让我们获取账户数据并进行反序列化。创建一个名为 fetchCount
的新函数,并在 Home
组件中位于 CountLayout
定义之后添加以下代码:
async function fetchCount() {
let { data } = await connection.getAccountInfo(counterPda) || {};
if (!data) throw new Error('账户未找到');
const deserialized = CountLayout.decode(data);
return deserialized.count.toString();
}
我们实际上是从我们的 PDA 获取账户数据,然后使用 CountLayout
进行反序列化。我们随后以字符串形式返回计数值。现在让我们在我们的 useEffect
钩子中调用此函数。添加以下代码到 useEffect
钩子中:
useEffect(() => {
generateQr();
fetchCount().then(setCount);
const subscribe = connection.onProgramAccountChange(
programId,
() => fetchCount().then(setCount),
'finalized'
)
return () => {
connection.removeProgramAccountChangeListener(subscribe);
}
}, []);
在这里,我们在挂载时调用我们的 fetchCount
函数,并设置 count
状态变量。这应该会在页面渲染时为我们提供当前计数。我们还创建了一个对程序账户改变事件的订阅,以便在我们的程序被调用时使用 onProgramAccountChange
更新计数。如果你需要有关 Solana WebSocket 方法的复习,请查看 这份指南。我们还返回了一个函数,用于在组件卸载时取消对程序账户更改事件的订阅。
太棒了,让我们回顾一下我们到目前为止所构建的内容。我们已经:
现在,我们所需要做的就是测试一下。
打开一个新终端窗口并运行以下命令以启动 Next.js 开发服务器:
npm run dev
## 或
yarn dev
这将在 3000 端口上启动 Next.js 开发服务器。导航至 http://localhost:3000
在浏览器中查看应用。你应该会看到一个二维码和当前计数。不幸的是,由于我们的应用在 localhost
上运行,我们的钱包应用在另一台设备上将无法访问我们的 API 端点。我们需要将应用部署到公共 URL 以解决此问题。如果你有兴趣,可以将项目发布到类似 Vercel 或 Netlify 的服务(只需确保像我们之前提到的那样保护你的端点)。不过,对于本指南的目的,我们将使用 ngrok,一个允许你将本地开发服务器公开到互联网的工具。在安装 ngrok 后,你需要按照说明创建帐户并注册 API 密钥。完成此操作后,在终端中运行以下命令:
ngrok http 3000
你应该会看到类似以下内容的消息:
ngrok (Ctrl+C to quit)
Session Status online
Account your@email.com (Plan: Free)
Version 3.2.2
Region United States (us)
Latency -
Web Interface http://127.0.0.1:xxxx
Forwarding https://wxyz-00-123-456-789.ngrok.io -> http://loc
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
按照转发的 URL,并单击“访问页面”。你应该会被指向正在运行你本地开发服务器(NextJS 应用)的 ngrok.io
页面。你应该看到你的最终应用正在运行,显示有效的二维码和更新的计数:
注意:上面的二维码无效,因为它指向一个已经不再活跃的 ngrok 后端。
现在我们有了正在运行的应用,让我们测试一下。打开你的 Solana Pay 兼容的钱包应用(目前,Android 上的 Phantom 存在已知问题——我们会在修复后更新此信息),确保网络设置为 Devnet
。然后,扫描二维码。你应该会被提示签署一笔将调用我们的计数器程序的交易:
一旦你批准了交易并且网络完成了确认,你应该会在你的应用中看到计数器增加!
如果你想查看我们完整的代码,请查看我们的 GitHub 页面 这里。
干得不错!这是一项艰巨的工作,但你成功地构建了一个 Solana Pay 与自定义 Solana 程序之间的集成。这种集成的可能性是无穷无尽的。我们迫不及待地想看到你所想出的东西!如果你需要灵感,可以查看 Solana 基金会构建的这个有趣的 拔河游戏。
如果你需要帮助或想与我们分享你所构建的内容,请在 Discord 或 Twitter 上告诉我们。
如果你对本指南有任何反馈或问题,请告诉我们。我们很想听到你的意见!
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!