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}