hopr_chain_rpc/lib.rs
1//! This crate contains types and traits that ensure correct interfacing with Ethereum RPC providers.
2//!
3//! The most important trait is [HoprRpcOperations] which allows to send arbitrary on-chain transactions
4//! and also to perform the selection of HOPR-related smart contract operations.
5//! Secondly, the [HoprIndexerRpcOperations] is a trait that contains all operations required by the
6//! Indexer to subscribe to the block with logs from the chain.
7//!
8//! Both of these traits implemented and realized via the [RpcOperations](rpc::RpcOperations) type,
9//! so this represents the main entry point to all RPC related operations.
10
11extern crate core;
12
13use std::{
14 cmp::Ordering,
15 collections::BTreeSet,
16 fmt::{Display, Formatter},
17 pin::Pin,
18 time::Duration,
19};
20
21use alloy::{primitives::B256, providers::PendingTransaction, rpc::types::TransactionRequest};
22use async_trait::async_trait;
23use errors::LogConversionError;
24use futures::Stream;
25use hopr_crypto_types::types::Hash;
26use hopr_internal_types::prelude::WinningProbability;
27use hopr_primitive_types::prelude::*;
28use serde::{Deserialize, Serialize};
29
30use crate::{RetryAction::NoRetry, errors::Result};
31
32pub mod client;
33pub mod errors;
34pub mod indexer;
35pub mod rpc;
36pub mod transport;
37
38pub use crate::transport::ReqwestClient;
39
40/// A type containing selected fields from the `eth_getLogs` RPC calls.
41///
42/// This is further restricted to already mined blocks.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Log {
45 /// Contract address
46 pub address: Address,
47 /// Topics
48 pub topics: Vec<Hash>,
49 /// Raw log data
50 pub data: Box<[u8]>,
51 /// Transaction index
52 pub tx_index: u64,
53 /// Corresponding block number
54 pub block_number: u64,
55 /// Corresponding block hash
56 pub block_hash: Hash,
57 /// Corresponding transaction hash
58 pub tx_hash: Hash,
59 /// Log index
60 pub log_index: U256,
61 /// Removed flag
62 pub removed: bool,
63}
64
65impl TryFrom<alloy::rpc::types::Log> for Log {
66 type Error = LogConversionError;
67
68 fn try_from(value: alloy::rpc::types::Log) -> std::result::Result<Self, Self::Error> {
69 Ok(Self {
70 address: value.address().into(),
71 topics: value.topics().iter().map(|t| Hash::from(t.0)).collect(),
72 data: Box::from(value.data().data.as_ref()),
73 tx_index: value
74 .transaction_index
75 .ok_or(LogConversionError::MissingTransactionIndex)?,
76 block_number: value.block_number.ok_or(LogConversionError::MissingBlockNumber)?,
77 block_hash: value.block_hash.ok_or(LogConversionError::MissingBlockHash)?.0.into(),
78 log_index: value.log_index.ok_or(LogConversionError::MissingLogIndex)?.into(),
79 tx_hash: value
80 .transaction_hash
81 .ok_or(LogConversionError::MissingTransactionHash)?
82 .0
83 .into(),
84 removed: value.removed,
85 })
86 }
87}
88
89impl From<Log> for alloy::rpc::types::RawLog {
90 fn from(value: Log) -> Self {
91 alloy::rpc::types::RawLog {
92 address: value.address.into(),
93 topics: value.topics.into_iter().map(|h| B256::from_slice(h.as_ref())).collect(),
94 data: value.data.into(),
95 }
96 }
97}
98
99impl From<SerializableLog> for Log {
100 fn from(value: SerializableLog) -> Self {
101 let topics = value
102 .topics
103 .into_iter()
104 .map(|topic| topic.into())
105 .collect::<Vec<Hash>>();
106
107 Self {
108 address: value.address,
109 topics,
110 data: Box::from(value.data.as_ref()),
111 tx_index: value.tx_index,
112 block_number: value.block_number,
113 block_hash: value.block_hash.into(),
114 log_index: value.log_index.into(),
115 tx_hash: value.tx_hash.into(),
116 removed: value.removed,
117 }
118 }
119}
120
121impl From<Log> for SerializableLog {
122 fn from(value: Log) -> Self {
123 SerializableLog {
124 address: value.address,
125 topics: value.topics.into_iter().map(|t| t.into()).collect(),
126 data: value.data.into_vec(),
127 tx_index: value.tx_index,
128 block_number: value.block_number,
129 block_hash: value.block_hash.into(),
130 tx_hash: value.tx_hash.into(),
131 log_index: value.log_index.as_u64(),
132 removed: value.removed,
133 // These fields stay empty for logs coming from the chain and will be populated by the
134 // indexer when processing the log.
135 processed: None,
136 processed_at: None,
137 checksum: None,
138 }
139 }
140}
141
142impl Display for Log {
143 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
144 write!(
145 f,
146 "log #{} in tx #{} in block #{} of address {} with {} topics",
147 self.log_index,
148 self.tx_index,
149 self.block_number,
150 self.address,
151 self.topics.len()
152 )
153 }
154}
155
156impl Ord for Log {
157 fn cmp(&self, other: &Self) -> Ordering {
158 let blocks = self.block_number.cmp(&other.block_number);
159 if blocks == Ordering::Equal {
160 let tx_indices = self.tx_index.cmp(&other.tx_index);
161 if tx_indices == Ordering::Equal {
162 self.log_index.cmp(&other.log_index)
163 } else {
164 tx_indices
165 }
166 } else {
167 blocks
168 }
169 }
170}
171
172impl PartialOrd<Self> for Log {
173 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
174 Some(self.cmp(other))
175 }
176}
177
178/// Represents a set of categorized blockchain log filters for optimized indexer performance.
179///
180/// This structure organizes filters into different categories to enable selective log
181/// processing based on the indexer's operational state. During initial synchronization,
182/// the indexer uses `no_token` filters to exclude irrelevant token events, significantly
183/// reducing processing time and storage requirements. During normal operation, it uses
184/// `all` filters for complete event coverage.
185///
186/// The `token` filters specifically target token-related events for the node's safe address.
187#[derive(Debug, Clone, Default)]
188pub struct FilterSet {
189 /// holds all filters for the indexer
190 pub all: Vec<alloy::rpc::types::Filter>,
191 /// holds only the token contract related filters
192 pub token: Vec<alloy::rpc::types::Filter>,
193 /// holds only filters not related to the token contract
194 pub no_token: Vec<alloy::rpc::types::Filter>,
195}
196
197/// Indicates what retry action should be taken, as result of a `RetryPolicy` implementation.
198pub enum RetryAction {
199 /// Request should not be retried
200 NoRetry,
201 /// Request should be retried after the given duration has elapsed.
202 RetryAfter(Duration),
203}
204
205/// Simple retry policy trait
206pub trait RetryPolicy<E> {
207 /// Indicates whether a client should retry the request given the last error, current number of retries
208 /// of this request and the number of other requests being retried by the client at this time.
209 fn is_retryable_error(&self, _err: &E, _retry_number: u32, _retry_queue_size: u32) -> RetryAction {
210 NoRetry
211 }
212}
213
214/// Common configuration for all native `HttpPostRequestor`s
215#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, smart_default::SmartDefault)]
216pub struct HttpPostRequestorConfig {
217 /// Timeout for HTTP POST request
218 ///
219 /// Defaults to 30 seconds.
220 #[default(Duration::from_secs(30))]
221 pub http_request_timeout: Duration,
222
223 /// Maximum number of HTTP redirects to follow
224 ///
225 /// Defaults to 3
226 #[default(3)]
227 pub max_redirects: u8,
228
229 /// Maximum number of requests per second.
230 /// If set to Some(0) or `None`, there will be no limit.
231 ///
232 /// Defaults to 10
233 #[default(Some(10))]
234 pub max_requests_per_sec: Option<u32>,
235}
236
237/// Represents the on-chain status for the Node Safe module.
238#[derive(Clone, Debug, Copy, PartialEq, Eq)]
239pub struct NodeSafeModuleStatus {
240 pub is_node_included_in_module: bool,
241 pub is_module_enabled_in_safe: bool,
242 pub is_safe_owner_of_module: bool,
243}
244
245impl NodeSafeModuleStatus {
246 /// Determines if the node passes all status checks.
247 pub fn should_pass(&self) -> bool {
248 self.is_node_included_in_module && self.is_module_enabled_in_safe && self.is_safe_owner_of_module
249 }
250}
251
252/// Trait defining a general set of operations an RPC provider
253/// must provide to the HOPR node.
254#[async_trait]
255pub trait HoprRpcOperations {
256 /// Retrieves the timestamp from the given block number.
257 async fn get_timestamp(&self, block_number: u64) -> Result<Option<u64>>;
258
259 /// Retrieves on-chain xdai balance of the given address.
260 async fn get_xdai_balance(&self, address: Address) -> Result<XDaiBalance>;
261
262 /// Retrieves on-chain wxHOPR token balance of the given address.
263 async fn get_hopr_balance(&self, address: Address) -> Result<HoprBalance>;
264
265 /// Retrieves the wxHOPR token allowance for the given owner and spender.
266 async fn get_hopr_allowance(&self, owner: Address, spender: Address) -> Result<HoprBalance>;
267
268 /// Retrieves the minimum incoming ticket winning probability by directly
269 /// calling the network's winning probability oracle.
270 async fn get_minimum_network_winning_probability(&self) -> Result<WinningProbability>;
271
272 /// Retrieves the minimum ticket prices by directly calling the network's
273 /// ticket price oracle.
274 async fn get_minimum_network_ticket_price(&self) -> Result<HoprBalance>;
275
276 /// Retrieves the node's eligibility status
277 async fn get_eligibility_status(&self, address: Address) -> Result<bool>;
278
279 /// Retrieves information of the given node module's target.
280 async fn get_node_management_module_target_info(&self, target: Address) -> Result<Option<U256>>;
281
282 /// Retrieves the safe address of the given node address from the registry.
283 async fn get_safe_from_node_safe_registry(&self, node: Address) -> Result<Address>;
284
285 /// Retrieves the target address of the node module.
286 async fn get_module_target_address(&self) -> Result<Address>;
287
288 /// Retrieves the notice period of channel closure from the Channels contract.
289 async fn get_channel_closure_notice_period(&self) -> Result<Duration>;
290
291 /// Retrieves the on-chain status of node, safe, and module.
292 async fn check_node_safe_module_status(&self, node_address: Address) -> Result<NodeSafeModuleStatus>;
293
294 /// Sends transaction to the RPC provider.
295 async fn send_transaction(&self, tx: TransactionRequest) -> Result<PendingTransaction>;
296}
297
298/// Structure containing filtered logs that all belong to the same block.
299#[derive(Debug, Clone, Default)]
300pub struct BlockWithLogs {
301 /// Block number
302 pub block_id: u64,
303 /// Filtered logs belonging to this block.
304 pub logs: BTreeSet<SerializableLog>,
305}
306
307impl Display for BlockWithLogs {
308 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
309 write!(f, "block #{} with {} logs", self.block_id, self.logs.len())
310 }
311}
312
313impl BlockWithLogs {
314 /// Returns `true` if no logs are contained within this block.
315 pub fn is_empty(&self) -> bool {
316 self.logs.is_empty()
317 }
318
319 /// Returns the number of logs within this block.
320 pub fn len(&self) -> usize {
321 self.logs.len()
322 }
323}
324
325/// Trait with RPC provider functionality required by the Indexer.
326#[async_trait]
327pub trait HoprIndexerRpcOperations {
328 /// Retrieves the latest block number.
329 async fn block_number(&self) -> Result<u64>;
330
331 /// Queries the HOPR token allowance between owner and spender addresses.
332 ///
333 /// This method queries the HOPR token contract to determine how many tokens
334 /// the owner has approved the spender to transfer on their behalf.
335 ///
336 /// # Arguments
337 /// * `owner` - The address that owns the tokens and grants the allowance
338 /// * `spender` - The address that is approved to spend the tokens
339 ///
340 /// # Returns
341 /// * `Result<HoprBalance>` - The current allowance amount
342 async fn get_hopr_allowance(&self, owner: Address, spender: Address) -> Result<HoprBalance>;
343
344 /// Queries the xDAI (native token) balance for a specific address.
345 ///
346 /// This method queries the current xDAI balance of the specified address
347 /// from the blockchain.
348 ///
349 /// # Arguments
350 /// * `address` - The Ethereum address to query the balance for
351 ///
352 /// # Returns
353 /// * `Result<XDaiBalance>` - The current xDAI balance
354 async fn get_xdai_balance(&self, address: Address) -> Result<XDaiBalance>;
355
356 /// Queries the HOPR token balance for a specific address.
357 ///
358 /// This method directly queries the HOPR token contract to get the current
359 /// token balance of the specified address.
360 ///
361 /// # Arguments
362 /// * `address` - The Ethereum address to query the balance for
363 ///
364 /// # Returns
365 /// * `Result<HoprBalance>` - The current HOPR token balance
366 async fn get_hopr_balance(&self, address: Address) -> Result<HoprBalance>;
367
368 /// Streams blockchain logs using selective filtering based on synchronization state.
369 ///
370 /// This method intelligently selects which log filters to use based on whether
371 /// the indexer is currently syncing historical data or processing live events.
372 /// During initial sync, it uses `no_token` filters to exclude irrelevant token
373 /// events. When synced, it uses all filters to capture complete event data.
374 ///
375 /// # Arguments
376 /// * `start_block_number` - Starting block number for log retrieval
377 /// * `filters` - Set of categorized filters (all, token, no_token)
378 /// * `is_synced` - Whether the indexer has completed initial synchronization
379 ///
380 /// # Returns
381 /// * `impl Stream<Item = Result<Log>>` - Stream of blockchain logs
382 ///
383 /// # Behavior
384 /// * When `is_synced` is `false`: Uses `filter_set.no_token` to reduce log volume
385 /// * When `is_synced` is `true`: Uses `filter_set.all` for complete coverage
386 fn try_stream_logs<'a>(
387 &'a self,
388 start_block_number: u64,
389 filters: FilterSet,
390 is_synced: bool,
391 ) -> Result<Pin<Box<dyn Stream<Item = BlockWithLogs> + Send + 'a>>>;
392}