如何使用 Solana Web3.js 2.0 利用 QuickNode 插件(第二部分)

  • QuickNode
  • 发布于 2025-01-06 16:39
  • 阅读 218

本文介绍了如何将多个QuickNode Add-ons集成到Solana Web3.js 2.0应用程序中,包括REST API的调用和与原生Solana方法的结合。

概述

在本指南的 第一部分 中,我们探讨了如何将 QuickNode 的优先费用附加组件与 Solana Web3.js 2.0 集成。在第二部分中,我们将基于此基础扩展,结合多个 QuickNode 附加组件,包括 REST APIs,并演示如何与原生 Solana 方法一起使用它们。

让我们开始吧!

你将会做什么

在本指南中,你将:

  • 将多个 QuickNode 附加组件集成到你的 Solana Web3.js 2.0 应用程序中
  • 在 Solana Web3.js 2.0 框架内实现 REST API 调用
  • 将 QuickNode 附加组件与原生 Solana 方法结合使用

你将需要什么

  • 完成本指南系列的 第一部分(我们将使用该指南中的代码作为起点)
  • 已安装 Node.js(版本 19 或更高)
  • TypeScript 经验以及已安装 ts-node
  • 具有访问 优先费用 API、Metis Jupiter V6 交换 API 和 IPFS 网关的 Solana 端点 的 QuickNode 账户(其他附加组件也可以工作,但你的代码可能会有所不同)。如果你没有这些附加组件,你可以从你的 QuickNode 仪表板 中添加它们。如果你还没有 QuickNode 账户,可以 在这里免费创建

本指南中使用的依赖项

依赖项 版本
@solana/web3.js ^2.0.0

扩展我们的项目

首先,复制我们在第一部分中创建的 example.ts 文件,并命名为 example2.ts。我们将使用这个新文件来实现我们更高级的功能。

cp example.ts example2.ts

现在,让我们更新我们的 example2.ts 文件,以包括我们新附加组件所需的导入和类型。

定义类型

首先,让我们为 Metis Jupiter V6 交换 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 类型

现在,让我们更新我们的 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 上提供了很大的灵活性。

同样,注意我们留出了空间以添加每个接口的其他方法。本指南的目的是展示如何集成附加组件方法,而不是 exhaustively 列出所有可用方法。可以探索 QuickNode 市场 及我们的 文档 来查找其他附加组件和方法以集成到你的应用程序中。

实现 REST API 调用

在之前的指南中,我们使用 createDefaultRpcTransport 函数创建了一个使用默认 JSON RPC 传输的运输方式。尽管这是一个良好的起点,但它不允许我们处理 REST API 调用(Metis API 和 QuickNode IPFS API 都需要 REST 调用)。要处理 REST API 调用,我们需要创建一个自定义传输,可以处理附加组件的特定要求(例如,端点、身份验证、请求头等)。

要在我们的 Solana Web3.js 2.0 框架内处理 REST API 调用,我们需要修改我们的 createQuickNodeTransport 函数,首先检查传递给传输函数的方法,然后相应地处理请求。如果方法未识别,我们可以默认为 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);
        }
    };
}

分解一下我们在这里所做的事情:

  1. 首先,我们将 params 更改为接受不仅是一个端点,还是一个可选的 metisEndpointipfsApiKey 参数。
  2. 接下来,如前所述,我们定义了一个默认的 JSON RPC 运输。
  3. 然后,我们使用 switch 语句检查传递给传输函数的方法。如果方法以 metis_ 开头,我们调用一个名为 handleMetisRequest 的新函数来处理 Metis API 请求(我们将使用这个处理器来处理所有 Metis 请求,不论具体的方法)。此外,如果方法是 ipfs_upload,我们调用一个新的函数 handleIpfsUpload 来处理 IPFS API 请求(我们仅使用这个处理器处理特定的方法)。如果方法未识别,我们返回默认的 JSON RPC 传输。

在这里需要注意的是,这个逻辑是完全可定制的。我们仅举例说明你如何使用 switch 语句来包含不同的 API 或方法,但你可以使用其他任何适合你应用程序的方法。

接下来让我们定义那些 API 处理器函数。

定义 Metis 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} 发起获取请求时出错: ${response.statusText}`);
    }

    const data = await response.json();
    return { result: data } as TResponse;
}

在这个函数中,我们正在做的事情:

  1. 如果未提供 Metis 端点,我们定义一个默认的 Metis 端点。
  2. 我们提取方法调用中的特定方法。
  3. 我们使用 Metis 端点和特定方法创建一个 URL 对象。
  4. 我们遍历对象的条目,并将每个键值对作为查询参数附加到 URL。
  5. 我们对 URL 进行获取请求,并将响应作为 TResponse 返回。

值得注意的是,我们将请求硬编码为 GET 请求。虽然这对我们的演示有效,但一些方法需要不同的请求类型(例如 swap 方法是一个 POST 请求)。你可以选择根据特定方法修改此函数以处理不同的请求类型,或者你可以根据请求类型创建单独的处理器(例如,handleMetisGetRequesthandleMetisPostRequest 等)。

定义 IPFS API 处理器

现在,让我们定义一个函数来处理 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} 发起获取请求时出错: ${response.statusText}`);
    }
    const data = await response.json();

    return { result: data } as T;
}

在这个函数中,我们正在做的事情:

  1. 检查是否提供了 IPFS API 密钥(与 Metis API 不同,IPFS API 没有公共端点,因此如果未提供密钥,则抛出错误)。
  2. 我们创建一个包含文件内容、文件名和文件类型的 File 对象。
  3. 创建一个 FormData 对象并根据 IPFS 文档 将文件附加到其中。
  4. 使用特定方法的 IPFS API 端点创建一个 URL 对象(与我们创建的 Metis 处理器不同,此处理器仅适用于单一方法)。
  5. 我们对 URL 发起异步请求,并将响应作为 TResponse 返回。

更新 RPC 创建函数

在之前的指南中,我们创建了一个用于创建我们的 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 函数非常相似。唯一的区别是:

  1. 我们传递了 CreateAddonsApiParams 接口,而不是 CreatePriorityFeeApiParams 接口(此接口包括我们的 Metis 和 IPFS 授权密钥)。
  2. 我们正在使用新的 createQuickNodeTransport 函数来创建传输。
  3. 我们使用 QuickNodeAddons 接口,而不是 PriorityFeeApi 接口。

如果我们一切都正确,此函数应返回一个 Rpc 接口,其中包括来自我们的 PriorityFeeApiMetisApiIpfsApi 接口的所有方法。让我们来测试一下!

使用组合功能

现在,让我们测试我们的新组合功能。将你的 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(`优先费用(每 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 报价(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 的输出,例如:

优先费用(每 CU 中等):29767
Metis 报价(Lamports):77848587
文件上传成功!CID: QmcPq5iyjgpuW1RdGKw55fV4RvWMNZYPHCmhPGXbCfbEC3

做得不错!

Solana 方法怎么办?

如果你注意到类型保护,你可能会对你的 quicknodeRpc 对象感到意外。你可能注意到 Intellisense 没有检测到任何原生 Solana 方法:

可用方法

这是由于我们构建 QuickNodeAddons 接口的方式。如前所述,我们排除了你附加组件定义的方法。

解决这个问题的一种方法是使用 Proxy 对象来拦截方法调用,并在它们执行之前修改它们。这可以通过 JavaScript 中的 Proxy 类完成。另一种方法是创建一个新的接口,该接口包括你附加组件的所有方法以及所有原生 Solana 方法。让我们试一下!

将以下导入添加到你的 example2.ts 文件:

import {
    // ... 现有导入
    SolanaRpcApi,
    address,
} from "@solana/web3.js";

现在,创建一个新的、更广泛的类型,包含你附加组件的所有方法以及所有原生 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 方法以及我们的自定义方法都有效:

Intellisense

继续在你的 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 APIs 到我们的 Solana Web3.js 2.0 应用程序中。我们演示了如何:

  1. 集成多个 QuickNode 附加组件
  2. 在 Solana Web3.js 2.0 框架内实现 REST API 调用
  3. 将 QuickNode 附加组件与原生 Solana 方法结合使用

这种方法允许在集成各种 QuickNode 附加组件和 API 时实现高度的定制化。我们希望你意识到这个过程允许很多灵活性,所以带上你的创意,构建一些惊人的东西!我们期待看到你的作品—在 QuickNode DiscordTwitter 上告诉我们你所构建的内容!

继续构建

要进一步提高你的 Solana 开发技能:

  1. 向你的 Metis 和 IPFS API 接口添加更多方法
  2. 探索其他 QuickNode 附加组件,并将它们集成到你的应用中
  3. 创建自己的 QuickNode 函数,将你的附加组件集成到你的应用中
  4. 在你的传输函数中实施错误处理和重试

额外资源

我们❤️反馈!

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

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

0 条评论

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