1. 事件监听基础:Solidity事件定义与emit语法
事件(Event)在Solidity里是个很有意思的东西。我刚开始接触时,觉得它不就是个日志吗?后来踩过坑才明白——事件是智能合约与外部世界沟通的桥梁。说白了,你的合约跑在EVM里,外面的人想知道里面发生了什么,全靠事件。
1.1 Solidity事件定义
事件的定义语法其实很简洁。你只需要用 event 关键字,后面跟上事件名和参数列表。来看个例子:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TokenTransfer {
// 定义一个转账事件
event Transfer(
address indexed from,
address indexed to,
uint256 value
);
// 定义一个授权事件
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}
这里有个关键点:indexed 关键字。加了 indexed 的参数会被存入 Topic,不加的存入 Data。我习惯把「谁干的」「对谁干的」这类需要检索的字段设为 indexed,把具体数值放 Data 里。你想想看,如果每个参数都 indexed,Gas 费用会高不少。
重要提示:一个事件最多可以有3个 indexed 参数。这是 EVM 的硬性限制,超过会报错。我见过有人试图塞4个 indexed 参数,结果编译都过不了。
1.2 emit 语法
定义好事件后,怎么触发它?用 emit 关键字。嗯,这里要注意:Solidity 0.4.21 之前不需要 emit,直接写事件名就行。但新版本强制要求加 emit,这样代码可读性更好。
contract TokenTransfer {
event Transfer(address indexed from, address indexed to, uint256 value);
function transfer(address to, uint256 amount) public {
// 执行转账逻辑
// ...
// 触发事件
emit Transfer(msg.sender, to, amount);
}
}
我个人习惯把 emit 放在函数末尾,所有状态变更完成后才触发事件。为什么?因为如果中间 revert 了,emit 也不会执行,避免产生虚假事件。
小技巧:可以在事件参数中使用 struct 类型。Solidity 0.8.0 之后支持了。但要注意,struct 参数不能加 indexed,只能放 Data 里。
1.3 事件在EVM中的存储机制
事件到底存在哪?很多人以为存在合约存储里,其实不是。事件数据存在交易收据(Transaction Receipt)的日志(Log)中。每个区块的日志数据是独立的,不会影响合约状态。
一个事件日志包含两部分:
| 组成部分 | 说明 | 最大数量 |
|---|---|---|
| Topic | 事件签名 + indexed 参数 | 最多4个(1个签名 + 3个参数) |
| Data | 非 indexed 参数,ABI编码后 | 无限制(受Gas限制) |
Topic 和 Data 的区别在哪?Topic 可以被索引和检索,Data 只能被读取。我在项目中遇到过一个问题:有个同事把所有参数都加了 indexed,结果 Gas 费用暴涨。后来改成只索引关键字段,费用降了将近一半。
避坑指南:我曾经在事件里塞了一个很大的 bytes 数组,结果 Gas 费用高得离谱。事件不是用来存数据的,是用来通知的。大块数据请用事件+链下存储的方案。
1.4 事件签名哈希计算
每个事件都有一个唯一的签名哈希。这个哈希怎么算?很简单:对事件名和参数类型做 Keccak256 哈希。
比如 Transfer(address,address,uint256) 这个事件:
// 事件签名字符串
"Transfer(address,address,uint256)"
// 计算Keccak256哈希
keccak256("Transfer(address,address,uint256)")
= 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
这个哈希值就是 Topic[0],也就是事件的第一个 Topic。你可以在区块链浏览器上验证——每个 Transfer 事件的第一个 Topic 都是这个值。
为什么需要这个签名哈希?说白了,就是让监听者能快速过滤出自己关心的事件类型。你监听 Transfer 事件时,客户端会先匹配 Topic[0] 是否等于 Transfer 的签名哈希,匹配上了才继续解析后面的参数。
实用技巧:在 JavaScript 中计算事件签名哈希:
const Web3 = require('web3');
const web3 = new Web3();
const signature = web3.utils.keccak256("Transfer(address,address,uint256)");
console.log(signature);
// 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
1.5 实战中的注意事项
最后分享几个我踩过的坑:
- 事件参数顺序很重要:ABI 编码是按参数顺序来的,监听端解析时也必须按相同顺序。我见过有人改了合约参数顺序但忘了更新前端,结果解析出来的数据全乱了。
- 匿名事件:Solidity 支持
event Transfer(...) anonymous;这种写法。匿名事件不生成 Topic[0],Gas 更省,但无法通过事件签名过滤。除非你特别清楚自己在做什么,否则别用。 - 事件重载:同一个合约里可以有同名但参数不同的事件。但我不建议这么做——监听端容易搞混。保持事件名唯一,这是好习惯。
嗯,事件监听的基础就这些。下一章我们会聊怎么在链下监听这些事件,以及如何解析日志数据。到时候我会分享一个我实际项目中用的事件监听框架,保证实用。