reth_rpc_eth_api/helpers/
transaction.rs1use alloy_consensus::{BlockHeader, Transaction, Typed2718};
5use alloy_dyn_abi::TypedData;
6use alloy_eips::{eip2718::Encodable2718, BlockId};
7use alloy_network::TransactionBuilder;
8use alloy_primitives::{Address, Bytes, TxHash, B256};
9use alloy_rpc_types_eth::{transaction::TransactionRequest, BlockNumberOrTag, TransactionInfo};
10use futures::Future;
11use reth_node_api::BlockBody;
12use reth_primitives::{
13 transaction::SignedTransactionIntoRecoveredExt, SealedBlockWithSenders, TransactionMeta,
14};
15use reth_primitives_traits::SignedTransaction;
16use reth_provider::{
17 BlockNumReader, BlockReaderIdExt, ProviderBlock, ProviderReceipt, ProviderTx, ReceiptProvider,
18 TransactionsProvider,
19};
20use reth_rpc_eth_types::{utils::binary_search, EthApiError, SignError, TransactionSource};
21use reth_rpc_types_compat::transaction::{from_recovered, from_recovered_with_block_context};
22use reth_transaction_pool::{PoolTransaction, TransactionOrigin, TransactionPool};
23use std::sync::Arc;
24
25use super::{EthApiSpec, EthSigner, LoadBlock, LoadReceipt, LoadState, SpawnBlocking};
26use crate::{
27 helpers::estimate::EstimateCall, FromEthApiError, FullEthApiTypes, IntoEthApiError,
28 RpcNodeCore, RpcNodeCoreExt, RpcReceipt, RpcTransaction,
29};
30
31pub trait EthTransactions: LoadTransaction<Provider: BlockReaderIdExt> {
55 #[expect(clippy::type_complexity)]
59 fn signers(&self) -> &parking_lot::RwLock<Vec<Box<dyn EthSigner<ProviderTx<Self::Provider>>>>>;
60
61 fn send_raw_transaction(
65 &self,
66 tx: Bytes,
67 ) -> impl Future<Output = Result<B256, Self::Error>> + Send;
68
69 fn send_typed_data_transaction(
73 &self,
74 tx: alloy_eips::eip712::TypedDataRequest,
75 ) -> impl Future<Output = Result<B256, Self::Error>> + Send;
76
77 #[expect(clippy::complexity)]
83 fn transaction_by_hash(
84 &self,
85 hash: B256,
86 ) -> impl Future<
87 Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
88 > + Send {
89 LoadTransaction::transaction_by_hash(self, hash)
90 }
91
92 #[expect(clippy::type_complexity)]
96 fn transactions_by_block(
97 &self,
98 block: B256,
99 ) -> impl Future<Output = Result<Option<Vec<ProviderTx<Self::Provider>>>, Self::Error>> + Send
100 {
101 async move {
102 self.cache()
103 .get_sealed_block_with_senders(block)
104 .await
105 .map(|b| b.map(|b| b.body.transactions().to_vec()))
106 .map_err(Self::Error::from_eth_err)
107 }
108 }
109
110 fn raw_transaction_by_hash(
118 &self,
119 hash: B256,
120 ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send {
121 async move {
122 if let Some(tx) =
124 self.pool().get_pooled_transaction_element(hash).map(|tx| tx.encoded_2718().into())
125 {
126 return Ok(Some(tx))
127 }
128
129 self.spawn_blocking_io(move |ref this| {
130 Ok(this
131 .provider()
132 .transaction_by_hash(hash)
133 .map_err(Self::Error::from_eth_err)?
134 .map(|tx| tx.encoded_2718().into()))
135 })
136 .await
137 }
138 }
139
140 #[expect(clippy::type_complexity)]
142 fn historical_transaction_by_hash_at(
143 &self,
144 hash: B256,
145 ) -> impl Future<
146 Output = Result<Option<(TransactionSource<ProviderTx<Self::Provider>>, B256)>, Self::Error>,
147 > + Send {
148 async move {
149 match self.transaction_by_hash_at(hash).await? {
150 None => Ok(None),
151 Some((tx, at)) => Ok(at.as_block_hash().map(|hash| (tx, hash))),
152 }
153 }
154 }
155
156 fn transaction_receipt(
161 &self,
162 hash: B256,
163 ) -> impl Future<Output = Result<Option<RpcReceipt<Self::NetworkTypes>>, Self::Error>> + Send
164 where
165 Self: LoadReceipt + 'static,
166 {
167 async move {
168 match self.load_transaction_and_receipt(hash).await? {
169 Some((tx, meta, receipt)) => {
170 self.build_transaction_receipt(tx, meta, receipt).await.map(Some)
171 }
172 None => Ok(None),
173 }
174 }
175 }
176
177 #[expect(clippy::complexity)]
179 fn load_transaction_and_receipt(
180 &self,
181 hash: TxHash,
182 ) -> impl Future<
183 Output = Result<
184 Option<(ProviderTx<Self::Provider>, TransactionMeta, ProviderReceipt<Self::Provider>)>,
185 Self::Error,
186 >,
187 > + Send
188 where
189 Self: 'static,
190 {
191 let provider = self.provider().clone();
192 self.spawn_blocking_io(move |_| {
193 let (tx, meta) = match provider
194 .transaction_by_hash_with_meta(hash)
195 .map_err(Self::Error::from_eth_err)?
196 {
197 Some((tx, meta)) => (tx, meta),
198 None => return Ok(None),
199 };
200
201 let receipt = match provider.receipt_by_hash(hash).map_err(Self::Error::from_eth_err)? {
202 Some(recpt) => recpt,
203 None => return Ok(None),
204 };
205
206 Ok(Some((tx, meta, receipt)))
207 })
208 }
209
210 fn transaction_by_block_and_tx_index(
214 &self,
215 block_id: BlockId,
216 index: usize,
217 ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
218 where
219 Self: LoadBlock,
220 {
221 async move {
222 if let Some(block) = self.block_with_senders(block_id).await? {
223 let block_hash = block.hash();
224 let block_number = block.number();
225 let base_fee_per_gas = block.base_fee_per_gas();
226 if let Some((signer, tx)) = block.transactions_with_sender().nth(index) {
227 let tx_type = Some(tx.ty() as isize);
228 let tx_info = TransactionInfo {
229 hash: Some(*tx.tx_hash()),
230 block_hash: Some(block_hash),
231 block_number: Some(block_number),
232 base_fee: base_fee_per_gas.map(u128::from),
233 index: Some(index as u64),
234 tx_type,
235 };
236
237 return Ok(Some(from_recovered_with_block_context(
238 tx.clone().with_signer(*signer),
239 tx_info,
240 self.tx_resp_builder(),
241 )?))
242 }
243 }
244
245 Ok(None)
246 }
247 }
248
249 fn get_transaction_by_sender_and_nonce(
251 &self,
252 sender: Address,
253 nonce: u64,
254 include_pending: bool,
255 ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
256 where
257 Self: LoadBlock + LoadState,
258 {
259 async move {
260 if include_pending {
262 if let Some(tx) =
263 RpcNodeCore::pool(self).get_transaction_by_sender_and_nonce(sender, nonce)
264 {
265 let transaction = tx.transaction.clone_into_consensus();
266 return Ok(Some(from_recovered(transaction, self.tx_resp_builder())?));
267 }
268 }
269
270 if self.get_code(sender, None).await?.len() > 0 {
272 return Ok(None);
273 }
274
275 let highest = self.transaction_count(sender, None).await?.saturating_to::<u64>();
276
277 if nonce >= highest {
280 return Ok(None);
281 }
282
283 let Ok(high) = self.provider().best_block_number() else {
284 return Err(EthApiError::HeaderNotFound(BlockNumberOrTag::Latest.into()).into());
285 };
286
287 let num = binary_search::<_, _, Self::Error>(1, high, |mid| async move {
290 let mid_nonce =
291 self.transaction_count(sender, Some(mid.into())).await?.saturating_to::<u64>();
292
293 Ok(mid_nonce > nonce)
294 })
295 .await?;
296
297 let block_id = num.into();
298 self.block_with_senders(block_id)
299 .await?
300 .and_then(|block| {
301 let block_hash = block.hash();
302 let block_number = block.number();
303 let base_fee_per_gas = block.base_fee_per_gas();
304
305 block
306 .transactions_with_sender()
307 .enumerate()
308 .find(|(_, (signer, tx))| **signer == sender && (*tx).nonce() == nonce)
309 .map(|(index, (signer, tx))| {
310 let tx_info = TransactionInfo {
311 hash: Some(*tx.tx_hash()),
312 block_hash: Some(block_hash),
313 block_number: Some(block_number),
314 base_fee: base_fee_per_gas.map(u128::from),
315 index: Some(index as u64),
316 tx_type: Some(tx.ty() as isize),
317 };
318 from_recovered_with_block_context(
319 tx.clone().with_signer(*signer),
320 tx_info,
321 self.tx_resp_builder(),
322 )
323 })
324 })
325 .ok_or(EthApiError::HeaderNotFound(block_id))?
326 .map(Some)
327 }
328 }
329
330 fn raw_transaction_by_block_and_tx_index(
334 &self,
335 block_id: BlockId,
336 index: usize,
337 ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send
338 where
339 Self: LoadBlock,
340 {
341 async move {
342 if let Some(block) = self.block_with_senders(block_id).await? {
343 if let Some(tx) = block.transactions().get(index) {
344 return Ok(Some(tx.encoded_2718().into()))
345 }
346 }
347
348 Ok(None)
349 }
350 }
351
352 fn send_transaction(
355 &self,
356 mut request: TransactionRequest,
357 ) -> impl Future<Output = Result<B256, Self::Error>> + Send
358 where
359 Self: EthApiSpec + LoadBlock + EstimateCall,
360 {
361 async move {
362 let from = match request.from {
363 Some(from) => from,
364 None => return Err(SignError::NoAccount.into_eth_err()),
365 };
366
367 if self.find_signer(&from).is_err() {
368 return Err(SignError::NoAccount.into_eth_err())
369 }
370
371 if request.nonce.is_none() {
373 let nonce = self.next_available_nonce(from).await?;
374 request.nonce = Some(nonce);
375 }
376
377 let chain_id = self.chain_id();
378 request.chain_id = Some(chain_id.to());
379
380 let estimated_gas =
381 self.estimate_gas_at(request.clone(), BlockId::pending(), None).await?;
382 let gas_limit = estimated_gas;
383 request.set_gas_limit(gas_limit.to());
384
385 let transaction = self.sign_request(&from, request).await?.with_signer(from);
386
387 let pool_transaction =
388 <<Self as RpcNodeCore>::Pool as TransactionPool>::Transaction::try_from_consensus(
389 transaction,
390 )
391 .map_err(|_| EthApiError::TransactionConversionError)?;
392
393 let hash = self
395 .pool()
396 .add_transaction(TransactionOrigin::Local, pool_transaction)
397 .await
398 .map_err(Self::Error::from_eth_err)?;
399
400 Ok(hash)
401 }
402 }
403
404 fn sign_request(
406 &self,
407 from: &Address,
408 txn: TransactionRequest,
409 ) -> impl Future<Output = Result<ProviderTx<Self::Provider>, Self::Error>> + Send {
410 async move {
411 self.find_signer(from)?
412 .sign_transaction(txn, from)
413 .await
414 .map_err(Self::Error::from_eth_err)
415 }
416 }
417
418 fn sign(
420 &self,
421 account: Address,
422 message: Bytes,
423 ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
424 async move {
425 Ok(self
426 .find_signer(&account)?
427 .sign(account, &message)
428 .await
429 .map_err(Self::Error::from_eth_err)?
430 .as_bytes()
431 .into())
432 }
433 }
434
435 fn sign_transaction(
438 &self,
439 request: TransactionRequest,
440 ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
441 async move {
442 let from = match request.from {
443 Some(from) => from,
444 None => return Err(SignError::NoAccount.into_eth_err()),
445 };
446
447 Ok(self.sign_request(&from, request).await?.encoded_2718().into())
448 }
449 }
450
451 fn sign_typed_data(&self, data: &TypedData, account: Address) -> Result<Bytes, Self::Error> {
453 Ok(self
454 .find_signer(&account)?
455 .sign_typed_data(account, data)
456 .map_err(Self::Error::from_eth_err)?
457 .as_bytes()
458 .into())
459 }
460
461 #[expect(clippy::type_complexity)]
463 fn find_signer(
464 &self,
465 account: &Address,
466 ) -> Result<Box<(dyn EthSigner<ProviderTx<Self::Provider>> + 'static)>, Self::Error> {
467 self.signers()
468 .read()
469 .iter()
470 .find(|signer| signer.is_signer_for(account))
471 .map(|signer| dyn_clone::clone_box(&**signer))
472 .ok_or_else(|| SignError::NoAccount.into_eth_err())
473 }
474}
475
476pub trait LoadTransaction: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt {
481 #[expect(clippy::complexity)]
487 fn transaction_by_hash(
488 &self,
489 hash: B256,
490 ) -> impl Future<
491 Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
492 > + Send {
493 async move {
494 let mut resp = self
496 .spawn_blocking_io(move |this| {
497 match this
498 .provider()
499 .transaction_by_hash_with_meta(hash)
500 .map_err(Self::Error::from_eth_err)?
501 {
502 None => Ok(None),
503 Some((tx, meta)) => {
504 let transaction = tx
508 .into_ecrecovered_unchecked()
509 .ok_or(EthApiError::InvalidTransactionSignature)?;
510
511 let tx = TransactionSource::Block {
512 transaction,
513 index: meta.index,
514 block_hash: meta.block_hash,
515 block_number: meta.block_number,
516 base_fee: meta.base_fee,
517 };
518 Ok(Some(tx))
519 }
520 }
521 })
522 .await?;
523
524 if resp.is_none() {
525 if let Some(tx) =
527 self.pool().get(&hash).map(|tx| tx.transaction.clone().into_consensus())
528 {
529 resp = Some(TransactionSource::Pool(tx.into()));
530 }
531 }
532
533 Ok(resp)
534 }
535 }
536
537 #[expect(clippy::type_complexity)]
541 fn transaction_by_hash_at(
542 &self,
543 transaction_hash: B256,
544 ) -> impl Future<
545 Output = Result<
546 Option<(TransactionSource<ProviderTx<Self::Provider>>, BlockId)>,
547 Self::Error,
548 >,
549 > + Send {
550 async move {
551 Ok(self.transaction_by_hash(transaction_hash).await?.map(|tx| match tx {
552 tx @ TransactionSource::Pool(_) => (tx, BlockId::pending()),
553 tx @ TransactionSource::Block { block_hash, .. } => {
554 (tx, BlockId::Hash(block_hash.into()))
555 }
556 }))
557 }
558 }
559
560 #[expect(clippy::type_complexity)]
562 fn transaction_and_block(
563 &self,
564 hash: B256,
565 ) -> impl Future<
566 Output = Result<
567 Option<(
568 TransactionSource<ProviderTx<Self::Provider>>,
569 Arc<SealedBlockWithSenders<ProviderBlock<Self::Provider>>>,
570 )>,
571 Self::Error,
572 >,
573 > + Send {
574 async move {
575 let (transaction, at) = match self.transaction_by_hash_at(hash).await? {
576 None => return Ok(None),
577 Some(res) => res,
578 };
579
580 let block_hash = match at {
582 BlockId::Hash(hash) => hash.block_hash,
583 _ => return Ok(None),
584 };
585 let block = self
586 .cache()
587 .get_sealed_block_with_senders(block_hash)
588 .await
589 .map_err(Self::Error::from_eth_err)?;
590 Ok(block.map(|block| (transaction, block)))
591 }
592 }
593}