如何使用 TypeScript 创建 QuickNode 函数

  • QuickNode
  • 发布于 2024-04-22 16:33
  • 阅读 11

本文介绍了如何使用TypeScript和QuickNode Functions来管理和获取Solana钱包的投资组合,包括设置开发环境、创建和部署函数、以及测试函数的过程。

概述

QuickNode Functions 允许开发者快速构建和部署无服务器函数,这些函数可以与区块链数据进行交互。在本指南中,我们将引导你使用 TypeScript 创建一个 QuickNode Function,用于管理动态的 Solana 钱包组合并获取它们的余额。

你将做什么

在本指南中,你将:

  1. 设置 TypeScript 开发环境
  2. 使用 QuickNode 的 Key-Value Store 创建一个用于管理 Solana 钱包组合的 QuickNode Function
  3. 构建并部署你的函数
  4. 使用 curl 命令测试你的函数

你需要什么

设置你的环境

首先,让我们创建一个新的项目目录并初始化它:

mkdir solana-portfolio-manager && cd solana-portfolio-manager

接下来,使用以下命令初始化你的项目:

npm init -y

现在,让我们安装必要的依赖:

npm install @solana/web3.js@1

在项目根目录下创建一个 tsconfig.json 文件:

npx tsc --init

打开 tsconfig.json 并更新为以下内容:

{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "outDir": "./",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

创建一个 src 目录,并在其中创建一个 index.ts 文件:

mkdir src && echo > src/index.ts

构建你的 QuickNode Function

让我们从创建函数开始。该函数将管理一个动态的 Solana 钱包地址列表。它将允许用户创建一个钱包列表,通过添加/删除地址来更新列表,检索列表,并获取列表中钱包的余额。为了在单个函数中启用多个指令,我们将使用 switch 语句来处理不同的指令。

导入依赖并定义常量

在代码编辑器中打开 src/index.ts 并添加以下代码:

import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';

interface QNLib {
    qnUpsertList: (key: string, options: { add_items?: string[], remove_items?: string[] }) => Promise<any>;
    qnGetList: (key: string) => Promise<string[]>;
}
declare const qnLib: QNLib;
const ENDPOINT = 'https://example.solana-mainnet.quiknode.pro/123456'; // 👈 替换为你的端点 URL

在这里,我们从 @solana/web3.js 库中导入了一些依赖项——我们将使用这些依赖项来验证用户输入并从 Solana 区块链中获取数据。我们还定义了一个 QNLib 接口,这将允许我们在使用 QuickNode Functions 全局作用域中可用的 qnLib 对象 时确保类型安全。

最后,我们用你自己的端点 URL 定义了 ENDPOINT 变量。你可以在 QuickNode 仪表板的 Endpoints 选项卡 中找到它。

定义类型和接口

现在我们已经定义了依赖项和常量,让我们定义将在函数中使用的类型和接口,以帮助实现类型安全和更好的代码组织。在常量下方添加以下代码:

type Instruction = 'createPortfolio' | 'updatePortfolio' | 'getPortfolio' | 'getPortfolioBalances';

interface UserData {
    instruction: Instruction;
    portfolioName: string;
    addAddresses?: string[];
    removeAddresses?: string[];
}

interface FunctionParams {
    user_data: UserData;
}

interface FunctionResult {
    message: string;
    portfolioName: string;
    addresses?: string[];
    balances?: { address: string; balance: number }[];
    error?: string;
}

让我们逐一了解这些类型和接口的职责:

  1. Instruction:这是一个 类型,定义了函数可以执行的不同指令。在本例中,我们定义了四个指令:createPortfolioupdatePortfoliogetPortfoliogetPortfolioBalances
  2. UserData:此接口定义了将传递给函数的数据。它包含三个属性:instruction(一个 Instruction)、portfolioName(组合的唯一标识符)以及 addAddressesremoveAddresses(Solana 钱包地址)。
  3. FunctionParams:此接口定义了将传递给函数的参数。它包含一个属性:类型为 UserDatauser_data
  4. FunctionResult:此接口定义了函数将返回的结果。所有响应必须包括 messageportfolioName 属性。根据指令的不同,响应还可能包括 addressesbalanceserror 属性。

添加辅助函数

让我们创建两个辅助函数来验证用户输入并处理错误。将以下代码添加到 index.ts 文件的底部:

function isValidSolanaAddress(address: string): boolean {
    try {
        new PublicKey(address);
        return true;
    } catch (error) {
        return false;
    }
}

function validateInput(params: FunctionParams): void {
    if (!ENDPOINT) {
        throw new Error('SOLANA_RPC_ENDPOINT is not set');
    }

    const validInstructions: Instruction[] = ['createPortfolio', 'updatePortfolio', 'getPortfolio', 'getPortfolioBalances'];
    if (!validInstructions.includes(params.user_data.instruction)) {
        throw new Error(`Invalid instruction: ${params.user_data.instruction}. Must be one of: ${validInstructions.join(', ')}`);
    }

    if (!params.user_data.portfolioName) {
        throw new Error('Portfolio name is required');
    }

    if (params.user_data.instruction === 'updatePortfolio') {
        if (!params.user_data.addAddresses && !params.user_data.removeAddresses) {
            throw new Error('At least one of addAddresses or removeAddresses is required for updatePortfolio instruction');
        }

        if (params.user_data.addAddresses) {
            const invalidAddAddresses = params.user_data.addAddresses.filter(addr => !isValidSolanaAddress(addr));
            if (invalidAddAddresses.length > 0) {
                throw new Error(`Invalid Solana addresses: ${invalidAddAddresses.join(', ')}`);
            }
        }
    }
}

在这里,我们定义了两个辅助函数:

  • isValidSolanaAddress:用于验证用户输入的 Solana 地址。它通过尝试从输入字符串创建一个新的 PublicKey 对象来检查输入是否是有效的 Solana 地址。如果输入不是有效的 Solana 地址,则返回 false。否则,返回 true
  • validateInput:用于验证端点以及用户输入的组合名称和指令。它检查是否提供了有效的指令以及组合名称是否为空。如果提供了 updatePortfolio 指令,它还检查是否至少提供了 addAddressesremoveAddresses 中的一个。如果向组合中添加新地址,它还检查地址是否是有效的 Solana 地址。如果这些条件中的任何一个未满足,它将抛出错误。

定义指令处理程序

接下来,我们将为每个指令定义一个指令处理程序。将以下代码添加到 index.ts 文件的底部:

async function createPortfolio(portfolioName: string): Promise<FunctionResult> {
    await qnLib.qnUpsertList(portfolioName, { add_items: [] });
    return {
        message: `Portfolio ${portfolioName} created successfully.`,
        portfolioName
    };
}

async function updatePortfolio(portfolioName: string, addAddresses: string[] = [], removeAddresses: string[] = []): Promise<FunctionResult> {
    await qnLib.qnUpsertList(portfolioName, { add_items: addAddresses, remove_items: removeAddresses });
    const updatedPortfolio = await qnLib.qnGetList(portfolioName);
    return {
        message: `Updated portfolio ${portfolioName}. Added ${addAddresses.length} addresses, removed ${removeAddresses.length} addresses.`,
        portfolioName,
        addresses: updatedPortfolio
    };
}

async function getPortfolio(portfolioName: string): Promise<FunctionResult> {
    const addresses = await qnLib.qnGetList(portfolioName);
    return {
        message: `Retrieved portfolio ${portfolioName}.`,
        portfolioName,
        addresses
    };
}

async function getPortfolioBalances(portfolioName: string): Promise<FunctionResult> {
    const addresses = await qnLib.qnGetList(portfolioName);

    // @ts-ignore - Already validated in validateInput
    const connection = new Connection(ENDPOINT);

    const balances = await Promise.all(
        addresses.map(async (address) => {
            const publicKey = new PublicKey(address);
            const balance = await connection.getBalance(publicKey);
            return {
                address,
                balance: balance / LAMPORTS_PER_SOL
            };
        })
    );

    return {
        message: `Retrieved balances for portfolio ${portfolioName}.`,
        portfolioName,
        balances
    };
}

在这里,我们定义了四个指令处理程序:

  • createPortfolio:使用 qnLib.upsertList 方法创建一个具有空地址列表的新组合。
  • updatePortfolio:使用 qnLib.upsertList 方法通过添加或删除地址来更新现有组合。
  • getPortfolio:使用 qnLib.getList 方法检索给定组合的地址列表。
  • getPortfolioBalances:使用 @solana/web3.js 库检索给定组合的余额。

定义主函数

现在我们已经定义了辅助函数和指令处理程序,让我们定义将作为入口点的 main 函数。将以下代码添加到 index.ts 文件的底部:

export async function main(params: FunctionParams): Promise<FunctionResult> {
    try {
        validateInput(params);

        const { instruction, portfolioName, addAddresses, removeAddresses } = params.user_data;

        switch (instruction) {
            case 'createPortfolio':
                return await createPortfolio(portfolioName);
            case 'updatePortfolio':
                return await updatePortfolio(portfolioName, addAddresses, removeAddresses);
            case 'getPortfolio':
                return await getPortfolio(portfolioName);
            case 'getPortfolioBalances':
                return await getPortfolioBalances(portfolioName);
            default:
                throw new Error('Invalid instruction');
        }
    } catch (error) {
        console.error('Error:', error);
        return {
            message: 'An error occurred',
            portfolioName: params.user_data.portfolioName,
            error: error instanceof Error ? error.message : String(error)
        };
    }
}

在这里,我们简单地验证输入参数,然后根据 user_data 对象中的 instruction 属性调用相应的指令处理程序。如果在验证或执行过程中发生任何错误,我们会捕获它们并返回一个包含 error 属性的 FunctionResult 对象。

创建构建和打包脚本

现在我们已经有了函数代码,让我们设置构建过程。将以下脚本添加到你的 package.json 中:

  "scripts": {
    "build": "tsc",
  },

这将创建一个 build 脚本,它将编译你的 TypeScript 代码并在项目根目录下创建一个 index.js 文件。这将是我们函数的入口点。

构建并打包你的函数

要构建并打包你的函数,请运行:

npm run build

这将编译你的 TypeScript 代码并在项目根目录下创建一个 index.js 文件。

将你的函数部署到 QuickNode

现在你已经构建了你的函数,你可以将其部署到 QuickNode。前往 QuickNode 仪表板的 Function 页面,然后点击 + 创建函数 按钮。

环境 设置为 Node.js 20,然后点击 创建函数

你可以定义函数的名称和描述,并使用默认设置:

函数设置

点击 创建函数 进入下一步。

在这里,你需要从下拉菜单中选择 Code。然后,你可以粘贴你刚刚创建的 index.js 文件的内容。完成后,点击 保存并关闭。你可以忽略“测试参数”部分,其中包括我们在本指南中不会使用的链。

干得好!你的函数现在已部署到 QuickNode。你应该会看到类似以下内容:

你的函数已部署

让我们测试一下(你将需要你的函数端点 URL 和 API 密钥——见上图)。

测试你的 QuickNode Function

现在你的函数已部署,你可以使用 curl 命令或任何类似 Postman 的工具进行测试。将 YOUR_FUNCTION_URLYOUR_API_KEY 替换为你在 QuickNode 中的实际函数 URL 和 API 密钥。

创建一个组合

继续创建一个名为 "TEST" 的组合。请注意,就像我们的函数一样,我们的参数是通过 user_data 对象传递的。我们将 instruction 定义为 createPortfolio,并将 portfolioName 定义为 TEST

curl -X POST "YOUR_FUNCTION_URL?result_only=true" \
  -H "accept: application/json" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "user_data": {
      "instruction": "createPortfolio",
      "portfolioName": "TEST"
    }
  }'

请注意,我们在 URL 中包含了 ?result_only=true,以便仅返回函数调用的结果。

你应该会看到类似以下的响应:

{
    "message": "Portfolio TEST created successfully.",
    "portfolioName": "TEST"
}

向组合中添加地址

现在让我们向我们的组合中添加几个地址。请注意,我们使用的是 updatePortfolio 指令,并传入要添加到组合中的地址数组。随意将下面的地址替换为你自己的一个或多个地址:

curl -X POST "YOUR_FUNCTION_URL?result_only=true" \
  -H "accept: application/json" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "user_data": {
      "instruction": "updatePortfolio",
      "portfolioName": "TEST",
      "addAddresses": ["7v91N7iZ9mNicL8WfG6cgSCKyRXydQjLh6UYBWwm6y1Q"]
    }
  }'

请注意,我们使用的是与之前相同的 portfolioName。你应该会看到类似以下的响应:

{
    "addresses": [\
        "7v91N7iZ9mNicL8WfG6cgSCKyRXydQjLh6UYBWwm6y1Q"\
    ],
    "message": "Updated portfolio TEST. Added 1 addresses, removed 0 addresses.",
    "portfolioName": "TEST"
}

获取组合余额

最后,让我们获取我们的组合的余额。请注意,我们使用的是 getPortfolioBalances 指令,并传入与之前相同的 portfolioName("TEST"):

curl -X POST "YOUR_FUNCTION_URL?result_only=true" \
  -H "accept: application/json" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "user_data": {
      "instruction": "getPortfolioBalances",
      "portfolioName": "TEST"
    }
  }'

你应该会看到类似以下的响应:

{
    "balances": [\
        {\
            "address": "7v91N7iZ9mNicL8WfG6cgSCKyRXydQjLh6UYBWwm6y1Q",\
            "balance": 0\
        }\
    ],
    "message": "Retrieved balances for portfolio TEST.",
    "portfolioName": "TEST"
}

干得好!

结论

恭喜!你已成功使用 TypeScript 创建了一个 QuickNode Function,用于管理 Solana 钱包组合。该函数演示了如何与 QuickNode 的 Key-Value Store 和 Solana 区块链进行交互,以创建、更新和检索组合信息。

要了解更多关于 QuickNode Functions 的信息并探索其他功能,请查看我们的 文档 并加入我们的 Discord 社区 以获得支持和讨论。

想继续使用 Functions 进行构建吗?查看我们的 Functions 库 获取更多示例和灵感。

我们 ❤️ 反馈!

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

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

0 条评论

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