原文:https://www.quicknode.com/guides/solana-development/anchor/kinobi-client
作者:Aaron Milano
kinobi 已经改为 https://github.com/codama-idl/codama
概述
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 是如何工作的
- 程序定义:你定义你的 Solana 程序和相应的 IDLs。
- 抽象:Kinobi 创建了一个与语言无关的节点树 (或客户端代表),访问者可以更新它。
- 客户端生成:Kinobi 的访问者处理树并生成特定语言的客户端。
关键元素
- 程序:与 IDLs 关联的 Solana 程序。
- IDLs:描述 Solana 程序的接口和功能。
- Kinobi 树:编组 IDLs,促进客户端生成。
- 访问者:为特定语言定制客户端生成过程的模块。
- 依赖项:包括必要的库和实用程序,如 HTTP 接口、RPC 接口和加密工具。
近期,Kinobi 增加了对从 Anchor IDLs 生成客户端的支持。这意味着你现在可以使用 Kinobi 为 Anchor 程序生成客户端。让我们看看怎么做。
创建 Anchor 程序
首先,让我们创建一个简单的 Anchor 程序。我们将创建一个有两条指令的程序:
-
initialize
:初始化数据帐号时使用 u64 值。 -
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
,并创建两个新文件:
-
generate-client.ts
用于客户端生成脚本 -
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();
我们来分析一下这个脚本的作用:
- 导入必要的 Kinobi 包。
- 导入由 Anchor 生成的 IDL 文件并从中创建 Kinobi 树(使用
rootNodeFromAnchorWithoutDefaultVisitor
函数)。 - 定义要生成的客户端(JavaScript、Umi 和 Rust)——可以随意注释掉任何你不需要的内容,并根据需要调整目录。
- 迭代客户端并使用
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 生成了我们的
initialize
和setData
函数。它们接收 Umi 实例和必要的参数,并返回一个可用于发送和确认交易的函数。简单,对吧?
- 注意,Kinobi 生成了我们的
最后,在
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 与之交互。现在你可以使用这个客户端与你的应用程式中的程序进行交互了。
总结
干得好,能走到这一步。以下是你已完成工作的简要回顾:
- 程序创建:你创建了一个简单的带有基本指令的 Anchor 程序。
- 测试:你编写并执行了测试,以确保程序正确运行。
- 客户端生成:你使用了 Kinobi 从你的 Anchor IDL 生成 JavaScript、Umi 和 Rust 客户端。
- 客户端交互:你编写了一个脚本,使用生成的客户端与程序交互,并确认其功能性。
通过利用 Kinobi,你已经简化了客户端生成过程,使你的开发工作流更加高效。如果你正在构建一个复杂的程序,为你的客户构建许多程序,或者只是想节省时间,Kinobi 可以是你工具包中一个有价值的工具。