Skip to main content

hopr_chain_connector/
utils.rs

1use std::{str::FromStr, time::Duration};
2
3use hopr_api::{
4    chain::{ChainInfo, DeployedSafe, DomainSeparators, RedemptionStats},
5    types::{
6        chain::{chain_events::ChainEvent, payload::GasEstimation},
7        crypto::types::Hash,
8        internal::prelude::*,
9        primitive::prelude::*,
10    },
11};
12
13use crate::errors::ConnectorError;
14
15pub(crate) fn model_to_account_entry(
16    model: blokli_client::api::types::Account,
17) -> Result<AccountEntry, ConnectorError> {
18    let entry_type = if !model.multi_addresses.is_empty() {
19        AccountType::Announced(
20            model
21                .multi_addresses
22                .into_iter()
23                .filter_map(|addr| match Multiaddr::from_str(&addr) {
24                    Ok(addr) => Some(addr),
25                    Err(_) => {
26                        tracing::error!(%addr, "invalid multiaddress");
27                        None
28                    }
29                })
30                .collect(),
31        )
32    } else {
33        AccountType::NotAnnounced
34    };
35
36    Ok(AccountEntry {
37        public_key: model.packet_key.parse()?,
38        chain_addr: model.chain_key.parse()?,
39        key_id: (model.keyid as u32).into(),
40        entry_type,
41        safe_address: model.safe_address.map(|addr| Address::from_hex(&addr)).transpose()?,
42    })
43}
44
45pub(crate) fn model_to_graph_entry(
46    model: blokli_client::api::types::OpenedChannelsGraphEntry,
47) -> Result<(AccountEntry, AccountEntry, ChannelEntry), ConnectorError> {
48    let src = model_to_account_entry(model.source)?;
49    let dst = model_to_account_entry(model.destination)?;
50    let channel = ChannelBuilder::default()
51        .between(src.chain_addr, dst.chain_addr)
52        .balance(model.channel.balance.0.parse()?)
53        .ticket_index(
54            model
55                .channel
56                .ticket_index
57                .0
58                .parse()
59                .map_err(|e| ConnectorError::TypeConversion(format!("invalid ticket index: {e}")))?,
60        )
61        .status(match model.channel.status {
62            blokli_client::api::types::ChannelStatus::Open => ChannelStatus::Open,
63            blokli_client::api::types::ChannelStatus::PendingToClose => ChannelStatus::PendingToClose(
64                model
65                    .channel
66                    .closure_time
67                    .as_ref()
68                    .ok_or(ConnectorError::TypeConversion("invalid closure time".into()))
69                    .and_then(|t| {
70                        hopr_api::chain::DateTime::from_str(&t.0)
71                            .map_err(|e| ConnectorError::TypeConversion(format!("invalid closure time: {e}")))
72                    })?
73                    .into(),
74            ),
75            blokli_client::api::types::ChannelStatus::Closed => ChannelStatus::Closed,
76        })
77        .epoch(model.channel.epoch as u32)
78        .build()?;
79
80    Ok((src, dst, channel))
81}
82
83pub(crate) fn model_to_redeemed_stats(
84    model: blokli_client::api::types::RedeemedStats,
85) -> Result<RedemptionStats, ConnectorError> {
86    Ok(RedemptionStats {
87        redeemed_count: model
88            .redemption_count
89            .0
90            .parse()
91            .map_err(|_| ConnectorError::TypeConversion("invalid redemption count".into()))?,
92        redeemed_value: model
93            .redeemed_amount
94            .0
95            .parse()
96            .map_err(|_| ConnectorError::TypeConversion("invalid redeemed amount".into()))?,
97    })
98}
99
100pub(crate) fn model_to_ticket_params(
101    model: blokli_client::api::types::TicketParameters,
102) -> Result<(HoprBalance, WinningProbability), ConnectorError> {
103    Ok((
104        model.ticket_price.0.parse()?,
105        WinningProbability::try_from_f64(model.min_ticket_winning_probability)?,
106    ))
107}
108
109#[derive(Debug, Clone)]
110pub(crate) struct ParsedChainInfo {
111    pub channel_closure_grace_period: Duration,
112    pub domain_separators: DomainSeparators,
113    pub key_binding_fee: HoprBalance,
114    pub max_fee_per_gas: u128,
115    pub max_priority_fee_per_gas: u128,
116    pub info: ChainInfo,
117    pub ticket_win_prob: WinningProbability,
118    pub ticket_price: HoprBalance,
119    pub finality: u32,
120    pub expected_block_time: Duration,
121}
122
123impl From<ParsedChainInfo> for GasEstimation {
124    fn from(value: ParsedChainInfo) -> Self {
125        Self {
126            max_fee_per_gas: value.max_fee_per_gas,
127            max_priority_fee_per_gas: value.max_priority_fee_per_gas,
128            ..Self::default()
129        }
130    }
131}
132
133pub(crate) fn model_to_chain_info(
134    model: blokli_client::api::types::ChainInfo,
135) -> Result<ParsedChainInfo, ConnectorError> {
136    let gas_defaults = GasEstimation::default();
137
138    Ok(ParsedChainInfo {
139        channel_closure_grace_period: model
140            .channel_closure_grace_period
141            .0
142            .parse()
143            .map(Duration::from_secs)
144            .map_err(|e| ConnectorError::TypeConversion(format!("invalid channel grace period: {e}")))?,
145        domain_separators: DomainSeparators {
146            ledger: model
147                .ledger_dst
148                .ok_or(ConnectorError::TypeConversion("missing ledger dst".into()))
149                .and_then(|v| {
150                    Hash::from_hex(&v).map_err(|e| ConnectorError::TypeConversion(format!("invalid ledger dst: {e}")))
151                })?,
152            safe_registry: model
153                .safe_registry_dst
154                .ok_or(ConnectorError::TypeConversion("missing safe registry dst".into()))
155                .and_then(|v| {
156                    Hash::from_hex(&v)
157                        .map_err(|e| ConnectorError::TypeConversion(format!("invalid safe registry dst: {e}")))
158                })?,
159            channel: model
160                .channel_dst
161                .ok_or(ConnectorError::TypeConversion("missing channel dst".into()))
162                .and_then(|v| {
163                    Hash::from_hex(&v).map_err(|e| ConnectorError::TypeConversion(format!("invalid channel dst: {e}")))
164                })?,
165        },
166        key_binding_fee: model
167            .key_binding_fee
168            .0
169            .parse()
170            .map_err(|e| ConnectorError::TypeConversion(format!("invalid key binding fee: {e}")))?,
171        max_fee_per_gas: model
172            .max_fee_per_gas
173            .as_deref()
174            .and_then(|raw| raw.parse::<u128>().ok())
175            .unwrap_or(gas_defaults.max_fee_per_gas),
176        max_priority_fee_per_gas: model
177            .max_priority_fee_per_gas
178            .as_deref()
179            .and_then(|raw| raw.parse::<u128>().ok())
180            .unwrap_or(gas_defaults.max_priority_fee_per_gas)
181            .min(
182                model
183                    .max_fee_per_gas
184                    .as_deref()
185                    .and_then(|raw| raw.parse::<u128>().ok())
186                    .unwrap_or(gas_defaults.max_fee_per_gas),
187            ),
188        info: ChainInfo {
189            chain_id: model.chain_id as u64,
190            hopr_network_name: model.network,
191            contract_addresses: serde_json::from_str(&model.contract_addresses.0)
192                .map_err(|e| ConnectorError::TypeConversion(format!("invalid contract addresses JSON: {e}")))?,
193        },
194        ticket_win_prob: WinningProbability::try_from_f64(model.min_ticket_winning_probability)
195            .map_err(|e| ConnectorError::TypeConversion(format!("invalid winning probability info: {e}")))?,
196        ticket_price: model
197            .ticket_price
198            .0
199            .parse()
200            .map_err(|e| ConnectorError::TypeConversion(format!("invalid ticket price: {e}")))?,
201        finality: model
202            .finality
203            .0
204            .parse::<u32>()
205            .map_err(|e| ConnectorError::TypeConversion(format!("failed to parse finality: {e}")))?
206            .max(1),
207        expected_block_time: Duration::from_secs(
208            model
209                .expected_block_time
210                .0
211                .parse()
212                .map_err(|e| ConnectorError::TypeConversion(format!("failed to parse expected block time: {e}")))?,
213        )
214        .max(Duration::from_secs(1)),
215    })
216}
217
218pub(crate) fn model_to_deployed_safe(model: blokli_client::api::types::Safe) -> Result<DeployedSafe, ConnectorError> {
219    Ok(DeployedSafe {
220        address: Address::from_hex(&model.address)?,
221        owners: model
222            .owners
223            .into_iter()
224            .map(|addr| Address::from_hex(&addr))
225            .collect::<Result<Vec<_>, _>>()?,
226        module: Address::from_hex(&model.module_address)?,
227        registered_nodes: model
228            .registered_nodes
229            .into_iter()
230            .map(|addr| Address::from_hex(&addr))
231            .collect::<Result<Vec<_>, _>>()?,
232        deployer: Address::from_hex(&model.chain_key)?,
233    })
234}
235
236pub(crate) async fn process_channel_changes_into_events(
237    new_channel: ChannelEntry,
238    changes: Vec<ChannelChange>,
239    me: &Address,
240    event_tx: &async_broadcast::Sender<ChainEvent>,
241) {
242    for change in changes {
243        tracing::trace!(id = %new_channel.get_id(), %change, "channel updated");
244        match change {
245            ChannelChange::Status {
246                left: ChannelStatus::Open,
247                right: ChannelStatus::PendingToClose(_),
248            } => {
249                tracing::debug!(id = %new_channel.get_id(), "channel pending to close");
250                let _ = event_tx
251                    .broadcast_direct(ChainEvent::ChannelClosureInitiated(new_channel))
252                    .await;
253            }
254            ChannelChange::Status {
255                left: ChannelStatus::PendingToClose(_),
256                right: ChannelStatus::Closed,
257            } => {
258                tracing::debug!(id = %new_channel.get_id(), "channel closed");
259                let _ = event_tx.broadcast_direct(ChainEvent::ChannelClosed(new_channel)).await;
260            }
261            ChannelChange::Status {
262                left: ChannelStatus::Closed,
263                right: ChannelStatus::Open,
264            } => {
265                tracing::debug!(id = %new_channel.get_id(), "channel reopened");
266                let _ = event_tx.broadcast_direct(ChainEvent::ChannelOpened(new_channel)).await;
267            }
268            ChannelChange::Balance { left, right } => {
269                if left > right {
270                    tracing::debug!(id = %new_channel.get_id(), "channel balance decreased");
271                    let _ = event_tx
272                        .broadcast_direct(ChainEvent::ChannelBalanceDecreased(new_channel, left - right))
273                        .await;
274                } else {
275                    tracing::debug!(id = %new_channel.get_id(), "channel balance increased");
276                    let _ = event_tx
277                        .broadcast_direct(ChainEvent::ChannelBalanceIncreased(new_channel, right - left))
278                        .await;
279                }
280            }
281            // Ticket index can wrap (left > right) on a channel re-open,
282            // but we're not interested in that here
283            ChannelChange::TicketIndex { left, right } if left < right => match new_channel.direction(me) {
284                Some(ChannelDirection::Incoming) => {
285                    // The corresponding event is raised in the ticket redeem tracker,
286                    // as the failure must be tracked there too.
287                    tracing::debug!(id = %new_channel.get_id(), "ticket redemption succeeded");
288                }
289                Some(ChannelDirection::Outgoing) => {
290                    tracing::debug!(id = %new_channel.get_id(), "counterparty has redeemed ticket on our channel");
291                    let _ = event_tx
292                        .broadcast_direct(ChainEvent::TicketRedeemed(new_channel, None))
293                        .await;
294                }
295                None => {
296                    tracing::debug!(id = %new_channel.get_id(), "ticket redeemed on foreign channel");
297                    let _ = event_tx
298                        .broadcast_direct(ChainEvent::TicketRedeemed(new_channel, None))
299                        .await;
300                }
301            },
302            _ => {}
303        }
304    }
305}