这篇文章详细介绍了如何使用 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 获取费用估算,并将其作为类型化的客户端方法公开| 依赖项 | 版本 |
|---|---|
| @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、payer 等)已附加的内容。.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.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";
/** 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_unit 和 per_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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!