├── .gitignore ├── Cargo.toml ├── README.md ├── examples ├── map.rs └── subscribe_mempool.rs └── src ├── action_submitter ├── map.rs ├── mod.rs ├── printer.rs └── telegram.rs ├── collector ├── block_collector.rs ├── full_block_collector.rs ├── interval_collector.rs ├── log_collector.rs ├── logs_in_block_collector.rs ├── mempool_collector.rs ├── mod.rs └── poll_full_block_collector.rs ├── engine.rs ├── executor ├── dummy.rs ├── mod.rs ├── raw_transaction.rs ├── telegram_message.rs └── transaction.rs ├── lib.rs ├── macros.rs └── types.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | .idea 13 | .vscode 14 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "burberry" 3 | version = "0.2.0" 4 | edition = "2021" 5 | rust-version = "1.76" 6 | 7 | [dependencies] 8 | alloy = { version = "1", features = ["full"], optional = true } 9 | async-stream = "0.3" 10 | async-trait = "0.1" 11 | eyre = "0.6" 12 | futures = "0.3" 13 | reqwest = { version = "0.12", features = ["json"], optional = true } 14 | serde_json = { version = "1.0", optional = true } 15 | thiserror = { version = "1.0", optional = true } 16 | tokio = { version = "1", features = ["rt"] } 17 | tracing = { version = "0.1", features = ["log"] } 18 | 19 | [features] 20 | default = ["ethereum", "telegram"] 21 | ethereum = ["dep:alloy", "dep:thiserror"] 22 | telegram = ["dep:reqwest", "dep:serde_json"] 23 | 24 | [dev-dependencies] 25 | tokio = { version = "1", features = ["full"] } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A fork of [paradigm/artemis](https://github.com/paradigmxyz/artemis/) with some awesome modifications. -------------------------------------------------------------------------------- /examples/map.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::marker::PhantomData; 3 | use std::sync::Arc; 4 | 5 | use alloy::primitives::B256; 6 | use alloy::providers::ProviderBuilder; 7 | use alloy::providers::{Provider, WsConnect}; 8 | use alloy::rpc::types::eth::Transaction; 9 | use alloy::rpc::types::Header; 10 | use burberry::collector::BlockCollector; 11 | use burberry::{ 12 | collector::MempoolCollector, map_collector, map_executor, submit_action, ActionSubmitter, 13 | Engine, Executor, Strategy, 14 | }; 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | let ws = WsConnect::new("wss://eth.merkle.io"); 19 | let provider = ProviderBuilder::new() 20 | .connect_ws(ws) 21 | .await 22 | .expect("fail to create ws provider"); 23 | 24 | let provider: Arc> = Arc::new(provider); 25 | 26 | let mut engine = Engine::new(); 27 | 28 | let mempool_collector = MempoolCollector::new(Arc::clone(&provider)); 29 | let block_collector = BlockCollector::new(Arc::clone(&provider)); 30 | 31 | engine.add_collector(map_collector!(mempool_collector, Event::Transaction)); 32 | engine.add_collector(map_collector!(block_collector, Event::Block)); 33 | 34 | engine.add_strategy(Box::new(EchoStrategy)); 35 | 36 | engine.add_executor(map_executor!(EchoExecutor::default(), Action::EchoBlock)); 37 | engine.add_executor(map_executor!( 38 | EchoExecutor::default(), 39 | Action::EchoTransaction 40 | )); 41 | 42 | engine.run_and_join().await.unwrap() 43 | } 44 | 45 | pub struct EchoStrategy; 46 | 47 | #[async_trait::async_trait] 48 | impl Strategy for EchoStrategy { 49 | async fn process_event(&mut self, event: Event, submitter: Arc>) { 50 | match event { 51 | Event::Block(block) => { 52 | submit_action!(submitter, Action::EchoBlock, block.number); 53 | } 54 | Event::Transaction(tx) => { 55 | submit_action!(submitter, Action::EchoTransaction, *tx.inner.tx_hash()); 56 | } 57 | } 58 | } 59 | } 60 | 61 | #[derive(Default)] 62 | pub struct EchoExecutor(PhantomData); 63 | 64 | #[async_trait::async_trait] 65 | impl Executor for EchoExecutor { 66 | async fn execute(&self, action: T) -> eyre::Result<()> { 67 | println!("action: {:?}", action); 68 | Ok(()) 69 | } 70 | } 71 | 72 | #[derive(Debug, Clone)] 73 | enum Event { 74 | Block(Header), 75 | Transaction(Transaction), 76 | } 77 | 78 | #[derive(Debug, Clone)] 79 | enum Action { 80 | EchoBlock(u64), 81 | EchoTransaction(B256), 82 | } 83 | -------------------------------------------------------------------------------- /examples/subscribe_mempool.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use alloy::providers::ProviderBuilder; 4 | use alloy::providers::WsConnect; 5 | use burberry::{collector::MempoolCollector, Collector}; 6 | use futures::StreamExt; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | let ws = WsConnect::new("wss://eth.merkle.io"); 11 | let provider = ProviderBuilder::new() 12 | .connect_ws(ws) 13 | .await 14 | .expect("fail to create ws provider"); 15 | 16 | let collector = MempoolCollector::new(Arc::new(provider)); 17 | let mut stream = collector 18 | .get_event_stream() 19 | .await 20 | .expect("fail to get event stream"); 21 | 22 | println!("start to receive tx from mempool"); 23 | 24 | while let Some(tx) = stream.next().await { 25 | println!("received tx: {:?}", tx); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/action_submitter/map.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use crate::ActionSubmitter; 4 | 5 | pub struct ActionSubmitterMap { 6 | submitter: Box>, 7 | f: F, 8 | _phantom: std::marker::PhantomData, 9 | } 10 | 11 | impl ActionSubmitterMap { 12 | pub fn new(submitter: Box>, f: F) -> Self { 13 | Self { 14 | submitter, 15 | f, 16 | _phantom: std::marker::PhantomData, 17 | } 18 | } 19 | } 20 | 21 | impl ActionSubmitter for ActionSubmitterMap 22 | where 23 | A1: Send + Sync + Clone + Debug + 'static, 24 | A2: Send + Sync + Clone + Debug + 'static, 25 | F: Fn(A1) -> Option + Send + Sync + Clone + 'static, 26 | { 27 | fn submit(&self, a: A1) { 28 | let a = match (self.f)(a) { 29 | Some(a) => a, 30 | None => return, 31 | }; 32 | 33 | self.submitter.submit(a); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/action_submitter/mod.rs: -------------------------------------------------------------------------------- 1 | mod map; 2 | mod printer; 3 | 4 | #[cfg(feature = "telegram")] 5 | mod telegram; 6 | 7 | use std::fmt::Debug; 8 | 9 | pub use map::ActionSubmitterMap; 10 | pub use printer::ActionPrinter; 11 | 12 | #[cfg(feature = "telegram")] 13 | pub use telegram::TelegramSubmitter; 14 | 15 | use tokio::sync::broadcast::Sender; 16 | 17 | use crate::ActionSubmitter; 18 | 19 | #[derive(Clone)] 20 | pub struct ActionChannelSubmitter { 21 | sender: Sender, 22 | } 23 | 24 | impl ActionChannelSubmitter { 25 | pub fn new(sender: Sender) -> Self { 26 | Self { sender } 27 | } 28 | } 29 | 30 | impl ActionSubmitter for ActionChannelSubmitter 31 | where 32 | A: Send + Sync + Clone + Debug + 'static, 33 | { 34 | fn submit(&self, action: A) { 35 | match self.sender.send(action) { 36 | Ok(_) => (), 37 | Err(e) => tracing::error!("error submitting action: {:?}", e), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/action_submitter/printer.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use crate::ActionSubmitter; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct ActionPrinter { 7 | _phantom: std::marker::PhantomData, 8 | } 9 | 10 | impl Default for ActionPrinter { 11 | fn default() -> Self { 12 | Self { 13 | _phantom: std::marker::PhantomData, 14 | } 15 | } 16 | } 17 | 18 | impl ActionSubmitter for ActionPrinter 19 | where 20 | A: Send + Clone + Debug + Sync + 'static, 21 | { 22 | fn submit(&self, a: A) { 23 | tracing::info!("action: {a:?}"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/action_submitter/telegram.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::executor::telegram_message::{Message, TelegramMessageDispatcher}; 4 | use crate::ActionSubmitter; 5 | 6 | pub struct TelegramSubmitter { 7 | executor: Arc, 8 | 9 | redirect_to: Option<(String, String, Option)>, 10 | } 11 | 12 | impl TelegramSubmitter { 13 | pub fn new_with_redirect(ot_token: String, chat_id: String, thread_id: Option) -> Self { 14 | let executor = Arc::new(TelegramMessageDispatcher::default()); 15 | 16 | Self { 17 | executor, 18 | redirect_to: Some((ot_token, chat_id, thread_id)), 19 | } 20 | } 21 | } 22 | 23 | impl Default for TelegramSubmitter { 24 | fn default() -> Self { 25 | let executor = Arc::new(TelegramMessageDispatcher::default()); 26 | Self { 27 | executor, 28 | redirect_to: None, 29 | } 30 | } 31 | } 32 | 33 | impl ActionSubmitter for TelegramSubmitter { 34 | fn submit(&self, action: Message) { 35 | let action = if let Some((bot_token, chat_id, thread_id)) = &self.redirect_to { 36 | Message { 37 | bot_token: bot_token.clone(), 38 | chat_id: chat_id.clone(), 39 | thread_id: thread_id.clone(), 40 | ..action 41 | } 42 | } else { 43 | action 44 | }; 45 | 46 | let executor = self.executor.clone(); 47 | 48 | std::thread::spawn(move || { 49 | send_message(executor, action); 50 | }) 51 | .join() 52 | .unwrap(); 53 | } 54 | } 55 | 56 | #[tokio::main(flavor = "current_thread")] 57 | async fn send_message(executor: Arc, action: Message) { 58 | executor.send_message(action).await; 59 | } 60 | -------------------------------------------------------------------------------- /src/collector/block_collector.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use alloy::{providers::Provider, rpc::types::Header}; 4 | use async_trait::async_trait; 5 | 6 | use crate::types::{Collector, CollectorStream}; 7 | 8 | pub struct BlockCollector { 9 | provider: Arc, 10 | } 11 | 12 | impl BlockCollector { 13 | pub fn new(provider: Arc) -> Self { 14 | Self { provider } 15 | } 16 | } 17 | 18 | #[async_trait] 19 | impl Collector
for BlockCollector { 20 | fn name(&self) -> &str { 21 | "BlockCollector" 22 | } 23 | 24 | async fn get_event_stream(&self) -> eyre::Result> { 25 | let stream = self.provider.subscribe_blocks().await?; 26 | 27 | Ok(Box::pin(stream.into_stream())) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/collector/full_block_collector.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::time::Duration; 3 | 4 | use alloy::{providers::Provider, rpc::types::eth::Block}; 5 | use async_trait::async_trait; 6 | use futures::StreamExt; 7 | use tracing::{error, warn}; 8 | 9 | use crate::types::{Collector, CollectorStream}; 10 | 11 | pub struct FullBlockCollector { 12 | provider: Arc, 13 | retry_interval: Duration, 14 | } 15 | 16 | impl FullBlockCollector { 17 | pub fn new(provider: Arc) -> Self { 18 | Self::new_with_config(provider, Duration::from_millis(50)) 19 | } 20 | 21 | /// Create a new `FullBlockCollector` with a custom retry interval. A retry will happen when the client returns 22 | /// "header not found" 23 | pub fn new_with_config(provider: Arc, retry_interval: Duration) -> Self { 24 | Self { 25 | provider, 26 | retry_interval, 27 | } 28 | } 29 | } 30 | 31 | #[async_trait] 32 | impl Collector for FullBlockCollector { 33 | fn name(&self) -> &str { 34 | "FullBlockCollector" 35 | } 36 | 37 | async fn get_event_stream(&self) -> eyre::Result> { 38 | let mut stream = self.provider.subscribe_blocks().await?.into_stream(); 39 | 40 | let mut attempts = 0; 41 | 42 | let stream = async_stream::stream! { 43 | while let Some(block) = stream.next().await { 44 | let block_number = block.number; 45 | 46 | loop { 47 | match self.provider.get_block_by_number(block_number.into()).full().await { 48 | Ok(Some(block)) => { 49 | yield block; 50 | } 51 | Ok(None) => { 52 | if attempts % 5 == 0 { 53 | warn!(block = block_number, "block not found yet"); 54 | } else { 55 | error!(block = block_number, "block not found yet"); 56 | } 57 | 58 | attempts += 1; 59 | tokio::time::sleep(self.retry_interval).await; 60 | continue; 61 | } 62 | Err(e) => { 63 | error!(block = block_number, "fail to get full block: {e:#}"); 64 | } 65 | }; 66 | 67 | break; 68 | } 69 | } 70 | }; 71 | 72 | Ok(Box::pin(stream)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/collector/interval_collector.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use crate::types::{Collector, CollectorStream}; 4 | use async_trait::async_trait; 5 | 6 | pub struct IntervalCollector { 7 | interval: Duration, 8 | } 9 | 10 | impl IntervalCollector { 11 | pub fn new(interval: Duration) -> Self { 12 | Self { interval } 13 | } 14 | } 15 | 16 | #[async_trait] 17 | impl Collector for IntervalCollector { 18 | fn name(&self) -> &str { 19 | "IntervalCollector" 20 | } 21 | 22 | async fn get_event_stream(&self) -> eyre::Result> { 23 | let stream = async_stream::stream! { 24 | loop { 25 | tokio::time::sleep(self.interval).await; 26 | yield Instant::now(); 27 | } 28 | }; 29 | 30 | Ok(Box::pin(stream)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/collector/log_collector.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use alloy::{ 4 | providers::Provider, 5 | rpc::types::eth::{Filter, Log}, 6 | }; 7 | use async_trait::async_trait; 8 | use futures::StreamExt; 9 | 10 | use crate::types::{Collector, CollectorStream}; 11 | 12 | pub struct LogCollector { 13 | provider: Arc, 14 | filter: Filter, 15 | } 16 | 17 | impl LogCollector { 18 | pub fn new(provider: Arc, filter: Filter) -> Self { 19 | Self { provider, filter } 20 | } 21 | } 22 | 23 | #[async_trait] 24 | impl Collector for LogCollector { 25 | fn name(&self) -> &str { 26 | "LogCollector" 27 | } 28 | 29 | async fn get_event_stream(&self) -> eyre::Result> { 30 | let stream = self.provider.subscribe_logs(&self.filter).await?; 31 | let stream = stream.into_stream().filter_map(|v| async move { Some(v) }); 32 | Ok(Box::pin(stream)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/collector/logs_in_block_collector.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use alloy::{ 4 | primitives::BlockHash, 5 | providers::Provider, 6 | rpc::types::{ 7 | eth::{Filter, Log}, 8 | Header, 9 | }, 10 | }; 11 | use async_trait::async_trait; 12 | use futures::StreamExt; 13 | 14 | use crate::types::{Collector, CollectorStream}; 15 | 16 | pub struct LogsInBlockCollector { 17 | provider: Arc, 18 | filter: Filter, 19 | } 20 | 21 | impl LogsInBlockCollector { 22 | pub fn new(provider: Arc, filter: Filter) -> Self { 23 | Self { provider, filter } 24 | } 25 | 26 | async fn block_to_logs(&self, block_hash: BlockHash) -> Option> { 27 | let logs = self 28 | .provider 29 | .get_logs(&self.filter.clone().at_block_hash(block_hash)) 30 | .await; 31 | 32 | match logs { 33 | Ok(logs) => Some(logs), 34 | Err(e) => { 35 | tracing::error!(?block_hash, "fail to get logs: {e:#}"); 36 | None 37 | } 38 | } 39 | } 40 | } 41 | 42 | #[async_trait] 43 | impl Collector<(Header, Vec)> for LogsInBlockCollector { 44 | fn name(&self) -> &str { 45 | "LogsInBlockCollector" 46 | } 47 | 48 | async fn get_event_stream(&self) -> eyre::Result)>> { 49 | let mut stream = self.provider.subscribe_blocks().await?.into_stream(); 50 | 51 | let stream = async_stream::stream! { 52 | while let Some(block) = stream.next().await { 53 | let logs = match self.block_to_logs(block.hash).await { 54 | Some(logs) => logs, 55 | None => continue, 56 | }; 57 | 58 | yield (block, logs); 59 | } 60 | }; 61 | 62 | Ok(Box::pin(stream)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/collector/mempool_collector.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::types::{Collector, CollectorStream}; 4 | use alloy::transports::{RpcError, TransportErrorKind}; 5 | use alloy::{primitives::B256, providers::Provider, rpc::types::eth::Transaction}; 6 | use async_trait::async_trait; 7 | use eyre::WrapErr; 8 | use futures::prelude::{stream::FuturesUnordered, Stream}; 9 | use futures::{FutureExt, StreamExt}; 10 | use std::future::Future; 11 | use std::{ 12 | collections::VecDeque, 13 | pin::Pin, 14 | task::{Context, Poll}, 15 | }; 16 | use tracing::error; 17 | 18 | pub struct MempoolCollector { 19 | provider: Arc, 20 | } 21 | 22 | impl MempoolCollector { 23 | pub fn new(provider: Arc) -> Self { 24 | Self { provider } 25 | } 26 | } 27 | 28 | #[async_trait] 29 | impl Collector for MempoolCollector { 30 | fn name(&self) -> &str { 31 | "MempoolCollector" 32 | } 33 | 34 | async fn get_event_stream(&self) -> eyre::Result> { 35 | let stream = self 36 | .provider 37 | .subscribe_pending_transactions() 38 | .await 39 | .wrap_err("fail to subscribe to pending transaction stream")? 40 | .into_stream(); 41 | 42 | let stream = TransactionStream::new(self.provider.as_ref(), stream, 256); 43 | let stream = stream.filter_map(|res| async move { res.ok() }); 44 | 45 | Ok(Box::pin(stream)) 46 | } 47 | } 48 | 49 | /// Errors `TransactionStream` can throw 50 | #[derive(Debug, thiserror::Error)] 51 | pub enum GetTransactionError { 52 | #[error("Failed to get transaction `{0}`: {1}")] 53 | ProviderError(B256, RpcError), 54 | 55 | /// `get_transaction` resulted in a `None` 56 | #[error("Transaction `{0}` not found")] 57 | NotFound(B256), 58 | } 59 | 60 | pub(crate) type TransactionFut<'a> = Pin + Send + 'a>>; 61 | 62 | pub(crate) type TransactionResult = Result; 63 | 64 | /// Drains a stream of transaction hashes and yields entire `Transaction`. 65 | #[must_use = "streams do nothing unless polled"] 66 | pub struct TransactionStream<'a, St> { 67 | /// Currently running futures pending completion. 68 | pub(crate) pending: FuturesUnordered>, 69 | /// Temporary buffered transaction that get started as soon as another future finishes. 70 | pub(crate) buffered: VecDeque, 71 | /// The provider that gets the transaction 72 | pub(crate) provider: &'a dyn Provider, 73 | /// A stream of transaction hashes. 74 | pub(crate) stream: St, 75 | /// Marks if the stream is done 76 | stream_done: bool, 77 | /// max allowed futures to execute at once. 78 | pub(crate) max_concurrent: usize, 79 | } 80 | 81 | impl<'a, St> TransactionStream<'a, St> { 82 | /// Create a new `TransactionStream` instance 83 | pub fn new(provider: &'a dyn Provider, stream: St, max_concurrent: usize) -> Self { 84 | Self { 85 | pending: Default::default(), 86 | buffered: Default::default(), 87 | provider, 88 | stream, 89 | stream_done: false, 90 | max_concurrent, 91 | } 92 | } 93 | 94 | /// Push a future into the set 95 | pub(crate) fn push_tx(&mut self, tx: B256) { 96 | let fut = self 97 | .provider 98 | .root() 99 | .raw_request::<_, Option>("eth_getTransactionByHash".into(), (tx,)) 100 | .then(move |res| match res { 101 | Ok(Some(tx)) => futures::future::ok(tx), 102 | Ok(None) => futures::future::err(GetTransactionError::NotFound(tx)), 103 | Err(err) => futures::future::err(GetTransactionError::ProviderError(tx, err)), 104 | }); 105 | self.pending.push(Box::pin(fut)); 106 | } 107 | } 108 | 109 | impl<'a, St> Stream for TransactionStream<'a, St> 110 | where 111 | St: Stream + Unpin + 'a, 112 | { 113 | type Item = TransactionResult; 114 | 115 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 116 | let this = self.get_mut(); 117 | 118 | // drain buffered transactions first 119 | while this.pending.len() < this.max_concurrent { 120 | if let Some(tx) = this.buffered.pop_front() { 121 | this.push_tx(tx); 122 | } else { 123 | break; 124 | } 125 | } 126 | 127 | if !this.stream_done { 128 | loop { 129 | match Stream::poll_next(Pin::new(&mut this.stream), cx) { 130 | Poll::Ready(Some(tx)) => { 131 | if this.pending.len() < this.max_concurrent { 132 | this.push_tx(tx); 133 | } else { 134 | this.buffered.push_back(tx); 135 | } 136 | } 137 | Poll::Ready(None) => { 138 | this.stream_done = true; 139 | break; 140 | } 141 | _ => break, 142 | } 143 | } 144 | } 145 | 146 | // poll running futures 147 | if let tx @ Poll::Ready(Some(_)) = this.pending.poll_next_unpin(cx) { 148 | return tx; 149 | } 150 | 151 | if this.stream_done && this.pending.is_empty() { 152 | // all done 153 | return Poll::Ready(None); 154 | } 155 | 156 | Poll::Pending 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/collector/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "ethereum")] 2 | mod block_collector; 3 | #[cfg(feature = "ethereum")] 4 | mod full_block_collector; 5 | #[cfg(feature = "ethereum")] 6 | mod log_collector; 7 | #[cfg(feature = "ethereum")] 8 | mod logs_in_block_collector; 9 | #[cfg(feature = "ethereum")] 10 | mod mempool_collector; 11 | #[cfg(feature = "ethereum")] 12 | mod poll_full_block_collector; 13 | 14 | #[cfg(feature = "ethereum")] 15 | pub use block_collector::BlockCollector; 16 | #[cfg(feature = "ethereum")] 17 | pub use full_block_collector::FullBlockCollector; 18 | #[cfg(feature = "ethereum")] 19 | pub use log_collector::LogCollector; 20 | #[cfg(feature = "ethereum")] 21 | pub use logs_in_block_collector::LogsInBlockCollector; 22 | #[cfg(feature = "ethereum")] 23 | pub use mempool_collector::MempoolCollector; 24 | #[cfg(feature = "ethereum")] 25 | pub use poll_full_block_collector::PollFullBlockCollector; 26 | 27 | mod interval_collector; 28 | pub use interval_collector::IntervalCollector; 29 | -------------------------------------------------------------------------------- /src/collector/poll_full_block_collector.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::Ordering; 2 | use std::sync::{atomic::AtomicU64, Arc}; 3 | use std::time::Duration; 4 | 5 | use alloy::rpc::types::eth::BlockId; 6 | use alloy::{providers::Provider, rpc::types::eth::Block}; 7 | use async_trait::async_trait; 8 | use tracing::error; 9 | 10 | use crate::types::{Collector, CollectorStream}; 11 | 12 | pub struct PollFullBlockCollector { 13 | provider: Arc, 14 | interval: Duration, 15 | current_block: AtomicU64, 16 | } 17 | 18 | impl PollFullBlockCollector { 19 | pub fn new(provider: Arc, interval: Duration) -> Self { 20 | Self { 21 | provider, 22 | interval, 23 | current_block: AtomicU64::new(0), 24 | } 25 | } 26 | } 27 | 28 | #[async_trait] 29 | impl Collector for PollFullBlockCollector { 30 | fn name(&self) -> &str { 31 | "PollFullBlockCollector" 32 | } 33 | 34 | async fn get_event_stream(&self) -> eyre::Result> { 35 | let stream = async_stream::stream! { 36 | loop { 37 | match self.provider.get_block(BlockId::latest()).full().await { 38 | Ok(Some(block)) => { 39 | let current_block = block.header.number; 40 | 41 | let old_block = self 42 | .current_block 43 | .fetch_max(current_block, Ordering::Relaxed); 44 | 45 | if old_block < current_block { 46 | yield block; 47 | } 48 | } 49 | Ok(None) => { 50 | error!("latest block not found"); 51 | } 52 | Err(e) => { 53 | error!("fail to get latest block: {e:#}") 54 | } 55 | }; 56 | 57 | tokio::time::sleep(self.interval).await; 58 | } 59 | }; 60 | 61 | Ok(Box::pin(stream)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/engine.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, sync::Arc}; 2 | 3 | use crate::{ 4 | action_submitter::ActionChannelSubmitter, 5 | types::{Collector, Executor, Strategy}, 6 | }; 7 | use eyre::Context; 8 | use futures::StreamExt; 9 | use tokio::{ 10 | sync::broadcast::{self, error::RecvError, Sender}, 11 | task::JoinSet, 12 | }; 13 | use tracing::{debug, error, warn}; 14 | 15 | pub struct Engine { 16 | collectors: Vec>>, 17 | strategies: Vec>>, 18 | executors: Vec>>, 19 | 20 | event_channel_capacity: usize, 21 | action_channel_capacity: usize, 22 | } 23 | 24 | impl Engine { 25 | pub fn new() -> Self { 26 | Self { 27 | collectors: vec![], 28 | strategies: vec![], 29 | executors: vec![], 30 | event_channel_capacity: 512, 31 | action_channel_capacity: 512, 32 | } 33 | } 34 | 35 | pub fn with_event_channel_capacity(mut self, capacity: usize) -> Self { 36 | self.event_channel_capacity = capacity; 37 | self 38 | } 39 | 40 | pub fn with_action_channel_capacity(mut self, capacity: usize) -> Self { 41 | self.action_channel_capacity = capacity; 42 | self 43 | } 44 | 45 | pub fn strategy_count(&self) -> usize { 46 | self.strategies.len() 47 | } 48 | 49 | pub fn executor_count(&self) -> usize { 50 | self.executors.len() 51 | } 52 | } 53 | 54 | impl Default for Engine { 55 | fn default() -> Self { 56 | Self::new() 57 | } 58 | } 59 | 60 | impl Engine 61 | where 62 | E: Send + Sync + Clone + 'static, 63 | A: Send + Sync + Clone + Debug + 'static, 64 | { 65 | pub fn add_collector(&mut self, collector: Box>) { 66 | self.collectors.push(collector); 67 | } 68 | 69 | pub fn add_strategy(&mut self, strategy: Box>) { 70 | self.strategies.push(strategy); 71 | } 72 | 73 | pub fn add_executor(&mut self, executor: Box>) { 74 | self.executors.push(executor); 75 | } 76 | 77 | pub async fn run_and_join(self) -> Result<(), Box> { 78 | let mut js = self.run().await?; 79 | 80 | while let Some(event) = js.join_next().await { 81 | if let Err(err) = event { 82 | error!("task terminated unexpectedly: {err:#}"); 83 | } 84 | } 85 | 86 | Ok(()) 87 | } 88 | 89 | pub async fn run(self) -> Result, Box> { 90 | let (event_sender, _): (Sender, _) = broadcast::channel(self.event_channel_capacity); 91 | let (action_sender, _): (Sender, _) = broadcast::channel(self.action_channel_capacity); 92 | 93 | let mut set = JoinSet::new(); 94 | 95 | if self.executors.is_empty() { 96 | return Err("no executors".into()); 97 | } 98 | 99 | if self.collectors.is_empty() { 100 | return Err("no collectors".into()); 101 | } 102 | 103 | if self.strategies.is_empty() { 104 | return Err("no strategies".into()); 105 | } 106 | 107 | // Spawn executors in separate threads. 108 | for executor in self.executors { 109 | let mut receiver = action_sender.subscribe(); 110 | 111 | set.spawn(async move { 112 | debug!(name = executor.name(), "starting executor... "); 113 | 114 | loop { 115 | match receiver.recv().await { 116 | Ok(action) => match executor.execute(action).await { 117 | Ok(_) => {} 118 | Err(e) => { 119 | error!(name = executor.name(), "error executing action: {}", e) 120 | } 121 | }, 122 | Err(RecvError::Closed) => { 123 | error!(name = executor.name(), "action channel closed!"); 124 | break; 125 | } 126 | Err(RecvError::Lagged(num)) => { 127 | warn!(name = executor.name(), "action channel lagged by {num}") 128 | } 129 | } 130 | } 131 | }); 132 | } 133 | 134 | // Spawn strategies in separate threads. 135 | for mut strategy in self.strategies { 136 | let mut event_receiver = event_sender.subscribe(); 137 | let action_sender = action_sender.clone(); 138 | 139 | let action_submitter = Arc::new(ActionChannelSubmitter::new(action_sender)); 140 | 141 | strategy 142 | .sync_state(action_submitter.clone()) 143 | .await 144 | .wrap_err("fail to sync state")?; 145 | 146 | set.spawn(async move { 147 | debug!(name = strategy.name(), "starting strategy... "); 148 | 149 | loop { 150 | match event_receiver.recv().await { 151 | Ok(event) => { 152 | strategy 153 | .process_event(event, action_submitter.clone()) 154 | .await 155 | } 156 | Err(RecvError::Closed) => { 157 | error!(name = strategy.name(), "event channel closed!"); 158 | break; 159 | } 160 | Err(RecvError::Lagged(num)) => { 161 | warn!(name = strategy.name(), "event channel lagged by {num}") 162 | } 163 | } 164 | } 165 | }); 166 | } 167 | 168 | // Spawn collectors in separate threads. 169 | for collector in self.collectors { 170 | let event_sender = event_sender.clone(); 171 | 172 | set.spawn(async move { 173 | debug!(name = collector.name(), "starting collector... "); 174 | let mut event_stream = collector.get_event_stream().await.unwrap(); 175 | 176 | while let Some(event) = event_stream.next().await { 177 | if let Err(e) = event_sender.send(event) { 178 | error!(name = collector.name(), "error sending event: {e:#}"); 179 | } 180 | } 181 | 182 | error!(name = collector.name(), "event stream ended!"); 183 | }); 184 | } 185 | 186 | Ok(set) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/executor/dummy.rs: -------------------------------------------------------------------------------- 1 | use crate::Executor; 2 | 3 | pub struct Dummy; 4 | 5 | #[async_trait::async_trait] 6 | impl Executor for Dummy { 7 | fn name(&self) -> &str { 8 | "Dummy" 9 | } 10 | 11 | async fn execute(&self, action: A) -> eyre::Result<()> { 12 | let _ = action; 13 | 14 | Ok(()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/executor/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dummy; 2 | 3 | #[cfg(feature = "ethereum")] 4 | pub mod raw_transaction; 5 | 6 | #[cfg(feature = "telegram")] 7 | pub mod telegram_message; 8 | 9 | #[cfg(feature = "ethereum")] 10 | pub mod transaction; 11 | -------------------------------------------------------------------------------- /src/executor/raw_transaction.rs: -------------------------------------------------------------------------------- 1 | use alloy::providers::ProviderBuilder; 2 | use alloy::{ 3 | primitives::{keccak256, Bytes}, 4 | providers::Provider, 5 | }; 6 | use async_trait::async_trait; 7 | use eyre::Result; 8 | use std::sync::Arc; 9 | 10 | use crate::types::Executor; 11 | 12 | pub struct RawTransactionSender { 13 | provider: Arc, 14 | } 15 | 16 | impl RawTransactionSender { 17 | pub fn new(provider: Arc) -> Self { 18 | Self { provider } 19 | } 20 | } 21 | 22 | impl RawTransactionSender { 23 | pub fn new_http(url: &str) -> Self { 24 | let provider = ProviderBuilder::default().connect_http(url.parse().unwrap()); 25 | let provider = Arc::new(provider); 26 | Self { provider } 27 | } 28 | 29 | pub fn new_with_flashbots() -> Self { 30 | Self::new_http("https://rpc.flashbots.net/fast") 31 | } 32 | 33 | pub fn new_with_bsc_bloxroute() -> Self { 34 | Self::new_http("https://bsc.rpc.blxrbdn.com") 35 | } 36 | 37 | pub fn new_with_48club() -> Self { 38 | Self::new_http("https://rpc-bsc.48.club") 39 | } 40 | 41 | pub fn new_with_polygon_bloxroute() -> Self { 42 | Self::new_http("https://polygon.rpc.blxrbdn.com") 43 | } 44 | 45 | pub fn new_with_arbitrum_sequencer() -> Self { 46 | Self::new_http("https://arb1-sequencer.arbitrum.io/rpc") 47 | } 48 | } 49 | 50 | #[async_trait] 51 | impl Executor for RawTransactionSender { 52 | fn name(&self) -> &str { 53 | "RawTransactionSender" 54 | } 55 | 56 | async fn execute(&self, action: Bytes) -> Result<()> { 57 | let send_result = self.provider.send_raw_transaction(&action).await; 58 | 59 | match send_result { 60 | Ok(tx) => { 61 | tracing::info!(tx = ?tx.tx_hash(), "sent tx"); 62 | } 63 | Err(err) => { 64 | let tx_hash = keccak256(&action); 65 | tracing::error!(tx = ?tx_hash, "failed to send tx: {:#}", err); 66 | } 67 | } 68 | 69 | Ok(()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/executor/telegram_message.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use reqwest::{Response, StatusCode}; 3 | use serde_json::{json, Map}; 4 | 5 | use crate::types::Executor; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct Message { 9 | pub bot_token: String, 10 | pub chat_id: String, 11 | pub thread_id: Option, 12 | pub text: String, 13 | pub disable_notification: Option, 14 | pub protect_content: Option, 15 | pub disable_link_preview: Option, 16 | pub parse_mode: Option, 17 | } 18 | 19 | #[derive(Clone, Default)] 20 | pub struct MessageBuilder { 21 | bot_token: Option, 22 | chat_id: Option, 23 | thread_id: Option, 24 | text: Option, 25 | disable_notification: Option, 26 | protect_content: Option, 27 | disable_link_preview: Option, 28 | parse_mode: Option, 29 | } 30 | 31 | impl MessageBuilder { 32 | pub fn new() -> Self { 33 | Self::default() 34 | } 35 | 36 | pub fn bot_token>(mut self, bot_token: T) -> Self { 37 | self.bot_token = Some(bot_token.into()); 38 | self 39 | } 40 | 41 | pub fn chat_id>(mut self, chat_id: T) -> Self { 42 | self.chat_id = Some(chat_id.into()); 43 | self 44 | } 45 | 46 | pub fn thread_id>(mut self, thread_id: T) -> Self { 47 | self.thread_id = Some(thread_id.into()); 48 | self 49 | } 50 | 51 | pub fn text>(mut self, text: T) -> Self { 52 | self.text = Some(text.into()); 53 | self 54 | } 55 | 56 | pub fn disable_notification(mut self, disable_notification: bool) -> Self { 57 | self.disable_notification = Some(disable_notification); 58 | self 59 | } 60 | 61 | pub fn protect_content(mut self, protect_content: bool) -> Self { 62 | self.protect_content = Some(protect_content); 63 | self 64 | } 65 | 66 | pub fn disable_link_preview(mut self, disable_link_preview: bool) -> Self { 67 | self.disable_link_preview = Some(disable_link_preview); 68 | self 69 | } 70 | 71 | pub fn parse_mode>(mut self, parse_mode: T) -> Self { 72 | self.parse_mode = Some(parse_mode.into()); 73 | self 74 | } 75 | 76 | pub fn build(self) -> Message { 77 | Message { 78 | bot_token: self.bot_token.unwrap_or_default(), 79 | chat_id: self.chat_id.unwrap_or_default(), 80 | thread_id: self.thread_id, 81 | text: self.text.unwrap_or_default(), 82 | disable_notification: self.disable_notification, 83 | protect_content: self.protect_content, 84 | disable_link_preview: self.disable_link_preview, 85 | parse_mode: self.parse_mode, 86 | } 87 | } 88 | } 89 | 90 | pub struct TelegramMessageDispatcher { 91 | error_report_bot_token: Option, 92 | error_report_chat_id: Option, 93 | error_report_thread_id: Option, 94 | 95 | client: reqwest::Client, 96 | } 97 | 98 | impl TelegramMessageDispatcher { 99 | pub fn new( 100 | error_report_bot_token: Option, 101 | error_report_chat_id: Option, 102 | error_report_thread_id: Option, 103 | ) -> Self { 104 | Self { 105 | error_report_bot_token, 106 | error_report_chat_id, 107 | error_report_thread_id, 108 | client: reqwest::ClientBuilder::new().build().unwrap(), 109 | } 110 | } 111 | 112 | pub fn new_without_error_report() -> Self { 113 | Self::new(None, None, None) 114 | } 115 | 116 | fn get_url(bot_token: T) -> String { 117 | format!("https://api.telegram.org/bot{}/sendMessage", bot_token) 118 | } 119 | 120 | pub async fn send_message(&self, message: Message) { 121 | let url = Self::get_url(&message.bot_token); 122 | 123 | let mut data = Map::new(); 124 | 125 | data.insert("chat_id".to_string(), json!(&message.chat_id)); 126 | data.insert("text".to_string(), json!(&message.text)); 127 | data.insert( 128 | "parse_mode".to_string(), 129 | json!(&message 130 | .parse_mode 131 | .clone() 132 | .unwrap_or("MarkdownV2".to_string())), 133 | ); 134 | 135 | if let Some(thread_id) = &message.thread_id { 136 | data.insert("message_thread_id".to_string(), json!(thread_id)); 137 | } 138 | 139 | if let Some(disable_notification) = &message.disable_notification { 140 | data.insert( 141 | "disable_notification".to_string(), 142 | json!(disable_notification), 143 | ); 144 | } 145 | 146 | if let Some(protect_content) = &message.protect_content { 147 | data.insert("protect_content".to_string(), json!(protect_content)); 148 | } 149 | 150 | if let Some(disable_link_preview) = &message.disable_link_preview { 151 | data.insert( 152 | "link_preview_options".to_string(), 153 | json!({ 154 | "is_disabled": disable_link_preview, 155 | }), 156 | ); 157 | } 158 | 159 | tracing::debug!("sending message to telegram: {data:?}"); 160 | 161 | let response = self.client.post(&url).json(&data).send().await; 162 | if let Err(err) = self.handle_response(response).await { 163 | tracing::error!("fail to send message to telegram: {err:#}"); 164 | 165 | self.report_error(message, format!("{err:#}")).await; 166 | } 167 | } 168 | 169 | pub async fn report_error(&self, original_message: Message, error_message: String) { 170 | let error_report_bot_token = match &self.error_report_bot_token { 171 | Some(token) => token, 172 | None => { 173 | tracing::warn!("telegram message fails to send but error reporting is disabled"); 174 | return; 175 | } 176 | }; 177 | 178 | let url = Self::get_url(error_report_bot_token); 179 | 180 | let mut data = Map::new(); 181 | 182 | data.insert("chat_id".to_string(), json!(self.error_report_chat_id)); 183 | data.insert( 184 | "link_preview_options".to_string(), 185 | json!({ 186 | "is_disabled": true, 187 | }), 188 | ); 189 | 190 | if let Some(thread_id) = &self.error_report_thread_id { 191 | data.insert("message_thread_id".to_string(), json!(thread_id)); 192 | } 193 | 194 | data.insert( 195 | "text".to_string(), 196 | json!(format!( 197 | "❌ Fail to send message\n\nOriginal message: {}\nError: {error_message}", 198 | json!(original_message.text) 199 | )), 200 | ); 201 | 202 | let response = self.client.post(&url).json(&data).send().await; 203 | if let Err(err) = self.handle_response(response).await { 204 | tracing::error!("fail to send error report to telegram: {err:#}"); 205 | } 206 | } 207 | 208 | async fn handle_response(&self, response: reqwest::Result) -> eyre::Result<()> { 209 | let response = match response { 210 | Ok(response) if response.status() != StatusCode::OK => { 211 | let status = response.status(); 212 | let body = response 213 | .text() 214 | .await 215 | .unwrap_or("fail to read body".to_string()); 216 | 217 | eyre::bail!("response status: {status}, body: {body}"); 218 | } 219 | Ok(response) => response, 220 | Err(err) => { 221 | eyre::bail!("failed to send message: {err:#}"); 222 | } 223 | }; 224 | 225 | match response.json::().await { 226 | Ok(value) => { 227 | tracing::debug!("response: {value}"); 228 | } 229 | Err(err) => { 230 | eyre::bail!("failed to parse response: {err:#}"); 231 | } 232 | }; 233 | 234 | Ok(()) 235 | } 236 | } 237 | 238 | impl Default for TelegramMessageDispatcher { 239 | fn default() -> Self { 240 | Self::new_without_error_report() 241 | } 242 | } 243 | 244 | #[async_trait] 245 | impl Executor for TelegramMessageDispatcher { 246 | fn name(&self) -> &str { 247 | "TelegramMessageDispatcher" 248 | } 249 | 250 | async fn execute(&self, action: Message) -> eyre::Result<()> { 251 | tracing::debug!("received message: {action:?}"); 252 | 253 | self.send_message(action).await; 254 | 255 | Ok(()) 256 | } 257 | } 258 | 259 | pub fn escape(raw: &str) -> String { 260 | let escaped_characters = r"\*_[]~`>#-|{}.!+()="; 261 | let escaped_string: String = raw 262 | .chars() 263 | .map(|c| { 264 | if escaped_characters.contains(c) { 265 | format!("\\{}", c) 266 | } else { 267 | c.to_string() 268 | } 269 | }) 270 | .collect(); 271 | 272 | escaped_string 273 | } 274 | -------------------------------------------------------------------------------- /src/executor/transaction.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | 4 | use alloy::signers::local::PrivateKeySigner; 5 | use alloy::{ 6 | network::{eip2718::Encodable2718, EthereumWallet, TransactionBuilder}, 7 | primitives::{keccak256, Address, Bytes}, 8 | providers::{Provider, RootProvider}, 9 | rpc::types::eth::TransactionRequest, 10 | }; 11 | 12 | use crate::types::Executor; 13 | 14 | pub struct TransactionSender { 15 | provider: Arc, 16 | signers: HashMap, 17 | tx_submission_provider: Option>, 18 | } 19 | 20 | impl TransactionSender { 21 | pub fn new(provider: Arc, signers: Vec) -> Self { 22 | let signers: HashMap<_, _> = signers 23 | .into_iter() 24 | .map(|s| (s.address(), EthereumWallet::new(s))) 25 | .collect(); 26 | 27 | for signer in signers.keys() { 28 | tracing::info!("setting up signer {:#x}", signer); 29 | } 30 | 31 | Self { 32 | provider, 33 | signers, 34 | tx_submission_provider: None, 35 | } 36 | } 37 | } 38 | 39 | impl TransactionSender { 40 | pub fn new_with_dedicated_tx_submission_endpoint( 41 | provider: Arc, 42 | tx_submission_provider: Arc, 43 | signers: Vec, 44 | ) -> Self { 45 | let signers: HashMap<_, _> = signers 46 | .into_iter() 47 | .map(|s| (s.address(), EthereumWallet::new(s))) 48 | .collect(); 49 | 50 | for signer in signers.keys() { 51 | tracing::info!("setting up signer {:#x}", signer); 52 | } 53 | 54 | Self { 55 | provider, 56 | signers, 57 | tx_submission_provider: Some(tx_submission_provider), 58 | } 59 | } 60 | 61 | pub fn new_http_dedicated( 62 | provider: Arc, 63 | tx_submission_endpoint: &str, 64 | signers: Vec, 65 | ) -> Self { 66 | let tx_submission_provider = Arc::new(RootProvider::<_>::new_http( 67 | tx_submission_endpoint.parse().unwrap(), 68 | )); 69 | 70 | Self::new_with_dedicated_tx_submission_endpoint(provider, tx_submission_provider, signers) 71 | } 72 | 73 | pub fn new_with_flashbots(provider: Arc, signers: Vec) -> Self { 74 | Self::new_http_dedicated(provider, "https://rpc.flashbots.net/fast", signers) 75 | } 76 | 77 | pub fn new_with_bsc_bloxroute( 78 | provider: Arc, 79 | signers: Vec, 80 | ) -> Self { 81 | Self::new_http_dedicated(provider, "https://bsc.rpc.blxrbdn.com", signers) 82 | } 83 | 84 | pub fn new_with_48club(provider: Arc, signers: Vec) -> Self { 85 | Self::new_http_dedicated(provider, "https://rpc-bsc.48.club", signers) 86 | } 87 | 88 | pub fn new_with_polygon_bloxroute( 89 | provider: Arc, 90 | signers: Vec, 91 | ) -> Self { 92 | Self::new_http_dedicated(provider, "https://polygon.rpc.blxrbdn.com", signers) 93 | } 94 | 95 | pub fn new_with_arbitrum_sequencer( 96 | provider: Arc, 97 | signers: Vec, 98 | ) -> Self { 99 | Self::new_http_dedicated(provider, "https://arb1-sequencer.arbitrum.io/rpc", signers) 100 | } 101 | } 102 | 103 | #[async_trait::async_trait] 104 | impl Executor for TransactionSender { 105 | fn name(&self) -> &str { 106 | "TransactionSender" 107 | } 108 | 109 | async fn execute(&self, action: TransactionRequest) -> eyre::Result<()> { 110 | let mut action = action; 111 | 112 | let account = match action.from { 113 | Some(v) => v, 114 | None => { 115 | tracing::error!("missing sender address"); 116 | return Ok(()); 117 | } 118 | }; 119 | 120 | let signer = match self.signers.get(&account) { 121 | Some(v) => v, 122 | None => { 123 | tracing::error!("missing signer for {:#x}", account); 124 | return Ok(()); 125 | } 126 | }; 127 | 128 | if action.nonce.is_none() { 129 | let nonce = match self.provider.get_transaction_count(account).await { 130 | Ok(v) => v, 131 | Err(err) => { 132 | tracing::error!(?account, "failed to get nonce: {err:#}"); 133 | return Ok(()); 134 | } 135 | }; 136 | 137 | action.set_nonce(nonce); 138 | } 139 | 140 | let raw_tx: Bytes = match action.build(signer).await { 141 | Ok(v) => v.encoded_2718().into(), 142 | Err(err) => { 143 | tracing::error!(?account, "failed to build tx: {err:#}"); 144 | return Ok(()); 145 | } 146 | }; 147 | 148 | tracing::debug!(?account, tx = ?raw_tx, "signed tx"); 149 | 150 | let send_result = match &self.tx_submission_provider { 151 | Some(dedicated_provider) => dedicated_provider 152 | .send_raw_transaction(&raw_tx) 153 | .await 154 | .map(|v| *v.tx_hash()), 155 | None => self 156 | .provider 157 | .send_raw_transaction(&raw_tx) 158 | .await 159 | .map(|v| *v.tx_hash()), 160 | }; 161 | 162 | let tx_hash = match send_result { 163 | Ok(v) => v, 164 | Err(err) => { 165 | let hash = keccak256(&raw_tx); 166 | tracing::error!(?account, tx = ?hash, "failed to send tx: {err:#}"); 167 | return Ok(()); 168 | } 169 | }; 170 | 171 | tracing::info!(?account, "sent tx: {:#x}", tx_hash); 172 | 173 | Ok(()) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod action_submitter; 2 | pub mod collector; 3 | pub mod engine; 4 | pub mod executor; 5 | mod macros; 6 | pub mod types; 7 | 8 | pub use async_trait::async_trait; 9 | pub use engine::Engine; 10 | pub use types::*; 11 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! map_boxed_executor { 3 | ($executor: expr, $variant: path) => { 4 | Box::new($crate::ExecutorMap::new($executor, |action| match action { 5 | $variant(value) => Some(value), 6 | _ => None, 7 | })) 8 | }; 9 | } 10 | 11 | #[macro_export] 12 | macro_rules! map_executor { 13 | ($executor: expr, $variant: path) => { 14 | $crate::map_boxed_executor!(Box::new($executor), $variant) 15 | }; 16 | } 17 | 18 | #[macro_export] 19 | macro_rules! map_boxed_collector { 20 | ($collector: expr, $variant: path) => { 21 | Box::new($crate::CollectorMap::new($collector, $variant)) 22 | }; 23 | } 24 | 25 | #[macro_export] 26 | macro_rules! map_collector { 27 | ($collector: expr, $variant: path) => { 28 | $crate::map_boxed_collector!(Box::new($collector), $variant) 29 | }; 30 | } 31 | 32 | #[macro_export] 33 | macro_rules! submit_action { 34 | ($submitter: expr, $variant: path, $action: expr) => { 35 | $submitter.submit($variant($action)); 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use std::{pin::Pin, sync::Arc}; 2 | 3 | use async_trait::async_trait; 4 | use eyre::Result; 5 | use futures::{Stream, StreamExt}; 6 | 7 | pub type CollectorStream<'a, E> = Pin + Send + 'a>>; 8 | 9 | #[async_trait] 10 | pub trait Collector: Send + Sync { 11 | fn name(&self) -> &str { 12 | "Unnamed" 13 | } 14 | 15 | async fn get_event_stream(&self) -> Result>; 16 | } 17 | 18 | pub trait ActionSubmitter: Send + Sync 19 | where 20 | A: Send + Sync + Clone + 'static, 21 | { 22 | fn submit(&self, action: A); 23 | } 24 | 25 | #[async_trait] 26 | pub trait Strategy: Send + Sync 27 | where 28 | E: Send + Sync + Clone + 'static, 29 | A: Send + Sync + Clone + 'static, 30 | { 31 | fn name(&self) -> &str { 32 | "Unnamed" 33 | } 34 | 35 | async fn sync_state(&mut self, _submitter: Arc>) -> Result<()> { 36 | Ok(()) 37 | } 38 | 39 | async fn process_event(&mut self, event: E, submitter: Arc>); 40 | } 41 | 42 | pub struct CollectorMap { 43 | inner: Box>, 44 | f: F, 45 | } 46 | 47 | impl CollectorMap { 48 | pub fn new(collector: Box>, f: F) -> Self { 49 | Self { 50 | inner: collector, 51 | f, 52 | } 53 | } 54 | } 55 | 56 | #[async_trait] 57 | impl Collector for CollectorMap 58 | where 59 | E1: Send + Sync + 'static, 60 | E2: Send + Sync + 'static, 61 | F: Fn(E1) -> E2 + Send + Sync + Clone + 'static, 62 | { 63 | fn name(&self) -> &str { 64 | self.inner.name() 65 | } 66 | 67 | async fn get_event_stream(&self) -> Result> { 68 | let stream = self.inner.get_event_stream().await?; 69 | let f = self.f.clone(); 70 | let stream = stream.map(f); 71 | Ok(Box::pin(stream)) 72 | } 73 | } 74 | 75 | pub struct CollectorFilterMap { 76 | inner: Box>, 77 | f: F, 78 | } 79 | 80 | impl CollectorFilterMap { 81 | pub fn new(collector: Box>, f: F) -> Self { 82 | Self { 83 | inner: collector, 84 | f, 85 | } 86 | } 87 | } 88 | 89 | #[async_trait] 90 | impl Collector for CollectorFilterMap 91 | where 92 | E1: Send + Sync + 'static, 93 | E2: Send + Sync + 'static, 94 | F: Fn(E1) -> Option + Send + Sync + Clone + Copy + 'static, 95 | { 96 | fn name(&self) -> &str { 97 | self.inner.name() 98 | } 99 | 100 | async fn get_event_stream(&self) -> Result> { 101 | let stream = self.inner.get_event_stream().await?; 102 | let f = self.f; 103 | let stream = stream.filter_map(move |v| async move { f(v) }); 104 | Ok(Box::pin(stream)) 105 | } 106 | } 107 | 108 | #[async_trait] 109 | pub trait Executor: Send + Sync { 110 | fn name(&self) -> &str { 111 | "Unnamed" 112 | } 113 | 114 | async fn execute(&self, action: A) -> Result<()>; 115 | } 116 | 117 | pub struct ExecutorMap { 118 | inner: Box>, 119 | f: F, 120 | } 121 | 122 | impl ExecutorMap { 123 | pub fn new(executor: Box>, f: F) -> Self { 124 | Self { inner: executor, f } 125 | } 126 | } 127 | 128 | #[async_trait] 129 | impl Executor for ExecutorMap 130 | where 131 | A1: Send + Sync + 'static, 132 | A2: Send + Sync + 'static, 133 | F: Fn(A1) -> Option + Send + Sync + Clone + 'static, 134 | { 135 | fn name(&self) -> &str { 136 | self.inner.name() 137 | } 138 | 139 | async fn execute(&self, action: A1) -> Result<()> { 140 | let action = (self.f)(action); 141 | match action { 142 | Some(action) => self.inner.execute(action).await, 143 | None => Ok(()), 144 | } 145 | } 146 | } 147 | --------------------------------------------------------------------------------