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