如何通过Solana Pay调用自定义Solana程序
本指南演示如何使用Solana Pay调用自定义Solana程序,超越常规支付场景。通过Next.js 13构建前端和后端API,生成QR码供钱包扫描调用自定义计数器程序。后端处理GET和POST请求,返回交易数据;前端实时显示计数器并监听链上变化。包含完整代码示例和步骤。
概述
Solana Pay 在 Solana 区块链上实现了快速、安全的支付通道。然而,鲜为人知的是,Solana Pay 背后的技术并不仅限于支付。本指南将向你展示如何使用 Solana Pay 调用自定义 Solana 程序。
你将完成的任务
创建一个 Next.js 13 应用程序,生成一个二维码,通过后端调用自定义 Solana 程序:
具体来说,你将:
- 创建一个新的 Next.js 项目。
- 使用 React 构建一个简单的用户界面。
- 使用 Next.js API 路由生成自定义程序交易。
- 渲染二维码供用户扫码并签署交易。
- 使用 Solana Websocket 监听程序,当程序被调用时更新界面中的计数器。
你需要准备的条件
本高级指南会涉及 Solana 开发中的多个概念。在开始之前,请确认你已满足以下前提条件。
- 已安装 Nodejs(版本 16.15 或更高)
- 具备 TypeScript 经验,并已安装 ts-node
- 有 Next.js 和 React 开发经验
- 了解 Solana Pay 会很有帮助:请回顾我们的 Solana Pay 入门指南 和 将 Solana Pay 添加到 dApp 的指南
- 一个支持 Solana Pay 的手机钱包(例如 Solflare)(注意:目前 Phantom 在 Android 上存在已知问题,如果你使用 Android 设备,我们建议改用 Solflare。问题修复后我们会更新此说明。)
- Ngrok 或其他用于将本地开发服务器暴露到互联网的服务(如果想将应用部署到云端,也可以使用 Vercel 等服务)
- 了解 Solana Websocket 订阅(请查看我们的指南:如何创建 Solana WebSocket 订阅)
- 了解 Solana 程序(请查看我们的指南:Solana 中你的第一个 Anchor 程序)
- 了解如何反序列化 Solana 账户数据(请查看我们的指南:如何反序列化 Solana 上的账户数据)
创建一个新的 Next.js 项目
首先,打开终端并运行以下命令来创建一个新的 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 端点来连接到 Solana devnet 以组装交易。
使用你的 Quicknode 端点连接到 Solana 集群
要在 Solana 上进行开发,你需要一个 API 端点来连接网络。欢迎你使用公共节点或自行部署和管理基础设施;但如果你希望获得 8 倍更快的响应速度,可以把繁重的工作交给我们。
了解为什么超过 50% 的 Solana 项目选择 Quicknode,并在此开始免费试用。我们将使用 Solana Devnet 端点。
复制 HTTP Provider 链接:
做得好。你已准备好开始构建应用程序。如果在设置时遇到困难或遇到任何问题,请通过 Discord 联系我们。
创建一个自定义 Solana 程序
在本演示中,我们将使用一个简单的计数器递增程序。最终,我们将通过二维码调用 increment 指令来调用该程序。我们已为你创建了一个可在本指南中使用的程序(Devnet yV5T4jugYYqkPfA2REktXugfJ3HvmvRLEw7JxuB2TUf)。该程序包含一个名为 increment 的函数,每次调用时计数器会增加 1。
关于该程序,需要了解的重要信息是:它创建了一个单一的 PDA,用于存储 count 状态。有关 PDA 的更多信息,请查看我们的指南:如何使用 PDA。 以下是我们程序的账户结构:
##[account]
pub struct Counter {
pub count: u64,
}
如果你想查看该程序的源代码或创建自己的版本,可以在 Solana Playground 上查看。
创建你的后端
在构建后端之前,我们先了解使用 Solana Pay 发送自定义交易所需的步骤。以下是使用 Solana Pay 发送自定义交易的规范和流程总结:
- 用户扫描前端的二维码。
- 用户的钱包向后端发送
GET请求。 - 后端收到请求,返回
label和iconURL,用于在钱包中向用户展示。 - 用户的钱包向后端发送
POST请求,其中包含用户的accountid(公钥字符串)* - 后端收到请求,组装一个 Solana 交易,该交易包含自定义程序上的
increment指令。 - 后端返回序列化后的交易。
- 用户在钱包中批准并签署交易。
- 用户的钱包将已签名的交易发送到集群进行处理。
简而言之,后端必须处理 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 Demo - Increment Counter`;
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: 'Method Not Allowed' });
}
}
我们在这里所做的是:
- 定义了一些将在应用程序中使用的关键变量。我们还从 Solana Pay、Solana-web3.js 和 crypto(一个 NodeJS 库) 导入了必要的包。
- 为演示目的定义了一些常量——你可能需要根据应用程序的需求使某些值可变(例如
message和label)。请确保将quicknodeEndpoint替换为你的 Quicknode 端点。如果你使用的程序与我们提供的不同,你可能需要修改counterSeed和functionName常量。 - 定义了我们的 API 处理程序。我们将使用一个处理程序来处理
GET和POST请求。使用req.method属性来判断请求类型。如果是GET请求,则响应label和iconURL——由于我们已经将这些定义在常量中,可以直接返回,调用res.status(200).json({ label, icon })。如果是POST请求,我们将生成一个交易。如果是其他请求方法,则返回错误。你可以为每个操作使用单独的处理程序,但为简单起见,我们使用一个处理程序。 - 定义了一个
getInstructionData函数,用于为increment指令生成数据。我们使用哈希函数生成序列化数据,然后将其传递到交易中。这是 Anchor 序列化账户指令的方式——你可以在此处查看源代码。
处理 POST 请求——生成交易
当钱包向后端发送 POST 请求时,我们需要生成一个交易。我们将使用钱包发送给我们的 account id(公钥)来创建交易。首先,我们需要确保钱包确实传递了 account。将以下代码添加到 POST 处理程序中:
if (req.method === 'POST') {
try {
const account: string = req.body?.account;
if (!account) res.status(400).json({ error: 'Missing account field' });
const transaction = await generateTx(account);
res.status(200).send({ transaction, message });
} catch (error) {
console.error('Error:', error);
res.status(500).json({ error: 'Internal Server 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. 构建调用 increment 函数的交易
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函数。 - 构建交易:创建一个新的 Transaction 并添加一个新的 TransactionInstruction。将
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 Demo - Increment Counter"
}
太棒了!你刚刚创建了一个 API 端点,它生成一笔交易,调用我们的自定义程序,并返回给钱包。现在来构建前端。
创建前端
后端已经设置好,现在创建前端。前端是一个简单的 React 应用,它:
- 在页面加载时生成二维码,触发扫描钱包向后端发送
GET请求 - 获取、反序列化并显示我们程序的账户数据(count)
- 创建程序账户数据的订阅,以实时更新计数
打开 /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: 添加 counter 接口
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 Demo: Quick Count</title>
<meta name="description" content="Quicknode Guide: 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 Demo: QuickCount</h1>
<h1 className='text-xl font-semibold'>Count: {count}</h1>
</div>
{qrCode && (
<Image
src={qrCode}
style={{ position: "relative", background: "white" }}
alt="QR Code"
width={200}
height={200}
priority
/>
)}
</main>
</>
);
}
这是一个很好的起点。我们来解释一下这里的内容:
- 从 React、Next 和 Solana 导入必要的依赖项。大部分内容与我们之前的将 Solana Pay 添加到 dApp 的指南相同,这里不再赘述。我们还从 Solana 的 buffer layout 库中添加了几个导入,用于反序列化程序的账户数据。
- 定义了一些常量(与后端相似):
quickNodeEndpoint、connection、programId和counterPda。用于连接程序并获取账户数据。
使用 .env 文件
注意,为简单起见,我们硬编码了端点。在生产应用中,你应该使用环境变量来存储端点。查看 Next.js 文档 了解有关使用环境变量的更多信息。
- 定义了一个
Home组件,用于渲染 UI。UI 会显示计数和二维码(如果已定义,但目前尚未定义)。我们还创建了一个useEffectHook,在组件挂载时运行,用于获取账户数据并生成二维码。
实现二维码生成器
我们将生成一个 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。这样 API 在localhost和部署的应用程序上都能正常工作。 - 定义
label和message,在本演示中作为占位符。 - 使用
@solana/pay中的encodeURL函数创建一个 URL,该 URL 会触发扫描钱包向后端发送GET请求。将apiUrl作为new URL传入。 - 最后,将二维码渲染为 base64 字符串,并存储在
qrCode状态变量中。
现在有了生成二维码的函数,在组件挂载时调用它。将以下代码添加到 useEffect Hook 中:
useEffect(() => {
generateQr();
}, []);
这会在页面加载时渲染二维码。如果你现在运行应用程序,应该会看到一个二维码!下面是示例效果:

获取并更新计数
让我们获取程序的计数并显示在前端,以确保程序调用正常工作。前端已经在 Home 组件中包含 <h1>Count: {count}</h1>,因此我们只需获取计数数据并反序列化账户。首先定义账户结构。要反序列化数据,需要知道链上程序中的账户结构——如果你还记得,我们定义了一个 u64 类型的 count 和一个 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('Account not found');
const deserialized = CountLayout.decode(data);
return deserialized.count.toString();
}
我们从 PDA 获取账户数据,然后使用 CountLayout 反序列化数据,并将计数作为字符串返回。现在在 useEffect Hook 中调用这个函数。将以下代码添加到 useEffect Hook 中:
useEffect(() => {
generateQr();
fetchCount().then(setCount);
const subscribe = connection.onProgramAccountChange(
programId,
() => fetchCount().then(setCount),
'finalized'
)
return () => {
connection.removeProgramAccountChangeListener(subscribe);
}
}, []);
这里在组件挂载时调用 fetchCount 函数,并设置 count 状态变量。这样在页面渲染时就会显示当前计数。我们还通过 onProgramAccountChange 创建了对程序账户变更事件的订阅,以便在程序被调用时更新计数。如果你需要复习 Solana WebSocket 方法,请查看该指南。同时返回了一个函数,用于在组件卸载时取消订阅。
太棒了!回顾一下到目前为止构建的内容。我们:
- 使用 Anchor 创建并部署了一个 Solana 计数器程序
- 构建了一个 Next.js 前端,它:
- 获取我们的程序计数器
- 订阅程序计数器的变更
- 生成并显示一个二维码,可由支持 Solana Pay 的钱包应用扫描
- 创建了一个后端 API 端点,支持 Solana Pay 的钱包应用在扫描二维码时可以调用它。API 端点遵循 Solana Pay API 规范,并将发送一个调用计数器的交易给用户钱包进行签名。
现在,只需要测试一下。
测试应用
打开一个新的终端窗口,运行以下命令启动 Next.js 开发服务器:
npm run dev
## 或
yarn dev
这会启动 Next.js 开发服务器,默认端口 3000。在浏览器中导航到 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 并点击“Visit Page”。你应该会被重定向到一个 ngrok.io 页面,运行着你的本地开发服务器(Next.js 应用程序)。你应该看到最终的应用程序正在运行,并带有有效的二维码和更新的计数:

注意:上面的二维码无效,因为它指向一个不再活跃的 ngrok 后端。
现在应用程序运行起来,我们来测试一下。打开你支持 Solana Pay 的钱包应用(目前 Phantom 在 Android 上存在已知问题——问题修复后我们会更新),并确保网络设置为 Devnet。然后扫描二维码。你应该会收到提示,要求签署一个调用计数器程序的交易:

一旦你批准交易并且网络最终确认,你应该会看到应用程序中的计数增加了!
如果你想查看完整的代码,请在我们的 GitHub 页面此处查看。
总结
干得漂亮!虽然工作量很大,但你已经成功构建了一个 Solana Pay 与自定义 Solana 程序之间的集成。这其中的可能性是无限的。我们迫不及待地想看到你的创造!如果你缺乏灵感,可以看看 Solana 基金会构建的这个有趣的拔河游戏。
如果你需要帮助或想分享你使用 Solana Pay 构建的内容,请通过 Discord 或 Twitter 告诉我们。
我们 ❤️ 反馈!
如果你对本指南有任何反馈或问题,请告诉我们。我们期待你的反馈!
资源
- Solana Pay 主页
- Solana Pay GitHub
- Solana Pay 文档
- 使用 Solana Pay 的拔河游戏
- Next.js 主页
- Next.js 部署
- Next.js API 路由
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
来源: