hopr_internal_types/
channels.rs

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