在以太坊智能合约开发中,事件(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 ethersnode 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 事件记录拍卖流程。 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!