本文详细介绍了如何使用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 获取费用估算,并将其公开为类型化的客户端方法| 依赖项 | 版本 |
|---|---|
| @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 插件是一个柯里化函数:不是一次性接收所有参数,而是分组接收。外部函数接收一个配置对象并返回一个新函数,该新函数再接收客户端。这样可以在设置时一次性配置插件,并在多个客户端之间复用生成的函数。
const myPlugin = (config) => (client) => ({ ...client, newMethod: ... });
存在三个不同的层次:
.use() 调用附加的所有内容(RPC、付款人等)。.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_unit 和 per_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-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
每 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码