BorshAccountsCoder
https://github.com/hpgo6688/anchor/blob/master/ts/packages/anchor/src/coder/borsh/accounts.ts
import bs58 from "bs58";
import { Buffer } from "buffer";
import { Layout } from "buffer-layout";
import { Idl, IdlDiscriminator } from "../../idl.js";
import { IdlCoder } from "./idl.js";
import { AccountsCoder } from "../index.js";
/**
* Encodes and decodes account objects.
*/
export class BorshAccountsCoder<A extends string = string>
implements AccountsCoder {
/**
* Maps account type identifier to a layout.
*/
private accountLayouts: Map<
A,
{ discriminator: IdlDiscriminator; layout: Layout }
>;
public constructor(private idl: Idl) {
if (!idl.accounts) {
this.accountLayouts = new Map();
return;
}
const types = idl.types;
if (!types) {
throw new Error("Accounts require `idl.types`");
}
const layouts = idl.accounts.map((acc) => {
const typeDef = types.find((ty) => ty.name === acc.name);
if (!typeDef) {
throw new Error(`Account not found: ${acc.name}`);
}
return [
acc.name as A,
{
discriminator: acc.discriminator,
layout: IdlCoder.typeDefLayout({ typeDef, types }),
},
] as const;
});
this.accountLayouts = new Map(layouts);
}
public async encode<T = any>(accountName: A, account: T): Promise<Buffer> {
console.log("BorshAccountsCoder-this.accountLayouts", this.accountLayouts);
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const layout = this.accountLayouts.get(accountName);
if (!layout) {
throw new Error(`Unknown account: ${accountName}`);
}
console.log("BorshAccountsCoder-layout", layout);
const len = layout.layout.encode(account, buffer);
const accountData = buffer.slice(0, len);
const discriminator = this.accountDiscriminator(accountName);
return Buffer.concat([discriminator, accountData]);
}
public decode<T = any>(accountName: A, data: Buffer): T {
// Assert the account discriminator is correct.
const discriminator = this.accountDiscriminator(accountName);
if (discriminator.compare(data.slice(0, discriminator.length))) {
throw new Error("Invalid account discriminator");
}
return this.decodeUnchecked(accountName, data);
}
public decodeAny<T = any>(data: Buffer): T {
for (const [name, layout] of this.accountLayouts) {
const givenDisc = data.subarray(0, layout.discriminator.length);
const matches = givenDisc.equals(Buffer.from(layout.discriminator));
if (matches) return this.decodeUnchecked(name, data);
}
throw new Error("Account not found");
}
public decodeUnchecked<T = any>(accountName: A, acc: Buffer): T {
// Chop off the discriminator before decoding.
const discriminator = this.accountDiscriminator(accountName);
const data = acc.subarray(discriminator.length);
const layout = this.accountLayouts.get(accountName);
if (!layout) {
throw new Error(`Unknown account: ${accountName}`);
}
return layout.layout.decode(data);
}
public memcmp(accountName: A, appendData?: Buffer): any {
const discriminator = this.accountDiscriminator(accountName);
return {
offset: 0,
bytes: bs58.encode(
appendData ? Buffer.concat([discriminator, appendData]) : discriminator
),
};
}
public size(accountName: A): number {
return (
this.accountDiscriminator(accountName).length +
IdlCoder.typeSize({ defined: { name: accountName } }, this.idl)
);
}
/**
* Get the unique discriminator prepended to all anchor accounts.
*
* @param name The name of the account to get the discriminator of.
*/
public accountDiscriminator(name: string): Buffer {
const account = this.idl.accounts?.find((acc) => acc.name === name);
if (!account) {
throw new Error(`Account not found: ${name}`);
}
return Buffer.from(account.discriminator);
}
}
这段代码定义了一个名为 BorshAccountsCoder 的类,用于在基于 Anchor 的 Solana 程序中对账户对象进行编码和解码操作。它主要负责将账户数据和账户结构(通过 IDL 描述的)进行序列化和反序列化操作,同时还支持账户的大小计算、账户类型验证等功能。
以下是对代码的详细分析:
主要功能概述
BorshAccountsCoder 类的主要功能包括:
-
编码(
encode):- 把一个账户对象序列化为二进制数据(
Buffer)。 - 在数据前面附加一个账户类型的唯一标识符(
discriminator)。
- 把一个账户对象序列化为二进制数据(
-
解码(
decode和decodeUnchecked):- 从二进制数据中解析出账户对象。
- 验证账户数据是否匹配预期的账户类型(通过
discriminator进行验证)。
-
账户类型识别(
decodeAny):- 从给定的二进制数据中自动识别账户类型并解码。
-
账户类型的字节匹配条件生成(
memcmp):- 生成一个字节 匹配条件,用于在 Solana 的
getProgramAccounts或其他 RPC 查询中筛选指定类型的账户。
- 生成一个字节 匹配条件,用于在 Solana 的
-
账户大小计算(
size):- 根据 IDL 中的账户定义,计算账户在内存中的大小。
-
账户类型标识符生成(
accountDiscriminator):- 生成账户类型的唯一标识符(
discriminator),该标识符是账户数据的前缀,用于区分不同账户类型。
- 生成账户类型的唯一标识符(
代码逐步解析
1. 成员变量
private accountLayouts: Map<
A,
{ discriminator: IdlDiscriminator; layout: Layout }
>;
accountLayouts是一个Map,用于存储账户类型与其对应的布局(Layout)和标识符(discriminator)。- 键(
A)是账户类型的名称。 - 值是一个对象,包含:
discriminator:账户类型的唯一标识符。layout:账户的二进制布局描述,用于序列化和反序列化账户数据。
2. 构造函数
public constructor(private idl: Idl) {
if (!idl.accounts) {
this.accountLayouts = new Map();
return;
}
const types = idl.types;
if (!types) {
throw new Error("Accounts require `idl.types`");
}
const layouts = idl.accounts.map((acc) => {
const typeDef = types.find((ty) => ty.name === acc.name);
if (!typeDef) {
throw new Error(`Account not found: ${acc.name}`);
}
return [
acc.name as A,
{
discriminator: acc.discriminator,
layout: IdlCoder.typeDefLayout({ typeDef, types }),
},
] as const;
});
this.accountLayouts = new Map(layouts);
}
-
参数:
idl是一个Idl对象,描述了程序的接口定义(IDL,Interface Definition Language),包括账户、类型、方法等定义。
-
逻辑:
- 如果 IDL 中没有定义 任何账户(
idl.accounts),直接初始化一个空的Map。 - 如果账户存在,遍历每个账户定义:
- 找到对应的类型定义(
typeDef)。 - 如果没有找到对应的类型定义,抛出错误。
- 为每个账户生成一个布局对象,包含:
discriminator:账户的类型标识符。layout:账户的二进制布局(通过IdlCoder.typeDefLayout生成)。
- 找到对应的类型定义(
- 将所有账户类型和对应的布局存储到
accountLayouts中。
- 如果 IDL 中没有定义 任何账户(
3. 编码函数
public async encode<T = any>(accountName: A, account: T): Promise<Buffer> {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const layout = this.accountLayouts.get(accountName);
if (!layout) {
throw new Error(`Unknown account: ${accountName}`);
}
const len = layout.layout.encode(account, buffer);
const accountData = buffer.slice(0, len);
const discriminator = this.accountDiscriminator(accountName);
return Buffer.concat([discriminator, accountData]);
}
-
参数:
accountName:账户的类型名称。account:需要编码的账户对象。
-
逻辑:
- 分配一个固定大小的缓冲区(
Buffer.alloc(1000))。这里的大小是一个临时值,实际应用中应该根据账户的大小动态分配。 - 从
accountLayouts中获取指定账户类型的布局。如果找不到对应的布局,抛出错误。 - 使用布局的
encode方法将账户对象编码到缓冲区中,返回编码后的长度(len)。 - 截取缓冲区中有效的数据部分。
- 获取账户类型的
discriminator。 - 将
discriminator和账户数据拼接在一起,返回最终的二进制数据。
- 分配一个固定大小的缓冲区(
4. 解码函数
decode
public decode<T = any>(accountName: A, data: Buffer): T {
const discriminator = this.accountDiscriminator(accountName);
if (discriminator.compare(data.slice(0, discriminator.length))) {
throw new Error("Invalid account discriminator");
}
return this.decodeUnchecked(accountName, data);
}
- 逻辑:
- 获取账户类型的
discriminator。 - 验证数据的前缀是否与
discriminator匹配。如果不匹配,抛出错误。 - 调用
decodeUnchecked方法解析账户数据。
- 获取账户类型的
decodeUnchecked
public decodeUnchecked<T = any>(accountName: A, acc: Buffer): T {
const discriminator = this.accountDiscriminator(accountName);
const data = acc.subarray(discriminator.length);
const layout = this.accountLayouts.get(accountName);
if (!layout) {
throw new Error(`Unknown account: ${accountName}`);
}
return layout.layout.decode(data);
}
- 逻辑:
- 获取账户类型的
discriminator。 - 从数据中移除
discriminator部分,获取实际的账户数据。 - 使用账户的布局(
layout)解码账户数据并返回。
- 获取账户类型的
5. 账户类型识别
public decodeAny<T = any>(data: Buffer): T {
for (const [name, layout] of this.accountLayouts) {
const givenDisc = data.subarray(0, layout.discriminator.length);
const matches = givenDisc.equals(Buffer.from(layout.discriminator));
if (matches) return this.decodeUnchecked(name, data);
}
throw new Error("Account not found");
}
- 逻辑:
- 遍历所有账户类型,逐一检查数据的前缀是否与账户的
discriminator匹配。 - 如果匹配,调用
decodeUnchecked解码账户数据。 - 如果没有找到匹配的账户类型,抛出错误。
- 遍历所有账户类型,逐一检查数据的前缀是否与账户的
6. 字节匹配条件
public memcmp(accountName: A, appendData?: Buffer): any {
const discriminator = this.accountDiscriminator(accountName);
return {
offset: 0,
bytes: bs58.encode(
appendData ? Buffer.concat([discriminator, appendData]) : discriminator
),
};
}
- 逻辑:
- 获取账户类型的
discriminator。 - 如果提供了额外的数据(
appendData),将其与discriminator拼接。 - 将结果编码为 Base58 格式,返回一个包含偏移量和字节匹配条件的对象。
- 获取账户类型的
7. 账户大小计算
public size(accountName: A): number {
return (
this.accountDiscriminator(accountName).length +
IdlCoder.typeSize({ defined: { name: accountName } }, this.idl)
);
}
- 逻辑:
- 获取账户类型的
discriminator长度。 - 调用
IdlCoder.typeSize方法计算账户的实际数据大小。 - 返回两者之和,表示账户的总大小。
- 获取账户类型的
总结
BorshAccountsCoder 是一个基于 buffer-layout 和 Anchor IDL 的工具,用于对 Solana 程序中的账户数据进行编码和解码操作。它的主要特点包括:
-
支持多账户类型:
- 通过
accountLayouts管理多个账户类型的布局和标识符。
- 通过
-
账户类型验证:
- 使用
discriminator验证账户数据是否匹配预期的账户类型。
- 使用
-
高效的二进制操作:
- 使用
buffer-layout实现高效的序列化和反序列化。
- 使用
-
与 Anchor IDL 的集成:
- 自动从 IDL 中解析账户类型和布局信息。
这是一个强大的工具,适用于基于 Anchor 的 Solana 开发中需要处理复杂账户数据的场景。