xenith-layerzero 0.1.0

LayerZero v2 transport implementation for xenith
Documentation
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

use async_trait::async_trait;
use bytes::Bytes;
use xenith_core::{
    ChainId, KeyMetadata, MessageId, MessageStatus, MessagingTransport, Result, SendOptions,
    StateKey, StateValue, XenithError,
};

/// LayerZero v2 transport for xenith.
///
/// Communicates with a LayerZero endpoint contract to relay cross-chain messages.
/// Each supported destination chain is mapped to its LayerZero endpoint ID (EID).
///
/// # Example
///
/// ```
/// use xenith_layerzero::LayerZeroTransport;
/// use xenith_core::ChainId;
///
/// let transport = LayerZeroTransport::new(
///     [0u8; 20],
///     vec![(ChainId::from(42161), 30110)],
/// );
/// ```
pub struct LayerZeroTransport {
    /// Address of the LayerZero endpoint contract on the source chain.
    pub endpoint_address: [u8; 20],
    /// Maps a xenith [`ChainId`] to the LayerZero endpoint ID for that chain.
    pub supported_chains: HashMap<ChainId, u32>,
    next_message_id: Arc<AtomicU64>,
}

impl LayerZeroTransport {
    /// Create a new transport pointing at `endpoint_address`.
    ///
    /// `chain_mappings` lists every destination chain this instance will accept,
    /// paired with its LayerZero endpoint ID.
    pub fn new(endpoint_address: [u8; 20], chain_mappings: Vec<(ChainId, u32)>) -> Self {
        Self {
            endpoint_address,
            supported_chains: chain_mappings.into_iter().collect(),
            next_message_id: Arc::new(AtomicU64::new(1)),
        }
    }
}

#[async_trait]
impl MessagingTransport for LayerZeroTransport {
    async fn send_message(
        &self,
        destination: ChainId,
        _payload: Bytes,
        _options: SendOptions,
    ) -> Result<MessageId> {
        if !self.supported_chains.contains_key(&destination) {
            return Err(XenithError::UnsupportedChain(destination));
        }

        // #[cfg(feature = "live")]
        // {
        //     // TODO: encode the payload into a LayerZero V2 lzSend() calldata,
        //     // submit the transaction to the endpoint contract at self.endpoint_address,
        //     // and derive MessageId from the emitted PacketSent event's nonce + srcEid.
        // }

        let id = self.next_message_id.fetch_add(1, Ordering::Relaxed);
        Ok(MessageId::from(id))
    }

    async fn estimate_fee(&self, _destination: ChainId, _payload: Bytes) -> Result<u128> {
        // #[cfg(feature = "live")]
        // {
        //     // TODO: call the LayerZero endpoint's quote() view function with
        //     // (dstEid, message, options, payInLzToken=false) and return
        //     // the nativeFee field from the returned MessagingFee struct.
        // }

        Ok(100_000u128)
    }

    async fn message_status(&self, _id: MessageId) -> Result<MessageStatus> {
        // #[cfg(feature = "live")]
        // {
        //     // TODO: query the LayerZero scan API or an on-chain nonce oracle
        //     // to map the MessageId back to a (srcEid, nonce) pair and return
        //     // the appropriate MessageStatus variant.
        // }

        Ok(MessageStatus::Delivered)
    }

    fn sender_address(&self) -> Option<[u8; 20]> {
        // Stub returns None — live feature will return Some(signer.address()).
        None
    }

    async fn poll_incoming(&self) -> Result<Vec<(StateKey, StateValue, Option<KeyMetadata>)>> {
        // Stub always returns empty. Live feature polls PacketReceived events.
        Ok(vec![])
    }
}

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

    xenith_core::transport_compliance_tests!(LayerZeroTransport::new(
        [0u8; 20],
        vec![
            (xenith_core::ChainId(1), 101u32),
            (xenith_core::ChainId(42161), 110u32)
        ]
    ));

    fn transport_with(chains: &[(u64, u32)]) -> LayerZeroTransport {
        LayerZeroTransport::new(
            [0u8; 20],
            chains
                .iter()
                .map(|&(c, eid)| (ChainId::from(c), eid))
                .collect(),
        )
    }

    #[tokio::test]
    async fn send_to_supported_chain_succeeds() {
        let t = transport_with(&[(42161, 30110)]);
        let id = t
            .send_message(ChainId::from(42161), Bytes::new(), Default::default())
            .await
            .unwrap();
        assert_eq!(id, MessageId::from(1));
    }

    #[tokio::test]
    async fn message_ids_increment_per_send() {
        let t = transport_with(&[(42161, 30110)]);
        let a = t
            .send_message(ChainId::from(42161), Bytes::new(), Default::default())
            .await
            .unwrap();
        let b = t
            .send_message(ChainId::from(42161), Bytes::new(), Default::default())
            .await
            .unwrap();
        assert_eq!(a, MessageId::from(1));
        assert_eq!(b, MessageId::from(2));
    }

    #[tokio::test]
    async fn send_to_unsupported_chain_returns_error() {
        let t = transport_with(&[(42161, 30110)]);
        let err = t
            .send_message(ChainId::from(1), Bytes::new(), Default::default())
            .await
            .unwrap_err();
        assert!(matches!(err, XenithError::UnsupportedChain(ChainId(1))));
    }

    #[tokio::test]
    async fn estimate_fee_returns_stub_value() {
        let t = transport_with(&[]);
        assert_eq!(
            t.estimate_fee(ChainId::from(1), Bytes::new())
                .await
                .unwrap(),
            100_000u128
        );
    }

    #[tokio::test]
    async fn message_status_returns_delivered() {
        let t = transport_with(&[]);
        assert!(matches!(
            t.message_status(MessageId::from(99)).await.unwrap(),
            MessageStatus::Delivered
        ));
    }
}