Skip to main content

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 类的主要功能包括:

  1. 编码(encode

    • 把一个账户对象序列化为二进制数据(Buffer)。
    • 在数据前面附加一个账户类型的唯一标识符(discriminator)。
  2. 解码(decodedecodeUnchecked

    • 从二进制数据中解析出账户对象。
    • 验证账户数据是否匹配预期的账户类型(通过 discriminator 进行验证)。
  3. 账户类型识别(decodeAny

    • 从给定的二进制数据中自动识别账户类型并解码。
  4. 账户类型的字节匹配条件生成(memcmp

    • 生成一个字节匹配条件,用于在 Solana 的 getProgramAccounts 或其他 RPC 查询中筛选指定类型的账户。
  5. 账户大小计算(size

    • 根据 IDL 中的账户定义,计算账户在内存中的大小。
  6. 账户类型标识符生成(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),包括账户、类型、方法等定义。
  • 逻辑

    1. 如果 IDL 中没有定义任何账户(idl.accounts),直接初始化一个空的 Map
    2. 如果账户存在,遍历每个账户定义:
      • 找到对应的类型定义(typeDef)。
      • 如果没有找到对应的类型定义,抛出错误。
      • 为每个账户生成一个布局对象,包含:
        • discriminator:账户的类型标识符。
        • layout:账户的二进制布局(通过 IdlCoder.typeDefLayout 生成)。
    3. 将所有账户类型和对应的布局存储到 accountLayouts 中。

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:需要编码的账户对象。
  • 逻辑

    1. 分配一个固定大小的缓冲区(Buffer.alloc(1000))。这里的大小是一个临时值,实际应用中应该根据账户的大小动态分配。
    2. accountLayouts 中获取指定账户类型的布局。如果找不到对应的布局,抛出错误。
    3. 使用布局的 encode 方法将账户对象编码到缓冲区中,返回编码后的长度(len)。
    4. 截取缓冲区中有效的数据部分。
    5. 获取账户类型的 discriminator
    6. 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);
}
  • 逻辑
    1. 获取账户类型的 discriminator
    2. 验证数据的前缀是否与 discriminator 匹配。如果不匹配,抛出错误。
    3. 调用 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);
}
  • 逻辑
    1. 获取账户类型的 discriminator
    2. 从数据中移除 discriminator 部分,获取实际的账户数据。
    3. 使用账户的布局(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");
}
  • 逻辑
    1. 遍历所有账户类型,逐一检查数据的前缀是否与账户的 discriminator 匹配。
    2. 如果匹配,调用 decodeUnchecked 解码账户数据。
    3. 如果没有找到匹配的账户类型,抛出错误。

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
),
};
}
  • 逻辑
    1. 获取账户类型的 discriminator
    2. 如果提供了额外的数据(appendData),将其与 discriminator 拼接。
    3. 将结果编码为 Base58 格式,返回一个包含偏移量和字节匹配条件的对象。

7. 账户大小计算

public size(accountName: A): number {
return (
this.accountDiscriminator(accountName).length +
IdlCoder.typeSize({ defined: { name: accountName } }, this.idl)
);
}
  • 逻辑
    1. 获取账户类型的 discriminator 长度。
    2. 调用 IdlCoder.typeSize 方法计算账户的实际数据大小。
    3. 返回两者之和,表示账户的总大小。

总结

BorshAccountsCoder 是一个基于 buffer-layout 和 Anchor IDL 的工具,用于对 Solana 程序中的账户数据进行编码和解码操作。它的主要特点包括:

  1. 支持多账户类型

    • 通过 accountLayouts 管理多个账户类型的布局和标识符。
  2. 账户类型验证

    • 使用 discriminator 验证账户数据是否匹配预期的账户类型。
  3. 高效的二进制操作

    • 使用 buffer-layout 实现高效的序列化和反序列化。
  4. 与 Anchor IDL 的集成

    • 自动从 IDL 中解析账户类型和布局信息。

这是一个强大的工具,适用于基于 Anchor 的 Solana 开发中需要处理复杂账户数据的场景。