😎

Solidity基礎学習15日目(Event)

に公開

Solidity基礎学習 - イベント(Event)システムとログ管理

日付: 2025年9月8日
学習内容: Solidityのイベント(Event)システム、ログ記録、フロントエンドとの通信、indexedパラメータによる検索・フィルタリングについて

1. イベント(Event)の基本概念

イベントの定義と役割

Solidityのイベントは、スマートコントラクトから外部アプリケーションへの通知システムです。ブロックチェーン上に永続的に記録されるログデータを作成し、トランザクションの実行履歴として残します。

重要なポイント

イベントの基本構造

// イベントの定義
event TokenTransfer(address indexed _sender, address indexed _receiver, uint _value);

// イベントの発火
emit TokenTransfer(msg.sender, _receiver, _value);

イベントの特徴:

  • 永続的記録: ブロックチェーンに永続的に保存される
  • ガス効率: ストレージ保存よりコストが安い
  • 検索可能: indexedパラメータで効率的な検索が可能
  • 外部通信: フロントエンドアプリケーションとの通信手段

2. イベントの主要な役割

2.1 ログの記録機能

実装コード

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.30;

contract TokenManager {
    mapping(address => uint256) public userBalance;
    
    // トークン転送イベントの定義
    event TokenTransfer(
        address indexed _sender, 
        address indexed _receiver, 
        uint256 _amount
    );
    
    // 残高更新イベントの定義
    event BalanceUpdated(
        address indexed _user, 
        uint256 _oldBalance, 
        uint256 _newBalance
    );
    
    constructor() {
        userBalance[msg.sender] = 1000;
    }
    
    function transferTokens(address _receiver, uint256 _amount) public returns(bool) {
        require(userBalance[msg.sender] >= _amount, "Insufficient balance");
        
        uint256 oldSenderBalance = userBalance[msg.sender];
        uint256 oldReceiverBalance = userBalance[_receiver];
        
        userBalance[msg.sender] -= _amount;
        userBalance[_receiver] += _amount;
        
        // イベントの発火
        emit TokenTransfer(msg.sender, _receiver, _amount);
        emit BalanceUpdated(msg.sender, oldSenderBalance, userBalance[msg.sender]);
        emit BalanceUpdated(_receiver, oldReceiverBalance, userBalance[_receiver]);
        
        return true;
    }
}

ログ記録の重要性:

  • 取引履歴: 全てのトークン転送の記録
  • 監査証跡: 変更履歴の追跡可能性
  • 透明性: ブロックチェーンの透明性の実現
  • デバッグ: 開発時の問題特定

2.2 フロントエンドとの通信

実装コード

contract UserActivityTracker {
    struct UserActivity {
        uint256 lastActivity;
        uint256 activityCount;
        string lastAction;
    }
    
    mapping(address => UserActivity) public userActivities;
    
    // ユーザーアクティビティイベント
    event UserAction(
        address indexed _user,
        string _action,
        uint256 _timestamp,
        uint256 _activityCount
    );
    
    // システム通知イベント
    event SystemNotification(
        string _message,
        uint256 _timestamp,
        address _affectedUser
    );
    
    function performAction(string memory _action) public {
        UserActivity storage activity = userActivities[msg.sender];
        activity.lastActivity = block.timestamp;
        activity.activityCount++;
        activity.lastAction = _action;
        
        // アクティビティイベントの発火
        emit UserAction(msg.sender, _action, block.timestamp, activity.activityCount);
        
        // 特定条件でのシステム通知
        if (activity.activityCount % 10 == 0) {
            emit SystemNotification(
                "Congratulations! You've completed 10 actions!",
                block.timestamp,
                msg.sender
            );
        }
    }
}

フロントエンド通信の特徴:

  • リアルタイム通知: 即座な状態変化の通知
  • イベント監視: 特定イベントの監視と処理
  • 状態同期: フロントエンドとコントラクトの状態同期
  • ユーザー体験: インタラクティブなユーザー体験の実現

3. indexedパラメータによる検索・フィルタリング

3.1 indexedパラメータの基本

実装コード

contract AdvancedEventSystem {
    // indexedパラメータを使用したイベント定義
    event UserRegistration(
        address indexed _user,           // indexed: 検索可能
        string _username,                // 非indexed: データのみ
        uint256 indexed _registrationTime, // indexed: 検索可能
        uint256 _userId                  // 非indexed: データのみ
    );
    
    event TokenPurchase(
        address indexed _buyer,          // indexed: 検索可能
        address indexed _seller,         // indexed: 検索可能
        uint256 indexed _tokenId,        // indexed: 検索可能
        uint256 _price,                  // 非indexed: データのみ
        string _tokenName                // 非indexed: データのみ
    );
    
    uint256 public nextUserId = 1;
    mapping(address => uint256) public userIds;
    
    function registerUser(string memory _username) public {
        require(userIds[msg.sender] == 0, "User already registered");
        
        uint256 userId = nextUserId++;
        userIds[msg.sender] = userId;
        
        // indexedパラメータを含むイベントの発火
        emit UserRegistration(
            msg.sender,
            _username,
            block.timestamp,
            userId
        );
    }
    
    function purchaseToken(
        address _seller,
        uint256 _tokenId,
        uint256 _price,
        string memory _tokenName
    ) public {
        // indexedパラメータを含むイベントの発火
        emit TokenPurchase(
            msg.sender,
            _seller,
            _tokenId,
            _price,
            _tokenName
        );
    }
}

indexedパラメータの特徴:

  • 検索可能: 特定の値でイベントを検索可能
  • フィルタリング: 条件に合致するイベントのみを取得
  • 効率的: 高速な検索とフィルタリング
  • 制限: 最大3つのindexedパラメータまで

3.2 indexedパラメータの制限とベストプラクティス

実装コード

contract OptimizedEventSystem {
    // 適切なindexedパラメータの使用例
    event OrderCreated(
        address indexed _customer,       // 顧客で検索
        address indexed _merchant,       // 店舗で検索
        uint256 indexed _orderId,        // 注文IDで検索
        uint256 _totalAmount,            // 非indexed: 金額データ
        string _orderDetails,            // 非indexed: 詳細情報
        uint256 _timestamp               // 非indexed: タイムスタンプ
    );
    
    event OrderStatusChanged(
        uint256 indexed _orderId,        // 注文IDで検索
        string _oldStatus,               // 非indexed: 旧ステータス
        string _newStatus,               // 非indexed: 新ステータス
        uint256 _timestamp               // 非indexed: 変更時刻
    );
    
    uint256 public nextOrderId = 1;
    mapping(uint256 => address) public orderCustomers;
    mapping(uint256 => address) public orderMerchants;
    
    function createOrder(
        address _merchant,
        uint256 _totalAmount,
        string memory _orderDetails
    ) public returns(uint256) {
        uint256 orderId = nextOrderId++;
        orderCustomers[orderId] = msg.sender;
        orderMerchants[orderId] = _merchant;
        
        emit OrderCreated(
            msg.sender,
            _merchant,
            orderId,
            _totalAmount,
            _orderDetails,
            block.timestamp
        );
        
        return orderId;
    }
    
    function updateOrderStatus(
        uint256 _orderId,
        string memory _newStatus
    ) public {
        require(
            orderMerchants[_orderId] == msg.sender,
            "Only merchant can update status"
        );
        
        emit OrderStatusChanged(
            _orderId,
            "Pending", // 簡略化のため固定値
            _newStatus,
            block.timestamp
        );
    }
}

indexedパラメータの制限:

  • 最大3つ: 1つのイベントで最大3つのindexedパラメータ
  • 32バイト制限: indexedパラメータは32バイトに制限
  • 完全一致: フィルタリングは完全一致のみ(部分一致不可)
  • 検索優先: 最も検索頻度の高いパラメータをindexedに設定

4. イベントの実用的な応用

4.1 複雑なイベントシステム

実装コード

contract ComprehensiveEventSystem {
    enum OrderStatus { Created, Confirmed, Shipped, Delivered, Cancelled }
    
    struct Order {
        uint256 orderId;
        address customer;
        address merchant;
        uint256 totalAmount;
        OrderStatus status;
        uint256 createdAt;
    }
    
    mapping(uint256 => Order) public orders;
    uint256 public nextOrderId = 1;
    
    // 注文関連イベント
    event OrderCreated(
        uint256 indexed _orderId,
        address indexed _customer,
        address indexed _merchant,
        uint256 _totalAmount,
        uint256 _timestamp
    );
    
    event OrderStatusUpdated(
        uint256 indexed _orderId,
        OrderStatus _oldStatus,
        OrderStatus _newStatus,
        uint256 _timestamp
    );
    
    event PaymentProcessed(
        uint256 indexed _orderId,
        address indexed _payer,
        uint256 _amount,
        string _paymentMethod,
        uint256 _timestamp
    );
    
    event RefundIssued(
        uint256 indexed _orderId,
        address indexed _recipient,
        uint256 _amount,
        string _reason,
        uint256 _timestamp
    );
    
    function createOrder(
        address _merchant,
        uint256 _totalAmount
    ) public returns(uint256) {
        uint256 orderId = nextOrderId++;
        
        orders[orderId] = Order({
            orderId: orderId,
            customer: msg.sender,
            merchant: _merchant,
            totalAmount: _totalAmount,
            status: OrderStatus.Created,
            createdAt: block.timestamp
        });
        
        emit OrderCreated(
            orderId,
            msg.sender,
            _merchant,
            _totalAmount,
            block.timestamp
        );
        
        return orderId;
    }
    
    function updateOrderStatus(
        uint256 _orderId,
        OrderStatus _newStatus
    ) public {
        require(
            orders[_orderId].merchant == msg.sender,
            "Only merchant can update status"
        );
        
        OrderStatus oldStatus = orders[_orderId].status;
        orders[_orderId].status = _newStatus;
        
        emit OrderStatusUpdated(
            _orderId,
            oldStatus,
            _newStatus,
            block.timestamp
        );
    }
    
    function processPayment(
        uint256 _orderId,
        string memory _paymentMethod
    ) public {
        require(
            orders[_orderId].customer == msg.sender,
            "Only customer can process payment"
        );
        
        emit PaymentProcessed(
            _orderId,
            msg.sender,
            orders[_orderId].totalAmount,
            _paymentMethod,
            block.timestamp
        );
    }
    
    function issueRefund(
        uint256 _orderId,
        address _recipient,
        uint256 _amount,
        string memory _reason
    ) public {
        require(
            orders[_orderId].merchant == msg.sender,
            "Only merchant can issue refund"
        );
        
        emit RefundIssued(
            _orderId,
            _recipient,
            _amount,
            _reason,
            block.timestamp
        );
    }
}

4.2 イベントベースの通知システム

実装コード

contract NotificationSystem {
    struct UserPreferences {
        bool emailNotifications;
        bool smsNotifications;
        bool pushNotifications;
        uint256 notificationFrequency;
    }
    
    mapping(address => UserPreferences) public userPreferences;
    
    // 通知関連イベント
    event NotificationSent(
        address indexed _recipient,
        string _type,
        string _message,
        uint256 _timestamp
    );
    
    event UserPreferencesUpdated(
        address indexed _user,
        bool _emailEnabled,
        bool _smsEnabled,
        bool _pushEnabled,
        uint256 _frequency
    );
    
    event SystemAlert(
        string _alertType,
        string _message,
        uint256 _severity,
        uint256 _timestamp
    );
    
    function updatePreferences(
        bool _emailNotifications,
        bool _smsNotifications,
        bool _pushNotifications,
        uint256 _notificationFrequency
    ) public {
        userPreferences[msg.sender] = UserPreferences({
            emailNotifications: _emailNotifications,
            smsNotifications: _smsNotifications,
            pushNotifications: _pushNotifications,
            notificationFrequency: _notificationFrequency
        });
        
        emit UserPreferencesUpdated(
            msg.sender,
            _emailNotifications,
            _smsNotifications,
            _pushNotifications,
            _notificationFrequency
        );
    }
    
    function sendNotification(
        address _recipient,
        string memory _type,
        string memory _message
    ) public {
        emit NotificationSent(
            _recipient,
            _type,
            _message,
            block.timestamp
        );
    }
    
    function broadcastSystemAlert(
        string memory _alertType,
        string memory _message,
        uint256 _severity
    ) public {
        emit SystemAlert(
            _alertType,
            _message,
            _severity,
            block.timestamp
        );
    }
}

5. フロントエンドでのイベント処理

5.1 Web3.jsでのイベント監視

実装コード

// イベントの監視と処理
class EventMonitor {
    constructor(web3, contractAddress, contractABI) {
        this.web3 = web3;
        this.contract = new web3.eth.Contract(contractABI, contractAddress);
        this.eventHandlers = new Map();
    }
    
    // リアルタイムイベント監視
    startEventMonitoring() {
        // トークン転送イベントの監視
        this.contract.events.TokenTransfer({
            fromBlock: 'latest'
        }, (error, event) => {
            if (error) {
                console.error('Event monitoring error:', error);
            } else {
                this.handleTokenTransfer(event);
            }
        });
        
        // 注文作成イベントの監視
        this.contract.events.OrderCreated({
            fromBlock: 'latest'
        }, (error, event) => {
            if (error) {
                console.error('Order event error:', error);
            } else {
                this.handleOrderCreated(event);
            }
        });
    }
    
    // イベントハンドラーの登録
    registerEventHandler(eventName, handler) {
        this.eventHandlers.set(eventName, handler);
    }
    
    // トークン転送イベントの処理
    handleTokenTransfer(event) {
        const { _sender, _receiver, _value } = event.returnValues;
        console.log(`Token transfer: ${_sender} → ${_receiver} (${_value})`);
        
        // カスタムハンドラーの実行
        const handler = this.eventHandlers.get('TokenTransfer');
        if (handler) {
            handler(event);
        }
    }
    
    // 注文作成イベントの処理
    handleOrderCreated(event) {
        const { _orderId, _customer, _merchant, _totalAmount } = event.returnValues;
        console.log(`New order created: #${_orderId} by ${_customer}`);
        
        // カスタムハンドラーの実行
        const handler = this.eventHandlers.get('OrderCreated');
        if (handler) {
            handler(event);
        }
    }
}

5.2 イベントの検索とフィルタリング

実装コード

// イベントの検索とフィルタリング
class EventSearcher {
    constructor(web3, contractAddress, contractABI) {
        this.web3 = web3;
        this.contract = new web3.eth.Contract(contractABI, contractAddress);
    }
    
    // 特定の送信者からのトークン転送を検索
    async getTransfersFrom(senderAddress, fromBlock = 0, toBlock = 'latest') {
        const filter = this.contract.filters.TokenTransfer(senderAddress, null, null);
        return await this.contract.getPastEvents('TokenTransfer', {
            filter: filter,
            fromBlock: fromBlock,
            toBlock: toBlock
        });
    }
    
    // 特定の受信者へのトークン転送を検索
    async getTransfersTo(receiverAddress, fromBlock = 0, toBlock = 'latest') {
        const filter = this.contract.filters.TokenTransfer(null, receiverAddress, null);
        return await this.contract.getPastEvents('TokenTransfer', {
            filter: filter,
            fromBlock: fromBlock,
            toBlock: toBlock
        });
    }
    
    // 特定の注文IDのイベントを検索
    async getOrderEvents(orderId, fromBlock = 0, toBlock = 'latest') {
        const filter = this.contract.filters.OrderStatusUpdated(orderId, null, null);
        return await this.contract.getPastEvents('OrderStatusUpdated', {
            filter: filter,
            fromBlock: fromBlock,
            toBlock: toBlock
        });
    }
    
    // 複数条件での検索
    async getComplexSearch(senderAddress, receiverAddress, fromBlock = 0, toBlock = 'latest') {
        const filter = this.contract.filters.TokenTransfer(senderAddress, receiverAddress, null);
        return await this.contract.getPastEvents('TokenTransfer', {
            filter: filter,
            fromBlock: fromBlock,
            toBlock: toBlock
        });
    }
    
    // イベントの統計情報を取得
    async getEventStatistics(fromBlock = 0, toBlock = 'latest') {
        const allTransfers = await this.contract.getPastEvents('TokenTransfer', {
            fromBlock: fromBlock,
            toBlock: toBlock
        });
        
        const statistics = {
            totalTransfers: allTransfers.length,
            uniqueSenders: new Set(),
            uniqueReceivers: new Set(),
            totalVolume: 0
        };
        
        allTransfers.forEach(event => {
            statistics.uniqueSenders.add(event.returnValues._sender);
            statistics.uniqueReceivers.add(event.returnValues._receiver);
            statistics.totalVolume += parseInt(event.returnValues._value);
        });
        
        statistics.uniqueSenders = statistics.uniqueSenders.size;
        statistics.uniqueReceivers = statistics.uniqueReceivers.size;
        
        return statistics;
    }
}

6. イベントの最適化とベストプラクティス

6.1 ガス効率の最適化

実装コード

contract OptimizedEventContract {
    // 効率的なイベント設計
    event UserAction(
        address indexed _user,           // 検索頻度の高いパラメータ
        uint256 indexed _actionType,     // アクションタイプでフィルタリング
        uint256 _timestamp,              // 非indexed: タイムスタンプ
        bytes32 _dataHash                // 非indexed: データハッシュ
    );
    
    // バッチイベント(複数の操作を1つのイベントで記録)
    event BatchOperation(
        address indexed _operator,
        uint256 indexed _operationCount,
        bytes32 _operationsHash,         // 操作のハッシュ
        uint256 _timestamp
    );
    
    uint256 public operationCount = 0;
    
    function performAction(uint256 _actionType, bytes32 _dataHash) public {
        operationCount++;
        
        // 個別のイベント
        emit UserAction(
            msg.sender,
            _actionType,
            block.timestamp,
            _dataHash
        );
    }
    
    function performBatchOperation(
        bytes32[] memory _operations
    ) public {
        uint256 batchSize = _operations.length;
        operationCount += batchSize;
        
        // バッチイベント(ガス効率が良い)
        bytes32 operationsHash = keccak256(abi.encodePacked(_operations));
        emit BatchOperation(
            msg.sender,
            batchSize,
            operationsHash,
            block.timestamp
        );
    }
}

6.2 イベントの構造化設計

実装コード

contract StructuredEventSystem {
    // 構造化されたイベント設計
    event SystemEvent(
        uint256 indexed _eventId,
        string _eventType,
        address indexed _actor,
        uint256 _timestamp,
        bytes _eventData
    );
    
    event UserEvent(
        address indexed _user,
        string _action,
        uint256 indexed _category,
        uint256 _timestamp,
        string _metadata
    );
    
    event SystemAlert(
        string _alertLevel,
        string _message,
        uint256 indexed _affectedSystem,
        uint256 _timestamp
    );
    
    uint256 public nextEventId = 1;
    
    // イベントの構造化された発火
    function emitSystemEvent(
        string memory _eventType,
        address _actor,
        bytes memory _eventData
    ) internal {
        emit SystemEvent(
            nextEventId++,
            _eventType,
            _actor,
            block.timestamp,
            _eventData
        );
    }
    
    function emitUserEvent(
        address _user,
        string memory _action,
        uint256 _category,
        string memory _metadata
    ) internal {
        emit UserEvent(
            _user,
            _action,
            _category,
            block.timestamp,
            _metadata
        );
    }
    
    function emitSystemAlert(
        string memory _alertLevel,
        string memory _message,
        uint256 _affectedSystem
    ) internal {
        emit SystemAlert(
            _alertLevel,
            _message,
            _affectedSystem,
            block.timestamp
        );
    }
}

7. 実装のポイント

7.1 イベント設計のベストプラクティス

適切なイベント設計の実装:

// 良い例: 適切なindexedパラメータの使用
event UserRegistration(
    address indexed _user,           // 検索頻度が高い
    uint256 indexed _timestamp,      // 時間範囲での検索
    string _username,                // 非indexed: データのみ
    uint256 _userId                  // 非indexed: データのみ
);

// 悪い例: 過度なindexedパラメータの使用
event BadEvent(
    address indexed _user,           // OK
    string indexed _username,        // 悪い: 文字列はindexedに不適切
    uint256 indexed _amount,         // OK
    uint256 indexed _timestamp,      // 悪い: 4つ目のindexed
    string _description              // OK
);

7.2 イベントの効率的な使用

効率的なイベント発火の実装:

// 効率的なイベント発火
function efficientEventEmission() public {
    // 必要な情報のみを含むイベント
    emit UserAction(
        msg.sender,
        block.timestamp,
        "action_performed"
    );
}

// 非効率なイベント発火
function inefficientEventEmission() public {
    // 不要な情報を含むイベント
    emit UserAction(
        msg.sender,
        block.timestamp,
        "action_performed",
        "unnecessary_data",
        "more_unnecessary_data"
    );
}

8. 学習の成果

8.1 習得した概念

  1. イベントシステム: Solidityのイベント機能の理解
  2. ログ記録: ブロックチェーン上での永続的記録
  3. フロントエンド通信: 外部アプリケーションとの連携
  4. indexedパラメータ: 効率的な検索・フィルタリング
  5. ガス最適化: コスト効率の良いイベント設計
  6. 実用的な応用: 実際のプロジェクトでの活用方法

8.2 実装スキル

  • イベント定義の適切な設計
  • indexedパラメータの効果的な使用
  • フロントエンド連携の実装
  • イベント監視システムの構築
  • 検索・フィルタリング機能の実装
  • ガス最適化の技術

8.3 技術的な理解

  • イベント: スマートコントラクトの通知システム
  • ログ: ブロックチェーン上の永続的記録
  • indexed: 効率的な検索のためのパラメータ
  • emit: イベントの発火メカニズム
  • フィルタリング: 条件に基づくイベント検索
  • フロントエンド連携: リアルタイムな状態同期

9. 今後の学習への応用

9.1 発展的な機能

  • 複雑なイベントシステム: 大規模アプリケーションでのイベント管理
  • イベント集約: 複数イベントの統合処理
  • リアルタイム監視: 高度なイベント監視システム
  • イベント分析: データ分析とレポート機能

9.2 パフォーマンスの向上

  • イベント最適化: ガス効率の向上
  • バッチ処理: 複数イベントの効率的な処理
  • キャッシュ機能: 頻繁なイベント検索の最適化
  • 並行処理: 複数イベントの同時処理

9.3 セキュリティの強化

  • イベント検証: 不正なイベントの検出
  • アクセス制御: イベント発火の権限管理
  • 監査機能: イベント履歴の追跡
  • 異常検知: 疑わしいイベントパターンの検出

参考:

Discussion