本文是系列教程的第二部分,展示如何使用Solana Kit(前Solana Web3.js 2.0)集成多个QuickNode附加组件,包括优先级费用API、Metis Jupiter Swap API和IPFS网关。通过自定义RPC传输和类型定义,实现REST API调用,并最终将自定义附加组件与原生Solana方法(如getAccountInfo)结合使用。文章提供了详细的代码示例和步骤,帮助开发者在Solana Kit框架中灵活集成QuickNode服务。
🛠️ 更新通知
本指南已更新,以反映 Solana Web3.js 2.0 的新名称 —— Solana Kit。我们遵循最新的最佳实践,帮助你保持与时俱进。在此了解 Solana Kit 更多信息。
在本指南的第一部分中,我们探讨了如何将 Quicknode 的 Priority Fees 插件与 Solana Kit 集成。在第二部分,我们将在此基础上进一步扩展,整合多个 Quicknode 插件(包括 REST API),并演示如何将它们与原生 Solana 方法结合使用。
让我们开始吧!
在本指南中,你将:
| 依赖 | 版本 |
|---|---|
| @solana/kit | ^2.0.0 |
首先,复制我们在第一部分创建的 example.ts 文件,并将其命名为 example2.ts。我们将使用这个新文件来实现高级功能。
cp example.ts example2.ts
现在,更新 example2.ts 文件,为新插件添加必要的导入和类型定义。
首先,为 Metis Jupiter Swap API(这些类型来自 Metis Jupiter API 文档)和 IPFS API(我们使用了 IPFS API 文档 的修改版本,因为我们将在 Transport 函数中操作请求)定义类型。
将以下代码添加到 types.ts 文件中:
// 保留你现有的类型...
declare const QuoteGetSwapModeEnum: {
readonly ExactIn: "ExactIn";
readonly ExactOut: "ExactOut";
};
type QuoteGetSwapModeEnum = typeof QuoteGetSwapModeEnum[keyof typeof QuoteGetSwapModeEnum];
declare const SwapMode: {
readonly ExactIn: "ExactIn";
readonly ExactOut: "ExactOut";
};
type SwapMode = typeof SwapMode[keyof typeof SwapMode];
interface QuoteGetRequest {
inputMint: string;
outputMint: string;
amount: number;
slippageBps?: number;
swapMode?: QuoteGetSwapModeEnum;
dexes?: Array<string>;
excludeDexes?: Array<string>;
restrictIntermediateTokens?: boolean;
onlyDirectRoutes?: boolean;
asLegacyTransaction?: boolean;
platformFeeBps?: number;
maxAccounts?: number;
}
interface PlatformFee {
amount?: string;
feeBps?: number;
}
interface RoutePlanStep {
swapInfo: SwapInfo;
percent: number;
}
interface SwapInfo {
ammKey: string;
label?: string;
inputMint: string;
outputMint: string;
inAmount: string;
outAmount: string;
feeAmount: string;
feeMint: string;
}
interface QuoteResponse {
inputMint: string;
inAmount: string;
outputMint: string;
outAmount: string;
otherAmountThreshold: string;
swapMode: SwapMode;
slippageBps: number;
platformFee?: PlatformFee;
priceImpactPct: string;
routePlan: Array<RoutePlanStep>;
contextSlot?: number;
timeTaken?: number;
}
interface IpfsUploadRequest {
filePath: string;
fileName: string;
fileType: string;
}
interface Pin {
cid: string;
name: string;
origins: string[];
meta: Record<string, any>;
}
interface Info {
size: string;
}
interface IpfsUploadResponse {
requestid: string;
status: string;
created: string;
pin: Pin;
info: Info;
delegates: string[];
}
export type {
// 现有的导出...
FeeEstimates,
EstimatePriorityFeesResponse,
EstimatePriorityFeesParams,
// 新导出...
QuoteGetRequest,
QuoteResponse,
IpfsUploadRequest,
IpfsUploadResponse
};
将新类型导入到 example2.ts 文件中:
import { QuoteGetRequest, QuoteResponse, IpfsUploadRequest, IpfsUploadResponse } from "./types";
另外,从 Node.js 标准库中导入 fs 以读取文件内容(上传文件到 IPFS 时需要):
import * as fs from "fs";
现在,更新 API 类型以包含 Metis 和 IPFS 方法。由于我们有多种类型的插件,让我们为每种类型创建一个新类型,然后将它们合并为一个类型,以便在 API 中使用。在 example2.ts 文件中的 PriorityFeeApi 类型后添加以下代码:
type PriorityFeeApi = {
qn_estimatePriorityFees(params: EstimatePriorityFeesParams): Promise<EstimatePriorityFeesResponse>;
}
type MetisApi = {
metis_quote(params: QuoteGetRequest): Promise<QuoteResponse>;
// 在此添加其他 Metis 方法,例如:
}
type IpfsApi = {
ipfs_upload(params: IpfsUploadRequest): Promise<IpfsUploadResponse>;
// 在此添加其他 IPFS 方法,例如:
}
type QuickNodeAddons = PriorityFeeApi & MetisApi & IpfsApi;
注意,我们使用 & 运算符合并类型。这允许我们创建一个单一类型,包含所有插件的方法。根据你的用例,你可以选择使用单独的类型,或创建一个仅包含所需方法的统一类型。该库在如何组织 API 方面提供了很大的灵活性。
另外注意,我们为每个接口预留了添加额外方法的空间。本指南的目的是展示如何集成一个插件方法,而不是提供所有可用方法的详尽列表。欢迎探索 Quicknode Marketplace 和我们的文档,寻找其他可以集成到应用中的插件和方法。
在之前的指南中,我们使用了 createDefaultRpcTransport 函数来创建一个使用默认 JSON RPC 传输的 transport。虽然这是一个好的起点,但它不允许我们处理 REST API 调用(而 Metis API 和 Quicknode IPFS API 都需要 REST 调用)。为了处理 REST API 调用,我们需要创建一个自定义 transport,能够处理我们插件的特定要求(例如,端点、认证、头部等)。
为了在 Solana Kit 框架内处理 REST API 调用,我们需要修改 createQuickNodeTransport 函数,首先检查传递给 transport 函数的方法,然后相应地处理请求。如果方法未被识别,我们可以默认使用 JSON RPC 传输。在 example2.ts 文件中更新它:
interface CreateAddonsApiParams {
endpoint: string;
metisEndpoint?: string;
ipfsApiKey?: string;
}
function createQuickNodeTransport({ endpoint, metisEndpoint, ipfsApiKey }: CreateAddonsApiParams): RpcTransport {
const jsonRpcTransport = createDefaultRpcTransport({ url: endpoint });
return async <TResponse>(...args: Parameters<RpcTransport>): Promise<TResponse> => {
const { method, params } = args[0].payload as { method: string; params: unknown };
switch (true) {
case method.startsWith('metis_'):
return handleMetisRequest<unknown, TResponse>(method, params, metisEndpoint);
case method === 'ipfs_upload':
return handleIpfsUpload<TResponse>(params as IpfsUploadRequest, ipfsApiKey);
default:
return jsonRpcTransport(...args);
}
};
}
让我们拆解一下这里做了什么:
metisEndpoint 和 ipfsApiKey 参数。metis_ 开头,我们调用一个新函数 handleMetisRequest 来处理 Metis API 请求(我们将使用这个处理程序来处理所有 Metis 请求,无论具体方法是什么)。此外,如果方法是 ipfs_upload,我们调用一个新函数 handleIpfsUpload 来处理 IPFS API 请求(我们只使用这个处理程序处理特定方法)。如果方法未被识别,我们返回默认的 JSON RPC 传输。这里需要注意的是,这个逻辑是完全可定制的。我们只是提供了一个示例,展示如何使用 switch 语句来包含不同的 API 或方法,但你可以使用任何其他适合你应用的方法。
接下来让我们定义这些 API 处理函数。
现在,定义一个函数来处理 Metis API 请求。我们将使用这个函数来处理所有 Metis API 请求,无论具体方法是什么。更新 example2.ts 文件:
async function handleMetisRequest<TParams, TResponse>(method: string, params: TParams, metisEndpoint?: string): Promise<TResponse> {
const DEFAULT_METIS_ENDPOINT = 'https://public.jupiterapi.com';
const jupiterMethod = method.replace('metis_', '');
const url = new URL(`${metisEndpoint || DEFAULT_METIS_ENDPOINT}/${jupiterMethod}`);
const paramsToUse = Array.isArray(params) ? params[0] : params;
if (typeof paramsToUse === 'object' && paramsToUse !== null) {
Object.entries(paramsToUse as Record<string, unknown>).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
}
const response = await fetch(url.toString(), {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
throw new Error(`向 ${url} 发起 fetch 请求时出错:${response.statusText}`);
}
const data = await response.json();
return { result: data } as TResponse;
}
这个函数中发生了什么:
值得注意,这里我们硬编码了请求为 GET 请求。虽然这对我们的演示有效,但某些方法需要不同的请求类型(例如,swap 方法是 POST 请求)。你可以选择修改此函数以根据具体方法处理不同的请求类型,或者根据请求类型创建单独的处理函数(例如,handleMetisGetRequest、handleMetisPostRequest 等)。
现在,定义一个函数来处理 IPFS API 请求。我们将使用这个函数来处理 ipfs_upload 方法。更新 example2.ts 文件:
async function handleIpfsUpload<T>(params: IpfsUploadRequest, ipfsApiKey?: string): Promise<T> {
if (!ipfsApiKey) {
throw new Error('未提供 IPFS API 密钥');
}
const { filePath, fileName, fileType } = params;
const fileContent = fs.readFileSync(filePath);
const file = new File([fileContent], fileName, { type: fileType });
const formData = new FormData();
formData.append("Body", file);
formData.append("Key", file.name);
formData.append("ContentType", file.type);
const url = new URL('https://api.quicknode.com/ipfs/rest/v1/s3/put-object');
const response = await fetch(url, {
method: 'POST',
headers: {
'x-api-key': ipfsApiKey,
},
body: formData,
redirect: "follow",
});
if (!response.ok) {
throw new Error(`向 ${url} 发起 fetch 请求时出错:${response.statusText}`);
}
const data = await response.json();
return { result: data } as T;
}
这个函数中发生了什么:
在之前的指南中,我们创建了一个函数来创建我们的 PriorityFeeApi,createPriorityFeeApi。由于我们的新接口包含多个插件,让我们创建一个更通用的函数来支持我们的 QuickNodeApi 接口。在 example2.ts 文件中添加一个 createAddonsApi 函数:
function createAddonsApi(params: CreateAddonsApiParams): Rpc<QuickNodeAddons> {
const api = createJsonRpcApi<QuickNodeAddons>({
requestTransformer: (request: RpcRequest<any>) => request.params[0],
responseTransformer: (response: any) => response.result,
});
const transport = createQuickNodeTransport(params);
return createRpc({ api, transport });
}
这个函数与我们之前在指南中创建的 createPriorityFeeApi 函数非常相似。唯一的区别是:
createQuickNodeTransport 函数来创建 transport。如果我们正确地完成了所有操作,这个函数应该返回一个包含 PriorityFeeApi、MetisApi 和 IpfsApi 接口中所有方法的 Rpc 接口。让我们测试一下!
现在,让我们测试新的组合功能。将 example2.ts 文件中的 main 函数替换为以下代码:
async function main() {
const quickNodeRpc = createAddonsApi({
endpoint: 'https://example.solana-mainnet.quiknode.pro/123456/', // 👈 替换为你的 Quicknode Solana 主网端点
ipfsApiKey: 'QN_REPLACE_WITH_YOUR_IPFS_API_KEY', // 👈 替换为你的 Quicknode IPFS API 密钥
// metisEndpoint: '' // 👈 (可选) 取消注释并替换为你的 Metis 端点
});
try {
const priorityFees = await quickNodeRpc.qn_estimatePriorityFees({
account: 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4',
last_n_blocks: 100,
api_version: 2
}).send();
console.log(`Priority Fees (Med Per CU): ${priorityFees.per_compute_unit.medium}`);
} catch (error) {
console.error('估算优先费用时出错:', error);
}
try {
const metisQuote = await quickNodeRpc.metis_quote({
inputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
outputMint: "So11111111111111111111111111111111111111112",
amount: 10 * 1e6,
}).send();
console.log(`Metis Quote (lamports): ${metisQuote.outAmount}`);
} catch (error) {
console.error('获取 Metis 报价时出错:', error);
}
try {
const result = await quickNodeRpc.ipfs_upload({
filePath: 'test.png', // 👈 替换为你的文件路径
fileName: 'test.png', // 👈 替换为你的文件名
fileType: 'image/png', // 👈 替换为你的文件类型
}).send();
console.log('文件上传成功!CID:', result.pin.cid);
} catch (error) {
console.error('上传文件时出错:', error);
}
}
main().catch(console.error);
确保将端点 URL、IPFS API 密钥和文件详情替换为实际值。你可以在 Quicknode 控制面板 中找到你的 Quicknode 端点和 IPFS API 密钥。
要运行我们的高级示例,请使用以下命令:
ts-node example2.ts
你应该会看到输出包含估算的优先费用、报价金额以及上传文件的 CID,例如:
Priority Fees (Med Per CU): 29767
Metis Quote (lamports): 77848587
File uploaded successfully! CID: QmcPq5iyjgpuW1RdGKw55fV4RvWMNZYPHCmhPGXbCfbEC3
干得不错!
如果你注意类型守卫,可能会发现 quicknodeRpc 对象有一些意料之外的情况。你可能已经注意到 Intellisense 没有检测到任何原生 Solana 方法:

这是由于我们构建 QuickNodeAddons 接口的方式。目前,我们只限于插件中定义的方法。
一种解决方法是使用 Proxy 对象来拦截方法调用并在执行前修改它们。这可以通过 JavaScript 中的 Proxy 类来实现。另一种方法是创建一个新的接口,包含所有插件的方法和所有原生 Solana 方法。让我们试试看!
在 example2.ts 文件中添加以下导入:
import {
// ... 现有导入
SolanaRpcApi,
address,
} from "@solana/kit";
现在,创建一个新的、更广泛的类型,包含所有插件的方法和所有原生 Solana 方法。在 example2.ts 文件中添加以下代码:
type QuickNodeSolana = SolanaRpcApi & QuickNodeAddons;
SolanaRpcApi 接口是一个类型,它包含一个类似我们插件接口的接口,用于每个原生 Solana 方法。现在,更新 createAddonsApi 函数,使其返回一个 QuickNodeSolana 接口而不是 QuickNodeAddons 接口:
export function createQuickNodeApi(params: CreateAddonsApiParams): Rpc<QuickNodeSolana> {
const METHODS_TO_TRANSFORM_PARAMS = [`qn_estimatePriorityFees`, `metis_quote`, `ipfs_upload`];
const quickNodeApi = createJsonRpcApi<QuickNodeSolana>({
requestTransformer: (request: RpcRequest<any>) => {
if (METHODS_TO_TRANSFORM_PARAMS.includes(request.methodName)) {
return Array.isArray(request.params) ? request.params[0] : request.params;
} else {
return request.params;
}
},
responseTransformer: (response: any) => response.result,
});
const transport = createQuickNodeTransport(params);
return createRpc({ api: quickNodeApi, transport });
}
这与我们之前所写的基本完全相同,但我们现在使用了 QuickNodeSolana 接口而不是 QuickNodeAddons 接口,并且在执行参数转换之前对方法进行了检查。这允许我们拦截并转换自定义方法的参数,同时允许原生 Solana 方法的参数通过。确保更新 main 函数以调用 createQuickNodeApi 函数,就这么简单!你现在应该会看到 Intellisense 对所有原生 Solana 方法以及自定义方法都有效:

继续在 example2.ts 文件的 main 函数中添加以下代码:
try {
const info = await quickNodeRpc.getAccountInfo(address('JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4')).send();
console.log(info.value);
} catch (error) {
console.error('获取账户信息时出错:', error);
}
如果你不想再上传另一个文件,请确保在 main 函数中删除或注释掉 ipfs_upload 调用。再次运行脚本,你应该会看到 getAccountInfo 调用的输出紧挨着自定义方法的输出!🎉
干得漂亮!
在本指南中,我们在之前知识的基础上进行了扩展,将多个 Quicknode 插件(包括 REST API)集成到了 Solana Kit 应用中。我们演示了如何:
这种方法允许高度自定义,以无缝集成各种 Quicknode 插件和 API。我们希望你已经看到这个过程提供了很大的灵活性,所以发挥你的创造力,构建一些令人惊叹的东西吧!我们很期待看到你的成果——在 Quicknode Discord 或 Twitter 上给我们留言,告诉我们你构建了什么!
为了进一步提升你的 Solana 开发技能:
告诉我们 如果你有任何反馈或对新主题的请求。我们期待你的来信。
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码