{"author":{"address":"0xdCaD094D1c1d8fB816242A313644A5F6A44FD8f0","user":"https://learnblockchain.cn/people/322"},"content":{"body":"## 概述\r\n\r\nKinobi 是一组库，它是一个强大的工具，可以用来为现有的 Solana 程序生成 JavaScript、Umi (JavaScript) 和 Rust 客户端。Kinobi 最近增加了从 Anchor IDLs 生成客户端的支持，所以我们现在可以使用 Kinobi 为 Anchor 程序创建客户端。在使用 Anchor 构建和测试新程序时，这可以节省时间。本指南将向你展示如何使用 Kinobi 为你的 Anchor 程序生成客户端。\r\n\r\n### 你要做什么\r\n\r\n* 创建一个简单的 Anchor 程序\r\n* 编写一个脚本，使用 Kinobi 为程序生成一个客户端\r\n* 测试客户端\r\n\r\n### 你需要什么\r\n\r\n本指南假设你对 Solana 编程和 Anchor 有基本的了解：\r\n* [Solana 基础参考指南](https://www.quicknode.com/guides/solana-development/getting-started/solana-fundamentals-reference-guide)\r\n* [指南：构建你的第一个 Anchor 程序](https://www.quicknode.com/guides/solana-development/anchor/how-to-write-your-first-anchor-program-in-solana-part-1)\r\n* [什么是 IDL](https://www.quicknode.com/guides/solana-development/anchor/what-is-an-idl)\r\n\r\n在开始之前，请确保你已安装了以下软件：\r\n* [Node.js](https://nodejs.org/en/download/)\r\n* [Anchor v.0.30.0 及以上版本](https://www.anchor-lang.com/docs/installation)\r\n* [Solana CLI v1.18.16 及以上版本](https://docs.solanalabs.com/cli/install)\r\n* [TypeScript](https://www.typescriptlang.org/download) 和 [ts-node](https://www.npmjs.com/package/ts-node)\r\n\r\n**原文推荐使用 Anchor 0.30，还是建议使用 Anchor 0.29，0.30 改动了多处，已经不兼容 0.29，更多内容请看 https://soldev.cn/topics/4**\r\n\r\n\u003cblockquote\u003e\r\n检查你的 Solana 和 Anchor 版本\r\n\r\n本指南适用于 Solana CLI 1.18.16 及以上版本。\r\n\r\n- 在终端中运行 `solana --version` 来检查你的 Solana 版本。如果你需要更新，请遵循 [这里](https://docs.solanalabs.com/cli/install) 的说明。\r\n- 在终端中运行 `anchor --version` 来检查你的 Anchor 版本。如果你需要更新，请遵循 [这里](https://www.anchor-lang.com/docs/installation) 的说明。\r\n\u003c/blockquote\u003e\r\n让我们开始吧！\r\n\r\n## 什么是 Kinobi?​\r\n\r\nKinobi 是由 Metaplex 基金会创建的一个库，旨在为 Solana 程序生成客户端。Kinobi 的工作原理是通过传递一个或多个程序的 IDL 来生成 Kinobi，这是一棵可以被访问者更新的节点树。这些访问者可以根据需要更新说明或帐户。随后，与语言无关的渲染访问者可以生成各种语言的客户端，以便你可以管理客户端堆栈/依赖项。\r\n\r\n### Kinobi 是如何工作的\r\n1. **程序定义**：你定义你的 Solana 程序和相应的 IDLs。\r\n2. **抽象**：Kinobi 创建了一个与语言无关的节点树 (或客户端代表)，访问者可以更新它。\r\n3. **客户端生成**：Kinobi 的访问者处理树并生成特定语言的客户端。\r\n\r\n![](/uploads/photo/Fifi/7e967cdc-f50d-4df9-a94a-6d242d85afb4.jpg!large)\r\n来源：[Metaplex Developers](https://developers.metaplex.com/)\r\n\r\n### 关键元素\r\n\r\n* **程序**：与 IDLs 关联的 Solana 程序。\r\n* **IDLs**：描述 Solana 程序的接口和功能。\r\n* **Kinobi 树**：编组 IDLs，促进客户端生成。\r\n* **访问者**：为特定语言定制客户端生成过程的模块。\r\n* **依赖项**：包括必要的库和实用程序，如 HTTP 接口、RPC 接口和加密工具。\r\n\r\n近期，Kinobi 增加了对从 Anchor IDLs 生成客户端的支持。这意味着你现在可以使用 Kinobi 为 Anchor 程序生成客户端。让我们看看怎么做。\r\n\r\n## 创建 Anchor 程序\r\n\r\n首先，让我们创建一个简单的 Anchor 程序。我们将创建一个有两条指令的程序：\r\n1. `initialize`：初始化数据帐号时使用 u64 值。\r\n2. `set_data`：设置数据帐号的值。\r\n\r\n### 初始化项目\r\n\r\n创建一个新的项目目录，并运行以下命令来创建一个新的 Anchor 程序：\r\n\r\n```\r\nanchor init kinobi-test\r\n```\r\n\r\n切换到项目目录：\r\n\r\n```\r\ncd kinobi-test\r\n```\r\n\r\n### 安装依赖项\r\n\r\n项目初始化后，可以运行 `npm install`，来确保安装了依赖项。然后我们将安装一些额外的依赖项。在终端中运行以下命令：\r\n\r\n```\r\nnpm install @kinobi-so/nodes-from-anchor @kinobi-so/renderers @kinobi-so/visitors-core @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults\r\n```\r\n\r\n这将安装一些 Kinobi 包，包括 `nodes-from-anchor` 包，它将帮助我们从 Anchor IDL 生成 Kinobi 树。\r\n\r\n### 更新 TSConfig\r\n\r\n将 `resolveJsonModule` 添加到 `tsconfg.json` 中，以确保我们可以加载 *IDL JSON* 对象来生成客户端和 DOM 到 `lib` 数组中，这样我们就可以在 Node.js 中运行我们的脚本。更新目录中的 `tsconfig.json` 文件，使其看起来像这样：\r\n```js\r\n{\r\n    \"compilerOptions\": {\r\n      \"types\": [\"mocha\", \"chai\"],\r\n      \"typeRoots\": [\"./node_modules/@types\"],\r\n      \"lib\": [\"ES2020\", \"DOM\"],\r\n      \"module\": \"commonjs\",\r\n      \"target\": \"es6\",\r\n      \"esModuleInterop\": true,\r\n      \"resolveJsonModule\": true,\r\n }\r\n}\r\n```\r\n\r\n### 编写 Anchor 程序\r\n\r\n下面让我们来编写 Anchor 程序。打开 `programs/kinobi-test/src/lib.rs` 文件，用以下代码替换内容，注意不要覆盖 `declare_id!` 宏：\r\n\r\n```rs\r\nuse anchor_lang::prelude::*;\r\n\r\ndeclare_id!(\"YOUR_PROGRAM_ID_HERE\"); // Replace with your program ID\r\n\r\n#[program]\r\npub mod kinobi_test {\r\n    use super::*;\r\n\r\n    pub fn initialize(ctx: Context\u003cInitialize\u003e) -\u003e Result\u003c()\u003e {\r\n        ctx.accounts.pda.set_inner(ExampleStruct {\r\n            data: 0,\r\n            authority: *ctx.accounts.payer.key,\r\n });\r\n        Ok(())\r\n }\r\n\r\n    pub fn set_data(ctx: Context\u003cSetData\u003e, data: u32) -\u003e Result\u003c()\u003e {\r\n        ctx.accounts.pda.data = data;\r\n        Ok(())\r\n }\r\n}\r\n\r\n#[derive(Accounts)]\r\npub struct Initialize\u003c'info\u003e {\r\n #[account(mut)]\r\n    payer: Signer\u003c'info\u003e,\r\n\r\n #[account(\r\n init,\r\n payer = payer,\r\n space = 45,\r\n seeds = [b\"example\".as_ref(), payer.key().as_ref()],\r\n        bump\r\n )]\r\n    pda: Account\u003c'info, ExampleStruct\u003e,\r\n\r\n    system_program: Program\u003c'info, System\u003e,\r\n}\r\n\r\n\r\n#[derive(Accounts)]\r\npub struct SetData\u003c'info\u003e {\r\n #[account(mut)]\r\n    authority: Signer\u003c'info\u003e,\r\n\r\n #[account(\r\n        mut,\r\n has_one = authority,\r\n seeds = [b\"example\".as_ref(), authority.key().as_ref()],\r\n        bump\r\n )]\r\n    pda: Account\u003c'info, ExampleStruct\u003e,\r\n}\r\n\r\n#[account]\r\npub struct ExampleStruct {\r\n    pub data: u32,\r\n    pub authority: Pubkey,\r\n}\r\n```\r\n\r\n这是一个基本的 Anchor 程序，允许用户初始化一个 ExampleStruct（一个拥有 u32 数据和一个 `authority` PublicKey 的 PDA）并设置数据值。PDAs 的 seed 是付款人的密钥和字符串\"example\"。你可以随意使用其他程序，或者根据需要修改这个程序——它仅用于演示。\r\n\r\n## 构建并测试程序\r\n\r\n现在我们有了程序，可以构建和测试它了。在终端中运行以下命令：\r\n```\r\nanchor build\r\n```\r\n\r\n这可能需要几分钟，但应该不会出现任何错误。当它运行时，让我们编写一个简单的测试脚本。打开你的 Anchor 生成的测试文件 `tests/kinobi-test.ts`，并用以下代码替换内容：\r\n\r\n```js\r\nimport * as anchor from \"@coral-xyz/anchor\";\r\nimport { Program } from \"@coral-xyz/anchor\";\r\nimport { KinobiTest } from \"../target/types/kinobi_test\";\r\n\r\ndescribe(\"kinobi-test\", () =\u003e {\r\n  anchor.setProvider(anchor.AnchorProvider.env());\r\n  const program = anchor.workspace.KinobiTest as Program\u003cKinobiTest\u003e;\r\n  const [pda] = anchor.web3.PublicKey.findProgramAddressSync(\r\n [\r\n      Buffer.from(\"example\"),\r\n      program.provider.publicKey.toBuffer()\r\n ],\r\n    program.programId\r\n )\r\n  it(\"Is initialized!\", async () =\u003e {\r\n    const tx = await program.methods\r\n .initialize()\r\n .accountsStrict({\r\n        payer: program.provider.publicKey,\r\n        pda,\r\n        systemProgram: anchor.web3.SystemProgram.programId\r\n })\r\n .rpc();\r\n });\r\n  it(\"Can set data!\", async () =\u003e {\r\n    const tx = await program.methods\r\n .setData(10)\r\n .accountsStrict({\r\n        authority: program.provider.publicKey,\r\n        pda\r\n })\r\n .rpc({skipPreflight: true});\r\n });\r\n});\r\n```\r\n\r\n这个脚本将测试我们程序中的两个指令。第一个测试将初始化数据帐户，第二个测试将数据值设置为 10。继续运行测试脚本：\r\n```\r\nanchor test\r\n```\r\n你应该会看到类似下面的内容：\r\n\r\n```\r\n  kinobi-test\r\n    ✔ Is initialized! (450ms)\r\n    ✔ Can set data! (463ms)\r\n\r\n\r\n  2 passing (916ms)\r\n\r\n  Done in 2.80s.\r\n```\r\n\r\n干得漂亮！\r\n\r\n## 用 Kinobi 生成一个客户端\r\n\r\n你的测试已经成功运行，Anchor 应该已经为你在 `target/idl/kinobi_test.json` 中自动生成了一个 IDL。定位此文件——我们将在下一节中使用它（注意：如果你为你的 Anchor 项目使用了不同的名称，此文件路径可能略有不同）。我们现在可以使用 Kinobi 为这个程序生成一个客户端。\r\n\r\n在根目录下创建一个新文件夹，`clients`，并创建两个新文件：\r\n1. `generate-client.ts` 用于客户端生成脚本\r\n2. `example.ts` 用于尝试生成客户端\r\n\r\n### 生成客户端\r\n\r\n打开 `generate-client.ts`，将以下代码添加到文件中：\r\n```js\r\nimport { AnchorIdl, rootNodeFromAnchorWithoutDefaultVisitor } from \"@kinobi-so/nodes-from-anchor\";\r\nimport { renderJavaScriptUmiVisitor, renderJavaScriptVisitor, renderRustVisitor } from \"@kinobi-so/renderers\";\r\nimport { visit } from \"@kinobi-so/visitors-core\";\r\nimport anchorIdl from \"../target/idl/kinobi_test.json\"; // Note: if you initiated your project with a different name, you may need to change this path\r\n\r\nasync function generateClients() {\r\n    const node = rootNodeFromAnchorWithoutDefaultVisitor(anchorIdl as AnchorIdl);\r\n\r\n    const clients = [\r\n        { type: \"JS\", dir: \"clients/generated/js/src\", renderVisitor: renderJavaScriptVisitor },\r\n        { type: \"Umi\", dir: \"clients/generated/umi/src\", renderVisitor: renderJavaScriptUmiVisitor },\r\n        { type: \"Rust\", dir: \"clients/generated/rust/src\", renderVisitor: renderRustVisitor }\r\n    ];\r\n\r\n    for (const client of clients) {\r\n        try {\r\n            await visit(\r\n                node,\r\n                await client.renderVisitor(client.dir)\r\n            ); console.log(`✅ Successfully generated ${client.type} client for directory: ${client.dir}!`);\r\n        } catch (e) {\r\n            console.error(`Error in ${client.renderVisitor.name}:`, e);\r\n            throw e;\r\n        }\r\n    }\r\n}\r\n\r\ngenerateClients();\r\n```\r\n\r\n我们来分析一下这个脚本的作用：\r\n\r\n1. 导入必要的 Kinobi 包。\r\n2. 导入由 Anchor 生成的 IDL 文件并从中创建 Kinobi 树（使用 `rootNodeFromAnchorWithoutDefaultVisitor` 函数）。\r\n3. 定义要生成的客户端（JavaScript、Umi 和 Rust）——可以随意注释掉任何你不需要的内容，并根据需要调整目录。\r\n4. 迭代客户端并使用 `visit` 函数使用适当的渲染访问者生成客户端。\r\n\r\n### 运行脚本\r\n\r\n现在我们有了脚本，可以运行它来生成客户端。在终端中运行以下命令：\r\n```\r\nts-node clients/generate-client.ts\r\n```\r\n你应该会看到类似下面的输出：\r\n```\r\nts-node clients/generate-client.ts\r\nSuccessfully generated JS client for directory: clients/generated/js/src!\r\nSuccessfully generated Umi client for directory: clients/generated/umi/src!\r\nSuccessfully generated Rust client for directory: clients/generated/rust/src!\r\n```\r\n现在，你应该已经在 `clients` 目录中为程序生成了客户端。干得漂亮！\r\n\r\n现在可以使用这些客户端与程序交互了。\r\n\r\n## 测试客户端\r\n\r\n打开你之前创建的 `example.ts` 文件，添加以下代码：\r\n```js\r\nimport { createUmi } from '@metaplex-foundation/umi-bundle-defaults';\r\nimport { TransactionBuilderSendAndConfirmOptions, generateSigner, keypairIdentity, sol } from '@metaplex-foundation/umi';\r\nimport { publicKey as publicKeySerializer, string } from '@metaplex-foundation/umi/serializers';\r\nimport { getKinobiTestProgramId } from './generated/umi/src/programs/kinobiTest';\r\nimport { initialize, setData } from './generated/umi/src/instructions';\r\n\r\nconst umi = createUmi('http://127.0.0.1:8899', { commitment: 'processed' });\r\nconst creator = generateSigner(umi);\r\numi.use(keypairIdentity(creator));\r\n\r\nconst options: TransactionBuilderSendAndConfirmOptions = {\r\n    confirm: { commitment: 'processed' }\r\n};\r\n\r\nconst pda = umi.eddsa.findPda(getKinobiTestProgramId(umi), [\r\n    string({ size: 'variable' }).serialize('example'),\r\n    publicKeySerializer().serialize(creator.publicKey),\r\n]);\r\n\r\nasync function logPda() {\r\n    console.log(`PDA: ${pda.toString()}`);\r\n}\r\n\r\nasync function airdropFunds() {\r\n    try {\r\n        await umi.rpc.airdrop(creator.publicKey, sol(100), options.confirm);\r\n        console.log(`1. ✅ - Airdropped 100 SOL to the ${creator.publicKey.toString()}`);\r\n    } catch (error) {\r\n        console.error('1. ❌ - Error airdropping SOL to the wallet.', error);\r\n    }\r\n}\r\n\r\nasync function initializeAccount() {\r\n    try {\r\n        await initialize(umi, { pda, payer: creator }).sendAndConfirm(umi, options);\r\n        console.log('2. ✅ - Initialized the account.');\r\n    } catch (error) {\r\n        console.error('2. ❌ - Error initializing the account.', error);\r\n    }\r\n}\r\n\r\nasync function setDataAccount(num: number, value: number) {\r\n    try {\r\n        await setData(umi, { authority: creator, pda, data: value }).sendAndConfirm(umi, options);\r\n        console.log(`${num}. ✅ - Set data to ${value}.`);\r\n    } catch (error) {\r\n        console.error(num, '. ❌ - Error setting data.', error);\r\n    }\r\n}\r\n\r\nasync function main() {\r\n    await logPda();\r\n    await airdropFunds();\r\n    await initializeAccount();\r\n    await setDataAccount(3, 10);\r\n    await setDataAccount(4, 20);\r\n    await setDataAccount(5, 30);\r\n    await setDataAccount(6, 40);\r\n}\r\n\r\nmain().then(() =\u003e {\r\n    console.log('🚀 - Done!');\r\n}).catch((error) =\u003e {\r\n    console.error('❌ - Error:', error);\r\n});\r\n```\r\n\r\n这个脚本将会：\r\n* 导入必要的 Umi 包和助手\r\n* 导入生成的客户端函数\r\n* 创建一个 Umi 实例\r\n* 根据我们程序的 seed 为签名者获取 PDA\r\n* 定义函数来记录 PDA、空投资金、初始化帐户和设置数据\r\n\r\n  * 注意，Kinobi 生成了我们的 `initialize` 和 `setData` 函数。它们接收 Umi 实例和必要的参数，并返回一个可用于发送和确认交易的函数。简单，对吧？\r\n* 最后，在 `main` 函数中按顺序运行这些函数。我们加入了几个 `setData` 调用来演示其功能。\r\n\r\n### 运行脚本\r\n现在我们有了脚本，可以运行它与程序交互了。在终端中运行以下命令：\r\n```\r\nts-node clients/example.ts\r\n```\r\n\r\n我猜你收到报错了，对吧？这是因为我们的本地验证器没有运行。让我们使用 `--detach` 标志重新运行测试，并让它保持在后台运行：\r\n```\r\nanchor test --detach\r\n```\r\n现在，在另一个终端中，再次运行 `example.ts` 脚本：\r\n```\r\nts-node clients/example.ts\r\n```\r\n这次运气好点？你应该会看到类似下面的输出：\r\n\r\n```\r\nPDA: 5GawRMyhgw8uDxanKeZd89AMeteuHmuAhyb2NSN7YEgJ,255\r\n1. ✅ - Airdropped 100 SOL to the 5L8siRBhjiAE4GJKZmZSSkfCYBSRZXMv44SVuoD888Yt\r\n2. ✅ - Initialized the account.\r\n3. ✅ - Set data to 10.\r\n4. ✅ - Set data to 20.\r\n5. ✅ - Set data to 30.\r\n6. ✅ - Set data to 40.\r\n🚀 - Done!\r\n```\r\n\r\n恭喜你！你已经成功地使用 Kinobi 为你的 Anchor 程序生成了一个客户端，并使用 Umi 与之交互。现在你可以使用这个客户端与你的应用程式中的程序进行交互了。\r\n\r\n## 总结\r\n\r\n干得好，能走到这一步。以下是你已完成工作的简要回顾：\r\n1. **程序创建**：你创建了一个简单的带有基本指令的 Anchor 程序。\r\n2. **测试**：你编写并执行了测试，以确保程序正确运行。\r\n3. **客户端生成**：你使用了 Kinobi 从你的 Anchor IDL 生成 JavaScript、Umi 和 Rust 客户端。\r\n4. **客户端交互**：你编写了一个脚本，使用生成的客户端与程序交互，并确认其功能性。\r\n\r\n通过利用 Kinobi，你已经简化了客户端生成过程，使你的开发工作流更加高效。如果你正在构建一个复杂的程序，为你的客户构建许多程序，或者只是想节省时间，Kinobi 可以是你工具包中一个有价值的工具。\r\n\r\nKinobi 对 Anchor 的支持仍然非常初期，正在积极开发中。代码可能会更改，新功能可能会添加。如果你遇到任何问题，请加入[Discord](https://discord.gg/quicknode)沟通。","title":"如何使用 Kinobi 创建 Anchor 程序客户端"},"history":null,"timestamp":1720346567,"version":1}