如何在网页应用中用 Solana Pay 构建支付门户
本文详细介绍了如何在Next.js 13和React应用中集成Solana Pay支付功能,通过创建API后端生成支付请求URL,生成QR码供用户扫描支付,并验证交易是否成功。文章包含完整的代码示例和测试步骤。
概述
你是否拥有一个在线商店,并希望添加由 Solana 提供支持的支付功能?Solana Pay 是一个基于 Solana 区块链的快速、易用且安全的支付解决方案。在本分步指南中,我们将向你展示如何利用 Solana Pay 通过二维码来接受支付。你将能够为客户的订单生成自定义二维码,客户扫描后即可完成结账!
你将完成什么
使用 Next.js 13 和 React 创建一个带有二维码的 Solana Pay 支付门户:
你的浏览器不支持视频标签。
我们将按以下步骤操作:
- 创建一个新的 Next.js 项目。
- 使用 React 构建一个简单的 UI。
- 使用 Next API 路由来创建一个后端终端,用于生成支付请求并验证支付成功。
- 为支付请求渲染二维码。
- 使用你手机上的 Phantom 钱包进行测试!
你需要准备什么
- 已安装 Nodejs(版本 16.15 或更高)
- 具备 TypeScript 经验并已安装 ts-node
- 具备 Next.js 和 React 的开发经验
- 了解 Solana Pay 会有帮助:请查看我们的 Solana Pay 入门指南
- 一个支持 Solana Pay 的手机钱包(例如 Phantom)
创建一个新的 Next.js 项目
首先,打开终端并运行以下命令来创建一个新的 Next.js 项目:
npx create-next-app@latest solana-pay-store
### 或
yarn create next-app solana-pay-store
系统会询问大约 5 个关于项目配置的问题。对于本指南,你可以接受所有默认值。这将在你的项目目录中创建一个新的 solana-pay-store 文件夹,并使用最新版本的 Next.js 进行初始化。进入你的新项目目录:
cd solana-pay-store
运行 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 网络,以便在链上验证支付。
使用你的 QuickNode 端点连接到 Solana 集群
要在 Solana 上进行开发,你需要一个 API 端点来连接网络。你可以使用公共节点,也可以部署和管理自己的基础设施;但如果你希望获得 8 倍的响应速度,你可以把繁重的工作交给我们。
了解为什么 Solana 上超过 50% 的项目选择 QuickNode,并开始你的免费试用 这里。我们将使用一个 Solana Mainnet 端点。
复制 HTTP Provider 链接:
做得好。你已经准备好开始构建你的应用程序了。如果在设置过程中遇到问题或在本指南中遇到其他问题,请通过 Discord 联系我们。
创建后端
在本演示中,我们将使用 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, Keypair, PublicKey } from '@solana/web3.js';
import { encodeURL, findReference, validateTransfer } from '@solana/pay';
import BigNumber from 'bignumber.js';
// 常量
const myWallet = 'DemoKMZWkk483hX4mUrcJoo3zVvsKhm8XXs28TuwZw9H'; // 替换为你的钱包地址(这是支付的目标地址)
const recipient = new PublicKey(myWallet);
const amount = new BigNumber(0.0001); // 0.0001 SOL
const label = 'Quicknode 指南商店';
const memo = 'QN Solana Pay 演示公共备注';
const quicknodeEndpoint = 'https://example.solana-devnet.quiknode.pro/123456/'; // 替换为你的 QuickNode 端点
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// 处理生成支付请求
if (req.method === 'POST') {
// 处理验证支付请求
} else if (req.method === 'GET') {
// 处理无效请求
} else {
res.status(405).json({ error: '方法不允许' });
}
}
我们在这里定义了一些在整个应用程序中使用的关键变量。我们还从 Solana Pay 和 Solana-web3.js 导入了必要的包。请注意,我们在演示中定义了一些常量——你可能需要根据应用程序的需求使其中一些值可变(例如 amount、memo 或 recipient)。确保将 quicknodeEndpoint 更新为你的 QuickNode 端点,并将 myWallet 更新为你的公钥(这是支付的目标地址——如果错误,支付 URL 会将支付导向错误的地址)。
我们还定义了 API 处理器。我们将使用一个处理器来处理生成支付请求和验证支付请求。我们使用 req.method 属性来决定执行哪个操作。如果请求方法是 POST,我们将生成一个支付请求。如果请求方法是 GET,我们将验证一个支付请求。如果请求方法是其他任何内容,我们将返回一个错误。你也可以为每个操作使用单独的处理器,但为了简单起见,我们将使用一个处理器。
生成支付请求
我们将使用我们在 Solana Pay 入门指南 中创建的一些工具来开始。在 pay.ts 的顶层,handler 函数之前添加一个 generateUrl 函数。这将生成一个 Solana 支付请求 URL,我们可以用它来生成二维码,供客户扫描并使用 Solana Pay 支付。
async function generateUrl(
recipient: PublicKey,
amount: BigNumber,
reference: PublicKey,
label: string,
message: string,
memo: string,
) {
const url: URL = encodeURL({
recipient,
amount,
reference,
label,
message,
memo,
});
return { url };
}
接下来,我们需要一种存储支付请求信息的方法。在本演示中,我们将使用一个简单的内存数据结构来存储支付请求信息。这将允许我们稍后验证支付请求。在 pay.ts 中添加一个 paymentRequests Map:
const paymentRequests = new Map<string, { recipient: PublicKey; amount: BigNumber; memo: string }>();
这将允许我们使用 reference 作为键来存储支付请求信息。我们将使用 reference 作为键,因为它是每个支付请求的唯一标识符。我们还将存储 recipient、amount 和 memo,以便稍后验证支付请求。请记住,这种方法仅适用于小型应用程序或开发阶段。在生产环境或更大型的应用程序中,你应该考虑使用更持久的存储解决方案(如数据库,例如 PostgreSQL、MongoDB、Redis)来存储支付请求信息。这将确保数据在服务器重启时不会丢失,并且如果你有分布式或负载均衡系统,可以在多个服务器实例之间访问。
现在让我们更新处理器,使用 generateUrl 函数并将支付请求信息存储到 paymentRequests Map 中。在 pay.ts 的 POST 处理器中添加以下代码:
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
try {
const reference = new Keypair().publicKey;
const message = `Quicknode 演示 - 订单号 #0${Math.floor(Math.random() * 999999) + 1}`;
const urlData = await generateUrl(
recipient,
amount,
reference,
label,
message,
memo
);
const ref = reference.toBase58();
paymentRequests.set(ref, { recipient, amount, memo });
const { url } = urlData;
res.status(200).json({ url: url.toString(), ref });
} catch (error) {
console.error('错误:', error);
res.status(500).json({ error: '内部服务器错误' });
}
}
//...
}
以下是我们所做的分解:
- 我们生成一个新的
reference密钥对,并将公钥存储在reference变量中。这是随机生成的,每个支付请求将是唯一的。 - 我们生成一条
message来显示用户的订单 ID。出于演示目的,我们生成一个随机的订单 ID。你可以根据自己的需要传递任何消息给用户(当他们被提示支付时,应该会在他们的钱包中显示)。 - 我们调用
generateUrl函数生成支付请求 URL。我们传入之前定义的recipient、amount、reference、label、message和memo变量。这将生成一个支付请求 URL,我们将其存储为urlData。 - 我们将
reference公钥转换为 base58 字符串并存储为ref,我们将其作为键,使用.set将支付请求信息存储到paymentRequestsMap 中。 - 我们向客户端响应一个
200状态码(成功)、支付请求url和ref键。
让我们测试一下!运行以下命令启动服务器:
npm run dev
## 或
yarn dev
然后在另一个终端窗口中,运行以下 cURL 脚本向 /api/pay 端点发送 POST 请求:
curl -X POST http://localhost:3000/api/pay
你应该会看到类似以下的响应:

干得好!你刚刚创建了一个 API 端点,它生成支付请求 URL 并将支付请求信息存储在内存中。现在让我们创建一个端点来验证支付请求。
验证支付请求
在进入前端之前,我们需要后端进行一些验证,以确保支付请求有效并且支付已由我们的 recipient 钱包接收。我们将使用之前生成的 reference 密钥对来查找支付。我们将使用 recipient、amount 和 memo 字段来验证支付是否发送到了正确的接收方,并且支付金额是否正确(请记住,我们将这些值存储在 paymentRequests Map 中,以便后端知道要查找什么)。
在 pay.ts 中创建一个 verifyTransaction 函数:
async function verifyTransaction(reference: PublicKey) {
// 1 - 检查支付请求是否存在
const paymentData = paymentRequests.get(reference.toBase58());
if (!paymentData) {
throw new Error('支付请求未找到');
}
const { recipient, amount, memo } = paymentData;
// 2 - 建立与 Solana 集群的连接
const connection = new Connection(quicknodeEndpoint, 'confirmed');
console.log('接收方', recipient.toBase58());
console.log('金额', amount);
console.log('引用', reference.toBase58());
console.log('备注', memo);
// 3 - 查找交易引用
const found = await findReference(connection, reference);
console.log(found.signature)
// 4 - 验证交易
const response = await validateTransfer(
connection,
found.signature,
{
recipient,
amount,
splToken: undefined,
reference,
//memo
},
{ commitment: 'confirmed' }
);
// 5 - 从本地存储中删除支付请求并返回响应
if (response) {
paymentRequests.delete(reference.toBase58());
}
return response;
}
以下是我们所做的分解:
- 我们检查支付请求是否存在于
paymentRequestsMap 中。如果不存在,则抛出错误。我们只应验证我们生成的支付请求。 - 我们建立与 Solana 集群的连接。我们使用之前定义的
quicknodeEndpoint变量。重要:支付者必须连接到与后端指定的相同的 Solana 集群。如果买家使用了错误的集群,将无法找到支付。 - 我们查找交易引用。这是包含支付请求信息的交易。我们使用从
solana-pay导入的findReference函数,并传入reference参数。 - 我们验证交易。我们使用从
solana-pay导入的validateTransfer函数。如果找到有效的支付,它将返回一个TransactionResponse;如果未找到或支付无效,则返回错误。 - 如果支付有效,我们从
paymentRequestsMap 中删除该支付请求,并返回TransactionResponse。
现在我们已经有了 verifyTransaction 函数,让我们更新 handler。在 handler 函数的 GET 条件中,添加以下代码:
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// ...
else if (req.method === 'GET') {
// 1 - 从 NextApiRequest 中获取 reference 查询参数
const reference = req.query.reference;
if (!reference) {
res.status(400).json({ error: '缺少 reference 查询参数' });
return;
}
// 2 - 验证交易
try {
const referencePublicKey = new PublicKey(reference as string);
const response = await verifyTransaction(referencePublicKey);
if (response) {
res.status(200).json({ status: 'verified' });
} else {
res.status(404).json({ status: 'not found' });
}
} catch (error) {
console.error('错误:', error);
res.status(500).json({ error: '内部服务器错误' });
}
}
// ...
}
以下是代码的快速分解:
- 我们从 NextApiRequest 中获取
reference查询参数并存储为reference。如果没有提供引用,则返回400状态码(错误请求)。请注意,我们期望前端将reference查询参数作为字符串传入。我们将在下一步中将其转换为PublicKey。 - 我们验证交易。我们将
reference查询参数作为PublicKey传入,并将响应存储为response。如果响应有效,我们返回200状态码(成功)和verified状态。如果响应无效,我们返回404状态码(未找到)和not found状态。如果出现错误,我们返回500状态码(内部服务器错误)和错误消息。
创建前端
后端已经设置好,现在让我们创建一个前端来与我们的 API 交互。首先,将默认的 pages/index.tsx 文件内容替换为以下代码:
import Head from 'next/head';
import Image from 'next/image';
import { useState } from 'react';
import { createQR } from '@solana/pay';
export default function Home() {
const handleGenerateClick = async () => {
};
const handleVerifyClick = async () => {
};
return (
<>
<Head>
<title>Quicknode Solana Pay 演示</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 演示</h1>
</div>
{<Image
style={{ position: "relative", background: "white" }}
src={''}
alt="二维码"
width={200}
height={200}
priority
/>}
<div>
<button
style={{ cursor: 'pointer', padding: '10px', marginRight: '10px' }}
onClick={handleGenerateClick}
>
生成 Solana Pay 订单
</button>
{<button
style={{ cursor: 'pointer', padding: '10px' }}
onClick={handleVerifyClick}
>
验证交易
</button>}
</div>
</main>
</>
);
}
我们将应用程序的主页改为一个简单的 UI,允许我们生成 Solana Pay 订单并验证交易。我们导入了必要的依赖项,并创建了两个空函数:handleGenerateClick 和 handleVerifyClick。每个函数在页面上都有一个对应的按钮。我们将在下一步中填充这些函数的功能。如果你现在运行应用,你应该会看到一个带有两个按钮的简单 UI:

实现二维码生成器
在同一个 pages/index.tsx 文件中,让我们首先创建两个状态变量:qrCode 和 reference。我们将使用这些变量来存储二维码和交易引用。在 Home 函数的顶部添加以下代码:
const [qrCode, setQrCode] = useState<string>();
const [reference, setReference] = useState<string>();
我们将生成一个二维码作为 base64 字符串,并将其存储在 qrCode 状态变量中,以便我们可以将其传递给 Image 组件。我们还将交易引用存储在 reference 状态变量中,以便稍后用于验证交易。
现在,让我们实现 handleGenerateClick 函数。我们将使用此函数向后端发送 POST 请求,并使用 URL 响应创建二维码。在 handleGenerateClick 函数中添加以下代码:
const handleGenerateClick = async () => {
// 1 - 向后端发送 POST 请求并记录响应 URL
const res = await fetch('/api/pay', { method: 'POST' });
const { url, ref } = await res.json();
console.log(url)
// 2 - 从 URL 生成二维码并生成一个 blob
const qr = createQR(url);
const qrBlob = await qr.getRawData('png');
if (!qrBlob) return;
// 3 - 将 blob 转换为 base64 字符串(使用 FileReader)并设置二维码状态
const reader = new FileReader();
reader.onload = (event) => {
if (typeof event.target?.result === 'string') {
setQrCode(event.target.result);
}
};
reader.readAsDataURL(qrBlob);
// 4 - 设置引用状态
setReference(ref);
};
让我们分解 handleGenerateClick 函数:
- 我们向后端(
/api/pay)发送POST请求,然后解构并存储响应的url和ref属性。 - 我们使用
@solana/pay中的createQR函数从url生成一个二维码。然后我们使用png格式从二维码创建一个 blob,并将其存储在qrBlob中。 - 我们使用
FileReader将 blob 转换为 base64 字符串,并将其存储在qrCode中。然后我们将qrCode状态设置为该 base64 字符串。 - 我们将
reference的状态设置为响应中的ref属性。这将允许我们稍后验证交易。
现在我们已经生成了二维码,让我们更新页面渲染,以便在可用时显示它。将 return 语句中的 Image 组件更新为以下内容:
{qrCode && (
<Image
src={qrCode}
style={{ position: "relative", background: "white" }}
alt="二维码"
width={200}
height={200}
priority
/>
)}
qrCode &&会在qrCode未设置时隐藏我们的Image组件,当设置时则显示二维码(src={qrCode})。
如果你现在运行应用,当你点击“生成 Solana Pay 订单”按钮时,你应该会看到生成的二维码!让我们创建一个函数来验证交易,以完成支付流程。
实现交易验证
现在我们已经生成了二维码,让我们创建一个函数来验证交易,以完成支付流程。我们将使用之前保存的 reference 状态传递给后端并验证交易。在 handleVerifyClick 函数中添加以下代码:
const handleVerifyClick = async () => {
// 1 - 检查引用是否已设置
if (!reference) {
alert('请先生成支付订单');
return;
}
// 2 - 向后端发送 GET 请求并返回响应状态
const res = await fetch(`/api/pay?reference=${reference}`);
const { status } = await res.json();
// 3 - 告知用户交易是否已验证,并重置二维码和引用
if (status === 'verified') {
alert('交易已验证');
setQrCode(undefined);
setReference(undefined);
} else {
alert('未找到交易');
}
};
以下是 handleVerifyClick 函数中的操作:
- 我们检查
reference状态是否已设置。如果未设置,我们提示用户先生成支付订单。 - 我们向后端(
/api/pay)发送GET请求(默认通过fetch方法),并将reference状态作为查询参数传递。然后我们解构并存储响应的status属性。 - 如果交易已验证,我们告知用户并重置
qrCode和reference状态。
最后,如果 reference 状态未设置,让我们隐藏“验证交易”按钮。将 return 语句中的 Button 组件更新为以下内容:
{reference && <button
style={{ cursor: 'pointer', padding: '10px' }}
onClick={handleVerifyClick}
>
验证交易
</button>}
干得好!现在来测试支付流程。
测试支付流程
通过在终端中运行以下命令启动你的支付终端:
npm run dev
### 或
yarn dev
按照以下步骤测试你的支付流程:
- 导航到 localhost:3000。
- 在浏览器中,点击“生成 Solana Pay 订单”按钮。你应该会看到生成的二维码。
- 使用支持 Solana Pay 的钱包(例如 Phantom)扫描二维码。
重要 - 验证你的集群
确保你的钱包连接到了正确的集群(例如 Devnet)。如果你连接到的集群与后端指定的不同,你将无法验证交易。你可以通过进入“开发者设置”并选择正确的集群来更改钱包的集群。
如果你的钱包设置为主网(mainnet),则会按照后端指定的金额转移真实资金。
如果你需要 devnet SOL,可以在这里请求:
🪂 请求 Devnet SOL
注意: 过于频繁地发送空投请求可能会触发 429(请求过多)错误。
空投 1 SOL(Devnet)
- 在钱包中验证交易详情是否符合预期,然后在钱包中批准支付。等待钱包中出现成功消息。
- 完成支付后,点击“验证交易”按钮。你应该会看到一条交易已验证的提示!请注意,如果你在网络确认交易之前点击此按钮,你将看到交易未找到的提示。
以下是验证支付后最终支付终端的外观:

做得好!
如果你想查看我们的完整代码,请查看我们的 GitHub 页面 这里。
总结
恭喜!你已经成功使用 Next.js 13 和 @solana/pay 库构建了一个 Solana Pay 终端!使用此终端,你可以通过二维码接受 Solana 支付,这些二维码可以被兼容 Solana Pay 的钱包应用扫描。要在生产环境中部署此项目,你应该考虑使用更持久的存储解决方案(如数据库,例如 PostgreSQL、MongoDB、Redis)来存储支付请求信息。这将确保数据在服务器重启时不会丢失,并且如果你有分布式或负载均衡系统,可以在多个服务器实例之间访问。
如果你需要帮助或想分享你正在使用 Solana Pay 构建的内容,请通过 Discord 或 Twitter 告诉我们。
我们 ❤️ 反馈!
如果你对本指南有任何反馈或问题,请告诉我们。我们很乐意听取你的意见!
资源
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~