Event در سالیدیتی روشی برای ثبت لاگ در رسید تراکنش است؛ از داخل قرارداد با کلمه emit فراخوانی می‌شود، با indexed می‌توانید بعضی پارامترها را قابل فیلتر کردن کنید و در فرانت‌اند با کتابخانه‌هایی مثل ethers.js یا web3.js می‌توان به رویدادها گوش داد یا آن‌ها را جست و جو کرد.

Event در سالیدیتی چیست و چه کاربردی دارد؟

رویدادها پیام‌هایی هستند که هنگام اجرای یک تراکنش ثبت می‌شوند و خارج از زنجیره (off-chain) مصرف می‌شوند؛ مثلا برای به‌روزرسانی رابط کاربری، نوتیفیکیشن یا ایندکس‌کردن داده در یک سرویس. لاگ‌های رویداد داخل رسید تراکنش ذخیره می‌شوند و بخشی از state قرارداد نیستند؛ یعنی قراردادها نمی‌توانند لاگ‌های قبلی را بخوانند و نباید منطق حیاتی را فقط بر رویدادها تکیه دهید.

هر رویداد شامل «topics» (برای فیلتر سریع) و «data» (باقی پارامترها) است. استفاده از رویداد معمولا از نوشتن در storage ارزان‌تر است، اما رایگان نیست و هر چه topics بیشتری داشته باشید، Gas بیشتری می‌پردازید.

تعریف Event و emit در سالیدیتی (با مثال)

در مثال زیر یک Vault ساده داریم که واریز و برداشت را لاگ می‌کند. این کد صرفا آموزشی است.

pragma solidity ^0.8.20;

contract Vault {
    event Deposited(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);

    mapping(address => uint256) public balance;

    function deposit() external payable {
        require(msg.value > 0, "No Ether");
        balance[msg.sender] += msg.value;
        emit Deposited(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) external {
        require(balance[msg.sender] >= amount, "Insufficient");
        balance[msg.sender] -= amount;

        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "Transfer failed");

        emit Withdrawn(msg.sender, amount);
    }
}

نکته امنیتی: الگوی برداشت مستقیم با call می‌تواند در شرایطی به ریسک بازدرآمد (Reentrancy) منجر شود. برای پرداخت‌های کاربرمحور، الگوی Pull Payment انتخاب مطمئن‌تری است. راهنمای عملی را در آموزش Pull Payment در سالیدیتی ببینید.

نکات مهم طراحی و Gas

  • رویدادها منبع حقیقت نیستند؛ وضعیت را در storage نگه دارید و از رویداد برای اطلاع‌رسانی استفاده کنید.
  • فقط پارامترهایی را indexed کنید که واقعا به فیلتر آن‌ها در فرانت‌اند نیاز دارید؛ هر پارامتر indexed یک topic اضافه می‌کند و هزینه گس را بالا می‌برد.
  • حداکثر 3 پارامتر را می‌توان indexed کرد. امضای رویداد نیز خودش یک topic است (به جز رویدادهای anonymous).

کلمه کلیدی indexed دقیقا چه می‌کند؟

وقتی پارامتری را indexed می‌کنید، مقدار آن در بخش topics قرار می‌گیرد تا بتوانید با RPC فیلترش کنید. تفاوت‌ها:

  • برای نوع‌های ثابت مثل address، uint256 یا bytes32، مقدار 32-بایتی مستقیما در topic ذخیره می‌شود.
  • برای نوع‌های پویا مثل string، bytes یا array، در topic «هش keccak256 از داده خام» ذخیره می‌شود. بنابراین مقدار اصلی از روی topic قابل بازیابی نیست و برای فیلتر کردن باید همان هش را بسازید.

اگر لازم است برای فیلتر یک مقدار پویا هش بسازید، به نحوه کدگذاری هنگام هش کردن دقت کنید. تفاوت encode و encodePacked در نتیجه هش تاثیر می‌گذارد. توضیح کامل را در تفاوت abi.encode و abi.encodePacked در سالیدیتی بخوانید.

رویداد anonymous چیست؟

اگر رویداد را anonymous تعریف کنید، topic مربوط به امضای رویداد حذف می‌شود و تنها topics پارامترهای indexed باقی می‌مانند. یافتن این رویداد بدون دانستن آدرس قرارداد و فیلتر دقیق سخت‌تر است و معمولا برای استفاده عمومی پیشنهاد نمی‌شود.

خواندن لاگ‌ها در فرانت‌اند با ethers.js

در فرانت‌اند دو کار اصلی انجام می‌دهید: گوش دادن زنده به رویدادها و جست‌وجوی رویدادهای گذشته. نمونه‌های زیر با ethers.js نوشته شده‌اند.

گوش دادن زنده به رویداد

import { ethers } from "ethers";

const provider = new ethers.BrowserProvider(window.ethereum);
// یا: const provider = new ethers.JsonRpcProvider(RPC_URL);

const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);

// شنود زنده
contract.on("Deposited", (user, amount, event) => {
  console.log("Deposited:", user, amount.toString(), "in block", event.blockNumber);
});

// برای لغو شنود:
// contract.off("Deposited", listener);

جست‌وجوی رویدادهای گذشته با فیلتر

برای فیلتر کردن، مقادیر پارامترهای indexed را به ترتیب پاس بدهید. مقدار null یعنی «هر مقدار».

import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider(RPC_URL);
const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);

// مثال: همه Deposited های یک کاربر مشخص در ۵ هزار بلاک اخیر
const current = await provider.getBlockNumber();
const fromBlock = Math.max(0, current - 5000);

const user = "0xUserAddress...";
const filter = contract.filters.Deposited(user, null); // user indexed است

const events = await contract.queryFilter(filter, fromBlock, "latest");

for (const ev of events) {
  const [u, amount] = ev.args;
  console.log("Past deposit:", u, amount.toString(), "block", ev.blockNumber);
}

اگر لازم دارید مستقیما از provider.getLogs استفاده کنید، می‌توانید ابتدا فیلتر را از contract.filters بسازید و همان را به provider بدهید تا با جزئیات topics درگیر نشوید:

const filter = contract.filters.Deposited(user, null);
const logs = await provider.getLogs({ ...filter, fromBlock, toBlock: "latest" });
// برای دیکود:
for (const log of logs) {
  const parsed = contract.interface.parseLog(log);
  console.log(parsed.name, parsed.args);
}

تحلیل لاگ‌های یک تراکنش خاص

برای بررسی نتیجه یک تراکنش، رسید آن را بگیرید و لاگ‌های مربوط به آدرس قرارداد را دیکود کنید.

const receipt = await provider.getTransactionReceipt("0xTxHash...");
for (const log of receipt.logs) {
  if (log.address.toLowerCase() !== CONTRACT_ADDRESS.toLowerCase()) continue;
  try {
    const parsed = contract.interface.parseLog(log);
    console.log("Event:", parsed.name, parsed.args);
  } catch (e) {
    // ممکن است لاگ متعلق به ABI فعلی نباشد (مثلا آپگرید)
  }
}

برای فیلتر کردن بر اساس string یا bytes که indexed شده‌اند، باید هش keccak256 همان مقدار را در topics قرار دهید. در جاوااسکریپت می‌توانید از ethers.id(“value”) برای تولید هش UTF-8 یک رشته استفاده کنید.

سناریوی کامل: Vault ساده + پایش رویدادها

قرارداد Vault بالا دو رویداد Deposited و Withdrawn را منتشر می‌کند. در فرانت‌اند می‌توانید هم‌زمان هم رویدادهای جدید را نشان دهید و هم گذشته را بازیابی کنید:

const provider = new ethers.JsonRpcProvider(RPC_URL);
const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);

// 1) بازیابی تاریخچه اخیر
const latest = await provider.getBlockNumber();
const history = await contract.queryFilter(contract.filters.Deposited(null, null), latest - 10_000, "latest");
render(history.map(ev => ({ user: ev.args[0], amount: ev.args[1].toString(), block: ev.blockNumber })));

// 2) شنود زنده و افزودن به لیست
contract.on("Deposited", (user, amount, ev) => {
  appendRow({ user, amount: amount.toString(), block: ev.blockNumber });
});

برای نمایش دقیق موجودی، همیشه به متغیرهای on-chain (مثل balance) رجوع کنید و رویداد را صرفا برای اطلاع‌رسانی UI به کار ببرید.

خطاهای رایج و روش تشخیص

  • خواندن رویدادها در داخل قرارداد: ممکن نیست. قراردادها به لاگ‌های قدیمی دسترسی ندارند.
  • سوءبرداشت درباره indexed string/bytes: فقط هش در topic ذخیره می‌شود؛ مقدار اصلی در دسترس نیست. برای فیلتر باید دقیقا همان هش را بسازید.
  • ترتیب پارامترها در فیلتر اشتباه است: ترتیب و تعداد پارامترهای indexed باید مطابق تعریف رویداد باشد.
  • پنجره جست‌وجو خیلی بزرگ: درخواست از بلاک صفر تا الان ممکن است توسط RPC رد شود یا کند باشد. بازه را قطعه‌بندی کنید.
  • تایید بلوک‌ها: به‌خاطر بازسازماندهی زنجیره، بهتر است برای منطق حساس، بعد از چند تایید (confirmations) روی رویدادها عمل کنید. برای تراکنش خودتان، از tx.wait(n) استفاده کنید.

گام بعدی چیست؟

اگر می‌خواهید مفاهیم رویدادها را در کنار مباحث ABI، امنیت و طراحی قراردادهای واقعی تمرین کنید، مسیر ساخت‌یافته با پروژه‌های عملی در آموزش جامع سالیدیتی + 5 پروژه عملی در دسترس است.

سالیدیتی را با ۵ پروژه عملی یاد بگیر
از مفاهیم پایه تا نوشتن، تست و دیپلوی قرارداد هوشمند: To Do List، انتخابات، قرارداد پیمانکاری، توکن ERC20 و NFT. حدود ۵۰ جلسه، با پشتیبانی تیکتی مستقیم با مدرس.
۷,۹۰۰,۰۰۰تومان
یا 4 قسط با اسنپ‌پی؛ هر قسط ۱,۹۷۵,۰۰۰تومان
مشاهده سرفصل‌ها و ثبت‌نام