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}