事件是能方便地调用以太坊虚拟机日志功能的接口。应用程序可以通过以太坊客户端的 RPC 接口订阅和监听这些事件。
重点:记录区块链的日志,可以使用状态变量,也可以使用事件 Event,但 Event 使用的 gas 费比状态变量低。
原则:改变状态变量时,一定要触发事件。
Soliddity Event 事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中。这些日志与合约的地址关联,并记录到区块链中。每个交易收据包含 0 到多个 log 记录,log 表明着智能合约所触发的事件。
事件的定义:使用 event
关键字来定义一个事件 Event,语法如下:
event EventName(<parameter list>);
事件的触发:只能使用 emit
关键字来触发事件 Event,语法如下:
emit EventName(<parameter list>);
- 不带参数的 event
- 带参数的 event
- 带参数名的 event
- 带 indexed 参数名的 event
- 这种事件也被称为索引事件
- 语法:
event EventName(TypeName indexed varibleName....);
- 事件中 indexed 标记过的参数,可以在链外进行搜索查询。
- 一个事件中 indexed 标记过的参数最多有 3 个。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Event {
// 普通 event
event Log1(address, string);
// 带名字的 event
event Log2(address ads, string msg);
// 带 indexed 的event
event Log3(address indexed ads, string msg);
// indexed 在一个事件内使用次数不能超过3次
event Transfer(
address indexed from,
address indexed to,
uint256 indexed amount
);
function log1() external {
emit Log1(msg.sender, "Log111");
}
function log2() external {
emit Log2(msg.sender, "Log222");
}
function log3() external {
emit Log3(msg.sender, "Log333");
}
function transfer(address _to, uint256 amount) external {
emit Transfer(msg.sender, _to, amount);
}
}
[
{
"from": "0x7874d94b8f9E2a28FCceCE404666C984f33a82b8",
"topic": "0x1732d0c17008d342618e7f03069177d8d39391d79811bb4e706d7c6c84108c0f",
"event": "Log1",
"args": {}
}
]
[
{
"from": "0x7874d94b8f9E2a28FCceCE404666C984f33a82b8",
"topic": "0x54010eb0426bdddd13273086604fca7ba750a84093c6839732d954056646e81b",
"event": "Log2",
"args": {
"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"1": "Log222"
}
}
]
[
{
"from": "0x7874d94b8f9E2a28FCceCE404666C984f33a82b8",
"topic": "0x940879bf2d29cdfe8084f2f033d2168f5859a6e10530b61fb84dc1c5ddc9ca40",
"event": "Log3",
"args": {
"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"1": "Log333",
"ads": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"msg": "Log333"
}
}
]
[
{
"from": "0xfB72aAdB17a855D27A68B565ee0a84CB30A387e4",
"topic": "0xf485c071883274befba21423da7f60203f9df753bf614bca26c4763ed4b240fb",
"event": "Log4",
"args": {
"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"1": "Log444",
"ads": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"msg": "Log444"
}
}
]
[
{
"from": "0xfB72aAdB17a855D27A68B565ee0a84CB30A387e4",
"topic": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"event": "Transfer",
"args": {
"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"1": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"2": "1",
"from": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"to": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"amount": "1"
}
}
]
indexed 数据会被记录到 topics
中,可以用于检索。已索引的部分,最多有 3 个(对于非匿名事件)或 4 个(对于匿名事件)
对于非匿名事件,最多三个参数可以接收 indexed
属性(它是一个特殊的名为: "主题" 的数据结构,而不作为日志的数据部分)。主题仅有 32 字节, 因此如果:引用类型 标记为索引项,则它们的 keccak-256 哈希值会被作为 主题(topic) 保存。
主题(topic)让我们可以可以搜索事件,比如在为某些事件过滤一些区块,还可以按发起事件的合同地址来过滤事件。
例如, 使用如下的 web3.js subscribe("logs")方法
去过滤符合特定地址的 主题(topic) :
var options = {
fromBlock: 0,
address: web3.eth.defaultAccount,
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null],
};
web3.eth
.subscribe("logs", options, function (error, result) {
if (!error) console.log(result);
})
.on("data", function (log) {
console.log(log);
})
.on("changed", function (log) {});
主要用在链下服务,可以通过 RPC 获取,比如 web3 的以下方法:
myContract.once
myContract.events.MyEvent
myContract.getPastEvents
除非你用 anonymous
声明事件,否则事件签名的哈希值是一个 主题(topic)。同时也意味着对于匿名事件无法通过名字来过滤,仅能按合约地址过滤。匿名事件的优势是他们部署和调用的成本更低。它也允许你声明 4 个索引参与而不是 3 个。
pragma solidity >=0.4.21 <0.9.0;
contract ClientReceipt {
event Deposit(
address indexed from,
bytes32 indexed id,
uint value
);
function deposit(bytes32 id) public payable {
// 事件使用 emit 触发事件。
// 我们可以过滤对 `Deposit` 的调用,从而用 Javascript API 来查明对这个函数的任何调用(甚至是深度嵌套调用)。
emit Deposit(msg.sender, id, msg.value);
}
}
使用 JavaScript API 调用事件的用法如下:
var abi = /* abi 由编译器产生 */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...xlb67" /* 地址 */);
var depositEvent = clientReceipt.Deposit();
// 监听变化
depositEvent.watch(function(error, result) {
// 结果包含 非索引参数 以及 主题 topic
if (!error)
console.log(result);
});
// 或者通过传入回调函数,立即开始听监
var depositEvent = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});
上面的输出如下所示(有删减):
{
"returnValues": {
"from": "0x1111…FFFFCCCC",
"id": "0x50…sd5adb20",
"value": "0x420042"
},
"raw": {
"data": "0x7f…91385",
"topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
}
}
Log 可以像函数一样重载
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Event {
event Log(address ads);
event Log(address indexed ads, string msg); // 重载
function log1() external {
emit Log(msg.sender);
}
function log2() external {
emit Log(msg.sender, "Log111");
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Croedfund {
/* ============ Type Declaration ============ */
// 出资人角色
// * 仅需记录地址/金额即可
struct Donor {
address addr; //出资人地址
uint256 amount; //出资人金额
}
// 募资人角色
// * 用于表示一个募资项目,其中包括募资人地址、目标金额、
// 已筹集金额、捐赠者人数、项目状态以及所有的出资人。
struct Donee {
address creator; // 募资人地址
uint256 goal; // 众筹目标数量
uint32 startAt; // 开始时间
uint32 endAt; // 结束时间
bool claimed; // 是否被领取
uint256 amount; // 已筹集金额
uint256 donorCount; // * 捐赠者人数
mapping(uint256 => Donor) donorMap; // * 出资人字典
}
/* ============ State Variables ============ */
address payable owner; //合约拥有者
uint256 public doneeCount; // 募资人数量
mapping(uint256 => Donee) public doneeMap; //募资人字典
/* ============ Events ============ */
event Launch(
uint256 id,
address indexed creator,
uint256 goal,
uint32 startAt,
uint32 endAt
);
event Cancel(uint256 id);
event Donate(uint256 indexed id, address indexed caller, uint256 amount);
event Unpledge(uint256 indexed id, address indexed caller, uint256 amount);
event Claim(uint256 id, address creator, uint256 amount);
event Refund(uint256 indexed id, address indexed caller, uint256 amount);
/* ============ Modifier ============ */
modifier onlyOwner() {
require(msg.sender == owner, "only owner");
_;
}
// 验证募捐活动ID是否有效
modifier validDonee(uint256 doneeID) {
require(doneeID > 0 && doneeID <= doneeCount);
_;
}
/* ============ Errors ============ */
error MyError(string);
/* ============ Constructor ============ */
constructor() {
owner = payable(msg.sender);
}
/* ============ Functions ============ */
// 启动新众筹
function launch(
address _addr,
uint256 _goal,
uint32 _startAt,
uint32 _endAt
) external onlyOwner {
require(_startAt >= block.timestamp, "start at < now");
require(_startAt <= _endAt, "start at > end at");
require(_endAt <= block.timestamp + 30 days, "end at > max duration");
doneeCount++;
Donee storage donee = doneeMap[doneeCount];
donee.creator = _addr;
donee.goal = _goal;
donee.startAt = _startAt;
donee.endAt = _endAt;
emit Launch(doneeCount, msg.sender, _goal, _startAt, _endAt);
}
// 取消指定ID的众筹
function cancel(uint256 _id) external onlyOwner {
// 不需要修改,需用 memeory ,但是包含mapping类型,所以需要用 storage
Donee storage campaign = doneeMap[_id];
require(block.timestamp < campaign.startAt, "started"); // 必须还没有开始
delete doneeMap[_id];
emit Cancel(_id);
}
// 出资人捐赠
function donate(uint256 _id) external payable validDonee(_id) {
Donee storage donee = doneeMap[_id]; // 需要修改,所以使用 storage
require(block.timestamp >= donee.startAt, "not start"); //
require(block.timestamp <= donee.endAt, "ended"); //
donee.donorCount++;
donee.amount += msg.value;
Donor storage donor = donee.donorMap[donee.donorCount];
donor.addr = msg.sender;
donor.amount = msg.value;
emit Donate(_id, msg.sender, msg.value);
}
// 完成目标给募资人转账
function transfer(uint256 doneeID) public onlyOwner validDonee(doneeID) {
Donee storage donee = doneeMap[doneeID];
require(!donee.claimed, "is claimed");
require(block.timestamp >= donee.endAt, "not ended");
require(donee.amount >= donee.goal, "amount < goal");
// 设置已经支付的状态
donee.claimed = true;
// 给募资人转账
payable(donee.creator).transfer(donee.goal);
emit Claim(doneeID, msg.sender, donee.amount);
}
/* ============ Helper ============ */
fallback() external {}
receive() external payable {}
// 获取当前合约的余额
function getBalance() public view returns (uint256) {
return address(this).balance;
}
// 合约的余额转账到拥有者
function withdraw(uint256 doneeID) public onlyOwner {
Donee storage donee = doneeMap[doneeID];
require(donee.claimed, "not claimed");
require(block.timestamp >= donee.endAt, "not ended");
payable(msg.sender).transfer(address(this).balance);
}
// 获取项目状态
function getStatus(uint256 doneeID)
public
view
validDonee(doneeID)
returns (bool)
{
Donee storage donee = doneeMap[doneeID];
return (block.timestamp >= donee.startAt &&
block.timestamp <= donee.endAt);
}
}
- address1 launch 一次活动
- goal 为 10
- 时间戳获取: https://tool.chinaz.com/tools/unixtime.aspx
- doneeMap 查询 id 1 信息
- getStatus 查询 id 1 是否开始
- address2 donate 6
- address3 donate 7
- doneeMap 查询 id 1 信息