如何通过Solana Pay调用自定义Solana程序

QuickNode 发布于 2026-07-10 阅读 12

本指南演示如何使用Solana Pay调用自定义Solana程序,超越常规支付场景。通过Next.js 13构建前端和后端API,生成QR码供钱包扫描调用自定义计数器程序。后端处理GET和POST请求,返回交易数据;前端实时显示计数器并监听链上变化。包含完整代码示例和步骤。

概述

Solana Pay 在 Solana 区块链上实现了快速、安全的支付通道。然而,鲜为人知的是,Solana Pay 背后的技术并不仅限于支付。本指南将向你展示如何使用 Solana Pay 调用自定义 Solana 程序。

你将完成的任务

创建一个 Next.js 13 应用程序,生成一个二维码,通过后端调用自定义 Solana 程序:

交易流程来源:Solana Pay 文档

具体来说,你将:

  • 创建一个新的 Next.js 项目。
  • 使用 React 构建一个简单的用户界面。
  • 使用 Next.js API 路由生成自定义程序交易。
  • 渲染二维码供用户扫码并签署交易。
  • 使用 Solana Websocket 监听程序,当程序被调用时更新界面中的计数器。

你需要准备的条件

本高级指南会涉及 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 欢迎页面:

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 发送自定义交易的规范和流程总结:

  1. 用户扫描前端的二维码。
  2. 用户的钱包向后端发送 GET 请求。
  3. 后端收到请求,返回 labelicon URL,用于在钱包中向用户展示。
  4. 用户的钱包向后端发送 POST 请求,其中包含用户的 account id(公钥字符串)*
  5. 后端收到请求,组装一个 Solana 交易,该交易包含自定义程序上的 increment 指令。
  6. 后端返回序列化后的交易。
  7. 用户在钱包中批准并签署交易。
  8. 用户的钱包将已签名的交易发送到集群进行处理。

简而言之,后端必须处理 GET 请求(返回 labelicon 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 库) 导入了必要的包。
  • 为演示目的定义了一些常量——你可能需要根据应用程序的需求使某些值可变(例如 messagelabel)。请确保将 quicknodeEndpoint 替换为你的 Quicknode 端点。如果你使用的程序与我们提供的不同,你可能需要修改 counterSeedfunctionName 常量。
  • 定义了我们的 API 处理程序。我们将使用一个处理程序来处理 GETPOST 请求。使用 req.method 属性来判断请求类型。如果是 GET 请求,则响应 labelicon URL——由于我们已经将这些定义在常量中,可以直接返回,调用 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' });
    }
  }

这里所做的是:

  1. 检查请求体中是否传递了 account 字段。如果没有,则返回 400 错误。
  2. 如果传递了 account 字段,则调用一个新函数 generateTx,并将 account 作为参数传递。该函数将生成一个递增计数器的交易(接下来会构建这个函数)。
  3. 如果成功生成交易,则将序列化的 transactionmessage(在常量中定义)返回给钱包。钱包将向用户展示消息,并要求用户确认交易。

现在创建 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;
}

下面逐步解释这些操作:

  1. 使用 counterSeedprogramId 生成计数器 PDA。必须将此账户传递给 increment 函数的交易指令。
  2. increment 函数生成数据缓冲区,使用之前定义的 getInstructionData 函数。
  3. 构建交易:创建一个新的 Transaction 并添加一个新的 TransactionInstruction。将 counterPda(作为可写、非付费账户)以及步骤 1 和 2 中生成的 data 传递进去,同时传入在常量中定义的 programId注意:如果你使用自己的程序,则需要根据程序中定义的上下文更新这些值。
  4. 获取最新的区块哈希并设置,同时将用户钱包设为付费者。
  5. 序列化交易。将 verifySignaturesrequireAllSignatures 设置为 false,因为我们不签署交易,由钱包处理签名。
  6. 最后,将交易数据编码为 base64(Base64 是二进制数据的常见编码格式)并返回给钱包。

做得好!你刚刚创建了一个用于生成递增计数器交易的函数。后端现在可以接收来自钱包的请求了。我们来测试一下!

运行以下命令启动服务器:

npm run dev
## 或
yarn dev

然后在另一个终端窗口中,运行以下 cURL 脚本向 /api/pay 端点发送 GET 请求:

curl -X GET http://localhost:3000/api/pay

这应该会返回我们在常量中定义的 labelicon。现在测试 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 库中添加了几个导入,用于反序列化程序的账户数据。
  • 定义了一些常量(与后端相似):quickNodeEndpointconnectionprogramIdcounterPda。用于连接程序并获取账户数据。

使用 .env 文件

注意,为简单起见,我们硬编码了端点。在生产应用中,你应该使用环境变量来存储端点。查看 Next.js 文档 了解有关使用环境变量的更多信息。

  • 定义了一个 Home 组件,用于渲染 UI。UI 会显示计数和二维码(如果已定义,但目前尚未定义)。我们还创建了一个 useEffect Hook,在组件挂载时运行,用于获取账户数据并生成二维码。

实现二维码生成器

我们将生成一个 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);
  }

我们来深入分析这个函数的作用:

  1. 定义 apiUrl,即后端 API 端点的 URL。使用 window.location 对象获取当前页面的协议和主机,然后在末尾附加 /api/pay。这样 API 在 localhost 和部署的应用程序上都能正常工作。
  2. 定义 labelmessage,在本演示中作为占位符。
  3. 使用 @solana/pay 中的 encodeURL 函数创建一个 URL,该 URL 会触发扫描钱包向后端发送 GET 请求。将 apiUrl 作为 new URL 传入。
  4. 最后,将二维码渲染为 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 前端,它:
    1. 获取我们的程序计数器
    2. 订阅程序计数器的变更
    3. 生成并显示一个二维码,可由支持 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 来解决这个问题。你可以将项目发布到 VercelNetlify 等服务(只需确保像之前提到的那样保护端点)。但出于本指南的目的,我们将使用 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 构建的内容,请通过 DiscordTwitter 告诉我们。

我们 ❤️ 反馈!

如果你对本指南有任何反馈或问题,请告诉我们。我们期待你的反馈!

资源

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论