use async_trait::async_trait;
pub use ethers::types::transaction::eip2718::TypedTransaction;
use futures::{FutureExt, Stream};
use http_types::convert::Deserialize;
use primitive_types::H256;
use serde::Serialize;
use std::cmp::Ordering;
use std::collections::BTreeSet;
use std::fmt::{Display, Formatter};
use std::future::{Future, IntoFuture};
use std::marker::PhantomData;
use std::pin::Pin;
use std::time::Duration;
use hopr_crypto_types::types::Hash;
use hopr_primitive_types::prelude::*;
use crate::errors::RpcError::{ProviderError, TransactionDropped};
use crate::errors::{HttpRequestError, Result};
use crate::RetryAction::NoRetry;
pub mod client;
pub mod errors;
mod helper;
pub mod indexer;
pub mod rpc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Log {
pub address: Address,
pub topics: Vec<Hash>,
pub data: Box<[u8]>,
pub tx_index: u64,
pub block_number: u64,
pub block_hash: Hash,
pub tx_hash: Hash,
pub log_index: U256,
pub removed: bool,
}
impl From<ethers::types::Log> for Log {
fn from(value: ethers::prelude::Log) -> Self {
Self {
address: value.address.into(),
topics: value.topics.into_iter().map(Hash::from).collect(),
data: Box::from(value.data.as_ref()),
tx_index: value.transaction_index.expect("tx index must be present").as_u64(),
block_number: value.block_number.expect("block id must be present").as_u64(),
block_hash: value.block_hash.expect("block hash must be present").into(),
log_index: value.log_index.expect("log index must be present"),
tx_hash: value.transaction_hash.expect("tx hash must be present").into(),
removed: value.removed.expect("removed flag must be present"),
}
}
}
impl From<Log> for ethers::abi::RawLog {
fn from(value: Log) -> Self {
ethers::abi::RawLog {
topics: value.topics.into_iter().map(H256::from).collect(),
data: value.data.into(),
}
}
}
impl From<SerializableLog> for Log {
fn from(value: SerializableLog) -> Self {
let topics = value
.topics
.into_iter()
.map(|topic| topic.into())
.collect::<Vec<Hash>>();
Self {
address: value.address,
topics,
data: Box::from(value.data.as_ref()),
tx_index: value.tx_index,
block_number: value.block_number,
block_hash: value.block_hash.into(),
log_index: value.log_index.into(),
tx_hash: value.tx_hash.into(),
removed: value.removed,
}
}
}
impl From<Log> for SerializableLog {
fn from(value: Log) -> Self {
SerializableLog {
address: value.address,
topics: value.topics.into_iter().map(|t| t.into()).collect(),
data: value.data.into_vec(),
tx_index: value.tx_index,
block_number: value.block_number,
block_hash: value.block_hash.into(),
tx_hash: value.tx_hash.into(),
log_index: value.log_index.as_u64(),
removed: value.removed,
processed: None,
processed_at: None,
checksum: None,
}
}
}
impl Display for Log {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"log #{} in tx #{} in block #{} of address {} with {} topics",
self.log_index,
self.tx_index,
self.block_number,
self.address,
self.topics.len()
)
}
}
impl Ord for Log {
fn cmp(&self, other: &Self) -> Ordering {
let blocks = self.block_number.cmp(&other.block_number);
if blocks == Ordering::Equal {
let tx_indices = self.tx_index.cmp(&other.tx_index);
if tx_indices == Ordering::Equal {
self.log_index.cmp(&other.log_index)
} else {
tx_indices
}
} else {
blocks
}
}
}
impl PartialOrd<Self> for Log {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, Default)]
pub struct LogFilter {
pub address: Vec<Address>,
pub topics: Vec<Hash>,
}
impl LogFilter {
pub fn is_empty(&self) -> bool {
self.address.is_empty() && self.topics.is_empty()
}
}
impl Display for LogFilter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"filter of {} contracts with {} topics",
self.address.len(),
self.topics.len()
)
}
}
impl From<LogFilter> for ethers::types::Filter {
fn from(value: LogFilter) -> Self {
ethers::types::Filter::new()
.address(
value
.address
.into_iter()
.map(ethers::types::Address::from)
.collect::<Vec<_>>(),
)
.topic0(value.topics)
}
}
pub enum RetryAction {
NoRetry,
RetryAfter(Duration),
}
pub trait RetryPolicy<E> {
fn is_retryable_error(&self, _err: &E, _retry_number: u32, _retry_queue_size: u32) -> RetryAction {
NoRetry
}
}
#[derive(Clone, Debug)]
pub struct ZeroRetryPolicy<E>(PhantomData<E>);
impl<E> Default for ZeroRetryPolicy<E> {
fn default() -> Self {
Self(PhantomData)
}
}
impl<E> RetryPolicy<E> for ZeroRetryPolicy<E> {}
#[async_trait]
pub trait HttpPostRequestor: Send + Sync {
async fn http_post<T>(&self, url: &str, data: T) -> std::result::Result<Box<[u8]>, HttpRequestError>
where
T: Serialize + Send + Sync;
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, smart_default::SmartDefault)]
pub struct HttpPostRequestorConfig {
#[default(Duration::from_secs(30))]
pub http_request_timeout: Duration,
#[default(3)]
pub max_redirects: u8,
#[default(Some(10))]
pub max_requests_per_sec: Option<u32>,
}
pub fn create_eip1559_transaction() -> TypedTransaction {
TypedTransaction::Eip1559(ethers::types::Eip1559TransactionRequest::new())
}
#[derive(Debug, Clone)]
pub struct TransactionReceipt {
pub tx_hash: Hash,
pub block_number: u64,
}
impl Display for TransactionReceipt {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "receipt of tx {} in block {}", self.tx_hash, self.block_number)
}
}
impl From<ethers::types::TransactionReceipt> for TransactionReceipt {
fn from(value: ethers::prelude::TransactionReceipt) -> Self {
Self {
tx_hash: value.transaction_hash.into(),
block_number: value.block_number.expect("invalid transaction receipt").as_u64(),
}
}
}
type Resolver<'a> = Box<dyn Future<Output = Result<TransactionReceipt>> + Send + 'a>;
pub struct PendingTransaction<'a> {
tx_hash: Hash,
resolver: Resolver<'a>,
}
impl PendingTransaction<'_> {
pub fn tx_hash(&self) -> Hash {
self.tx_hash
}
}
impl<'a, P: ethers::providers::JsonRpcClient> From<ethers::providers::PendingTransaction<'a, P>>
for PendingTransaction<'a>
{
fn from(value: ethers::providers::PendingTransaction<'a, P>) -> Self {
let tx_hash = Hash::from(value.tx_hash());
Self {
tx_hash,
resolver: Box::new(value.map(move |result| match result {
Ok(Some(tx)) => Ok(TransactionReceipt::from(tx)),
Ok(None) => Err(TransactionDropped(tx_hash.to_string())),
Err(err) => Err(ProviderError(err)),
})),
}
}
}
impl Display for PendingTransaction<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "pending tx {}", self.tx_hash)
}
}
impl<'a> IntoFuture for PendingTransaction<'a> {
type Output = Result<TransactionReceipt>;
type IntoFuture = Pin<Resolver<'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::into_pin(self.resolver)
}
}
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub struct NodeSafeModuleStatus {
pub is_node_included_in_module: bool,
pub is_module_enabled_in_safe: bool,
pub is_safe_owner_of_module: bool,
}
impl NodeSafeModuleStatus {
pub fn should_pass(&self) -> bool {
self.is_node_included_in_module && self.is_module_enabled_in_safe && self.is_safe_owner_of_module
}
}
#[async_trait]
pub trait HoprRpcOperations {
async fn get_timestamp(&self, block_number: u64) -> Result<Option<u64>>;
async fn get_balance(&self, address: Address, balance_type: BalanceType) -> Result<Balance>;
async fn get_eligibility_status(&self, address: Address) -> Result<bool>;
async fn get_node_management_module_target_info(&self, target: Address) -> Result<Option<U256>>;
async fn get_safe_from_node_safe_registry(&self, node: Address) -> Result<Address>;
async fn get_module_target_address(&self) -> Result<Address>;
async fn get_channel_closure_notice_period(&self) -> Result<Duration>;
async fn check_node_safe_module_status(&self, node_address: Address) -> Result<NodeSafeModuleStatus>;
async fn send_transaction(&self, tx: TypedTransaction) -> Result<PendingTransaction>;
}
#[derive(Debug, Clone, Default)]
pub struct BlockWithLogs {
pub block_id: u64,
pub logs: BTreeSet<SerializableLog>,
}
impl Display for BlockWithLogs {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "block #{} with {} logs", self.block_id, self.logs.len())
}
}
impl BlockWithLogs {
pub fn is_empty(&self) -> bool {
self.logs.is_empty()
}
pub fn len(&self) -> usize {
self.logs.len()
}
}
#[cfg_attr(test, mockall::automock)]
#[async_trait]
pub trait HoprIndexerRpcOperations {
async fn block_number(&self) -> Result<u64>;
fn try_stream_logs<'a>(
&'a self,
start_block_number: u64,
filter: LogFilter,
) -> Result<Pin<Box<dyn Stream<Item = BlockWithLogs> + Send + 'a>>>;
}