hopr_chain_connector/connector/
sequencer.rs1use std::{
2 sync::atomic::AtomicU64,
3 time::{Duration, Instant},
4};
5
6use blokli_client::api::{BlokliQueryClient, BlokliTransactionClient};
7use futures::{FutureExt, SinkExt, StreamExt, TryFutureExt};
8use hopr_api::{
9 Address,
10 types::{
11 chain::prelude::{GasEstimation, SignableTransaction},
12 crypto::prelude::*,
13 },
14};
15
16use crate::{
17 errors::{self, ConnectorError},
18 utils::model_to_chain_info,
19};
20
21type TxRequest<T> = (
22 T,
23 Option<ChainKeypair>,
24 futures::channel::oneshot::Sender<errors::Result<blokli_client::api::TxId>>,
25);
26
27pub struct TransactionSequencer<C, R> {
31 sender: futures::channel::mpsc::Sender<TxRequest<R>>,
32 client: std::sync::Arc<C>,
33}
34
35const TX_QUEUE_CAPACITY: usize = 2048;
36
37const OTHER_SIGNER_NONCE_EXPIRATION: Duration = Duration::from_mins(5);
39
40struct FixedTti {
41 fixed: Address,
42 tti: std::time::Duration,
43}
44
45impl FixedTti {
46 #[inline]
47 fn duration_for(&self, key: &Address) -> Option<Duration> {
48 if key == &self.fixed {
49 None } else {
51 Some(self.tti) }
53 }
54}
55
56impl moka::Expiry<Address, std::sync::Arc<AtomicU64>> for FixedTti {
57 fn expire_after_create(&self, key: &Address, _: &std::sync::Arc<AtomicU64>, _: Instant) -> Option<Duration> {
58 self.duration_for(key)
59 }
60
61 fn expire_after_read(
62 &self,
63 key: &Address,
64 _: &std::sync::Arc<AtomicU64>,
65 _: Instant,
66 _: Option<Duration>,
67 _: Instant,
68 ) -> Option<Duration> {
69 self.duration_for(key)
70 }
71
72 fn expire_after_update(
73 &self,
74 key: &Address,
75 _: &std::sync::Arc<AtomicU64>,
76 _: Instant,
77 _: Option<Duration>,
78 ) -> Option<Duration> {
79 self.duration_for(key)
80 }
81}
82
83impl<C, R> TransactionSequencer<C, R>
84where
85 C: BlokliQueryClient + BlokliTransactionClient + Send + Sync + 'static,
86 R: SignableTransaction + Send + Sync + 'static,
87{
88 pub fn new(signer: ChainKeypair, client: std::sync::Arc<C>) -> Self {
89 tracing::debug!(signer = %signer.public().to_address(), "starting transaction sequencer");
90
91 let client_clone = client.clone();
92 let (sender, receiver) = futures::channel::mpsc::channel::<TxRequest<R>>(TX_QUEUE_CAPACITY);
93
94 let current_nonce = moka::sync::CacheBuilder::new(1024)
96 .expire_after(FixedTti {
97 fixed: signer.public().to_address(),
98 tti: OTHER_SIGNER_NONCE_EXPIRATION,
99 })
100 .build();
101
102 let current_nonce_clone = current_nonce.clone();
103 hopr_utils::runtime::prelude::spawn(
104 receiver
105 .then(move |(tx, tx_signer, notifier): (R, _, _)| {
106 let client = client_clone.clone();
107 let signer = tx_signer.unwrap_or_else(|| signer.clone());
108 let signer_addr = signer.public().to_address();
109 let current_nonce = current_nonce.clone();
110 async move {
111 let chain_info = match client.query_chain_info().map_err(ConnectorError::from).await {
112 Ok(chain_info) => {
113 tracing::debug!(chain_id = chain_info.chain_id, "chain info retrieved for tx");
114 chain_info
115 }
116 Err(e) => return (Err(e), signer_addr, notifier),
117 };
118
119 let parsed_chain_info = match model_to_chain_info(chain_info) {
120 Ok(parsed_chain_info) => parsed_chain_info,
121 Err(error) => return (Err(error), signer_addr, notifier),
122 };
123 let chain_id = parsed_chain_info.info.chain_id;
124 let gas_estimation = GasEstimation::from(parsed_chain_info);
125 tracing::debug!(?gas_estimation, "gas estimation used for tx");
126
127 match client
130 .query_transaction_count(&signer_addr.into())
131 .map_err(ConnectorError::from)
132 .await
133 {
134 Ok(tx_count) => {
135 let prev_nonce = current_nonce
136 .entry(signer_addr)
137 .or_default()
138 .value()
139 .fetch_max(tx_count, std::sync::atomic::Ordering::Relaxed);
140
141 tracing::debug!(prev_nonce, tx_count, "transaction count retrieved");
142 }
143 Err(e) => return (Err(e), signer_addr, notifier),
144 }
145
146 let nonce = current_nonce
148 .entry(signer_addr)
149 .or_default()
150 .value()
151 .load(std::sync::atomic::Ordering::Relaxed);
152 tracing::debug!(nonce, signer = %signer_addr, "nonce used for the tx");
153
154 tx.sign_and_encode_to_eip2718(nonce, chain_id, gas_estimation.into(), &signer)
155 .map_err(ConnectorError::from)
156 .and_then(move |tx| {
157 tracing::debug!(nonce, signer = %signer_addr, "submitting transaction");
158 let client = client.clone();
159 async move {
160 client
161 .submit_and_track_transaction(&tx)
162 .map_err(ConnectorError::from)
163 .await
164 }
165 })
166 .map(|res| (res, signer_addr, notifier))
167 .await
168 }
169 })
170 .for_each(move |(res, signer_addr, notifier)| {
171 if res.is_ok()
174 || res
175 .as_ref()
176 .is_err_and(|error| error.as_transaction_rejection_error().is_some())
177 {
178 let prev_nonce = current_nonce_clone
180 .entry(signer_addr)
181 .or_default()
182 .value()
183 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
184
185 tracing::debug!(prev_nonce, signer = %signer_addr, ?res, "nonce incremented due to tx success or rejection");
186 } else {
187 tracing::warn!(?res, signer = %signer_addr, "nonce not incremented due to tx failure other than rejection");
188 }
189
190 if notifier.send(res).is_err() {
191 tracing::debug!(
192 "failed to notify transaction result - the caller may not want to await the result \
193 anymore."
194 );
195 }
196 futures::future::ready(())
197 })
198 .inspect(|_| tracing::warn!("transaction sequencer queue stopped")),
199 );
200
201 Self { sender, client }
202 }
203}
204
205impl<C, R> TransactionSequencer<C, R>
206where
207 C: BlokliTransactionClient + Send + Sync + 'static,
208{
209 pub async fn enqueue_transaction(
213 &self,
214 transaction: R,
215 timeout_until_finalized: std::time::Duration,
216 custom_signer: Option<ChainKeypair>,
217 ) -> errors::Result<impl Future<Output = errors::Result<blokli_client::api::types::Transaction>>> {
218 let (notifier_tx, notifier_rx) = futures::channel::oneshot::channel();
219
220 self.sender
221 .clone()
222 .send((transaction, custom_signer, notifier_tx))
223 .await
224 .map_err(|_| ConnectorError::InvalidState("transaction queue dropped"))?;
225
226 Ok(notifier_rx
227 .inspect_ok(|res| tracing::debug!(?res, "transaction tracking id received"))
228 .map(move |result| {
229 result
230 .map_err(|_| ConnectorError::InvalidState("transaction notifier dropped"))
231 .and_then(|tx_res| tx_res.map(|id| (id, timeout_until_finalized)))
232 })
233 .and_then(|(tx_id, timeout)| {
234 self.client
235 .track_transaction(tx_id, timeout)
236 .map_err(ConnectorError::from)
237 .inspect(|res| tracing::debug!(?res, "transaction tracking done"))
238 }))
239 }
240}
241
242impl<C, R> Drop for TransactionSequencer<C, R> {
243 fn drop(&mut self) {
244 self.sender.close_channel();
246 }
247}