hopr_internal_types/
channels.rs

1use std::{
2    fmt::{Display, Formatter},
3    time::{Duration, SystemTime},
4};
5
6use hopr_crypto_types::prelude::*;
7use hopr_primitive_types::prelude::*;
8
9/// Describes status of a channel
10#[derive(Copy, Clone, Debug, smart_default::SmartDefault, strum::Display, strum::EnumDiscriminants)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[strum_discriminants(vis(pub))]
13#[strum_discriminants(derive(strum::FromRepr, strum::EnumCount), repr(i8))]
14#[strum(serialize_all = "PascalCase")]
15pub enum ChannelStatus {
16    /// The channel is closed.
17    #[default]
18    Closed,
19    /// The channel is opened.
20    Open,
21    /// The channel is pending to be closed.
22    /// The timestamp marks the *earliest* possible time when the channel can transition into the `Closed` state.
23    #[strum(serialize = "PendingToClose")]
24    PendingToClose(SystemTime),
25}
26
27// Cannot use #[repr(u8)] due to PendingToClose
28impl From<ChannelStatus> for i8 {
29    fn from(value: ChannelStatus) -> Self {
30        match value {
31            ChannelStatus::Closed => 0,
32            ChannelStatus::Open => 1,
33            ChannelStatus::PendingToClose(_) => 2,
34        }
35    }
36}
37
38// Manual implementation of PartialEq, because we need only precision up to seconds in PendingToClose
39impl PartialEq for ChannelStatus {
40    fn eq(&self, other: &Self) -> bool {
41        // Use pattern matching to avoid recursion
42        match (self, other) {
43            (Self::Open, Self::Open) => true,
44            (Self::Closed, Self::Closed) => true,
45            (Self::PendingToClose(ct_1), Self::PendingToClose(ct_2)) => {
46                let diff = ct_1.max(ct_2).saturating_sub(*ct_1.min(ct_2));
47                diff.as_secs() == 0
48            }
49            _ => false,
50        }
51    }
52}
53impl Eq for ChannelStatus {}
54
55/// Describes a direction of node's own channel.
56/// The direction of a channel that is not own is undefined.
57#[repr(u8)]
58#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display, strum::EnumString)]
59#[strum(serialize_all = "lowercase")]
60pub enum ChannelDirection {
61    /// The other party is initiator of the channel.
62    Incoming = 0,
63    /// Our own node is the initiator of the channel.
64    Outgoing = 1,
65}
66
67/// Alias for the [`Hash`](struct@Hash) representing a channel ID.
68pub type ChannelId = Hash;
69
70/// Overall description of a channel
71#[derive(Copy, Clone, Debug, PartialEq)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73pub struct ChannelEntry {
74    pub source: Address,
75    pub destination: Address,
76    pub balance: HoprBalance,
77    pub ticket_index: U256,
78    pub status: ChannelStatus,
79    pub channel_epoch: U256,
80    id: ChannelId,
81}
82
83impl ChannelEntry {
84    /// Maximum possible balance of a channel: 10^25 wxHOPR
85    pub const MAX_CHANNEL_BALANCE: u128 = 10_u128.pow(25);
86
87    pub fn new(
88        source: Address,
89        destination: Address,
90        balance: HoprBalance,
91        ticket_index: U256,
92        status: ChannelStatus,
93        channel_epoch: U256,
94    ) -> Self {
95        ChannelEntry {
96            source,
97            destination,
98            balance,
99            ticket_index,
100            status,
101            channel_epoch,
102            id: generate_channel_id(&source, &destination),
103        }
104    }
105
106    /// Generates the channel ID using the source and destination address
107    pub fn get_id(&self) -> ChannelId {
108        self.id
109    }
110
111    /// Checks if the closure time of this channel has passed.
112    /// Also returns `false` if the channel closure has not been initiated (it is in `Open` state).
113    /// Returns also `true`, if the channel is in `Closed` state.
114    pub fn closure_time_passed(&self, current_time: SystemTime) -> bool {
115        match self.status {
116            ChannelStatus::Open => false,
117            ChannelStatus::PendingToClose(closure_time) => closure_time <= current_time,
118            ChannelStatus::Closed => true,
119        }
120    }
121
122    /// Calculates the remaining channel closure grace period.
123    /// Returns `None` if the channel closure has not been initiated yet (channel is in `Open` state).
124    pub fn remaining_closure_time(&self, current_time: SystemTime) -> Option<Duration> {
125        match self.status {
126            ChannelStatus::Open => None,
127            ChannelStatus::PendingToClose(closure_time) => Some(closure_time.saturating_sub(current_time)),
128            ChannelStatus::Closed => Some(Duration::ZERO),
129        }
130    }
131
132    /// Returns the earliest time the channel can transition from `PendingToClose` into `Closed`.
133    /// If the channel is not in `PendingToClose` state, returns `None`.
134    pub fn closure_time_at(&self) -> Option<SystemTime> {
135        match self.status {
136            ChannelStatus::PendingToClose(ct) => Some(ct),
137            _ => None,
138        }
139    }
140
141    /// Determines the channel direction given the self address.
142    /// Returns `None` if neither source nor destination are equal to `me`.
143    pub fn direction(&self, me: &Address) -> Option<ChannelDirection> {
144        if self.source.eq(me) {
145            Some(ChannelDirection::Outgoing)
146        } else if self.destination.eq(me) {
147            Some(ChannelDirection::Incoming)
148        } else {
149            None
150        }
151    }
152
153    /// Determines the channel's direction and counterparty relative to `me`.
154    /// Returns `None` if neither source nor destination are equal to `me`.
155    pub fn orientation(&self, me: &Address) -> Option<(ChannelDirection, Address)> {
156        if self.source.eq(me) {
157            Some((ChannelDirection::Outgoing, self.destination))
158        } else if self.destination.eq(me) {
159            Some((ChannelDirection::Incoming, self.source))
160        } else {
161            None
162        }
163    }
164}
165
166impl Display for ChannelEntry {
167    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
168        write!(f, "{} channel {}", self.status, self.get_id(),)
169    }
170}
171
172/// Generates channel ID hash from `source` and `destination` addresses.
173pub fn generate_channel_id(source: &Address, destination: &Address) -> Hash {
174    Hash::create(&[source.as_ref(), destination.as_ref()])
175}
176
177/// Lists possible changes on a channel entry update
178#[derive(Clone, Copy, Debug)]
179pub enum ChannelChange {
180    /// Channel status has changed
181    Status { left: ChannelStatus, right: ChannelStatus },
182
183    /// Channel balance has changed
184    CurrentBalance { left: HoprBalance, right: HoprBalance },
185
186    /// Channel epoch has changed
187    Epoch { left: u32, right: u32 },
188
189    /// Ticket index has changed
190    TicketIndex { left: u64, right: u64 },
191}
192
193impl Display for ChannelChange {
194    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
195        match self {
196            ChannelChange::Status { left, right } => {
197                write!(f, "Status: {left} -> {right}")
198            }
199
200            ChannelChange::CurrentBalance { left, right } => {
201                write!(f, "Balance: {left} -> {right}")
202            }
203
204            ChannelChange::Epoch { left, right } => {
205                write!(f, "Epoch: {left} -> {right}")
206            }
207
208            ChannelChange::TicketIndex { left, right } => {
209                write!(f, "TicketIndex: {left} -> {right}")
210            }
211        }
212    }
213}
214
215impl ChannelChange {
216    /// Compares the two given channels and returns a vector of `ChannelChange`s
217    /// Both channels must have the same ID (source,destination and direction) to be comparable using this function.
218    /// The function panics if `left` and `right` do not have equal ids.
219    /// Note that only some fields are tracked for changes, and therefore an empty vector returned
220    /// does not imply the two `ChannelEntry` instances are equal.
221    pub fn diff_channels(left: &ChannelEntry, right: &ChannelEntry) -> Vec<Self> {
222        assert_eq!(left.id, right.id, "must have equal ids"); // misuse
223        let mut ret = Vec::with_capacity(4);
224        if left.status != right.status {
225            ret.push(ChannelChange::Status {
226                left: left.status,
227                right: right.status,
228            });
229        }
230
231        if left.balance != right.balance {
232            ret.push(ChannelChange::CurrentBalance {
233                left: left.balance,
234                right: right.balance,
235            });
236        }
237
238        if left.channel_epoch != right.channel_epoch {
239            ret.push(ChannelChange::Epoch {
240                left: left.channel_epoch.as_u32(),
241                right: right.channel_epoch.as_u32(),
242            });
243        }
244
245        if left.ticket_index != right.ticket_index {
246            ret.push(ChannelChange::TicketIndex {
247                left: left.ticket_index.as_u64(),
248                right: right.ticket_index.as_u64(),
249            })
250        }
251
252        ret
253    }
254}
255
256/// A wrapper around [`ChannelId`] representing a Channel that is corrupted.
257#[derive(Copy, Clone, Debug, PartialEq)]
258#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
259pub struct CorruptedChannelEntry(ChannelId);
260
261impl From<ChannelId> for CorruptedChannelEntry {
262    fn from(value: ChannelId) -> Self {
263        CorruptedChannelEntry(value)
264    }
265}
266
267impl CorruptedChannelEntry {
268    /// Returns the channel ID of the corrupted channel.
269    pub fn channel_id(&self) -> &ChannelId {
270        &self.0
271    }
272}
273
274/// A pair of source and destination addresses representing a channel.
275#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
276pub struct SrcDstPair(Address, Address);
277
278impl From<ChannelEntry> for SrcDstPair {
279    fn from(channel: ChannelEntry) -> Self {
280        SrcDstPair(channel.source, channel.destination)
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use std::{
287        ops::Add,
288        str::FromStr,
289        time::{Duration, SystemTime},
290    };
291
292    use hex_literal::hex;
293
294    use super::*;
295
296    lazy_static::lazy_static! {
297        static ref ALICE: ChainKeypair = ChainKeypair::from_secret(&hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775")).expect("lazy static keypair should be constructible");
298        static ref BOB: ChainKeypair = ChainKeypair::from_secret(&hex!("48680484c6fc31bc881a0083e6e32b6dc789f9eaba0f8b981429fd346c697f8c")).expect("lazy static keypair should be constructible");
299
300        static ref ADDRESS_1: Address = "3829b806aea42200c623c4d6b9311670577480ed".parse().expect("lazy static address should be constructible");
301        static ref ADDRESS_2: Address = "1a34729c69e95d6e11c3a9b9be3ea0c62c6dc5b1".parse().expect("lazy static address should be constructible");
302    }
303
304    #[test]
305    pub fn test_generate_id() -> anyhow::Result<()> {
306        let from = Address::from_str("0xa460f2e47c641b64535f5f4beeb9ac6f36f9d27c")?;
307        let to = Address::from_str("0xb8b75fef7efdf4530cf1688c933d94e4e519ccd1")?;
308        let id = generate_channel_id(&from, &to).to_string();
309        assert_eq!("0x1a410210ce7265f3070bf0e8885705dce452efcfbd90a5467525d136fcefc64a", id);
310
311        Ok(())
312    }
313
314    #[test]
315    fn channel_status_names() {
316        assert_eq!("Open", ChannelStatus::Open.to_string());
317        assert_eq!("Closed", ChannelStatus::Closed.to_string());
318        assert_eq!(
319            "PendingToClose",
320            ChannelStatus::PendingToClose(SystemTime::now()).to_string()
321        );
322    }
323
324    #[test]
325    fn channel_status_repr_compat() {
326        assert_eq!(ChannelStatusDiscriminants::Open as i8, i8::from(ChannelStatus::Open));
327        assert_eq!(
328            ChannelStatusDiscriminants::Closed as i8,
329            i8::from(ChannelStatus::Closed)
330        );
331        assert_eq!(
332            ChannelStatusDiscriminants::PendingToClose as i8,
333            i8::from(ChannelStatus::PendingToClose(SystemTime::now()))
334        );
335    }
336
337    #[test]
338    pub fn channel_entry_closure_time() {
339        let mut ce = ChannelEntry::new(
340            *ADDRESS_1,
341            *ADDRESS_2,
342            10.into(),
343            23u64.into(),
344            ChannelStatus::Open,
345            3u64.into(),
346        );
347
348        assert!(
349            !ce.closure_time_passed(SystemTime::now()),
350            "opened channel cannot pass closure time"
351        );
352        assert!(
353            ce.remaining_closure_time(SystemTime::now()).is_none(),
354            "opened channel cannot have remaining closure time"
355        );
356
357        let current_time = SystemTime::now();
358        ce.status = ChannelStatus::PendingToClose(current_time.add(Duration::from_secs(60)));
359
360        assert!(
361            !ce.closure_time_passed(current_time),
362            "must not have passed closure time"
363        );
364        assert_eq!(
365            60,
366            ce.remaining_closure_time(current_time)
367                .expect("must have closure time")
368                .as_secs()
369        );
370
371        let current_time = current_time.add(Duration::from_secs(120));
372
373        assert!(ce.closure_time_passed(current_time), "must have passed closure time");
374        assert_eq!(
375            Duration::ZERO,
376            ce.remaining_closure_time(current_time).expect("must have closure time")
377        );
378    }
379}