文章/教程 如何使用 Kinobi 创建 Anchor 程序客户端

Fifi · 2024年07月01日 · 172 次阅读

原文:https://www.quicknode.com/guides/solana-development/anchor/kinobi-client

作者:Aaron Milano

概述

Kinobi 是一组库,它是一个强大的工具,可以用来为现有的 Solana 程序生成 JavaScript、Umi (JavaScript) 和 Rust 客户端。Kinobi 最近增加了从 Anchor IDLs 生成客户端的支持,所以我们现在可以使用 Kinobi 为 Anchor 程序创建客户端。在使用 Anchor 构建和测试新程序时,这可以节省时间。本指南将向你展示如何使用 Kinobi 为你的 Anchor 程序生成客户端。

你要做什么

  • 创建一个简单的 Anchor 程序
  • 编写一个脚本,使用 Kinobi 为程序生成一个客户端
  • 测试客户端

你需要什么

本指南假设你对 Solana 编程和 Anchor 有基本的了解:

在开始之前,请确保你已安装了以下软件:

原文推荐使用 Anchor 0.30,还是建议使用 Anchor 0.29,0.30 改动了多处,已经不兼容 0.29,更多内容请看 https://soldev.cn/topics/4

检查你的 Solana 和 Anchor 版本 本指南适用于 Solana CLI 1.18.16 及以上版本。 - 在终端中运行 `solana --version` 来检查你的 Solana 版本。如果你需要更新,请遵循 [这里](https://docs.solanalabs.com/cli/install) 的说明。 - 在终端中运行 `anchor --version` 来检查你的 Anchor 版本。如果你需要更新,请遵循 [这里](https://www.anchor-lang.com/docs/installation) 的说明。

让我们开始吧!

什么是 Kinobi?​

Kinobi 是由 Metaplex 基金会创建的一个库,旨在为 Solana 程序生成客户端。Kinobi 的工作原理是通过传递一个或多个程序的 IDL 来生成 Kinobi,这是一棵可以被访问者更新的节点树。这些访问者可以根据需要更新说明或帐户。随后,与语言无关的渲染访问者可以生成各种语言的客户端,以便你可以管理客户端堆栈/依赖项。

Kinobi 是如何工作的

  1. 程序定义:你定义你的 Solana 程序和相应的 IDLs。
  2. 抽象:Kinobi 创建了一个与语言无关的节点树 (或客户端代表),访问者可以更新它。
  3. 客户端生成:Kinobi 的访问者处理树并生成特定语言的客户端。

来源:Metaplex Developers

关键元素

  • 程序:与 IDLs 关联的 Solana 程序。
  • IDLs:描述 Solana 程序的接口和功能。
  • Kinobi 树:编组 IDLs,促进客户端生成。
  • 访问者:为特定语言定制客户端生成过程的模块。
  • 依赖项:包括必要的库和实用程序,如 HTTP 接口、RPC 接口和加密工具。

近期,Kinobi 增加了对从 Anchor IDLs 生成客户端的支持。这意味着你现在可以使用 Kinobi 为 Anchor 程序生成客户端。让我们看看怎么做。

创建 Anchor 程序

首先,让我们创建一个简单的 Anchor 程序。我们将创建一个有两条指令的程序:

  1. initialize:初始化数据帐号时使用 u64 值。
  2. set_data:设置数据帐号的值。

初始化项目

创建一个新的项目目录,并运行以下命令来创建一个新的 Anchor 程序:

anchor init kinobi-test

切换到项目目录:

cd kinobi-test

安装依赖项

项目初始化后,可以运行 npm install,来确保安装了依赖项。然后我们将安装一些额外的依赖项。在终端中运行以下命令:

npm install @kinobi-so/nodes-from-anchor @kinobi-so/renderers @kinobi-so/visitors-core @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults

这将安装一些 Kinobi 包,包括 nodes-from-anchor 包,它将帮助我们从 Anchor IDL 生成 Kinobi 树。

更新 TSConfig

resolveJsonModule 添加到 tsconfg.json 中,以确保我们可以加载 IDL JSON 对象来生成客户端和 DOM 到 lib 数组中,这样我们就可以在 Node.js 中运行我们的脚本。更新目录中的 tsconfig.json 文件,使其看起来像这样:

{
    "compilerOptions": {
      "types": ["mocha", "chai"],
      "typeRoots": ["./node_modules/@types"],
      "lib": ["ES2020", "DOM"],
      "module": "commonjs",
      "target": "es6",
      "esModuleInterop": true,
      "resolveJsonModule": true,
 }
}

编写 Anchor 程序

下面让我们来编写 Anchor 程序。打开 programs/kinobi-test/src/lib.rs 文件,用以下代码替换内容,注意不要覆盖 declare_id! 宏:

use anchor_lang::prelude::*;

declare_id!("YOUR_PROGRAM_ID_HERE"); // Replace with your program ID

#[program]
pub mod kinobi_test {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        ctx.accounts.pda.set_inner(ExampleStruct {
            data: 0,
            authority: *ctx.accounts.payer.key,
 });
        Ok(())
 }

    pub fn set_data(ctx: Context<SetData>, data: u32) -> Result<()> {
        ctx.accounts.pda.data = data;
        Ok(())
 }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
 #[account(mut)]
    payer: Signer<'info>,

 #[account(
 init,
 payer = payer,
 space = 45,
 seeds = [b"example".as_ref(), payer.key().as_ref()],
        bump
 )]
    pda: Account<'info, ExampleStruct>,

    system_program: Program<'info, System>,
}


#[derive(Accounts)]
pub struct SetData<'info> {
 #[account(mut)]
    authority: Signer<'info>,

 #[account(
        mut,
 has_one = authority,
 seeds = [b"example".as_ref(), authority.key().as_ref()],
        bump
 )]
    pda: Account<'info, ExampleStruct>,
}

#[account]
pub struct ExampleStruct {
    pub data: u32,
    pub authority: Pubkey,
}

这是一个基本的 Anchor 程序,允许用户初始化一个 ExampleStruct(一个拥有 u32 数据和一个 authority PublicKey 的 PDA)并设置数据值。PDAs 的 seed 是付款人的密钥和字符串"example"。你可以随意使用其他程序,或者根据需要修改这个程序——它仅用于演示。

构建并测试程序

现在我们有了程序,可以构建和测试它了。在终端中运行以下命令:

anchor build

这可能需要几分钟,但应该不会出现任何错误。当它运行时,让我们编写一个简单的测试脚本。打开你的 Anchor 生成的测试文件 tests/kinobi-test.ts,并用以下代码替换内容:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KinobiTest } from "../target/types/kinobi_test";

describe("kinobi-test", () => {
  anchor.setProvider(anchor.AnchorProvider.env());
  const program = anchor.workspace.KinobiTest as Program<KinobiTest>;
  const [pda] = anchor.web3.PublicKey.findProgramAddressSync(
 [
      Buffer.from("example"),
      program.provider.publicKey.toBuffer()
 ],
    program.programId
 )
  it("Is initialized!", async () => {
    const tx = await program.methods
 .initialize()
 .accountsStrict({
        payer: program.provider.publicKey,
        pda,
        systemProgram: anchor.web3.SystemProgram.programId
 })
 .rpc();
 });
  it("Can set data!", async () => {
    const tx = await program.methods
 .setData(10)
 .accountsStrict({
        authority: program.provider.publicKey,
        pda
 })
 .rpc({skipPreflight: true});
 });
});

这个脚本将测试我们程序中的两个指令。第一个测试将初始化数据帐户,第二个测试将数据值设置为 10。继续运行测试脚本:

anchor test

你应该会看到类似下面的内容:

kinobi-test
  ✔ Is initialized! (450ms)
  ✔ Can set data! (463ms)


2 passing (916ms)

Done in 2.80s.

干得漂亮!

用 Kinobi 生成一个客户端

你的测试已经成功运行,Anchor 应该已经为你在 target/idl/kinobi_test.json 中自动生成了一个 IDL。定位此文件——我们将在下一节中使用它(注意:如果你为你的 Anchor 项目使用了不同的名称,此文件路径可能略有不同)。我们现在可以使用 Kinobi 为这个程序生成一个客户端。

在根目录下创建一个新文件夹,clients,并创建两个新文件:

  1. generate-client.ts 用于客户端生成脚本
  2. example.ts 用于尝试生成客户端

生成客户端

打开 generate-client.ts,将以下代码添加到文件中:

import { AnchorIdl, rootNodeFromAnchorWithoutDefaultVisitor } from "@kinobi-so/nodes-from-anchor";
import { renderJavaScriptUmiVisitor, renderJavaScriptVisitor, renderRustVisitor } from "@kinobi-so/renderers";
import { visit } from "@kinobi-so/visitors-core";
import anchorIdl from "../target/idl/kinobi_test.json"; // Note: if you initiated your project with a different name, you may need to change this path

async function generateClients() {
    const node = rootNodeFromAnchorWithoutDefaultVisitor(anchorIdl as AnchorIdl);

    const clients = [
        { type: "JS", dir: "clients/generated/js/src", renderVisitor: renderJavaScriptVisitor },
        { type: "Umi", dir: "clients/generated/umi/src", renderVisitor: renderJavaScriptUmiVisitor },
        { type: "Rust", dir: "clients/generated/rust/src", renderVisitor: renderRustVisitor }
    ];

    for (const client of clients) {
        try {
            await visit(
                node,
                await client.renderVisitor(client.dir)
            ); console.log(`✅ Successfully generated ${client.type} client for directory: ${client.dir}!`);
        } catch (e) {
            console.error(`Error in ${client.renderVisitor.name}:`, e);
            throw e;
        }
    }
}

generateClients();

我们来分析一下这个脚本的作用:

  1. 导入必要的 Kinobi 包。
  2. 导入由 Anchor 生成的 IDL 文件并从中创建 Kinobi 树(使用 rootNodeFromAnchorWithoutDefaultVisitor 函数)。
  3. 定义要生成的客户端(JavaScript、Umi 和 Rust)——可以随意注释掉任何你不需要的内容,并根据需要调整目录。
  4. 迭代客户端并使用 visit 函数使用适当的渲染访问者生成客户端。

运行脚本

现在我们有了脚本,可以运行它来生成客户端。在终端中运行以下命令:

ts-node clients/generate-client.ts

你应该会看到类似下面的输出:

ts-node clients/generate-client.ts
Successfully generated JS client for directory: clients/generated/js/src!
Successfully generated Umi client for directory: clients/generated/umi/src!
Successfully generated Rust client for directory: clients/generated/rust/src!

现在,你应该已经在 clients 目录中为程序生成了客户端。干得漂亮!

现在可以使用这些客户端与程序交互了。

测试客户端

打开你之前创建的 example.ts 文件,添加以下代码:

import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import { TransactionBuilderSendAndConfirmOptions, generateSigner, keypairIdentity, sol } from '@metaplex-foundation/umi';
import { publicKey as publicKeySerializer, string } from '@metaplex-foundation/umi/serializers';
import { getKinobiTestProgramId } from './generated/umi/src/programs/kinobiTest';
import { initialize, setData } from './generated/umi/src/instructions';

const umi = createUmi('http://127.0.0.1:8899', { commitment: 'processed' });
const creator = generateSigner(umi);
umi.use(keypairIdentity(creator));

const options: TransactionBuilderSendAndConfirmOptions = {
    confirm: { commitment: 'processed' }
};

const pda = umi.eddsa.findPda(getKinobiTestProgramId(umi), [
    string({ size: 'variable' }).serialize('example'),
    publicKeySerializer().serialize(creator.publicKey),
]);

async function logPda() {
    console.log(`PDA: ${pda.toString()}`);
}

async function airdropFunds() {
    try {
        await umi.rpc.airdrop(creator.publicKey, sol(100), options.confirm);
        console.log(`1. ✅ - Airdropped 100 SOL to the ${creator.publicKey.toString()}`);
    } catch (error) {
        console.error('1. ❌ - Error airdropping SOL to the wallet.', error);
    }
}

async function initializeAccount() {
    try {
        await initialize(umi, { pda, payer: creator }).sendAndConfirm(umi, options);
        console.log('2. ✅ - Initialized the account.');
    } catch (error) {
        console.error('2. ❌ - Error initializing the account.', error);
    }
}

async function setDataAccount(num: number, value: number) {
    try {
        await setData(umi, { authority: creator, pda, data: value }).sendAndConfirm(umi, options);
        console.log(`${num}. ✅ - Set data to ${value}.`);
    } catch (error) {
        console.error(num, '. ❌ - Error setting data.', error);
    }
}

async function main() {
    await logPda();
    await airdropFunds();
    await initializeAccount();
    await setDataAccount(3, 10);
    await setDataAccount(4, 20);
    await setDataAccount(5, 30);
    await setDataAccount(6, 40);
}

main().then(() => {
    console.log('🚀 - Done!');
}).catch((error) => {
    console.error('❌ - Error:', error);
});

这个脚本将会:

  • 导入必要的 Umi 包和助手
  • 导入生成的客户端函数
  • 创建一个 Umi 实例
  • 根据我们程序的 seed 为签名者获取 PDA
  • 定义函数来记录 PDA、空投资金、初始化帐户和设置数据

    • 注意,Kinobi 生成了我们的 initializesetData 函数。它们接收 Umi 实例和必要的参数,并返回一个可用于发送和确认交易的函数。简单,对吧?
  • 最后,在 main 函数中按顺序运行这些函数。我们加入了几个 setData 调用来演示其功能。

运行脚本

现在我们有了脚本,可以运行它与程序交互了。在终端中运行以下命令:

ts-node clients/example.ts

我猜你收到报错了,对吧?这是因为我们的本地验证器没有运行。让我们使用 --detach 标志重新运行测试,并让它保持在后台运行:

anchor test --detach

现在,在另一个终端中,再次运行 example.ts 脚本:

ts-node clients/example.ts

这次运气好点?你应该会看到类似下面的输出:

PDA: 5GawRMyhgw8uDxanKeZd89AMeteuHmuAhyb2NSN7YEgJ,255
1. ✅ - Airdropped 100 SOL to the 5L8siRBhjiAE4GJKZmZSSkfCYBSRZXMv44SVuoD888Yt
2. ✅ - Initialized the account.
3. ✅ - Set data to 10.
4. ✅ - Set data to 20.
5. ✅ - Set data to 30.
6. ✅ - Set data to 40.
🚀 - Done!

恭喜你!你已经成功地使用 Kinobi 为你的 Anchor 程序生成了一个客户端,并使用 Umi 与之交互。现在你可以使用这个客户端与你的应用程式中的程序进行交互了。

总结

干得好,能走到这一步。以下是你已完成工作的简要回顾:

  1. 程序创建:你创建了一个简单的带有基本指令的 Anchor 程序。
  2. 测试:你编写并执行了测试,以确保程序正确运行。
  3. 客户端生成:你使用了 Kinobi 从你的 Anchor IDL 生成 JavaScript、Umi 和 Rust 客户端。
  4. 客户端交互:你编写了一个脚本,使用生成的客户端与程序交互,并确认其功能性。

通过利用 Kinobi,你已经简化了客户端生成过程,使你的开发工作流更加高效。如果你正在构建一个复杂的程序,为你的客户构建许多程序,或者只是想节省时间,Kinobi 可以是你工具包中一个有价值的工具。

Kinobi 对 Anchor 的支持仍然非常初期,正在积极开发中。代码可能会更改,新功能可能会添加。如果你遇到任何问题,请加入Discord沟通。

shooter 将本帖设为了精华贴。 07月06日 15:11
需要 登录 后方可回复, 如果你还没有账号请 注册新账号