1use super::{EthApiSpec, EthSigner, LoadBlock, LoadFee, LoadReceipt, LoadState, SpawnBlocking};
5use crate::{
6 helpers::{estimate::EstimateCall, spec::SignersForRpc},
7 FromEthApiError, FullEthApiTypes, IntoEthApiError, RpcNodeCore, RpcNodeCoreExt, RpcReceipt,
8 RpcTransaction,
9};
10use alloy_consensus::{
11 transaction::{SignerRecoverable, TransactionMeta, TxHashRef},
12 BlockHeader, Transaction,
13};
14use alloy_dyn_abi::TypedData;
15use alloy_eip7928::bal::DecodedBal;
16use alloy_eips::{eip2718::Encodable2718, BlockId};
17use alloy_network::{TransactionBuilder, TransactionBuilder4844};
18use alloy_primitives::{Address, Bytes, TxHash, B256, U256};
19use alloy_rpc_types_eth::{state::EvmOverrides, TransactionInfo};
20use futures::{Future, StreamExt};
21use reth_chain_state::CanonStateSubscriptions;
22use reth_primitives_traits::{
23 BlockBody, Recovered, RecoveredBlock, SignedTransaction, TxTy, WithEncoded,
24};
25use reth_rpc_convert::{transaction::RpcConvert, RpcTxReq, TransactionConversionError};
26use reth_rpc_eth_types::{
27 block::convert_transaction_receipt,
28 utils::binary_search,
29 EthApiError::{self, TransactionConfirmationTimeout},
30 FillTransaction, SignError, TransactionSource,
31};
32use reth_storage_api::{
33 BlockNumReader, BlockReaderIdExt, ProviderBlock, ProviderReceipt, ProviderTx, ReceiptProvider,
34 TransactionsProvider,
35};
36use reth_transaction_pool::{
37 AddedTransactionOutcome, PoolPooledTx, PoolTransaction, PoolTx, TransactionOrigin,
38 TransactionPool,
39};
40use revm::state::bal::Bal as RevmBal;
41use std::{sync::Arc, time::Duration};
42
43pub trait EthTransactions: LoadTransaction<Provider: BlockReaderIdExt> {
66 fn signers(&self) -> &SignersForRpc<Self::Provider, Self::NetworkTypes>;
70
71 fn accounts(&self) -> Vec<Address> {
73 self.signers().read().iter().flat_map(|s| s.accounts()).collect()
74 }
75
76 fn send_raw_transaction_sync_timeout(&self) -> Duration;
78
79 fn send_raw_transaction(
83 &self,
84 tx: Bytes,
85 ) -> impl Future<Output = Result<B256, Self::Error>> + Send {
86 async move {
87 let pool_transaction =
88 <PoolTx<Self::Pool> as PoolTransaction>::recover_raw_transaction(&tx)
89 .map_err(Self::Error::from_eth_err)?;
90 self.send_pool_transaction(
91 TransactionOrigin::Local,
92 WithEncoded::new(tx, pool_transaction),
93 )
94 .await
95 }
96 }
97
98 fn send_transaction(
100 &self,
101 origin: TransactionOrigin,
102 tx: WithEncoded<Recovered<PoolPooledTx<Self::Pool>>>,
103 ) -> impl Future<Output = Result<B256, Self::Error>> + Send {
104 async move {
105 let (encoded, recovered) = tx.split();
106 let pool_transaction =
107 <Self::Pool as TransactionPool>::Transaction::from_pooled(recovered);
108
109 self.send_pool_transaction(origin, WithEncoded::new(encoded, pool_transaction)).await
110 }
111 }
112
113 fn send_pool_transaction(
115 &self,
116 origin: TransactionOrigin,
117 tx: WithEncoded<PoolTx<Self::Pool>>,
118 ) -> impl Future<Output = Result<B256, Self::Error>> + Send;
119
120 fn send_raw_transaction_sync(
124 &self,
125 tx: Bytes,
126 timeout_ms: Option<u64>,
127 ) -> impl Future<Output = Result<RpcReceipt<Self::NetworkTypes>, Self::Error>> + Send
128 where
129 Self: LoadReceipt + 'static,
130 {
131 let this = self.clone();
132 let configured_timeout = self.send_raw_transaction_sync_timeout();
133 let timeout_duration = timeout_ms
134 .filter(|timeout_ms| *timeout_ms > 0)
135 .map(Duration::from_millis)
136 .map(|timeout| timeout.min(configured_timeout))
137 .unwrap_or(configured_timeout);
138 async move {
139 let mut stream = this.provider().canonical_state_stream();
140 let hash = EthTransactions::send_raw_transaction(&this, tx).await?;
141 tokio::time::timeout(timeout_duration, async {
142 while let Some(notification) = stream.next().await {
143 let chain = notification.committed();
144 if let Some((block, tx, receipt, all_receipts)) =
145 chain.find_transaction_and_receipt_by_hash(hash) &&
146 let Some(receipt) = convert_transaction_receipt(
147 block,
148 all_receipts,
149 tx,
150 receipt,
151 this.converter(),
152 )
153 .transpose()
154 .map_err(Self::Error::from)?
155 {
156 return Ok(receipt);
157 }
158 }
159 Err(Self::Error::from_eth_err(TransactionConfirmationTimeout {
160 hash,
161 duration: timeout_duration,
162 }))
163 })
164 .await
165 .unwrap_or_else(|_elapsed| {
166 Err(Self::Error::from_eth_err(TransactionConfirmationTimeout {
167 hash,
168 duration: timeout_duration,
169 }))
170 })
171 }
172 }
173
174 #[expect(clippy::complexity)]
180 fn transaction_by_hash(
181 &self,
182 hash: B256,
183 ) -> impl Future<
184 Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
185 > + Send {
186 LoadTransaction::transaction_by_hash(self, hash)
187 }
188
189 fn pending_transactions(&self) -> Result<Vec<RpcTransaction<Self::NetworkTypes>>, Self::Error> {
191 self.pool()
192 .pending_transactions()
193 .into_iter()
194 .map(|tx| self.converter().fill_pending(tx.transaction.clone_into_consensus()))
195 .collect::<Result<Vec<_>, _>>()
196 .map_err(Self::Error::from)
197 }
198
199 #[expect(clippy::type_complexity)]
203 fn transactions_by_block(
204 &self,
205 block: B256,
206 ) -> impl Future<Output = Result<Option<Vec<ProviderTx<Self::Provider>>>, Self::Error>> + Send
207 {
208 async move {
209 self.cache()
210 .get_recovered_block(block)
211 .await
212 .map(|b| b.map(|b| b.body().transactions().to_vec()))
213 .map_err(Self::Error::from_eth_err)
214 }
215 }
216
217 fn raw_transaction_by_hash(
225 &self,
226 hash: B256,
227 ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send {
228 async move {
229 if let Some(tx) =
231 self.pool().get_pooled_transaction_element(hash).map(|tx| tx.encoded_2718().into())
232 {
233 return Ok(Some(tx))
234 }
235
236 self.spawn_blocking_io(move |ref this| {
237 Ok(this
238 .provider()
239 .transaction_by_hash(hash)
240 .map_err(Self::Error::from_eth_err)?
241 .map(|tx| tx.encoded_2718().into()))
242 })
243 .await
244 }
245 }
246
247 #[expect(clippy::type_complexity)]
249 fn historical_transaction_by_hash_at(
250 &self,
251 hash: B256,
252 ) -> impl Future<
253 Output = Result<Option<(TransactionSource<ProviderTx<Self::Provider>>, B256)>, Self::Error>,
254 > + Send {
255 async move {
256 match self.transaction_by_hash_at(hash).await? {
257 None => Ok(None),
258 Some((tx, at)) => Ok(at.as_block_hash().map(|hash| (tx, hash))),
259 }
260 }
261 }
262
263 fn transaction_receipt(
268 &self,
269 hash: B256,
270 ) -> impl Future<Output = Result<Option<RpcReceipt<Self::NetworkTypes>>, Self::Error>> + Send
271 where
272 Self: LoadReceipt + 'static,
273 {
274 async move {
275 match self.load_transaction_and_receipt(hash).await? {
276 Some((tx, meta, receipt, all_receipts, block)) => self
277 .build_transaction_receipt(tx, meta, receipt, all_receipts, block)
278 .await
279 .map(Some),
280 None => Ok(None),
281 }
282 }
283 }
284
285 #[expect(clippy::complexity)]
289 fn load_transaction_and_receipt(
290 &self,
291 hash: TxHash,
292 ) -> impl Future<
293 Output = Result<
294 Option<(
295 Recovered<ProviderTx<Self::Provider>>,
296 TransactionMeta,
297 ProviderReceipt<Self::Provider>,
298 Option<Arc<Vec<ProviderReceipt<Self::Provider>>>>,
299 Option<Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>>,
300 )>,
301 Self::Error,
302 >,
303 > + Send
304 where
305 Self: 'static,
306 {
307 async move {
308 if let Some(cached) = self.cache().get_transaction_by_hash(hash).await &&
309 let Some(tx) = cached.recovered_transaction().map(|tx| tx.cloned())
310 {
311 let meta = cached.transaction_meta(hash);
312
313 if let Some(all_receipts) = cached.receipts.clone() &&
315 let Some(receipt) = all_receipts.get(cached.tx_index).cloned()
316 {
317 return Ok(Some((tx, meta, receipt, Some(all_receipts), Some(cached.block))));
318 }
319
320 if let Some(receipts) = self
324 .cache()
325 .get_receipts(cached.block.hash())
326 .await
327 .map_err(Self::Error::from_eth_err)? &&
328 let Some(receipt) = receipts.get(cached.tx_index).cloned()
329 {
330 return Ok(Some((tx, meta, receipt, Some(receipts), Some(cached.block))));
331 }
332 }
333
334 self.spawn_blocking_io(move |this| {
336 let provider = this.provider();
337 let Some((tx, meta)) = provider
338 .transaction_by_hash_with_meta(hash)
339 .map_err(Self::Error::from_eth_err)?
340 else {
341 return Ok(None);
342 };
343
344 let tx = tx.try_into_recovered_unchecked().map_err(Self::Error::from_eth_err)?;
345
346 let receipt = provider.receipt_by_hash(hash).map_err(Self::Error::from_eth_err)?;
347
348 Ok(receipt.map(|receipt| (tx, meta, receipt, None, None)))
349 })
350 .await
351 }
352 }
353
354 fn transaction_by_block_and_tx_index(
358 &self,
359 block_id: BlockId,
360 index: usize,
361 ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
362 where
363 Self: LoadBlock,
364 {
365 async move {
366 if let Some(block) = self.recovered_block(block_id).await? {
367 let block_hash = block.hash();
368 let block_number = block.number();
369 let block_timestamp = block.timestamp();
370 let base_fee_per_gas = block.base_fee_per_gas();
371 if let Some((signer, tx)) = block.transactions_with_sender().nth(index) {
372 let tx_info = TransactionInfo {
373 hash: Some(*tx.tx_hash()),
374 block_hash: Some(block_hash),
375 block_number: Some(block_number),
376 block_timestamp: Some(block_timestamp),
377 base_fee: base_fee_per_gas,
378 index: Some(index as u64),
379 };
380
381 return Ok(Some(
382 self.converter().fill(tx.clone().with_signer(*signer), tx_info)?,
383 ))
384 }
385 }
386
387 Ok(None)
388 }
389 }
390
391 fn get_transaction_by_sender_and_nonce(
393 &self,
394 sender: Address,
395 nonce: u64,
396 include_pending: bool,
397 ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
398 where
399 Self: LoadBlock + LoadState,
400 {
401 async move {
402 if include_pending &&
404 let Some(tx) =
405 RpcNodeCore::pool(self).get_transaction_by_sender_and_nonce(sender, nonce)
406 {
407 let transaction = tx.transaction.clone_into_consensus();
408 return Ok(Some(self.converter().fill_pending(transaction)?));
409 }
410
411 let highest = self.transaction_count(sender, None).await?.saturating_to::<u64>();
415
416 if nonce >= highest {
419 return Ok(None);
420 }
421
422 let high = self.provider().best_block_number().map_err(Self::Error::from_eth_err)?;
423
424 let num = binary_search::<_, _, Self::Error>(1, high, |mid| async move {
427 let mid_nonce =
428 self.transaction_count(sender, Some(mid.into())).await?.saturating_to::<u64>();
429
430 Ok(mid_nonce > nonce)
431 })
432 .await?;
433
434 let block_id = num.into();
435 self.recovered_block(block_id)
436 .await?
437 .and_then(|block| {
438 let block_hash = block.hash();
439 let block_number = block.number();
440 let block_timestamp = block.timestamp();
441 let base_fee_per_gas = block.base_fee_per_gas();
442
443 block
444 .transactions_with_sender()
445 .enumerate()
446 .find(|(_, (signer, tx))| **signer == sender && (*tx).nonce() == nonce)
447 .map(|(index, (signer, tx))| {
448 let tx_info = TransactionInfo {
449 hash: Some(*tx.tx_hash()),
450 block_hash: Some(block_hash),
451 block_number: Some(block_number),
452 block_timestamp: Some(block_timestamp),
453 base_fee: base_fee_per_gas,
454 index: Some(index as u64),
455 };
456 Ok(self.converter().fill(tx.clone().with_signer(*signer), tx_info)?)
457 })
458 })
459 .ok_or(EthApiError::HeaderNotFound(block_id))?
460 .map(Some)
461 }
462 }
463
464 fn raw_transaction_by_block_and_tx_index(
468 &self,
469 block_id: BlockId,
470 index: usize,
471 ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send
472 where
473 Self: LoadBlock,
474 {
475 async move {
476 if let Some(block) = self.recovered_block(block_id).await? &&
477 let Some(tx) = block.body().transactions().get(index)
478 {
479 return Ok(Some(tx.encoded_2718().into()))
480 }
481
482 Ok(None)
483 }
484 }
485
486 fn send_transaction_request(
489 &self,
490 mut request: RpcTxReq<Self::NetworkTypes>,
491 ) -> impl Future<Output = Result<B256, Self::Error>> + Send
492 where
493 Self: EthApiSpec + LoadBlock + EstimateCall,
494 {
495 async move {
496 let from = match request.as_ref().from() {
497 Some(from) => from,
498 None => return Err(SignError::NoAccount.into_eth_err()),
499 };
500
501 if self.find_signer(&from).is_err() {
502 return Err(SignError::NoAccount.into_eth_err())
503 }
504
505 if request.as_ref().nonce().is_none() {
507 let nonce = self.next_available_nonce_for(&request).await?;
508 request.as_mut().set_nonce(nonce);
509 }
510
511 let chain_id = self.chain_id();
512 if let Some(request_chain_id) = request.as_ref().chain_id() &&
516 request_chain_id != chain_id.to::<u64>()
517 {
518 return Err(EthApiError::InvalidParams(format!(
519 "chainId does not match node's (have={request_chain_id}, want={})",
520 chain_id.to::<u64>()
521 ))
522 .into())
523 }
524 request.as_mut().set_chain_id(chain_id.to());
525
526 if request.as_ref().gas_limit().is_none() {
527 let estimated_gas = self
528 .estimate_gas_at(request.clone(), BlockId::pending(), EvmOverrides::default())
529 .await?;
530 request.as_mut().set_gas_limit(estimated_gas.to());
531 }
532
533 let transaction = self.sign_request(&from, request).await?.with_signer(from);
534
535 let pool_transaction =
536 <<Self as RpcNodeCore>::Pool as TransactionPool>::Transaction::try_from_consensus(
537 transaction,
538 )
539 .map_err(|e| {
540 Self::Error::from_eth_err(TransactionConversionError::Other(e.to_string()))
541 })?;
542
543 let AddedTransactionOutcome { hash, .. } = self
545 .pool()
546 .add_transaction(TransactionOrigin::Local, pool_transaction)
547 .await
548 .map_err(Self::Error::from_eth_err)?;
549
550 Ok(hash)
551 }
552 }
553
554 fn fill_transaction(
556 &self,
557 mut request: RpcTxReq<Self::NetworkTypes>,
558 ) -> impl Future<Output = Result<FillTransaction<TxTy<Self::Primitives>>, Self::Error>> + Send
559 where
560 Self: EthApiSpec + LoadBlock + EstimateCall + LoadFee,
561 {
562 async move {
563 if request.as_ref().value().is_none() {
564 request.as_mut().set_value(U256::ZERO);
565 }
566
567 if request.as_ref().nonce().is_none() {
568 let nonce = self.next_available_nonce_for(&request).await?;
569 request.as_mut().set_nonce(nonce);
570 }
571
572 let chain_id = self.chain_id();
573 if let Some(request_chain_id) = request.as_ref().chain_id() &&
577 request_chain_id != chain_id.to::<u64>()
578 {
579 return Err(EthApiError::InvalidParams(format!(
580 "chainId does not match node's (have={request_chain_id}, want={})",
581 chain_id.to::<u64>()
582 ))
583 .into())
584 }
585 request.as_mut().set_chain_id(chain_id.to());
586
587 if request.as_ref().has_eip4844_fields() &&
588 request.as_ref().max_fee_per_blob_gas().is_none()
589 {
590 let blob_fee = self.blob_base_fee().await?;
591 request.as_mut().set_max_fee_per_blob_gas(blob_fee.to());
592 }
593
594 if request.as_ref().sidecar.is_some() &&
597 request.as_ref().blob_versioned_hashes.is_none()
598 {
599 request.as_mut().populate_blob_hashes();
600 }
601
602 if request.as_ref().gas_limit().is_none() {
603 let estimated_gas = self
604 .estimate_gas_at(request.clone(), BlockId::pending(), EvmOverrides::default())
605 .await?;
606 request.as_mut().set_gas_limit(estimated_gas.to());
607 }
608
609 if request.as_ref().gas_price().is_none() {
610 let tip = if let Some(tip) = request.as_ref().max_priority_fee_per_gas() {
611 tip
612 } else {
613 let tip = self.suggested_priority_fee().await?.to::<u128>();
614 request.as_mut().set_max_priority_fee_per_gas(tip);
615 tip
616 };
617 if request.as_ref().max_fee_per_gas().is_none() {
618 let header =
619 self.provider().latest_header().map_err(Self::Error::from_eth_err)?;
620 let base_fee = header.and_then(|h| h.base_fee_per_gas()).unwrap_or_default();
621 request.as_mut().set_max_fee_per_gas(base_fee as u128 * 2 + tip);
628 }
629 }
630
631 let tx = self.converter().build_simulate_v1_transaction(request)?;
632
633 let raw = tx.encoded_2718().into();
634
635 Ok(FillTransaction { raw, tx })
636 }
637 }
638
639 fn sign_request(
641 &self,
642 from: &Address,
643 txn: RpcTxReq<Self::NetworkTypes>,
644 ) -> impl Future<Output = Result<ProviderTx<Self::Provider>, Self::Error>> + Send {
645 async move {
646 self.find_signer(from)?
647 .sign_transaction(txn, from)
648 .await
649 .map_err(Self::Error::from_eth_err)
650 }
651 }
652
653 fn sign(
655 &self,
656 account: Address,
657 message: Bytes,
658 ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
659 async move {
660 Ok(self
661 .find_signer(&account)?
662 .sign(account, &message)
663 .await
664 .map_err(Self::Error::from_eth_err)?
665 .as_bytes()
666 .into())
667 }
668 }
669
670 fn sign_transaction(
673 &self,
674 request: RpcTxReq<Self::NetworkTypes>,
675 ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
676 async move {
677 let from = match request.as_ref().from() {
678 Some(from) => from,
679 None => return Err(SignError::NoAccount.into_eth_err()),
680 };
681
682 Ok(self.sign_request(&from, request).await?.encoded_2718().into())
683 }
684 }
685
686 fn sign_typed_data(&self, data: &TypedData, account: Address) -> Result<Bytes, Self::Error> {
688 Ok(self
689 .find_signer(&account)?
690 .sign_typed_data(account, data)
691 .map_err(Self::Error::from_eth_err)?
692 .as_bytes()
693 .into())
694 }
695
696 #[expect(clippy::type_complexity)]
698 fn find_signer(
699 &self,
700 account: &Address,
701 ) -> Result<
702 Box<dyn EthSigner<ProviderTx<Self::Provider>, RpcTxReq<Self::NetworkTypes>> + 'static>,
703 Self::Error,
704 > {
705 self.signers()
706 .read()
707 .iter()
708 .find(|signer| signer.is_signer_for(account))
709 .map(|signer| dyn_clone::clone_box(&**signer))
710 .ok_or_else(|| SignError::NoAccount.into_eth_err())
711 }
712}
713
714pub trait LoadTransaction: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt {
719 #[expect(clippy::complexity)]
725 fn transaction_by_hash(
726 &self,
727 hash: B256,
728 ) -> impl Future<
729 Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
730 > + Send {
731 async move {
732 if let Some(cached) = self.cache().get_transaction_by_hash(hash).await &&
734 let Some(source) = cached.to_transaction_source()
735 {
736 return Ok(Some(source));
737 }
738
739 if let Some((tx, meta)) = self
741 .spawn_blocking_io(move |this| {
742 this.provider()
743 .transaction_by_hash_with_meta(hash)
744 .map_err(Self::Error::from_eth_err)
745 })
746 .await?
747 {
748 let transaction = tx
752 .try_into_recovered_unchecked()
753 .map_err(|_| EthApiError::InvalidTransactionSignature)?;
754
755 return Ok(Some(TransactionSource::Block {
756 transaction,
757 index: meta.index,
758 block_hash: meta.block_hash,
759 block_number: meta.block_number,
760 block_timestamp: meta.timestamp,
761 base_fee: meta.base_fee,
762 }));
763 }
764
765 if let Some(tx) = self.pool().get(&hash).map(|tx| tx.transaction.clone_into_consensus())
767 {
768 return Ok(Some(TransactionSource::Pool(tx.into())));
769 }
770
771 Ok(None)
772 }
773 }
774
775 #[expect(clippy::type_complexity)]
779 fn transaction_by_hash_at(
780 &self,
781 transaction_hash: B256,
782 ) -> impl Future<
783 Output = Result<
784 Option<(TransactionSource<ProviderTx<Self::Provider>>, BlockId)>,
785 Self::Error,
786 >,
787 > + Send {
788 async move {
789 Ok(self.transaction_by_hash(transaction_hash).await?.map(|tx| match tx {
790 tx @ TransactionSource::Pool(_) => (tx, BlockId::pending()),
791 tx @ TransactionSource::Block { block_hash, .. } => {
792 (tx, BlockId::Hash(block_hash.into()))
793 }
794 }))
795 }
796 }
797
798 #[expect(clippy::type_complexity)]
800 fn transaction_and_block(
801 &self,
802 hash: B256,
803 ) -> impl Future<
804 Output = Result<
805 Option<(
806 TransactionSource<ProviderTx<Self::Provider>>,
807 Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>,
808 )>,
809 Self::Error,
810 >,
811 > + Send {
812 async move {
813 Ok(self
814 .transaction_and_block_and_maybe_bal(hash)
815 .await?
816 .map(|(transaction, block, _)| (transaction, block)))
817 }
818 }
819
820 #[expect(clippy::type_complexity)]
825 fn transaction_and_block_and_maybe_bal(
826 &self,
827 hash: B256,
828 ) -> impl Future<
829 Output = Result<
830 Option<(
831 TransactionSource<ProviderTx<Self::Provider>>,
832 Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>,
833 Option<Arc<DecodedBal<Arc<RevmBal>>>>,
834 )>,
835 Self::Error,
836 >,
837 > + Send {
838 async move {
839 let (transaction, at) = match self.transaction_by_hash_at(hash).await? {
840 None => return Ok(None),
841 Some(res) => res,
842 };
843
844 let block_hash = match at {
846 BlockId::Hash(hash) => hash.block_hash,
847 _ => return Ok(None),
848 };
849 let block_and_bal = self
850 .cache()
851 .get_recovered_block_and_maybe_bal(block_hash)
852 .await
853 .map_err(Self::Error::from_eth_err)?;
854 Ok(block_and_bal.map(|(block, bal)| (transaction, block, bal)))
855 }
856 }
857}