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 ChannelChange::TicketIndex { left, right } if left < right => match new_channel.direction(me) {
262 Some(ChannelDirection::Incoming) => {
263 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}