Solana 程序可以触发事件,这与 Ethereum 触发事件的方式类似,尽管存在一些我们将要讨论的差异。
具体来说,Solana 中的事件旨在将信息传递给前端,而不是记录过去的交易。要获取过往历史,可以通过地址查询 Solana 交易。
Solana 日志与事件
下面的程序有两个事件:MyEvent 和 MySecondEvent。与 Ethereum 事件具有“参数”类似,Solana 事件在结构体中具有字段:
use anchor_lang::prelude::*;
declare_id!("FmyZrMmPvRzmJCG3p5R1AnbkPqSmzdJrcYzgnQiGKuBq");
#[program]
pub mod emit {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
emit!(MyEvent { value: 42 });
emit!(MySecondEvent { value: 3, message: "hello world".to_string() });
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}
#[event]
pub struct MyEvent {
pub value: u64,
}
#[event]
pub struct MySecondEvent {
pub value: u64,
pub message: String,
}
事件成为 Solana 程序的 IDL 的一部分,类似于事件是 Solidity 智能合约 ABI 的一部分。下面我们对上述程序的 IDL 进行了截图,并高亮显示了相关部分:

在 Solana 中,没有像 Ethereum 那样的“indexed”(已索引)或“non-indexed”(未索引)信息之分(尽管上面的截图中有个 “index” 字段,但它没有任何实际用途)。
与 Ethereum 不同,我们无法直接查询特定区块号范围内的历史事件。我们只能在事件发生时监听它们。(稍后我们将了解 Solana 审计历史交易的方法)。下面的代码展示了如何在 Solana 中监听事件:
import * as anchor from "@coral-xyz/anchor";
import { BorshCoder, EventParser, Program } from "@coral-xyz/anchor";
import { Emit } from "../target/types/emit";
describe("emit", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Emit as Program<Emit>;
it("Is initialized!", async () => {
const listenerMyEvent = program.addEventListener('MyEvent', (event, slot) => {
console.log(`slot ${slot} event value ${event.value}`);
});
const listenerMySecondEvent = program.addEventListener('MySecondEvent', (event, slot) => {
console.log(`slot ${slot} event value ${event.value} event message ${event.message}`);
});
await program.methods.initialize().rpc();
// This line is only for test purposes to ensure the event
// listener has time to listen to event.
await new Promise((resolve) => setTimeout(resolve, 5000));
program.removeEventListener(listenerMyEvent);
program.removeEventListener(listenerMySecondEvent);
});
});
不可能像在 Ethereum 中那样扫描历史日志,必须在交易发生时对它们进行观察。

日志在底层的运行原理
在 EVM 中,日志是通过运行 log0、log1、log2 等操作码(opcode)来触发的。在 Solana 中,日志是通过调用系统调用 sol_log_data 来运行的。作为一个参数,它仅仅是一个字节序列:
https://docs.rs/solana-program/latest/src/solana_program/log.rs.html#116-124
下面是 Solana 客户端中该系统调用的函数:
/// Print some slices as base64.
pub fn sol_log_data(data: &[&[u8]]) {
#[cfg(target_os = "solana")]
unsafe {
crate::syscalls::sol_log_data(data as *const _ as *const u8, data.len() as u64)
};
#[cfg(not(target_os = "solana"))]
crate::program_stubs::sol_log_data(data);
}
我们用来创建事件的 “struct” 结构体是对该字节序列的抽象。在幕后,Anchor 会将结构体转换为字节序列以传递给此函数。Solana 的系统调用只接受字节序列,而不接受结构体。
Solana 日志不用于历史查询
在 Ethereum 中,日志用于审计目的,但在 Solana 中,日志不能以这种方式使用,因为它们只能在发生时被查询。因此,它们更适合用于向前端应用程序传递信息。Solana 函数不能像 Solidity view 函数那样向前端返回数据,因此 Solana 日志是实现此目的的一种轻量级方式。
不过,事件会保留在区块浏览器中。请看这笔交易的最下方作为示例:
https://explorer.solana.com/tx/JgyHQPxL3cPLFtV4cx5i842ZgBx57R2fkNn2TZn1wsQZqVXKfijd43CEHo88C3ridK27Kw8KkMzfvDdqaS398SX
与 Ethereum 不同,Solana 交易可以通过地址查询
在 Ethereum 中,没有直接的方法可以查询发送到智能合约或来自特定钱包的交易。
我们可以使用 eth_getTransactionCount 来统计某个地址发送的交易数量。我们可以使用 eth_getTransactionByHash 通过交易哈希获取特定交易。我们可以使用 eth_getBlockByNumber 或 eth_getBlockByHash 获取特定区块中的交易。
然而,不可能通过地址获取所有交易。这必须通过解析自钱包激活或智能合约部署以来的每个区块来间接完成。
为了审计智能合约中的交易,开发者会添加 智能合约事件 以查询感兴趣的交易。
获取 Solana 中的交易历史
另一方面,Solana 有一个 RPC 函数 getSignaturesForAddress,它可以列出一个地址完成的所有交易。该地址可以是一个程序或一个钱包。
以下是一个列出某地址交易的脚本:
let web3 = require('@solana/web3.js');
const solanaConnection = new web3.Connection(web3.clusterApiUrl("mainnet-beta"));
const getTransactions = async(address,limit) => {
const pubKey = new web3.PublicKey(address);
let transactionList = await solanaConnection.getSignaturesForAddress(pubKey, {limit: limit});
let signatureList = transactionList.map(transaction => transaction.signature);
console.log(signatureList);
for await (const sig of signatureList) {
console.log(await solanaConnection.getParsedTransaction(sig, {maxSupportedTransactionVersion: 0}));
}
}
let myAddress = "enter and address here";
getTransactions(myAddress, 3);
请注意,交易的实际内容是使用 getParsedTransaction RPC 方法检索的,该方法仅在 @solana/web3.js SDK 中受支持。对于其他库,请改用 getTransaction。
首发于 2024 年 2 月 20 日