在以太坊智能合约开发中,事件(Events)和监听器(Listeners)是实现合约间通信、链上链下交互以及状态跟踪的重要机制。Solidity的事件机制允许合约记录关键操作并通知外部系统(如前端、链下服务或其他合约),而监听器则通过监听这些事件实现实时响应。事件(Events)简介什么是事件
在以太坊智能合约开发中,事件(Events)和监听器(Listeners)是实现合约间通信、链上链下交互以及状态跟踪的重要机制。Solidity 的事件机制允许合约记录关键操作并通知外部系统(如前端、链下服务或其他合约),而监听器则通过监听这些事件实现实时响应。
Solidity 的事件是一种链上日志机制,用于记录合约状态变化或关键操作。事件存储在以太坊区块链的日志(Logs)中,供链下系统(如前端或服务器)或链上其他合约读取。事件的主要特点:
事件在 Solidity 中使用 event
关键字定义,通常包含参数以传递数据。事件可以包含索引参数(indexed
),以便在链下高效查询。
语法:
event EventName(type param1, type indexed param2, type param3);
indexed
:最多 3 个参数可标记为 indexed
,用于日志过滤。data
字段,查询成本较高。emit
关键字。以下是一个简单的 ERC20 代币合约,定义并触发转账事件。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Token {
mapping(address => uint256) public balances;
string public name = "MyToken";
string public symbol = "MTK";
uint256 public totalSupply;
// 定义转账事件
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(uint256 initialSupply) {
totalSupply = initialSupply;
balances[msg.sender] = initialSupply;
}
function transfer(address to, uint256 value) public returns (bool) {
require(to != address(0), "Invalid address");
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] -= value;
balances[to] += value;
// 触发转账事件
emit Transfer(msg.sender, to, value);
return true;
}
}
说明:
Transfer
事件记录转账的发送者(from
)、接收者(to
)和金额(value
)。from
和 to
使用 indexed
,便于链下查询。emit Transfer
在转账后触发,记录操作。topics
字段,查询更快。indexed
。indexed
,节省 Gas。链下应用通过 Web3.js 或 ethers.js 监听事件,实时响应合约状态变化。以下以 ethers.js 为例,展示如何监听 Transfer
事件。
const { ethers } = require("ethers");
async function listenTransfer() {
// 连接到以太坊节点(例如 Sepolia 测试网)
const provider = new ethers.JsonRpcProvider("https://sepolia.infura.io/v3/YOUR_INFURA_KEY");
// 合约地址和 ABI
const contractAddress = "0xYOUR_CONTRACT_ADDRESS";
const abi = [
"event Transfer(address indexed from, address indexed to, uint256 value)",
"function transfer(address to, uint256 value) returns (bool)"
];
// 创建合约实例
const contract = new ethers.Contract(contractAddress, abi, provider);
// 监听 Transfer 事件
contract.on("Transfer", (from, to, value, event) => {
console.log(`Transfer from ${from} to ${to} with value ${ethers.formatEther(value)}`);
console.log("Event details:", event);
});
console.log("Listening for Transfer events...");
}
listenTransfer().catch(console.error);
说明:
contract.on
监听 Transfer
事件,实时打印转账信息。event
参数包含日志详细信息(如块号、交易哈希)。npm install ethers
node listen.js
注意事项:
contract.filters.Transfer
) 按特定参数(如 from
地址)查询。合约间通信可以通过触发和监听事件实现。一种常见模式是:一个合约触发事件,另一个合约通过调用或链下监听间接响应。
以下是两个合约:Emitter
触发事件,Listener
通过调用获取事件数据(模拟监听)。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Emitter {
event DataSent(address indexed sender, uint256 value, bytes data);
function sendData(uint256 value, bytes memory data) public {
emit DataSent(msg.sender, value, data);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./Emitter.sol";
contract Listener {
address public emitterAddress;
mapping(address => uint256) public receivedValues;
event DataReceived(address indexed sender, uint256 value, bytes data);
constructor(address _emitter) {
emitterAddress = _emitter;
}
// 模拟监听:调用 Emitter 的函数并记录结果
function receiveData(uint256 value, bytes memory data) public {
// 假设通过链下监听 Emitter 的事件后调用此函数
require(msg.sender == emitterAddress, "Only emitter can call");
receivedValues[msg.sender] = value;
emit DataReceived(msg.sender, value, data);
}
}
说明:
Emitter
触发 DataSent
事件,记录发送者、值和数据。Listener
模拟监听,通过 receiveData
记录数据(实际中需链下触发)。receiveData
,模拟事件响应。限制:
使用 Hardhat 部署和测试上述合约。
Hardhat 配置:
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: "0.8.20",
networks: {
hardhat: {},
sepolia: {
url: "https://sepolia.infura.io/v3/YOUR_INFURA_KEY",
accounts: ["YOUR_PRIVATE_KEY"]
}
}
};
部署脚本:
async function main() {
const Emitter = await ethers.getContractFactory("Emitter");
const emitter = await Emitter.deploy();
await emitter.deployed();
console.log("Emitter deployed to:", emitter.address);
const Listener = await ethers.getContractFactory("Listener");
const listener = await Listener.deploy(emitter.address);
await listener.deployed();
console.log("Listener deployed to:", listener.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
测试用例:
const { expect } = require("chai");
describe("Emitter and Listener", function () {
let Emitter, Listener, emitter, listener, owner;
beforeEach(async function () {
Emitter = await ethers.getContractFactory("Emitter");
Listener = await ethers.getContractFactory("Listener");
[owner] = await ethers.getSigners();
emitter = await Emitter.deploy();
await emitter.deployed();
listener = await Listener.deploy(emitter.address);
await listener.deployed();
});
it("should emit and receive data", async function () {
// 触发 Emitter 的事件
const tx = await emitter.sendData(100, "0x1234");
const receipt = await tx.wait();
// 获取事件
const event = receipt.logs[0];
expect(event.eventName).to.equal("DataSent");
expect(event.args.sender).to.equal(owner.address);
expect(event.args.value).to.equal(100);
// 模拟 Listener 接收
await listener.receiveData(100, "0x1234");
expect(await listener.receivedValues(emitter.address)).to.equal(100);
// 验证 Listener 事件
await expect(listener.receiveData(100, "0x1234"))
.to.emit(listener, "DataReceived")
.withArgs(emitter.address, 100, "0x1234");
});
});
运行测试:
npx hardhat test
说明:
Emitter
触发 DataSent
事件。Listener
接收数据并触发 DataReceived
事件。expect().to.emit()
检查事件触发。Contract A
触发事件,链下脚本监听并调用 Contract B
。Contract A
触发事件(如 DataSent
)。Contract B
的函数,传递事件数据。示例:链下监听脚本
const { ethers } = require("ethers");
async function listenAndCall() {
const provider = new ethers.JsonRpcProvider("https://sepolia.infura.io/v3/YOUR_INFURA_KEY");
const wallet = new ethers.Wallet("YOUR_PRIVATE_KEY", provider);
const emitterAddress = "0xYOUR_EMITTER_ADDRESS";
const listenerAddress = "0xYOUR_LISTENER_ADDRESS";
const emitterAbi = ["event DataSent(address indexed sender, uint256 value, bytes data)"];
const listenerAbi = ["function receiveData(uint256 value, bytes memory data)"];
const emitter = new ethers.Contract(emitterAddress, emitterAbi, provider);
const listener = new ethers.Contract(listenerAddress, listenerAbi, wallet);
emitter.on("DataSent", async (sender, value, data) => {
console.log(`Received: ${value} from ${sender}`);
const tx = await listener.receiveData(value, data);
await tx.wait();
console.log("Data forwarded to Listener");
});
console.log("Listening for DataSent events...");
}
listenAndCall().catch(console.error);
Contract A
触发事件并直接调用 Contract B
的函数。Contract A
触发事件并调用 Contract B
的函数。Contract B
记录数据并触发响应事件。示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Emitter {
event DataSent(address indexed sender, uint256 value);
function sendData(address listener, uint256 value) public {
emit DataSent(msg.sender, value);
Listener(listener).receiveData(value);
}
}
contract Listener {
mapping(address => uint256) public receivedValues;
event DataReceived(address indexed sender, uint256 value);
function receiveData(uint256 value) public {
receivedValues[msg.sender] = value;
emit DataReceived(msg.sender, value);
}
}
说明:
Emitter
直接调用 Listener.receiveData
。Contract A
触发事件。Contract B
。实现提示:
ChainlinkClient
合约。事件设计:
indexed
参数优化查询效率。监听器实现:
wss://
)提高实时性。fromBlock
回溯)。ethers.Contract
或 Web3.js 的 contract.events
。合约间通信:
Gas 优化:
测试与验证:
安全性:
ReentrancyGuard
。require(msg.sender == emitterAddress)
)。以下是一个 NFT 拍卖系统,展示事件和跨合约通信的应用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract NFTAuction is ERC721, ReentrancyGuard {
struct Auction {
uint256 tokenId;
address seller;
uint256 highestBid;
address highestBidder;
uint256 endTime;
bool ended;
}
mapping(uint256 => Auction) public auctions;
uint256 public auctionCount;
event AuctionCreated(uint256 indexed auctionId, uint256 tokenId, address seller, uint256 endTime);
event BidPlaced(uint256 indexed auctionId, address bidder, uint256 amount);
event AuctionEnded(uint256 indexed auctionId, address winner, uint256 amount);
constructor() ERC721("NFTAuction", "NFT") {}
function createAuction(uint256 tokenId, uint256 duration) public {
require(ownerOf(tokenId) == msg.sender, "Not owner");
uint256 auctionId = auctionCount++;
auctions[auctionId] = Auction({
tokenId: tokenId,
seller: msg.sender,
highestBid: 0,
highestBidder: address(0),
endTime: block.timestamp + duration,
ended: false
});
_transfer(msg.sender, address(this), tokenId);
emit AuctionCreated(auctionId, tokenId, msg.sender, block.timestamp + duration);
}
function bid(uint256 auctionId) public payable nonReentrant {
Auction storage auction = auctions[auctionId];
require(!auction.ended, "Auction ended");
require(block.timestamp < auction.endTime, "Time expired");
require(msg.value > auction.highestBid, "Bid too low");
if (auction.highestBidder != address(0)) {
payable(auction.highestBidder).transfer(auction.highestBid);
}
auction.highestBid = msg.value;
auction.highestBidder = msg.sender;
emit BidPlaced(auctionId, msg.sender, msg.value);
}
function endAuction(uint256 auctionId) public nonReentrant {
Auction storage auction = auctions[auctionId];
require(!auction.ended, "Auction already ended");
require(block.timestamp >= auction.endTime, "Auction not yet ended");
require(msg.sender == auction.seller, "Not seller");
auction.ended = true;
if (auction.highestBidder != address(0)) {
_transfer(address(this), auction.highestBidder, auction.tokenId);
payable(auction.seller).transfer(auction.highestBid);
} else {
_transfer(address(this), auction.seller, auction.tokenId);
}
emit AuctionEnded(auctionId, auction.highestBidder, auction.highestBid);
}
}
测试用例:
const { expect } = require("chai");
describe("NFTAuction", function () {
let NFTAuction, auction, owner, bidder1, bidder2;
beforeEach(async function () {
NFTAuction = await ethers.getContractFactory("NFTAuction");
[owner, bidder1, bidder2] = await ethers.getSigners();
auction = await NFTAuction.deploy();
await auction.deployed();
// 铸造 NFT 并创建拍卖
await auction._mint(owner.address, 1);
await auction.createAuction(1, 3600);
});
it("should create and end auction", async function () {
// 出价
await auction.connect(bidder1).bid(0, { value: ethers.parseEther("1") });
await expect(auction.connect(bidder1).bid(0, { value: ethers.parseEther("1") }))
.to.emit(auction, "BidPlaced")
.withArgs(0, bidder1.address, ethers.parseEther("1"));
// 快进时间
await ethers.provider.send("evm_increaseTime", [3600]);
await ethers.provider.send("evm_mine");
// 结束拍卖
await expect(auction.connect(owner).endAuction(0))
.to.emit(auction, "AuctionEnded")
.withArgs(0, bidder1.address, ethers.parseEther("1"));
expect(await auction.ownerOf(1)).to.equal(bidder1.address);
});
});
说明:
AuctionCreated
, BidPlaced
, AuctionEnded
事件记录拍卖流程。如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!