Skip to main content

hopr_chain_connector/
utils.rs

1use std::{str::FromStr, time::Duration};
2
3use hopr_api::{
4    chain::{ChainInfo, DeployedSafe, DomainSeparators},
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_ticket_params(
84    model: blokli_client::api::types::TicketParameters,
85) -> Result<(HoprBalance, WinningProbability), ConnectorError> {
86    Ok((
87        model.ticket_price.0.parse()?,
88        WinningProbability::try_from_f64(model.min_ticket_winning_probability)?,
89    ))
90}
91
92#[derive(Debug, Clone)]
93pub(crate) struct ParsedChainInfo {
94    pub channel_closure_grace_period: Duration,
95    pub domain_separators: DomainSeparators,
96    pub key_binding_fee: HoprBalance,
97    pub max_fee_per_gas: u128,
98    pub max_priority_fee_per_gas: u128,
99    pub info: ChainInfo,
100    pub ticket_win_prob: WinningProbability,
101    pub ticket_price: HoprBalance,
102    pub finality: u32,
103    pub expected_block_time: Duration,
104}
105
106impl From<ParsedChainInfo> for GasEstimation {
107    fn from(value: ParsedChainInfo) -> Self {
108        Self {
109            max_fee_per_gas: value.max_fee_per_gas,
110            max_priority_fee_per_gas: value.max_priority_fee_per_gas,
111            ..Self::default()
112        }
113    }
114}
115
116pub(crate) fn model_to_chain_info(
117    model: blokli_client::api::types::ChainInfo,
118) -> Result<ParsedChainInfo, ConnectorError> {
119    let gas_defaults = GasEstimation::default();
120
121    Ok(ParsedChainInfo {
122        channel_closure_grace_period: model
123            .channel_closure_grace_period
124            .0
125            .parse()
126            .map(Duration::from_secs)
127            .map_err(|e| ConnectorError::TypeConversion(format!("invalid channel grace period: {e}")))?,
128        domain_separators: DomainSeparators {
129            ledger: model
130                .ledger_dst
131                .ok_or(ConnectorError::TypeConversion("missing ledger dst".into()))
132                .and_then(|v| {
133                    Hash::from_hex(&v).map_err(|e| ConnectorError::TypeConversion(format!("invalid ledger dst: {e}")))
134                })?,
135            safe_registry: model
136                .safe_registry_dst
137                .ok_or(ConnectorError::TypeConversion("missing safe registry dst".into()))
138                .and_then(|v| {
139                    Hash::from_hex(&v)
140                        .map_err(|e| ConnectorError::TypeConversion(format!("invalid safe registry dst: {e}")))
141                })?,
142            channel: model
143                .channel_dst
144                .ok_or(ConnectorError::TypeConversion("missing channel dst".into()))
145                .and_then(|v| {
146                    Hash::from_hex(&v).map_err(|e| ConnectorError::TypeConversion(format!("invalid channel dst: {e}")))
147                })?,
148        },
149        key_binding_fee: model
150            .key_binding_fee
151            .0
152            .parse()
153            .map_err(|e| ConnectorError::TypeConversion(format!("invalid key binding fee: {e}")))?,
154        max_fee_per_gas: model
155            .max_fee_per_gas
156            .as_deref()
157            .and_then(|raw| raw.parse::<u128>().ok())
158            .unwrap_or(gas_defaults.max_fee_per_gas),
159        max_priority_fee_per_gas: model
160            .max_priority_fee_per_gas
161            .as_deref()
162            .and_then(|raw| raw.parse::<u128>().ok())
163            .unwrap_or(gas_defaults.max_priority_fee_per_gas)
164            .min(
165                model
166                    .max_fee_per_gas
167                    .as_deref()
168                    .and_then(|raw| raw.parse::<u128>().ok())
169                    .unwrap_or(gas_defaults.max_fee_per_gas),
170            ),
171        info: ChainInfo {
172            chain_id: model.chain_id as u64,
173            hopr_network_name: model.network,
174            contract_addresses: serde_json::from_str(&model.contract_addresses.0)
175                .map_err(|e| ConnectorError::TypeConversion(format!("invalid contract addresses JSON: {e}")))?,
176        },
177        ticket_win_prob: WinningProbability::try_from_f64(model.min_ticket_winning_probability)
178            .map_err(|e| ConnectorError::TypeConversion(format!("invalid winning probability info: {e}")))?,
179        ticket_price: model
180            .ticket_price
181            .0
182            .parse()
183            .map_err(|e| ConnectorError::TypeConversion(format!("invalid ticket price: {e}")))?,
184        finality: model
185            .finality
186            .0
187            .parse::<u32>()
188            .map_err(|e| ConnectorError::TypeConversion(format!("failed to parse finality: {e}")))?
189            .max(1),
190        expected_block_time: Duration::from_secs(
191            model
192                .expected_block_time
193                .0
194                .parse()
195                .map_err(|e| ConnectorError::TypeConversion(format!("failed to parse expected block time: {e}")))?,
196        )
197        .max(Duration::from_secs(1)),
198    })
199}
200
201pub(crate) fn model_to_deployed_safe(model: blokli_client::api::types::Safe) -> Result<DeployedSafe, ConnectorError> {
202    Ok(DeployedSafe {
203        address: Address::from_hex(&model.address)?,
204        owner: Address::from_hex(&model.chain_key)?,
205        module: Address::from_hex(&model.module_address)?,
206        registered_nodes: model
207            .registered_nodes
208            .into_iter()
209            .map(|addr| Address::from_hex(&addr))
210            .collect::<Result<Vec<_>, _>>()?,
211    })
212}
213
214pub(crate) async fn process_channel_changes_into_events(
215    new_channel: ChannelEntry,
216    changes: Vec<ChannelChange>,
217    me: &Address,
218    event_tx: &async_broadcast::Sender<ChainEvent>,
219) {
220    for change in changes {
221        tracing::trace!(id = %new_channel.get_id(), %change, "channel updated");
222        match change {
223            ChannelChange::Status {
224                left: ChannelStatus::Open,
225                right: ChannelStatus::PendingToClose(_),
226            } => {
227                tracing::debug!(id = %new_channel.get_id(), "channel pending to close");
228                let _ = event_tx
229                    .broadcast_direct(ChainEvent::ChannelClosureInitiated(new_channel))
230                    .await;
231            }
232            ChannelChange::Status {
233                left: ChannelStatus::PendingToClose(_),
234                right: ChannelStatus::Closed,
235            } => {
236                tracing::debug!(id = %new_channel.get_id(), "channel closed");
237                let _ = event_tx.broadcast_direct(ChainEvent::ChannelClosed(new_channel)).await;
238            }
239            ChannelChange::Status {
240                left: ChannelStatus::Closed,
241                right: ChannelStatus::Open,
242            } => {
243                tracing::debug!(id = %new_channel.get_id(), "channel reopened");
244                let _ = event_tx.broadcast_direct(ChainEvent::ChannelOpened(new_channel)).await;
245            }
246            ChannelChange::Balance { left, right } => {
247                if left > right {
248                    tracing::debug!(id = %new_channel.get_id(), "channel balance decreased");
249                    let _ = event_tx
250                        .broadcast_direct(ChainEvent::ChannelBalanceDecreased(new_channel, left - right))
251                        .await;
252                } else {
253                    tracing::debug!(id = %new_channel.get_id(), "channel balance increased");
254                    let _ = event_tx
255                        .broadcast_direct(ChainEvent::ChannelBalanceIncreased(new_channel, right - left))
256                        .await;
257                }
258            }
259            // Ticket index can wrap (left > right) on a channel re-open,
260            // but we're not interested in that here
261            ChannelChange::TicketIndex { left, right } if left < right => match new_channel.direction(me) {
262                Some(ChannelDirection::Incoming) => {
263                    // The corresponding event is raised in the ticket redeem tracker,
264                    // as the failure must be tracked there too.
265                    tracing::debug!(id = %new_channel.get_id(), "ticket redemption succeeded");
266                }
267                Some(ChannelDirection::Outgoing) => {
268                    tracing::debug!(id = %new_channel.get_id(), "counterparty has redeemed ticket on our channel");
269                    let _ = event_tx
270                        .broadcast_direct(ChainEvent::TicketRedeemed(new_channel, None))
271                        .await;
272                }
273                None => {
274                    tracing::debug!(id = %new_channel.get_id(), "ticket redeemed on foreign channel");
275                    let _ = event_tx
276                        .broadcast_direct(ChainEvent::TicketRedeemed(new_channel, None))
277                        .await;
278                }
279            },
280            _ => {}
281        }
282    }
283}