newton-chain-watcher 0.5.2

newton chain watcher — smart event filter for direct on-chain tasks
//! Chain event types for the watcher
//!
//! Defines the event types that the chain watcher emits after filtering.
//! These are the events that get relayed to the gateway.

use alloy::primitives::{Address, FixedBytes, B256};
use newton_core::newton_prover_task_manager::INewtonProverTaskManager::Task;
use serde::{Deserialize, Serialize};

/// an event detected by the chain watcher
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainEvent {
    /// chain id where the event occurred
    pub chain_id: u64,
    /// the type of event
    pub event_type: ChainEventType,
    /// block number where the event was emitted
    pub block_number: u64,
    /// transaction hash containing the event
    pub tx_hash: B256,
}

/// the type of chain event detected by the watcher
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChainEventType {
    /// direct on-chain task (not seen in gateway broadcasts)
    DirectOnchainTask {
        /// task id from the contract event
        task_id: FixedBytes<32>,
        /// the full task struct from the event (boxed to reduce enum size)
        task: Box<Task>,
    },
    /// operator was added to an operator set
    OperatorAdded {
        /// the operator address
        operator: Address,
        /// the operator set AVS address
        operator_set_avs: Address,
        /// the operator set id
        operator_set_id: u32,
    },
    /// operator was removed from an operator set
    OperatorRemoved {
        /// the operator address
        operator: Address,
        /// the operator set AVS address
        operator_set_avs: Address,
        /// the operator set id
        operator_set_id: u32,
    },
    /// Identity data was registered on-chain (IdentityBound event from IdentityRegistry)
    IdentityDataBound {
        /// identity owner address
        identity_owner: Address,
        /// identity domain (bytes32)
        identity_domain: FixedBytes<32>,
        /// the data ref id (content hash or legacy encrypted data)
        data_ref_id: String,
    },
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy::primitives::Address;

    #[test]
    fn chain_event_serialization_roundtrip() {
        let event = ChainEvent {
            chain_id: 31337,
            event_type: ChainEventType::OperatorAdded {
                operator: Address::ZERO,
                operator_set_avs: Address::ZERO,
                operator_set_id: 0,
            },
            block_number: 100,
            tx_hash: B256::ZERO,
        };

        let json = serde_json::to_string(&event).expect("serialization should succeed");
        let deserialized: ChainEvent = serde_json::from_str(&json).expect("deserialization should succeed");

        assert_eq!(deserialized.chain_id, 31337);
        assert_eq!(deserialized.block_number, 100);
    }

    #[test]
    fn chain_event_type_variants_serialize() {
        let variants: Vec<ChainEventType> = vec![
            ChainEventType::OperatorAdded {
                operator: Address::ZERO,
                operator_set_avs: Address::ZERO,
                operator_set_id: 1,
            },
            ChainEventType::OperatorRemoved {
                operator: Address::ZERO,
                operator_set_avs: Address::ZERO,
                operator_set_id: 2,
            },
            ChainEventType::IdentityDataBound {
                identity_owner: Address::ZERO,
                identity_domain: FixedBytes::ZERO,
                data_ref_id: "0xdeadbeef".to_string(),
            },
        ];

        for variant in &variants {
            let json = serde_json::to_string(variant).expect("serialization should succeed");
            let _: ChainEventType = serde_json::from_str(&json).expect("deserialization should succeed");
        }
    }

    #[test]
    fn identity_data_bound_serialization_roundtrip() {
        let event = ChainEvent {
            chain_id: 11155111,
            event_type: ChainEventType::IdentityDataBound {
                identity_owner: Address::ZERO,
                identity_domain: FixedBytes::from([0xABu8; 32]),
                data_ref_id: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string(),
            },
            block_number: 42,
            tx_hash: B256::ZERO,
        };

        let json = serde_json::to_string(&event).expect("serialization should succeed");
        let deserialized: ChainEvent = serde_json::from_str(&json).expect("deserialization should succeed");

        assert_eq!(deserialized.chain_id, 11155111);
        assert_eq!(deserialized.block_number, 42);
        match &deserialized.event_type {
            ChainEventType::IdentityDataBound {
                identity_owner,
                data_ref_id,
                ..
            } => {
                assert_eq!(identity_owner, &Address::ZERO);
                assert_eq!(
                    data_ref_id,
                    "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
                );
            }
            _ => panic!("expected IdentityDataBound variant"),
        }
    }

    #[test]
    fn direct_onchain_task_serialization_roundtrip() {
        use alloy::primitives::{Bytes, U256};
        use newton_core::newton_prover_task_manager::NewtonMessage;

        let task = Task {
            taskId: FixedBytes::from([1u8; 32]),
            taskCreatedBlock: 42,
            quorumThresholdPercentage: 67,
            quorumNumbers: Bytes::from(vec![0]),
            policyClient: Address::ZERO,
            intent: NewtonMessage::Intent {
                from: Address::ZERO,
                to: Address::ZERO,
                value: U256::ZERO,
                data: Bytes::default(),
                chainId: U256::from(31337),
                functionSignature: Bytes::default(),
            },
            intentSignature: Bytes::default(),
            wasmArgs: Bytes::default(),
            initializationTimestamp: U256::ZERO,
        };

        let event = ChainEvent {
            chain_id: 31337,
            event_type: ChainEventType::DirectOnchainTask {
                task_id: FixedBytes::from([1u8; 32]),
                task: Box::new(task),
            },
            block_number: 200,
            tx_hash: B256::ZERO,
        };

        let json = serde_json::to_string(&event).expect("serialization should succeed");
        let deserialized: ChainEvent = serde_json::from_str(&json).expect("deserialization should succeed");

        assert_eq!(deserialized.chain_id, 31337);
        assert_eq!(deserialized.block_number, 200);
        match &deserialized.event_type {
            ChainEventType::DirectOnchainTask { task_id, task } => {
                assert_eq!(task_id, &FixedBytes::from([1u8; 32]));
                assert_eq!(task.taskCreatedBlock, 42);
                assert_eq!(task.quorumThresholdPercentage, 67);
            }
            _ => panic!("expected DirectOnchainTask variant"),
        }
    }
}