Solidityのイベントとは

イベント(Event)は、スマートコントラクト内で発生した出来事をブロックチェーンのトランザクションログに記録する仕組みです。記録されたイベントは、フロントエンドアプリケーションからリアルタイムで監視したり、過去のログをフィルタリングして検索したりできます。

イベントの主な用途は以下のとおりです。

  • フロントエンドへの通知: トランザクション完了後にUIを更新する
  • 履歴の記録: 重要なアクションの監査証跡を残す
  • オフチェーン索引: The Graphなどのインデクサーがデータを整理するための入力
  • ガスコストの節約: ストレージに保存するよりもイベント発行のほうが安い

イベントの基本構文

イベントの宣言と発行の基本形を見てみましょう。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract EventExample {
    // イベントの宣言
    event MessageChanged(
        address indexed sender,    // indexed: フィルタリング可能
        string oldMessage,
        string newMessage,
        uint256 timestamp
    );

    string public message;

    constructor(string memory _message) {
        message = _message;
    }

    function setMessage(string memory _newMessage) public {
        string memory oldMessage = message;
        message = _newMessage;

        // イベントの発行(emit)
        emit MessageChanged(
            msg.sender,
            oldMessage,
            _newMessage,
            block.timestamp
        );
    }
}

indexed パラメータ

indexed キーワードを付けたパラメータは、イベントの「トピック」として記録され、効率的なフィルタリングが可能になります。1つのイベントにつき最大3つのパラメータに indexed を付けられます。

event Transfer(
    address indexed from,     // トピック1: 送信元でフィルタ可能
    address indexed to,       // トピック2: 送信先でフィルタ可能
    uint256 indexed tokenId   // トピック3: トークンIDでフィルタ可能
);

実践的なイベント実装

文化財管理を題材に、複数のイベントを持つコントラクトを作成しましょう。

// contracts/EventLogger.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract EventLogger {
    // 各種イベント
    event ItemRegistered(
        uint256 indexed itemId,
        string name,
        address indexed registeredBy,
        uint256 timestamp
    );

    event ItemUpdated(
        uint256 indexed itemId,
        string field,
        string oldValue,
        string newValue,
        address indexed updatedBy,
        uint256 timestamp
    );

    event ItemTransferred(
        uint256 indexed itemId,
        address indexed from,
        address indexed to,
        uint256 timestamp
    );

    struct Item {
        string name;
        string description;
        address owner;
        bool exists;
    }

    mapping(uint256 => Item) public items;
    uint256 public nextItemId;

    /// @notice アイテムを登録する
    function registerItem(
        string memory name,
        string memory description
    ) public returns (uint256) {
        uint256 itemId = nextItemId++;

        items[itemId] = Item({
            name: name,
            description: description,
            owner: msg.sender,
            exists: true
        });

        emit ItemRegistered(
            itemId,
            name,
            msg.sender,
            block.timestamp
        );

        return itemId;
    }

    /// @notice アイテムの説明を更新する
    function updateDescription(
        uint256 itemId,
        string memory newDescription
    ) public {
        require(items[itemId].exists, "Item does not exist");
        require(
            items[itemId].owner == msg.sender,
            "Not the owner"
        );

        string memory oldDescription = items[itemId].description;
        items[itemId].description = newDescription;

        emit ItemUpdated(
            itemId,
            "description",
            oldDescription,
            newDescription,
            msg.sender,
            block.timestamp
        );
    }

    /// @notice アイテムの所有者を変更する
    function transferItem(uint256 itemId, address to) public {
        require(items[itemId].exists, "Item does not exist");
        require(
            items[itemId].owner == msg.sender,
            "Not the owner"
        );

        address from = items[itemId].owner;
        items[itemId].owner = to;

        emit ItemTransferred(
            itemId,
            from,
            to,
            block.timestamp
        );
    }
}

テストでイベントを検証する

Hardhatのテストでは、Chaiの emit マッチャーを使ってイベントの発行を検証できます。

// test/EventLogger.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";

describe("EventLogger", function () {
  async function deployFixture() {
    const [owner, other] = await ethers.getSigners();
    const EventLogger = await ethers.getContractFactory("EventLogger");
    const logger = await EventLogger.deploy();
    await logger.waitForDeployment();
    return { logger, owner, other };
  }

  describe("registerItem", function () {
    it("ItemRegistered イベントが発行される", async function () {
      const { logger, owner } = await deployFixture();

      await expect(logger.registerItem("古文書", "江戸時代の文書"))
        .to.emit(logger, "ItemRegistered")
        .withArgs(
          0,              // itemId
          "古文書",       // name
          owner.address,  // registeredBy
          (value: any) => true // timestamp(任意の値を許容)
        );
    });
  });

  describe("updateDescription", function () {
    it("ItemUpdated イベントが発行される", async function () {
      const { logger } = await deployFixture();

      // まずアイテムを登録
      await logger.registerItem("古文書", "江戸時代の文書");

      // 説明を更新
      await expect(
        logger.updateDescription(0, "江戸時代中期の公文書")
      )
        .to.emit(logger, "ItemUpdated")
        .withArgs(
          0,
          "description",
          "江戸時代の文書",
          "江戸時代中期の公文書",
          (value: any) => true,
          (value: any) => true
        );
    });
  });

  describe("transferItem", function () {
    it("ItemTransferred イベントが発行される", async function () {
      const { logger, owner, other } = await deployFixture();

      await logger.registerItem("掛軸", "室町時代の水墨画");

      await expect(logger.transferItem(0, other.address))
        .to.emit(logger, "ItemTransferred")
        .withArgs(
          0,
          owner.address,
          other.address,
          (value: any) => true
        );
    });
  });
});

ethers.js でイベントを取得する

ethers.js v6を使って、過去のイベントログを取得する方法を見てみましょう。

過去のイベントを取得(queryFilter)

// scripts/queryEvents.ts
import { ethers } from "hardhat";

async function main() {
  const logger = await ethers.getContractAt(
    "EventLogger",
    "デプロイ済みアドレス"
  );

  // すべてのItemRegisteredイベントを取得
  const filter = logger.filters.ItemRegistered();
  const events = await logger.queryFilter(filter);

  console.log(`=== 登録イベント(${events.length}件)===`);
  for (const event of events) {
    const args = (event as any).args;
    console.log(`- ID: ${args.itemId}`);
    console.log(`  名前: ${args.name}`);
    console.log(`  登録者: ${args.registeredBy}`);
    console.log(`  ブロック: ${event.blockNumber}`);
    console.log(`  TX: ${event.transactionHash}`);
    console.log();
  }

  // 特定のitemIdでフィルタリング
  const itemFilter = logger.filters.ItemRegistered(0);
  const itemEvents = await logger.queryFilter(itemFilter);
  console.log(
    `itemId=0 のイベント: ${itemEvents.length}件`
  );

  // ブロック範囲を指定してフィルタリング
  const rangeEvents = await logger.queryFilter(
    filter,
    0,       // fromBlock
    "latest" // toBlock
  );
  console.log(
    `全ブロック範囲のイベント: ${rangeEvents.length}件`
  );
}

main().catch(console.error);

リアルタイムでイベントを監視(on)

// scripts/watchEvents.ts
import { ethers } from "hardhat";

async function main() {
  const logger = await ethers.getContractAt(
    "EventLogger",
    "デプロイ済みアドレス"
  );

  console.log("イベントを監視中...");

  // ItemRegisteredイベントをリアルタイム監視
  logger.on("ItemRegistered", (itemId, name, registeredBy, timestamp) => {
    console.log("--- 新しいアイテムが登録されました ---");
    console.log(`  ID: ${itemId}`);
    console.log(`  名前: ${name}`);
    console.log(`  登録者: ${registeredBy}`);
    console.log(
      `  日時: ${new Date(Number(timestamp) * 1000).toLocaleString("ja-JP")}`
    );
  });

  // ItemTransferredイベントをリアルタイム監視
  logger.on("ItemTransferred", (itemId, from, to, timestamp) => {
    console.log("--- アイテムが移管されました ---");
    console.log(`  ID: ${itemId}`);
    console.log(`  移管元: ${from}`);
    console.log(`  移管先: ${to}`);
  });

  // 一度だけ検知する場合は once を使用
  logger.once("ItemUpdated", (...args) => {
    console.log("最初の更新イベントを検知:", args);
  });
}

main().catch(console.error);

ガスコストに関する注意点

イベントはストレージへの書き込みより安価ですが、完全に無料ではありません。特にパラメータの数や文字列の長さがガスコストに影響します。

// ガスコストの目安
emit SimpleEvent(42);                    // 低コスト
emit EventWithString("短い文字列");       // 中程度
emit EventWithLongString("非常に長い..."); // 高コスト

重要なのは、イベントはあくまで「ログ」であり、コントラクト内からイベントのデータを読み取ることはできないという点です。コントラクト内で参照する必要があるデータは、ストレージ(state変数)に保存する必要があります。

まとめ

本章では、Solidityのイベント機能について学びました。イベントの宣言と発行、indexed パラメータによるフィルタリング、テストでの検証方法、ethers.jsを使った過去のイベント取得とリアルタイム監視を実践しました。

イベントは、フロントエンドとの連携や監査証跡の記録において不可欠な機能です。次章では、ブロックチェーンとWeb3開発の全体像を俯瞰し、これまで学んだ要素がどのように関連しているかを整理します。

関連記事