构建一个用于 Quicknode 优先费用 API 的 Solana Kit 插件

这篇文章详细介绍了如何使用 Solana Kit 的插件系统来构建一个自定义插件,该插件集成了 Quicknode 的优先费用 API。通过该插件,开发者可以动态地获取 Solana 链上的优先费用估算,并自动将其应用于交易中,从而优化交易的执行速度。

概述

Solana Kit 是由 Anza 开发的用于构建 Solana 应用程序的现代 JavaScript/TypeScript SDK。它取代了传统的 @solana/web3.js 库,从头重写,专注于模块化、类型安全和性能。

Kit 的另一个优势是能够使用自定义功能扩展客户端。Kit 的插件系统让你无需连接自定义传输或一次性辅助函数,即可一次性定义一个功能,并通过一个 .use() 调用将其附加到任何客户端。

本指南将引导你构建一个封装 Quicknode 的 Solana Priority Fee API 的 Kit 插件。最终结果是一个独立的插件,用于获取优先费用,以及一个通过动态设置的优先费用发送 SOL 转账的实际示例。

TL;DR

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

你将需要

  • 熟悉 Solana Kit
  • Node.js 22 或更高版本
  • TypeScript 经验
  • 一个 Quicknode Solana 主网 Beta 端点,并启用了 Priority Fee API 附加组件。如果你没有,请在此处创建一个免费账户。
本指南中使用的依赖项
依赖项 版本
@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、payer 等)已附加的内容。
  3. 返回值:一个新对象,它展开现有客户端并添加新属性(方法、值)。TypeScript 会自动推断组合类型。

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

const client = createEmptyClient()
  .use(rpc(ENDPOINT))          // adds client.rpc
  .use(myPlugin({ option: 'value' })); // receives { rpc }, returns { rpc, newMethod }

client.newMethod(); // fully typed, no casting

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

构建插件

该插件从 Quicknode 的 Solana Priority Fee 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.jsontsx 运行示例不需要它,但编译或将插件发布到 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";

  /** The fee tier to select from a PriorityFeeEstimate. */
  export enum PriorityFeeTier {
    Low = "low",
    Medium = "medium",
    High = "high",
    Extreme = "extreme",
    Recommended = "recommended",
  }

  /** Per-level breakdown for one dimension of the estimate. */
  export type PriorityFeeLevels = {
    low: number;
    medium: number;
    high: number;
    extreme: number;
    percentiles: Record<string, number>;
  };

  /** Full response shape from qn_estimatePriorityFees (api_version: 2) */
  export type PriorityFeeEstimate = {
    context: { slot: number };
    per_compute_unit: PriorityFeeLevels;
    per_transaction: PriorityFeeLevels;
    recommended: number;
  };

  /** Parameters accepted by qn_estimatePriorityFees */
  export type EstimatePriorityFeesParams = {
    /** Program or account address to scope the estimate to */
    account?: string;
    /** Number of recent blocks to analyse (default: 100) */
    last_n_blocks?: number;
    /** API version — use 2 for the full response shape above */
    api_version?: number;
  };

  /** Configuration for the quicknodePriorityFees plugin */
  export type QuicknodePriorityFeesConfig = {
    /**
     * Your Quicknode endpoint URL.
     * The endpoint must have the Priority Fee API add-on enabled.
     */
    url: string;
    /**
     * Optional default params merged into every estimatePriorityFees call.
     * Call-time params override these defaults.
     */
    defaults?: EstimatePriorityFeesParams;
  };

  /** Configuration for the quicknodeTransactionPlanner plugin */
  export type QuicknodeTransactionPlannerConfig = {
    /** Fee tier to fetch on every transaction. Default: 'recommended' */
    tier?: PriorityFeeTier;
    /**
     * The transaction signer who will pay for fees.
     * Alternatively, set `payer` on the client before applying this plugin.
     */
    payer?: TransactionSigner;
    /** Optional callback invoked with the priority fee (in micro-lamports) used for each transaction. */
    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 反映了 Priority Fees API v2 的响应,它返回单独的 per_compute_unitper_transaction 明细以及一个顶层 recommended 值。

MicroLamports 是 Kit 用于每计算单位费用的品牌化 bigint。交易规划器通过其 priorityFees 选项直接接受此类型,因此从插件返回它意味着在调用处无需任何胶水代码。


获取优先费用

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

src/index.ts

/**
 * 将 Quicknode 优先费用估算添加到 Solana Kit 客户端的插件。
 *
 * @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 从 Priority Fee 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 的交易费用模型按消耗的计算单位收取优先费用,交易规划器通过 SetComputeUnitPrice 指令设置费用,该指令需要一个每计算单位的值(以 micro-lamports 为单位)。在此处使用 per_transaction 值将导致支付过多费用。

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

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

添加交易规划器

添加 quicknodeTransactionPlanner 函数,以便在每次交易时使用 Quicknode Priority Fee 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() 从 Priority Fee API 获取新的费用,然后使用该费用调用 rpcTransactionPlanner 来构建一个应用该费用的内部规划器。如果提供了 onFee 回调,它会在规划开始前随费用值触发——这对于日志记录或分析非常有用。

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

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

使用插件

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

创建本地钱包

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

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

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

设置环境变量

在项目根目录中创建 .env 文件,其中包含你的凭据:

.env

## 在此处使用启用了 Priority Fees 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 的客户端——包括如果只需要费用查询而不需要 RPC 或发送交易的裸 createEmptyClient()

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('Per-CU fees (micro-lamports):');
    console.log('  low     :', estimate.per_compute_unit.low);
    console.log('  medium  :', estimate.per_compute_unit.medium);
    console.log('  high    :', estimate.per_compute_unit.high);
    console.log('  extreme :', estimate.per_compute_unit.extreme);
    console.log('Recommended:', 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('Transaction confirmed!');
    console.log('Signature:', 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('Priority fee used (micro-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-rpc 中的 rpcTransactionPlanner。如果你需要直接控制,或者想要替换为你自己的自定义规划器,你可以使用异步 .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
Per-CU fees (micro-lamports):
  low     : 15000
  medium  : 101760
  high    : 612373
  extreme : 1826194
Recommended: 1108408

Transaction confirmed!
Signature: 3iEZx...WsbC

我应该使用哪个费用层级?

对于大多数用例,recommended 是一个安全的默认值,它反映了 Quicknode 根据最近网络活动提出的建议值。对于 DEX 交易或 NFT 铸造等时间敏感的交易,请使用 high;仅当网络严重拥堵且包含速度至关重要时,才使用 extreme

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

通过 account 参数将估算范围限定为你正在交互的程序。针对 Jupiter 程序(如本示例所示)的费用估算对于 Jupiter 互换而言将比全网络估算更准确。

发布插件

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

总结

在本指南中,你构建了一个独立的 Solana Kit 插件,它将 Quicknode 的 Priority Fee API 直接集成到客户端链中。在此过程中,你了解了 Kit 的柯里化插件模式如何使自定义功能可组合并完全类型化,如何同时公开完整的估算值和单值便捷方法,以及如何将动态获取的费用连接到交易规划器中,使其在发送时自动应用。

在此基础上,你可以采用相同的插件模式来封装其他 Quicknode 附加组件——代币元数据、DAS API 调用或自定义 RPC 方法——并将它们组合在一个 .use() 链中。你还可以将该插件作为独立的 npm 包发布,以便其他开发人员可以在他们的项目中使用它。

资源

我们 ❤️ 反馈!

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

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

0 条评论

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