Solana - 如何使用自定义 Solana 程序进行 Solana Pay

  • QuickNode
  • 发布于 2025-01-30 14:59
  • 阅读 19

这篇文章详细介绍了如何使用 Solana Pay 与自定义 Solana 程序集成,采用 Next.js 框架构建应用。用户将学习如何生成二维码以用作与后端交互,并运行自定义交易指令,通过使用 Solana 的 WebSocket 订阅机制实现实时数据更新。文章提供了清晰的结构和丰富的技术细节,适合具有相关经验的开发者阅读。

概述

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

你将做什么

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

交易流程来源: Solana Pay 文档

具体来说,你将:

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

你将需要什么

本高级指南将涉及构建 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 devnet 的 Solana 端点,以组装交易。

使用你的 QuickNode 端点连接到 Solana 集群

要在 Solana 上构建,你需要一个 API 端点以连接到网络。你可以使用公共节点或部署并管理自己的基础设施;但是,如果你想要 8 倍的响应速度,可以将重任留给我们。

查看为什么超过 50% 的 Solana 项目选择 QuickNode,并在 这里 注册一个免费账户。我们将使用 Solana Devnet 端点。

复制 HTTP 提供者链接:

干得不错。你已准备好开始构建应用程序。如果你在设置过程中需要帮助或遇到任何问题,请在 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. 后端接收到请求,并组装一个包含我们自定义程序上 increment 指令的 Solana 交易。
  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 演示 - 递增计数器`;
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: '方法不被允许' });
  }
}

我们在这里做的是:

  • 定义我们在整个应用程序中将使用的一些关键变量。我们还从 Solana Pay、Solana-web3.js 和 crypto(NodeJS 库) 导入必需的包。
  • 为演示定义了一些常量——你可能希望根据应用程序的需要将其中一些值设为变量(例如,messagelabel)。确保用你的 QuickNode 端点更新 quicknodeEndpoint。如果你使用与我们提供的程序不同的程序,则可能需要更改 counterSeedfunctionName 常量。
  • 定义我们的 API 处理程序。我们将使用一个处理程序处理 GETPOST 请求。我们将使用 req.method 属性来确定采取何种操作。如果请求方法是 GET,则以 labelicon URL 响应——由于我们在常量中定义了这些,因此我们可以直接返回,调用 res.status(200).json({ label, icon })。如果请求方法是 POST,我们将生成交易。如果请求方法是其他任何方法,我们将返回错误。你可以为每个动作使用单独的处理程序,但为了简单起见,我们将使用一个处理程序。
  • 定义 getInstructionData 函数。该函数将为我们的 increment 指令生成数据。我们使用哈希函数生成可以传入我们的 Transaction 的序列化数据。这是 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: '缺少账户字段' });
      const transaction = await generateTx(account);
      res.status(200).send({ transaction, message });
    } catch (error) {
      console.error('错误:', error);
      res.status(500).json({ error: '内部服务器错误' });
    }
  }

我们在这里做的是:

  1. 检查请求正文中是否传入了 account 字段。如果没有,我们将返回 400 错误。
  2. 如果传入了 account 字段,我们将调用一个新函数 generateTx,并将 account 作为参数传递。该函数将在下一个步骤中生成一个交易,递增计数器(我们将接着构建它)。
  3. 如果交易成功生成,我们将返回序列化的 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;
}

让我们走过我们在这里做的事情:

  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 演示 - 递增计数器"
}

干得不错!你刚刚创建了一个 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>
    </>
  );
}

这为我们工作提供了一个很好的起点。让我们走过这里的内容:

  • 从 React、Next 和 Solana 导入必要的依赖项。这部分与我们的上一个 将 Solana Pay 添加到你的 dApp 的指南 雷同,因此我们不会在这里过多详细阐述。我们还从 Solana 的缓冲区布局库添加了一些导入。我们将使用这些来反序列化我们程序的账户数据。
  • 定义一些常量(这些对我们后端来说应该很熟悉):quickNodeEndpointconnectionprogramIdcounterPda。我们将这些用于连接到我们的程序并获取账户数据。

使用 .env 文件

请注意,我们在这里哈德编码了端点以简化操作。在生产应用中,你应该使用环境变量来存储你的端点。查看 Next.js 文档 以了解有关使用环境变量的更多信息。

  • 定义了一个 Home 组件,用于渲染我们的 UI。UI 将显示计数和二维码(尽管我们尚未定义它们)。我们还创建了一个 useEffect 钩子,它将在组件挂载时运行。我们将使用此钩子来获取账户数据并生成二维码。

实现 QR 生成器

我们将生成一个二维码作为 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 附加到 URL 的末尾。这将允许我们的 API 在 localhost 和已部署的应用上有效(而不会硬编码 URL)。
  2. 我们定义了一个 labelmessage,实际上是此演示的占位符。
  3. 我们使用 @solana/payencodeURL 函数创建一个将触发扫描钱包对我们的后端发出 GET 请求的 URL。我们将 apiUrl 作为 new URL 传递。
  4. 最后,我们将二维码呈现为 base64 字符串并存储在 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 方法的复习,请查看 这份指南。我们还返回了一个函数,用于在组件卸载时取消对程序账户更改事件的订阅。

太棒了,让我们回顾一下我们到目前为止所构建的内容。我们已经:

  • 使用 Anchor 创建并部署了一个 Solana 计数器程序
  • 构建了一个 Next.js 前端
    1. 获取我们的程序计数
    2. 订阅对我们程序计数的变化
    3. 生成并显示一个二维码,供 Solana Pay 兼容的钱包应用扫描
  • 创建了可以被 Solana Pay 兼容的钱包应用在扫描钱包时调用的后端 API 端点。该 API 端点遵循 Solana Pay API 规范,并将发送调用我们计数器的交易到用户的钱包以供其签名。

现在,我们所需要做的就是测试一下。

测试应用

打开一个新终端窗口并运行以下命令以启动 Next.js 开发服务器:

npm run dev
## 或
yarn dev

这将在 3000 端口上启动 Next.js 开发服务器。导航至 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,并单击“访问页面”。你应该会被指向正在运行你本地开发服务器(NextJS 应用)的 ngrok.io 页面。你应该看到你的最终应用正在运行,显示有效的二维码和更新的计数:

自定义计数器登陆页面

注意:上面的二维码无效,因为它指向一个已经不再活跃的 ngrok 后端。

现在我们有了正在运行的应用,让我们测试一下。打开你的 Solana Pay 兼容的钱包应用(目前,Android 上的 Phantom 存在已知问题——我们会在修复后更新此信息),确保网络设置为 Devnet。然后,扫描二维码。你应该会被提示签署一笔将调用我们的计数器程序的交易:

一旦你批准了交易并且网络完成了确认,你应该会在你的应用中看到计数器增加!

如果你想查看我们完整的代码,请查看我们的 GitHub 页面 这里

总结

干得不错!这是一项艰巨的工作,但你成功地构建了一个 Solana Pay 与自定义 Solana 程序之间的集成。这种集成的可能性是无穷无尽的。我们迫不及待地想看到你所想出的东西!如果你需要灵感,可以查看 Solana 基金会构建的这个有趣的 拔河游戏

如果你需要帮助或想与我们分享你所构建的内容,请在 DiscordTwitter 上告诉我们。

我们 ❤️ 反馈!

如果你对本指南有任何反馈或问题,请告诉我们。我们很想听到你的意见!

资源

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。