Skip to main content

BorshEventCoder

import { Buffer } from "buffer";
import { Layout } from "buffer-layout";
import * as base64 from "../../utils/bytes/base64.js";
import { Idl, IdlDiscriminator } from "../../idl.js";
import { IdlCoder } from "./idl.js";
import { EventCoder } from "../index.js";

export class BorshEventCoder implements EventCoder {
/**
* Maps account type identifier to a layout.
*/
private layouts: Map<
string,
{ discriminator: IdlDiscriminator; layout: Layout }
>;

public constructor(idl: Idl) {
if (!idl.events) {
this.layouts = new Map();
return;
}

const types = idl.types;
if (!types) {
throw new Error("Events require `idl.types`");
}

const layouts = idl.events.map((ev) => {
const typeDef = types.find((ty) => ty.name === ev.name);
if (!typeDef) {
throw new Error(`Event not found: ${ev.name}`);
}
return [
ev.name,
{
discriminator: ev.discriminator,
layout: IdlCoder.typeDefLayout({ typeDef, types }),
},
] as const;
});
this.layouts = new Map(layouts);
}

public decode(log: string): {
name: string;
data: any;
} | null {
let logArr: Buffer;
// This will throw if log length is not a multiple of 4.
try {
logArr = base64.decode(log);
} catch (e) {
return null;
}

for (const [name, layout] of this.layouts) {
const givenDisc = logArr.subarray(0, layout.discriminator.length);
const matches = givenDisc.equals(Buffer.from(layout.discriminator));
if (matches) {
return {
name,
data: layout.layout.decode(logArr.subarray(givenDisc.length)),
};
}
}

return null;
}
}

event 看上去与accounts相似

确实,event(事件)和 account(账户)的编码和解码逻辑在 Anchor 框架中有许多相似之处。它们都依赖于 discriminator(区分符)来标识类型,并且通过 buffer-layout 来定义和解析数据结构。然而,eventaccount 的目的和使用场景不同,导致它们在设计上有一些关键差异。

以下是对 eventaccount 的关系与区别的分析:


相似点

1. 都使用 discriminator 作为类型标识符

  • 账户

    • 每种账户类型都有一个唯一的 discriminator,用于标识账户类型。
    • 在账户数据中,discriminator 是数据的前 8 个字节,用于验证账户数据的类型。
    • 示例:
      const discriminator = this.accountDiscriminator(accountName);
      if (discriminator.compare(data.slice(0, discriminator.length))) {
      throw new Error("Invalid account discriminator");
      }
  • 事件

    • 每种事件类型也有一个唯一的 discriminator,用于标识事件类型。
    • 在事件日志中,discriminator 是日志数据的前几个字节,用于识别事件类型。
    • 示例:
      const givenDisc = logArr.subarray(0, layout.discriminator.length);
      const matches = givenDisc.equals(Buffer.from(layout.discriminator));
      if (matches) {
      return {
      name,
      data: layout.layout.decode(logArr.subarray(givenDisc.length)),
      };
      }

2. 都使用 buffer-layout 进行数据解析

  • 无论是账户还是事件,都使用 buffer-layout 来定义数据的二进制结构,并通过 layout 进行编码和解码。

  • 账户

    • 账户数据的结构通过 layout 定义,用于序列化和反序列化账户数据。
    • 示例:
      const len = layout.layout.encode(account, buffer);
      const accountData = buffer.slice(0, len);
  • 事件

    • 事件数据的结构同样通过 layout 定义,用于解析事件日志中的数据。
    • 示例:
      const data = layout.layout.decode(logArr.subarray(givenDisc.length));

3. 都依赖 IDL(Interface Definition Language)

  • 账户

    • 账户类型和结构定义存储在 IDL 的 accounts 部分。
    • 在构造 BorshAccountsCoder 时,会从 IDL 中加载账户的定义和布局。
    • 示例:
      const layouts = idl.accounts.map((acc) => {
      const typeDef = types.find((ty) => ty.name === acc.name);
      return [
      acc.name as A,
      {
      discriminator: acc.discriminator,
      layout: IdlCoder.typeDefLayout({ typeDef, types }),
      },
      ] as const;
      });
  • 事件

    • 事件类型和结构定义存储在 IDL 的 events 部分。
    • 在构造 BorshEventCoder 时,会从 IDL 中加载事件的定义和布局。
    • 示例:
      const layouts = idl.events.map((ev) => {
      const typeDef = types.find((ty) => ty.name === ev.name);
      return [
      ev.name,
      {
      discriminator: ev.discriminator,
      layout: IdlCoder.typeDefLayout({ typeDef, types }),
      },
      ] as const;
      });

区别点

1. 数据来源不同

  • 账户

    • 账户数据存储在 Solana 区块链的账户中。
    • 通过 RPC 调用(如 getAccountInfogetProgramAccounts)从链上获取账户数据。
    • 数据通常是静态的,存储了程序的状态信息。
  • 事件

    • 事件数据存储在 Solana 的交易日志(transaction logs)中。
    • 通过 RPC 调用(如 getTransaction)从链上获取交易的日志数据。
    • 数据是动态的,用于描述程序在执行过程中触发的某些行为(如转账、状态变化等)。

2. 数据的生命周期

  • 账户

    • 账户数据的生命周期与账户本身相关,通常是长期存在的。
    • 账户数据可以在多次交易中被修改。
  • 事件

    • 事件数据的生命周期仅限于交易的执行过程。
    • 一旦交易完成,事件数据就不会被修改,只能通过历史记录查询。

3. 数据结构的用途

  • 账户

    • 数据结构用于存储程序的状态,通常包含复杂的嵌套字段。
    • 示例:用户账户可能包含余额、权限、交易历史等字段。
  • 事件

    • 数据结构用于描述程序执行过程中的某些行为,通常是简洁的。
    • 示例:事件可能只包含一个金额字段和一个接收者地址字段,用于描述一次转账操作。

4. 编码和解码的目标

  • 账户

    • 账户的编码和解码目标是对链上账户数据进行读写操作。
    • 编码:将账户对象序列化为二进制数据,写入链上。
    • 解码:将链上的二进制数据解析为账户对象。
  • 事件

    • 事件的解码目标是从交易日志中解析事件的类型和数据。
    • 通常不需要对事件数据进行编码,因为事件数据是由程序在执行时自动生成的。

5. IDL 中的定义

  • 账户

    • IDL 中的 accounts 部分定义了账户的名称、类型和布局。
    • 示例:
      {
      "name": "MyAccount",
      "type": {
      "kind": "struct",
      "fields": [
      { "name": "balance", "type": "u64" },
      { "name": "owner", "type": "publicKey" }
      ]
      }
      }
  • 事件

    • IDL 中的 events 部分定义了事件的名称、类型和布局。
    • 示例:
      {
      "name": "TransferEvent",
      "fields": [
      { "name": "amount", "type": "u64" },
      { "name": "to", "type": "publicKey" }
      ]
      }

总结

虽然 eventaccount 在编码解码逻辑、数据结构定义、discriminator 的使用等方面非常相似,但它们的核心区别在于 用途和数据来源

  • 账户

    • 用于存储程序的状态信息。
    • 数据长期存在,存储在链上的账户中。
    • 需要对账户数据进行读写操作。
  • 事件

    • 用于描述程序执行过程中的动态行为。
    • 数据是短暂的,仅存在于交易日志中。
    • 主要用于解析和分析程序的运行结果。

如果你熟悉 BorshAccountsCoder 的逻辑,那么理解 BorshEventCoder 会非常容易,因为它们的实现思路几乎一致,只是应用场景不同。