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