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