为Quicknode优先级费API构建Solana Kit插件

  • QuickNode
  • 发布于 2026-05-11 18:17
  • 阅读 33

本文详细介绍了如何使用Solana Kit构建一个与Quicknode优先级费API集成的插件。文章从插件的工作原理(柯里化函数)讲起,逐步指导读者创建项目、定义类型、实现获取优先级费和交易规划器的函数,并最终在示例中演示如何获取费用估值和发送带动态优先级费的SOL转账。通过本指南,读者可以学习到Solana Kit的插件模式、类型安全的客户端扩展以及如何将外部服务无缝集成到交易流程中。

概述

Solana Kit 是由 Anza 开发的现代 JavaScript/TypeScript SDK,用于构建 Solana 应用程序。它取代了旧的 @solana/web3.js 库,以模块化、类型安全和性能为核心进行了彻底重写。

Kit 的另一个优势是能够通过自定义功能扩展客户端。Kit 的插件系统无需手动设置自定义传输或编写一次性辅助函数,只需定义一次功能,然后通过单个 .use() 调用将其附加到任何客户端即可。

本指南将逐步介绍如何构建一个封装 Quicknode Solana 优先级费用 API 的 Kit 插件。最终结果是一个自包含的插件,用于获取优先级费用,以及一个发送 SOL 转账并动态设置优先级费用的可运行示例。

快速总结

  • 你将构建的内容: 一个可重用的 quicknodePriorityFees Kit 插件,从 Quicknode 获取费用估算,并将其公开为类型化的客户端方法
  • 你将学到的内容: Kit 插件模式的工作原理,以及如何将实时费用估算接入交易规划器
  • 最终结果: 一个可运行的脚本,检索所有费用层级,并使用动态设置的优先级费用发送 SOL 转账
  • 所需时间: 约 30 分钟

准备工作

本指南使用的依赖项
依赖项 版本
@solana/kit ^6.1.0
@solana/kit-plugin-instruction-plan ^0.6.0
@solana/kit-plugin-payer ^0.6.0
@solana/kit-plugin-rpc ^0.6.0
@solana-program/system ^0.12.0

Kit 插件的工作原理

Kit 插件是一个柯里化函数:不是一次性接收所有参数,而是分组接收。外部函数接收一个配置对象并返回一个新函数,该新函数再接收客户端。这样可以在设置时一次性配置插件,并在多个客户端之间复用生成的函数。

const myPlugin = (config) => (client) => ({ ...client, newMethod: ... });

存在三个不同的层次:

  1. 配置:外部调用在设置时接收插件的选项(URL、默认值、功能标志)。
  2. 客户端:内部调用接收当前客户端对象,该对象包含了之前 .use() 调用附加的所有内容(RPC、付款人等)。
  3. 返回值:一个新对象,扩展了现有客户端并添加了新属性(方法、值)。TypeScript 会自动推断组合后的类型。

.use() 将这些转换链接起来。每个插件接收上一个插件的输出,因此当你的代码调用某个方法时,整个链已经运行完毕:

const client = createEmptyClient()
  .use(rpc(ENDPOINT))          // 添加 client.rpc
  .use(myPlugin({ option: 'value' })); // 接收 { rpc },返回 { rpc, newMethod }

client.newMethod(); // 完全类型化,无需类型断言

这也是所有官方 Solana Kit 插件包 使用的模式。任何插件都是一个标准的 npm 包,发布一次后可与生态系统中的任何其他插件组合使用。

构建插件

该插件从 Quicknode 的 Solana 优先级费用 API 获取优先级费用估算,并自动将其应用于交易。示例脚本使用一个基本的 SOL 转账以保持简单,但相同的插件和方法适用于任何交易类型——DEX 交换、NFT 铸造或程序调用。

创建新项目

mkdir qn-priority-fees-plugin && cd qn-priority-fees-plugin
npm init -y

安装依赖项:

npm install @solana/kit @solana/kit-plugin-instruction-plan @solana/kit-plugin-payer @solana/kit-plugin-rpc @solana-program/system

安装开发依赖项:

npm install --save-dev typescript tsx @types/node

添加 tsconfig.json。这不是 tsx 运行示例所必需,但编译或发布插件到 npm 注册表时需要。

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

创建源代码目录和 index.ts 文件:

mkdir src
touch src/index.ts

定义类型

插件位于单个文件 src/index.ts 中,该文件导出插件函数及其所有 TypeScript 类型。

src/index.ts

import {
    type GetEpochInfoApi,
    type GetLatestBlockhashApi,
    type GetSignatureStatusesApi,
    type MicroLamports,
    type Rpc,
    type RpcSubscriptions,
    type SendTransactionApi,
    type SignatureNotificationsApi,
    type SimulateTransactionApi,
    type SlotNotificationsApi,
    type TransactionPlanner,
    type TransactionSigner,
  } from "@solana/kit";
  import { rpcTransactionPlanner } from "@solana/kit-plugin-rpc";

  /** 从 PriorityFeeEstimate 中选择的费用层级。 */
  export enum PriorityFeeTier {
    Low = "low",
    Medium = "medium",
    High = "high",
    Extreme = "extreme",
    Recommended = "recommended",
  }

  /** 估算中某一维度的每个层级细分。 */
  export type PriorityFeeLevels = {
    low: number;
    medium: number;
    high: number;
    extreme: number;
    percentiles: Record<string, number>;
  };

  /** qn_estimatePriorityFees 的完整响应结构(api_version: 2) */
  export type PriorityFeeEstimate = {
    context: { slot: number };
    per_compute_unit: PriorityFeeLevels;
    per_transaction: PriorityFeeLevels;
    recommended: number;
  };

  /** qn_estimatePriorityFees 接受的参数 */
  export type EstimatePriorityFeesParams = {
    /** 要限定估算范围的程序或账户地址 */
    account?: string;
    /** 要分析的最近区块数量(默认:100) */
    last_n_blocks?: number;
    /** API 版本——使用 2 获取上述完整响应结构 */
    api_version?: number;
  };

  /** quicknodePriorityFees 插件的配置 */
  export type QuicknodePriorityFeesConfig = {
    /**
     * 你的 Quicknode 端点 URL。
     * 端点必须已启用优先级费用 API 附加组件。
     */
    url: string;
    /**
     * 可选的默认参数,每次调用 estimatePriorityFees 时合并。
     * 调用时传递的参数会覆盖这些默认值。
     */
    defaults?: EstimatePriorityFeesParams;
  };

  /** quicknodeTransactionPlanner 插件的配置 */
  export type QuicknodeTransactionPlannerConfig = {
    /** 每次交易时获取的费用层级。默认:'recommended' */
    tier?: PriorityFeeTier;
    /**
     * 将支付费用的交易签名者。
     * 或者,在应用此插件之前,在客户端上设置 `payer`。
     */
    payer?: TransactionSigner;
    /** 可选回调,每次交易使用的优先级费用(以微 lamports 为单位)触发。 */
    onFee?: (fee: MicroLamports) => void;
  };

  type RpcRequirements = Rpc<
    GetEpochInfoApi &
      GetLatestBlockhashApi &
      GetSignatureStatusesApi &
      SendTransactionApi &
      SimulateTransactionApi
  >;

  type RpcSubscriptionsRequirements = RpcSubscriptions<
    SignatureNotificationsApi & SlotNotificationsApi
  >;

  export type QuicknodePriorityFeesExtension = {
    estimatePriorityFees: (
      params?: EstimatePriorityFeesParams,
    ) => Promise<PriorityFeeEstimate>;
    getPriorityFee: (
      tier?: PriorityFeeTier,
      params?: EstimatePriorityFeesParams,
    ) => Promise<MicroLamports>;
  };

PriorityFeeEstimate 反映了优先级费用 API v2 响应,该响应返回独立的 per_compute_unitper_transaction 细分,以及顶层的 recommended 值。

MicroLamports 是 Kit 为每计算单元费用标记的 bigint 类型。交易规划器直接通过其 priorityFees 选项接受此类型,因此从插件返回它意味着调用端无需任何粘合代码。


获取优先级费用

仍在 src/index.ts 中,添加 quicknodePriorityFees 函数以按层级获取优先级费用:

src/index.ts

/**
 * 为 Solana Kit 客户端添加 Quicknode 优先级费用估算的插件。
 *
 * @see https://www.quicknode.com/docs/solana/qn_estimatePriorityFees
 */
export function quicknodePriorityFees(config: QuicknodePriorityFeesConfig) {
    return function <TClient extends object>(
      client: TClient,
    ): TClient & QuicknodePriorityFeesExtension {
      const estimatePriorityFees = async (
        params?: EstimatePriorityFeesParams,
      ): Promise<PriorityFeeEstimate> => {
        const mergedParams: EstimatePriorityFeesParams = {
          last_n_blocks: 100,
          api_version: 2,
          ...config.defaults,
          ...params,
        };

        const response = await fetch(config.url, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            jsonrpc: "2.0",
            id: crypto.randomUUID(),
            method: "qn_estimatePriorityFees",
            params: mergedParams,
          }),
        });

        if (!response.ok) {
          throw new Error(
            `qn_estimatePriorityFees HTTP error: ${response.status} ${response.statusText}`,
          );
        }

        const json = (await response.json()) as {
          result?: PriorityFeeEstimate;
          error?: { code: number; message: string };
        };

        if (json.error) {
          throw new Error(
            `qn_estimatePriorityFees RPC error ${json.error.code}: ${json.error.message}`,
          );
        }

        if (!json.result) {
          throw new Error("qn_estimatePriorityFees returned an empty result");
        }

        return json.result;
      };

      const getPriorityFee = async (
        tier: PriorityFeeTier = PriorityFeeTier.Recommended,
        params?: EstimatePriorityFeesParams,
      ): Promise<MicroLamports> => {
        const estimate = await estimatePriorityFees(params);
        const fee =
          tier === PriorityFeeTier.Recommended
            ? estimate.recommended
            : estimate.per_compute_unit[tier];
        return BigInt(Math.ceil(fee)) as MicroLamports;
      };

      return { ...client, estimatePriorityFees, getPriorityFee };
    };
  }

以下是每个部分的功能:

estimatePriorityFees 从优先级费用 API 获取费用估算。调用时传递的任何参数都会合并到插件设置时设置的 defaults 之上,因此每次调用时传入的参数总是覆盖默认值。当你需要完整响应时使用此函数。例如,显示所有费用层级,以便用户选择自己的优先级级别。

预期结果:

{
  context: { slot: 402491299 },
  per_compute_unit: {
    extreme: 2000000,
    high: 535996,
    low: 25172,
    medium: 176531,
    percentiles: {
      '0': 1,
      '5': 5,
      '10': 1864,
      [...],
      '100': 14265640
    }
  },
  per_transaction: {
    extreme: 401724596244,
    high: 99999974109,
    low: 4561710418,
    medium: 36171896086,
    percentiles: {
      '0': 62567,
      '5': 485035,
      '10': 943903818,
      [...],
      '100': 10000000000000
    }
  },
  recommended: 1142471
}

在大多数情况下,你会使用 per_compute_unit。Solana 的交易费用模型按消耗的计算单元收取优先级费用,交易规划器通过一个期望每计算单元值(以微 lamports 为单位)的 SetComputeUnitPrice 指令来设置费用。在此处使用 per_transaction 值会导致超额支付。

getPriorityFee 调用 estimatePriorityFees 并返回请求层级的单个费用值,类型为 MicroLamports,因此可以直接传递给交易规划器。当你只需要一个费用值插入交易,而不需要完整细分时使用此函数。

泛型 <TClient extends object> 是标准的 Kit 插件约束。插件展开传入的客户端并添加新属性。TypeScript 自动推断组合类型,因此无论客户端上已有其他什么内容,下游的 .use() 调用和属性访问都保持完全类型化。

添加交易规划器

添加 quicknodeTransactionPlanner 函数,以在每次交易时使用 Quicknode 优先级费用 API 获取并应用新的优先级费用:

src/index.ts

/**
 * 包装默认 Solana Kit 交易规划器的插件,每次交易时从 Quicknode 获取新的优先级费用。
 */
export function quicknodeTransactionPlanner(
    config?: QuicknodeTransactionPlannerConfig,
  ) {
    return function <
      TClient extends object & {
        getPriorityFee: QuicknodePriorityFeesExtension["getPriorityFee"];
        rpc: RpcRequirements;
        rpcSubscriptions: RpcSubscriptionsRequirements;
        payer?: TransactionSigner;
      },
    >(client: TClient) {
      const payer = config?.payer ?? client.payer;

      const transactionPlanner: TransactionPlanner = async (
        instructionPlan,
        plannerConfig,
      ) => {
        const priorityFees = await client.getPriorityFee(
          config?.tier ?? PriorityFeeTier.Recommended,
        );
        config?.onFee?.(priorityFees);

        const { transactionPlanner: inner } = rpcTransactionPlanner({
          priorityFees,
          payer,
        })(client);
        return inner(instructionPlan, plannerConfig);
      };

      return { ...client, transactionPlanner };
    };
  }

transactionPlanner 是标准 Kit 规划器的自定义包装。每次被调用时,它都会调用 client.getPriorityFee() 从优先级费用 API 获取新的费用,然后使用该费用调用 rpcTransactionPlanner 以构建一个应用该费用的内部规划器。如果提供了 onFee 回调,它会在规划开始之前触发回调并传入费用值——这对于日志记录或分析非常有用。

此插件仅在客户端上设置 transactionPlanner。执行器(rpcTransactionPlanExecutor)会在你的 .use() 链中单独添加,因此每个关注点都位于自己的插件中。

当你希望每次发送时自动获取并应用费用,而无需在调用端进行手动连接时,使用 quicknodeTransactionPlanner

使用插件

示例脚本演示了两件事:获取完整费用估算以检查所有可用层级,以及发送应用了特定层级的交易。

创建本地钱包

在运行示例之前,为此项目创建一个专用的本地密钥对:

solana-keygen new --outfile ~/.config/solana/kit-plugin-id.json

然后为其充值至少 0.003 SOL,以覆盖转账金额和交易费用。

设置环境变量

在项目根目录下创建一个 .env 文件,包含你的凭据:

.env

## 在此处使用启用了优先级费用 API 的 Quicknode 端点
QUICKNODE_ENDPOINT=https://qn-demo-endpoint.quiknode.pro/abcd1234
JUPITER_PROGRAM=JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4
PAYER_KEYPAIR_PATH=/path/to/keypair/kit-plugin-id.json
RECIPIENT_ADDRESS=ADDRESS_TO_RECEIVE_THE_TRANSFER

然后创建 src/example.ts

src/example.ts

import { setTimeout as sleep } from 'node:timers/promises';
import {
  address,
  createEmptyClient,
  lamports,
  type ClusterUrl,
  type ClientWithTransactionSending,
  type TransactionSigner,
} from '@solana/kit';
import { planAndSendTransactions } from '@solana/kit-plugin-instruction-plan';
import { payerFromFile } from '@solana/kit-plugin-payer';
import { rpc, rpcTransactionPlanExecutor } from '@solana/kit-plugin-rpc';
import { getTransferSolInstruction } from '@solana-program/system';
import {
  quicknodePriorityFees,
  quicknodeTransactionPlanner,
  PriorityFeeTier,
  type QuicknodePriorityFeesExtension,
} from './index';

const QUICKNODE_ENDPOINT = process.env.QUICKNODE_ENDPOINT!;
const JUPITER_PROGRAM = process.env.JUPITER_PROGRAM!;
const PAYER_KEYPAIR_PATH = process.env.PAYER_KEYPAIR_PATH!;
const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS!;

独立费用估算

demonstrateFeeEstimation 接受 main 中共享的客户端,并直接调用 estimatePriorityFees 以打印完整的费用细分。该函数类型化后仅要求 estimatePriorityFees,因此可以与任何附加了 quicknodePriorityFees 的客户端一起使用——包括一个裸的 createEmptyClient(),如果你只需要费用查询而不需要 RPC 或交易发送:

src/example.ts

async function demonstrateFeeEstimation(client: {
    estimatePriorityFees: QuicknodePriorityFeesExtension['estimatePriorityFees'];
  }) {
    // 完整响应——当你想向用户显示所有层级时非常有用。
    const estimate = await client.estimatePriorityFees({
      account: JUPITER_PROGRAM,
      last_n_blocks: 150,
    });

    console.log('Slot:', estimate.context.slot);
    console.log('每 CU 费用(微 lamports):');
    console.log('  低     :', estimate.per_compute_unit.low);
    console.log('  中     :', estimate.per_compute_unit.medium);
    console.log('  高     :', estimate.per_compute_unit.high);
    console.log('  极高   :', estimate.per_compute_unit.extreme);
    console.log('推荐值:', estimate.recommended);
  }

使用动态优先级费用进行转账

sendTransactionWithPriorityFees 接受共享的客户端(该客户端已通过 payerFromFile 附加了 payer),使用 client.payer 构建一个转账指令,然后调用 client.sendTransaction。由于客户端是使用 quicknodeTransactionPlanner 创建的,因此在交易签名和发送之前,会自动从 API 获取新的优先级费用——调用端无需手动连接费用。

src/example.ts

async function sendTransactionWithPriorityFees(
    client: ClientWithTransactionSending & { payer: TransactionSigner },
  ) {
    const recipient = address(RECIPIENT_ADDRESS);
    const result = await client.sendTransaction(
      getTransferSolInstruction({
        source: client.payer,
        destination: recipient,
        amount: lamports(1_000_000n), // 0.001 SOL
      }),
    );

    console.log('交易已确认!');
    console.log('签名:', result.context.signature);
  }

创建客户端

现在添加客户端代码以将所有内容整合在一起:

src/example.ts

async function main() {
    const client = await createEmptyClient()
      .use(rpc(QUICKNODE_ENDPOINT as ClusterUrl))
      .use(payerFromFile(PAYER_KEYPAIR_PATH))
      .use(quicknodePriorityFees({ url: QUICKNODE_ENDPOINT, defaults: { account: JUPITER_PROGRAM } }))
      .use(quicknodeTransactionPlanner({
        tier: PriorityFeeTier.Recommended,
        onFee: (fee) => console.log('使用的优先级费用(微 lamports):', fee),
      }))
      .use(rpcTransactionPlanExecutor())
      .use(planAndSendTransactions());

    await demonstrateFeeEstimation(client);
    await sleep(1000); // 暂停以防止速率限制
    await sendTransactionWithPriorityFees(client);
  }

  main()

main 构建了一个可组合的客户端,同时使用了其他 Kit 插件。顺序很重要:quicknodePriorityFees 必须在 quicknodeTransactionPlanner 之前,因为规划器会调用 client.getPriorityFee,而该函数是由费用插件添加的。onFee 回调记录了每次使用的优先级费用——这对于调试或分析非常方便。rpcTransactionPlanExecutor 在规划器之后作为单独步骤添加,将规划与执行关注点保持在不同的插件中。

quicknodeTransactionPlanner 在底层使用了来自 @solana/kit-plugin-rpcrpcTransactionPlanner。如果你需要直接控制,或者想替换成自己的自定义规划器,可以使用异步 .use() 调用以相同的方式内联实现:

const client = await createEmptyClient()
  .use(rpc(QUICKNODE_ENDPOINT as ClusterUrl))
  .use(payerFromFile(PAYER_KEYPAIR_PATH))
  .use(quicknodePriorityFees({ url: QUICKNODE_ENDPOINT, defaults: { account: JUPITER_PROGRAM } }))
  .use(rpcTransactionPlanExecutor())
  .use(async (c) => {
    const priorityFees = await c.getPriorityFee(PriorityFeeTier.Recommended);
    return rpcTransactionPlanner({ priorityFees, payer: c.payer })(c);
  })
  .use(planAndSendTransactions());

这提供了与 quicknodeTransactionPlanner 相同的自动费用注入,但可以直接访问 rpcTransactionPlanner,如果你想传递其他选项或替换为不同的规划器。

运行示例

tsx --env-file=.env src/example.ts

预期输出:

Slot: 402493495
每 CU 费用(微 lamports):
  低     : 15000
  中     : 101760
  高     : 612373
  极高   : 1826194
推荐值: 1108408

交易已确认!
签名: 3iEZx...WsbC

应该使用哪个费用层级?

对于大多数用例,recommended 是一个安全的默认值,它反映了 Quicknode 基于近期网络活动的建议值。对于时间敏感的交易(如 DEX 交易或 NFT 铸造),使用 high;仅当网络严重拥堵且确认速度至关重要时,才使用 extreme

层级 使用场景
low 非时间敏感操作(例如,非高峰期的元数据更新)
medium 标准 dApp 交互
high 竞争性交易(DEX 交易、NFT 铸造)
extreme 高负载下的时间关键操作
recommended 通用默认值;Quicknode 自己的建议值

通过 account 参数将估算范围限定为你交互的程序。限定在 Jupiter 程序范围内的费用估算(如本示例所示)对于 Jupiter 交换来说比全局估算更准确。

发布插件

由于 Kit 插件只是一个标准的 ESM 包,除了对 @solana/kit 的对等依赖外,没有 Solana 特定的运行时依赖,你可以将其发布到 npm,其他开发者可以安装它,并像任何其他依赖项一样将其放入他们自己的 .use() 链中。

总结

在本指南中,你构建了一个自包含的 Solana Kit 插件,将 Quicknode 的优先级费用 API 直接集成到客户端链中。在此过程中,你了解了 Kit 的柯里化插件模式如何保持自定义功能的可组合性和完全类型化,如何公开完整估算和单值便利方法,以及如何将动态获取的费用接入交易规划器,以便在发送时自动应用。

从这里开始,你可以调整相同的插件模式来封装其他 Quicknode 附加组件——代币元数据、DAS API 调用或自定义 RPC 方法——并将它们组合到单个 .use() 链中。你还可以将插件作为独立的 npm 包发布,以便其他开发者在他们的项目中使用。

资源

我们喜欢反馈!

如果你有任何反馈或对新主题的请求,请告诉我们。我们很乐意听取你的意见。

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

0 条评论

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