功能

通过 Kinobi 生成 Umi 客户端

Umi 框架为在 JavaScript 中构建 Solana 客户端提供了基础。当程序提供与 Umi 兼容的库时,它变得更加强大,因为它允许最终用户只需将他们的 Umi 实例插入到这些库提供的任何辅助函数中。为了简化和自动化创建与 Umi 兼容库的过程,Umi 提供了一个强大的代码生成器,称为 Kinobi。

Kinobi 引入了 Solana 客户端的语言无关表示,可以由一个或多个程序组成。它通过使用可被 Visitor 类访问的节点树来实现这一点。访问者可用于更新树的任何方面,允许开发者根据需要定制客户端。一旦树符合开发者的需求,就可以使用特定语言的访问者来为目标语言或框架生成代码。

好消息是 Kinobi 附带了一个 RenderJavaScriptVisitor,可以为我们生成与 Umi 兼容的库。

以下是如何使用 Kinobi 和 Umi 为 Solana 程序创建 JavaScript 客户端的快速概述。请注意,您可能对这个逐步讲解此图表的帖子感兴趣

Kinobi 入门

您可能想查看 Kinobi 文档以了解更多详细信息,但以下是如何开始使用 Kinobi 的快速概述。

首先,您需要安装 Kinobi:

npm install @metaplex-foundation/kinobi

然后,您需要创建一个 JavaScript 文件——例如 kinobi.js——来创建和渲染 Kinobi 树。这是通过创建一个 Kinobi 实例并向其传递 IDL 文件路径数组来完成的。您可能想查看 Shank JS 库来生成您的 IDL 文件。然后,您可以使用访问者来更新树,并通过 RenderJavaScriptVisitor 将其渲染为与 Umi 兼容的库。以下是一个示例。

import { createFromIdls, RenderJavaScriptVisitor } from "@metaplex-foundation/kinobi";
// 实例化 Kinobi。
const kinobi = createFromIdls([
path.join(__dirname, "idls", "my_idl.json"),
path.join(__dirname, "idls", "my_other_idl.json"),
]);
// 使用访问者更新 Kinobi 树...
// 渲染 JavaScript。
const jsDir = path.join(__dirname, "clients", "js", "src", "generated");
kinobi.accept(new RenderJavaScriptVisitor(jsDir));

现在,您只需要像这样使用 Node.js 运行此文件。

node ./kinobi.js

首次生成 JS 客户端时,请确保根据需要准备好库。您至少需要创建其 package.json 文件、安装其依赖项,并提供一个导入生成文件夹的顶级 index.ts 文件。

Kinobi 生成客户端的功能

现在我们知道如何通过 Kinobi 生成与 Umi 兼容的库,让我们看看它们能做什么。

类型和序列化器

Kinobi 生成的库为程序上定义的每个类型、账户和指令提供序列化器。它还导出创建序列化器所需的两个 TypeScript 类型——即其 FromTo 类型参数。它会将 From 类型后缀加上 Args 以区分两者。例如,如果您的 IDL 中定义了一个 MyType 类型,您可以使用以下代码来序列化和反序列化它。

const serializer: Serializer<MyTypeArgs, MyType> = getMyTypeSerializer();
serializer.serialize(myType);
serializer.deserialize(myBuffer);

对于指令,类型名称后缀为 InstructionData,对于账户,后缀为 AccountData。这允许无后缀的账户名称用作 Account<T> 类型。例如,如果您的程序上有一个 Token 账户和一个 Transfer 指令,您将获得以下类型和序列化器。

// 对于账户。
type Token = Account<TokenAccountData>;
type TokenAccountData = {...};
type TokenAccountDataArgs = {...};
const tokenDataSerializer = getTokenAccountDataSerializer();
// 对于指令。
type TransferInstructionData = {...};
type TransferInstructionDataArgs = {...};
const transferDataSerializer = getTransferInstructionDataSerializer();

数据枚举辅助函数

如果生成的类型被识别为数据枚举,将创建额外的辅助方法来帮助改善开发者体验。例如,假设您有以下生成的数据枚举类型。

type Message =
| { __kind: 'Quit' } // 空变体。
| { __kind: 'Write'; fields: [string] } // 元组变体。
| { __kind: 'Move'; x: number; y: number }; // 结构体变体。

然后,除了生成类型和 getMessageSerializer 函数外,它还会生成一个 messageisMessage 函数,可分别用于创建新的数据枚举和检查其变体的类型。

message('Quit'); // -> { __kind: 'Quit' }
message('Write', ['Hi']); // -> { __kind: 'Write', fields: ['Hi'] }
message('Move', { x: 5, y: 6 }); // -> { __kind: 'Move', x: 5, y: 6 }
isMessage('Quit', message('Quit')); // -> true
isMessage('Write', message('Quit')); // -> false

账户辅助函数

Kinobi 还将为账户提供额外的辅助方法,为我们提供一种获取和反序列化它们的简单方法。假设账户名称是 Metadata,以下是可用的额外辅助方法。

// 将原始账户反序列化为已解析的账户。
deserializeMetadata(rawAccount); // -> Metadata
// 通过公钥获取并反序列化账户。
await fetchMetadata(umi, publicKey); // -> Metadata 或失败
await safeFetchMetadata(umi, publicKey); // -> Metadata 或 null
// 通过公钥获取所有已反序列化的账户。
await fetchAllMetadata(umi, publicKeys); // -> Metadata[],如果任何账户缺失则失败
await safeFetchAllMetadata(umi, publicKeys) // -> Metadata[],过滤掉缺失的账户
// 为账户创建 getProgramAccount 构建器。
await getMetadataGpaBuilder()
.whereField('updateAuthority', updateAuthority)
.selectField('mint')
.getDataAsPublicKeys() // -> PublicKey[]
// 获取账户数据的字节大小(如果它有固定大小)。
getMetadataSize() // -> number
// 从账户的种子查找 PDA 地址。
findMetadataPda(umi, seeds) // -> Pda

您可能想查看 GpaBuilder 的文档以了解更多它们能做什么。

交易构建器

每个生成的指令还有自己的函数,可用于创建包含该指令的交易构建器。例如,如果您有一个 Transfer 指令,它会生成一个返回 TransactionBuildertransfer 函数。

await transfer(umi, { from, to, amount }).sendAndConfirm();

因为交易构建器可以组合在一起,这允许我们像这样轻松创建包含多个指令的交易。

await transfer(umi, { from, to: destinationA, amount })
.add(transfer(umi, { from, to: destinationB, amount }))
.add(transfer(umi, { from, to: destinationC, amount }))
.sendAndConfirm();

错误和程序

Kinobi 还会为客户端中定义的每个程序生成一个返回 Program 类型的函数,以及一些访问它们的辅助函数。例如,假设您的客户端定义了一个 MplTokenMetadata 程序,则会生成以下辅助函数。

// 程序的公钥作为常量变量。
MPL_TOKEN_METADATA_PROGRAM_ID; // -> PublicKey
// 创建可以注册到程序仓库的程序对象。
createMplTokenMetadataProgram(); // -> Program
// 从程序仓库获取程序对象。
getMplTokenMetadataProgram(umi); // -> Program
// 从程序仓库获取程序的公钥。
getMplTokenMetadataProgramId(umi); // -> PublicKey

请注意,Kinobi 不会为您的客户端自动生成 Umi 插件,允许您根据需要进行自定义。这意味着您需要自己创建插件,并至少注册客户端定义的程序。以下是使用 MplTokenMetadata 程序的示例。

export const mplTokenMetadata = (): UmiPlugin => ({
install(umi) {
umi.programs.add(createMplTokenMetadataProgram(), false);
},
});

此外,每个程序为其可能抛出的每个错误生成一个自定义 ProgramError。例如,如果您的程序定义了一个 UpdateAuthorityIncorrect 错误,它会生成以下类。

export class UpdateAuthorityIncorrectError extends ProgramError {
readonly name: string = 'UpdateAuthorityIncorrect';
readonly code: number = 0x7; // 7
constructor(program: Program, cause?: Error) {
super('Update Authority given does not match', program, cause);
}
}

每个生成的错误也会注册在 codeToErrorMapnameToErrorMap 中,允许库提供两个辅助方法,可以从名称或代码查找任何错误类。

getMplTokenMetadataErrorFromCode(0x7, program); // -> UpdateAuthorityIncorrectError
getMplTokenMetadataErrorFromName('UpdateAuthorityIncorrect', program); // -> UpdateAuthorityIncorrectError

请注意,这些方法被 createMplTokenMetadataProgram 函数用于填充 Program 对象的 getErrorFromCodegetErrorFromName 函数。

Previous
实现