Skip to main content

xenith_layerzero/
transport.rs

1use std::collections::HashMap;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use bytes::Bytes;
7use xenith_core::{
8    ChainId, KeyMetadata, MessageId, MessageStatus, MessagingTransport, Result, SendOptions,
9    StateKey, StateValue, XenithError,
10};
11
12/// LayerZero v2 transport for xenith.
13///
14/// Communicates with a LayerZero endpoint contract to relay cross-chain messages.
15/// Each supported destination chain is mapped to its LayerZero endpoint ID (EID).
16///
17/// # Example
18///
19/// ```
20/// use xenith_layerzero::LayerZeroTransport;
21/// use xenith_core::ChainId;
22///
23/// let transport = LayerZeroTransport::new(
24///     [0u8; 20],
25///     vec![(ChainId::from(42161), 30110)],
26/// );
27/// ```
28pub struct LayerZeroTransport {
29    /// Address of the LayerZero endpoint contract on the source chain.
30    pub endpoint_address: [u8; 20],
31    /// Maps a xenith [`ChainId`] to the LayerZero endpoint ID for that chain.
32    pub supported_chains: HashMap<ChainId, u32>,
33    next_message_id: Arc<AtomicU64>,
34}
35
36impl LayerZeroTransport {
37    /// Create a new transport pointing at `endpoint_address`.
38    ///
39    /// `chain_mappings` lists every destination chain this instance will accept,
40    /// paired with its LayerZero endpoint ID.
41    pub fn new(endpoint_address: [u8; 20], chain_mappings: Vec<(ChainId, u32)>) -> Self {
42        Self {
43            endpoint_address,
44            supported_chains: chain_mappings.into_iter().collect(),
45            next_message_id: Arc::new(AtomicU64::new(1)),
46        }
47    }
48}
49
50#[async_trait]
51impl MessagingTransport for LayerZeroTransport {
52    async fn send_message(
53        &self,
54        destination: ChainId,
55        _payload: Bytes,
56        _options: SendOptions,
57    ) -> Result<MessageId> {
58        if !self.supported_chains.contains_key(&destination) {
59            return Err(XenithError::UnsupportedChain(destination));
60        }
61
62        // #[cfg(feature = "live")]
63        // {
64        //     // TODO: encode the payload into a LayerZero V2 lzSend() calldata,
65        //     // submit the transaction to the endpoint contract at self.endpoint_address,
66        //     // and derive MessageId from the emitted PacketSent event's nonce + srcEid.
67        // }
68
69        let id = self.next_message_id.fetch_add(1, Ordering::Relaxed);
70        Ok(MessageId::from(id))
71    }
72
73    async fn estimate_fee(&self, _destination: ChainId, _payload: Bytes) -> Result<u128> {
74        // #[cfg(feature = "live")]
75        // {
76        //     // TODO: call the LayerZero endpoint's quote() view function with
77        //     // (dstEid, message, options, payInLzToken=false) and return
78        //     // the nativeFee field from the returned MessagingFee struct.
79        // }
80
81        Ok(100_000u128)
82    }
83
84    async fn message_status(&self, _id: MessageId) -> Result<MessageStatus> {
85        // #[cfg(feature = "live")]
86        // {
87        //     // TODO: query the LayerZero scan API or an on-chain nonce oracle
88        //     // to map the MessageId back to a (srcEid, nonce) pair and return
89        //     // the appropriate MessageStatus variant.
90        // }
91
92        Ok(MessageStatus::Delivered)
93    }
94
95    fn sender_address(&self) -> Option<[u8; 20]> {
96        // Stub returns None — live feature will return Some(signer.address()).
97        None
98    }
99
100    async fn poll_incoming(&self) -> Result<Vec<(StateKey, StateValue, Option<KeyMetadata>)>> {
101        // Stub always returns empty. Live feature polls PacketReceived events.
102        Ok(vec![])
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    xenith_core::transport_compliance_tests!(LayerZeroTransport::new(
111        [0u8; 20],
112        vec![
113            (xenith_core::ChainId(1), 101u32),
114            (xenith_core::ChainId(42161), 110u32)
115        ]
116    ));
117
118    fn transport_with(chains: &[(u64, u32)]) -> LayerZeroTransport {
119        LayerZeroTransport::new(
120            [0u8; 20],
121            chains
122                .iter()
123                .map(|&(c, eid)| (ChainId::from(c), eid))
124                .collect(),
125        )
126    }
127
128    #[tokio::test]
129    async fn send_to_supported_chain_succeeds() {
130        let t = transport_with(&[(42161, 30110)]);
131        let id = t
132            .send_message(ChainId::from(42161), Bytes::new(), Default::default())
133            .await
134            .unwrap();
135        assert_eq!(id, MessageId::from(1));
136    }
137
138    #[tokio::test]
139    async fn message_ids_increment_per_send() {
140        let t = transport_with(&[(42161, 30110)]);
141        let a = t
142            .send_message(ChainId::from(42161), Bytes::new(), Default::default())
143            .await
144            .unwrap();
145        let b = t
146            .send_message(ChainId::from(42161), Bytes::new(), Default::default())
147            .await
148            .unwrap();
149        assert_eq!(a, MessageId::from(1));
150        assert_eq!(b, MessageId::from(2));
151    }
152
153    #[tokio::test]
154    async fn send_to_unsupported_chain_returns_error() {
155        let t = transport_with(&[(42161, 30110)]);
156        let err = t
157            .send_message(ChainId::from(1), Bytes::new(), Default::default())
158            .await
159            .unwrap_err();
160        assert!(matches!(err, XenithError::UnsupportedChain(ChainId(1))));
161    }
162
163    #[tokio::test]
164    async fn estimate_fee_returns_stub_value() {
165        let t = transport_with(&[]);
166        assert_eq!(
167            t.estimate_fee(ChainId::from(1), Bytes::new())
168                .await
169                .unwrap(),
170            100_000u128
171        );
172    }
173
174    #[tokio::test]
175    async fn message_status_returns_delivered() {
176        let t = transport_with(&[]);
177        assert!(matches!(
178            t.message_status(MessageId::from(99)).await.unwrap(),
179            MessageStatus::Delivered
180        ));
181    }
182}