1use crate::EthApi;
4use alloy_consensus::BlobTransactionValidationError;
5use alloy_eips::{eip7594::BlobTransactionSidecarVariant, BlockId, Typed2718};
6use alloy_primitives::{hex, B256};
7use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
8use reth_primitives_traits::{AlloyBlockHeader, WithEncoded};
9use reth_rpc_convert::RpcConvert;
10use reth_rpc_eth_api::{
11 helpers::{spec::SignersForRpc, EthTransactions, LoadTransaction},
12 FromEvmError, RpcNodeCore,
13};
14use reth_rpc_eth_types::{error::RpcPoolError, EthApiError};
15use reth_storage_api::BlockReaderIdExt;
16use reth_transaction_pool::{
17 error::Eip4844PoolTransactionError, AddedTransactionOutcome, EthBlobTransactionSidecar,
18 EthPoolTransaction, PoolTransaction, PoolTx,
19};
20
21impl<N, Rpc> EthTransactions for EthApi<N, Rpc>
22where
23 N: RpcNodeCore,
24 EthApiError: FromEvmError<N::Evm>,
25 Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
26{
27 #[inline]
28 fn signers(&self) -> &SignersForRpc<Self::Provider, Self::NetworkTypes> {
29 self.inner.signers()
30 }
31
32 async fn send_pool_transaction(
33 &self,
34 origin: reth_transaction_pool::TransactionOrigin,
35 tx: WithEncoded<PoolTx<Self::Pool>>,
36 ) -> Result<B256, Self::Error> {
37 let (tx, mut pool_transaction) = tx.split();
38
39 if self.inner.force_blob_sidecar_upcasting() && pool_transaction.is_eip4844() {
42 let EthBlobTransactionSidecar::Present(sidecar) = pool_transaction.take_blob() else {
43 return Err(EthApiError::PoolError(RpcPoolError::Eip4844(
44 Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
45 )));
46 };
47 let sidecar = sidecar.into_sidecar();
48
49 let sidecar = match sidecar {
50 BlobTransactionSidecarVariant::Eip4844(sidecar) => {
51 let latest = self
52 .provider()
53 .latest_header()?
54 .ok_or(EthApiError::HeaderNotFound(BlockId::latest()))?;
55 if self
57 .provider()
58 .chain_spec()
59 .is_osaka_active_at_timestamp(latest.timestamp().saturating_add(12))
60 {
61 BlobTransactionSidecarVariant::Eip7594(
62 self.blob_sidecar_converter().convert(sidecar).await.ok_or_else(
63 || {
64 RpcPoolError::Eip4844(
65 Eip4844PoolTransactionError::InvalidEip4844Blob(
66 BlobTransactionValidationError::InvalidProof,
67 ),
68 )
69 },
70 )?,
71 )
72 } else {
73 BlobTransactionSidecarVariant::Eip4844(sidecar)
74 }
75 }
76 sidecar => sidecar,
77 };
78
79 pool_transaction =
80 EthPoolTransaction::try_from_eip4844(pool_transaction.into_consensus(), sidecar)
81 .ok_or_else(|| {
82 RpcPoolError::Eip4844(
83 Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
84 )
85 })?;
86 }
87
88 if let Some(client) = self.raw_tx_forwarder() {
90 tracing::debug!(target: "rpc::eth", hash = %pool_transaction.hash(), "forwarding raw transaction to forwarder");
91 let rlp_hex = hex::encode_prefixed(&tx);
92
93 self.broadcast_raw_transaction(tx);
95
96 let hash =
97 client.request("eth_sendRawTransaction", (rlp_hex,)).await.inspect_err(|err| {
98 tracing::debug!(target: "rpc::eth", %err, hash=% *pool_transaction.hash(), "failed to forward raw transaction");
99 }).map_err(EthApiError::other)?;
100
101 let _ = self.inner.add_pool_transaction(origin, pool_transaction).await;
103
104 return Ok(hash);
105 }
106
107 self.broadcast_raw_transaction(tx);
109
110 let AddedTransactionOutcome { hash, .. } =
111 self.inner.add_pool_transaction(origin, pool_transaction).await?;
112
113 Ok(hash)
114 }
115}
116
117impl<N, Rpc> LoadTransaction for EthApi<N, Rpc>
118where
119 N: RpcNodeCore,
120 EthApiError: FromEvmError<N::Evm>,
121 Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
122{
123}
124
125#[cfg(test)]
126mod tests {
127 use std::time::Duration;
128
129 use super::*;
130 use crate::eth::helpers::{signer::DevSigner, types::EthRpcConverter};
131 use alloy_consensus::{
132 BlobTransactionSidecar, Block, Header, SidecarBuilder, SimpleCoder, Transaction,
133 };
134 use alloy_eips::eip2718::Encodable2718;
135 use alloy_primitives::{map::AddressMap, Address, Bytes, Signature, U256};
136 use alloy_rpc_types_eth::request::TransactionRequest;
137 use reth_chainspec::{ChainSpec, ChainSpecBuilder};
138 use reth_ethereum_primitives::TransactionSigned;
139 use reth_evm::SenderRecoveryCache;
140 use reth_evm_ethereum::EthEvmConfig;
141 use reth_network_api::noop::NoopNetwork;
142 use reth_primitives_traits::SignedTransaction;
143 use reth_provider::{
144 test_utils::{ExtendedAccount, MockEthProvider},
145 ChainSpecProvider,
146 };
147 use reth_rpc_eth_api::node::RpcNodeCoreAdapter;
148 use reth_transaction_pool::{
149 test_utils::{testing_pool, TestPool, TransactionGenerator},
150 EthPooledTransaction, PoolPooledTx, TransactionOrigin, TransactionPool,
151 };
152
153 fn mock_eth_api(
154 accounts: AddressMap<ExtendedAccount>,
155 ) -> EthApi<
156 RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
157 EthRpcConverter<ChainSpec>,
158 > {
159 mock_eth_api_with_sync_timeout(accounts, Duration::from_secs(30))
160 }
161
162 fn mock_eth_api_with_sync_timeout(
163 accounts: AddressMap<ExtendedAccount>,
164 send_raw_transaction_sync_timeout: Duration,
165 ) -> EthApi<
166 RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
167 EthRpcConverter<ChainSpec>,
168 > {
169 mock_eth_api_builder(accounts)
170 .send_raw_transaction_sync_timeout(send_raw_transaction_sync_timeout)
171 .build()
172 }
173
174 fn mock_eth_api_builder(
175 accounts: AddressMap<ExtendedAccount>,
176 ) -> crate::EthApiBuilder<
177 RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
178 EthRpcConverter<ChainSpec>,
179 > {
180 let mock_provider = MockEthProvider::default()
181 .with_chain_spec(ChainSpecBuilder::mainnet().cancun_activated().build());
182 mock_provider.extend_accounts(accounts);
183
184 let evm_config = EthEvmConfig::new(mock_provider.chain_spec());
185 let pool = testing_pool();
186
187 let genesis_header = Header {
188 number: 0,
189 gas_limit: 30_000_000,
190 timestamp: 1,
191 excess_blob_gas: Some(0),
192 base_fee_per_gas: Some(1000000000),
193 blob_gas_used: Some(0),
194 ..Default::default()
195 };
196
197 let genesis_hash = B256::ZERO;
198 mock_provider.add_block(genesis_hash, Block::new(genesis_header, Default::default()));
199
200 EthApi::builder(mock_provider, pool, NoopNetwork::default(), evm_config)
201 }
202
203 fn raw_transfer_tx() -> Bytes {
204 Bytes::from(hex!(
206 "02f871018303579880850555633d1b82520894eee27662c2b8eba3cd936a23f039f3189633e4c887ad591c62bdaeb180c080a07ea72c68abfb8fca1bd964f0f99132ed9280261bdca3e549546c0205e800f7d0a05b4ef3039e9c9b9babc179a1878fb825b5aaf5aed2fa8744854150157b08d6f3"
207 ))
208 }
209
210 #[tokio::test]
211 async fn raw_transaction_recovery_shares_sender_cache() {
212 let cache = SenderRecoveryCache::new(16);
213 let eth_api = mock_eth_api_builder(Default::default())
214 .sender_recovery_cache(Some(cache.clone()))
215 .build();
216 let mut generator = TransactionGenerator::new(rand::rng());
217
218 for (transaction, warm_cache) in [
219 (generator.transaction().into_legacy(), false),
220 (generator.transaction().nonce(1).into_eip1559(), true),
221 ] {
222 let raw = Bytes::from(transaction.encoded_2718());
223 let hash = *transaction.tx_hash();
224 let sender = transaction.try_recover().unwrap();
225 assert_eq!(cache.get(&hash), None);
226 if warm_cache {
227 let recovered =
228 eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap();
229 assert_eq!(recovered.signer(), sender);
230 assert_eq!(cache.get(&hash), Some(sender));
231 }
232
233 assert_eq!(eth_api.send_raw_transaction(raw.clone()).await.unwrap(), hash);
234 assert_eq!(cache.get(&hash), Some(sender));
235 let recovered =
236 eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap();
237 assert_eq!(recovered.signer(), sender);
238 assert_eq!(eth_api.recover_raw_pool_transaction(&raw).unwrap().sender(), sender);
239 }
240 }
241
242 #[tokio::test]
243 async fn raw_transaction_recovery_uses_pool_overrides() {
244 let raw = raw_transfer_tx();
245 for enabled in [false, true] {
246 let cache = SenderRecoveryCache::new(16);
247 let eth_api = mock_eth_api_builder(Default::default())
248 .sender_recovery_cache(enabled.then(|| cache.clone()))
249 .build();
250
251 for _ in 0..2 {
253 let transaction = eth_api.recover_raw_pool_transaction(&raw).unwrap();
254 assert_eq!(transaction.encoded_length(), raw.len());
255 assert_eq!(cache.get(transaction.hash()), enabled.then_some(transaction.sender()));
256 }
257 }
258 }
259
260 #[tokio::test]
261 async fn raw_transaction_recovery_preserves_errors() {
262 let invalid = TransactionSigned::new_unhashed(
263 alloy_consensus::TxLegacy::default().into(),
264 Signature::new(U256::ZERO, U256::ZERO, false),
265 );
266 let mut trailing = raw_transfer_tx().to_vec();
267 trailing.push(0);
268
269 for enabled in [false, true] {
270 let cache = SenderRecoveryCache::new(16);
271 let eth_api = mock_eth_api_builder(Default::default())
272 .sender_recovery_cache(enabled.then(|| cache.clone()))
273 .build();
274 let valid = eth_api.recover_raw_pool_transaction(&raw_transfer_tx()).unwrap();
275 assert_eq!(cache.get(valid.hash()), enabled.then_some(valid.sender()));
276 for (raw, expected) in [
277 (vec![], EthApiError::EmptyRawTransactionData),
278 (vec![2], EthApiError::FailedToDecodeSignedTransaction),
279 (trailing.clone(), EthApiError::FailedToDecodeSignedTransaction),
280 (invalid.encoded_2718(), EthApiError::InvalidTransactionSignature),
281 ] {
282 let err = eth_api.send_raw_transaction(raw.clone().into()).await.unwrap_err();
283 assert_eq!(err.to_string(), expected.to_string());
284 let err =
285 eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap_err();
286 assert_eq!(err.to_string(), expected.to_string());
287 }
288 assert!(eth_api.pool().is_empty());
289 assert_eq!(cache.get(invalid.tx_hash()), None);
290 }
291 }
292
293 #[tokio::test]
294 async fn cached_raw_transaction_preserves_blob_sidecar() {
295 let cache = SenderRecoveryCache::new(16);
296 let provider = MockEthProvider::default();
297 let evm_config = EthEvmConfig::new(provider.chain_spec());
298 let eth_api = EthApi::builder(
299 provider,
300 reth_transaction_pool::noop::NoopTransactionPool::default(),
301 NoopNetwork::default(),
302 evm_config,
303 )
304 .sender_recovery_cache(Some(cache.clone()))
305 .build();
306 let mut builder = SidecarBuilder::<SimpleCoder>::new();
307 builder.ingest(b"cached blob transaction");
308 let sidecar =
309 BlobTransactionSidecarVariant::from(builder.build::<BlobTransactionSidecar>().unwrap());
310 let transaction = TransactionSigned::new_unhashed(
311 alloy_consensus::TxEip4844 {
312 blob_versioned_hashes: sidecar.versioned_hashes().collect(),
313 ..Default::default()
314 }
315 .into(),
316 Signature::test_signature(),
317 );
318 let hash = *transaction.tx_hash();
319 let sender = transaction.try_recover().unwrap();
320 let raw = transaction.try_into_pooled_eip4844(sidecar.clone()).unwrap().encoded_2718();
321
322 for _ in 0..2 {
323 let mut recovered = eth_api.recover_raw_pool_transaction(&raw).unwrap();
324 assert_eq!(recovered.sender(), sender);
325 assert_eq!(cache.get(&hash), Some(sender));
326 let EthBlobTransactionSidecar::Present(actual) = recovered.take_blob() else {
327 panic!("missing blob sidecar")
328 };
329 assert_eq!(actual.into_sidecar(), sidecar);
330 }
331 }
332
333 #[tokio::test]
334 async fn send_raw_transaction_sync_populates_sender_cache() {
335 let cache = SenderRecoveryCache::new(16);
336 let eth_api = mock_eth_api_builder(Default::default())
337 .sender_recovery_cache(Some(cache.clone()))
338 .build();
339 let raw = raw_transfer_tx();
340 let transaction = EthPooledTransaction::decode_raw_transaction(&raw).unwrap();
341 let sender = transaction.try_recover().unwrap();
342
343 let err = eth_api.send_raw_transaction_sync(raw, Some(1)).await.unwrap_err();
344 assert!(matches!(err, EthApiError::TransactionConfirmationTimeout { .. }));
345 assert_eq!(cache.get(transaction.tx_hash()), Some(sender));
346 assert_eq!(eth_api.pool().len(), 1);
347 }
348
349 #[tokio::test]
350 async fn send_raw_transaction() {
351 let eth_api = mock_eth_api(Default::default());
352 let pool = eth_api.pool();
353
354 let tx_1 = raw_transfer_tx();
355
356 let tx_1_result = eth_api.send_raw_transaction(tx_1).await.unwrap();
357 assert_eq!(
358 pool.len(),
359 1,
360 "expect 1 transaction in the pool, but pool size is {}",
361 pool.len()
362 );
363
364 let tx_2 = Bytes::from(hex!(
366 "02f9043c018202b7843b9aca00850c807d37a08304d21d94ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b881bc16d674ec80000b903c43593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063e2d99f00000000000000000000000000000000000000000000000000000000000000030b000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000065717fe021ea67801d1088cc80099004b05b64600000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95fd5965fd1f1a6f0d4600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000428dca9537116148616a5a3e44035af17238fe9dc080a0c6ec1e41f5c0b9511c49b171ad4e04c6bb419c74d99fe9891d74126ec6e4e879a032069a753d7a2cfa158df95421724d24c0e9501593c09905abf3699b4a4405ce"
367 ));
368
369 let tx_2_result = eth_api.send_raw_transaction(tx_2).await.unwrap();
370 assert_eq!(
371 pool.len(),
372 2,
373 "expect 2 transactions in the pool, but pool size is {}",
374 pool.len()
375 );
376
377 assert!(pool.get(&tx_1_result).is_some(), "tx1 not found in the pool");
378 assert!(pool.get(&tx_2_result).is_some(), "tx2 not found in the pool");
379 assert_eq!(pool.get(&tx_1_result).unwrap().origin, TransactionOrigin::Local);
380 assert_eq!(pool.get(&tx_2_result).unwrap().origin, TransactionOrigin::Local);
381 }
382
383 #[tokio::test]
384 async fn send_raw_transaction_sync_uses_request_timeout() {
385 let eth_api = mock_eth_api(Default::default());
386
387 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(1)).await.unwrap_err();
388
389 assert!(matches!(
390 err,
391 EthApiError::TransactionConfirmationTimeout { duration, .. }
392 if duration == Duration::from_millis(1)
393 ));
394 assert_eq!(eth_api.pool().len(), 1);
395 }
396
397 #[tokio::test]
398 async fn send_raw_transaction_sync_uses_configured_timeout_when_omitted() {
399 let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
400
401 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), None).await.unwrap_err();
402
403 assert!(matches!(
404 err,
405 EthApiError::TransactionConfirmationTimeout { duration, .. }
406 if duration == Duration::from_millis(1)
407 ));
408 assert_eq!(eth_api.pool().len(), 1);
409 }
410
411 #[tokio::test]
412 async fn send_raw_transaction_sync_uses_configured_timeout_when_zero() {
413 let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
414
415 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(0)).await.unwrap_err();
416
417 assert!(matches!(
418 err,
419 EthApiError::TransactionConfirmationTimeout { duration, .. }
420 if duration == Duration::from_millis(1)
421 ));
422 assert_eq!(eth_api.pool().len(), 1);
423 }
424
425 #[tokio::test]
426 async fn send_raw_transaction_sync_caps_request_timeout() {
427 let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
428
429 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(50)).await.unwrap_err();
430
431 assert!(matches!(
432 err,
433 EthApiError::TransactionConfirmationTimeout { duration, .. }
434 if duration == Duration::from_millis(1)
435 ));
436 assert_eq!(eth_api.pool().len(), 1);
437 }
438
439 #[tokio::test]
440 async fn send_transaction_preserves_provided_gas_limit() {
441 let signers = DevSigner::random_signers(1);
442 let address = signers[0].accounts()[0];
443 let accounts = AddressMap::from_iter([(
444 address,
445 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
446 )]);
447 let eth_api = mock_eth_api(accounts);
448 eth_api.signers().write().extend(signers);
449
450 let provided_gas_limit = 90_000;
451 let tx_req = TransactionRequest {
452 from: Some(address),
453 to: Some(address.into()),
454 gas: Some(provided_gas_limit),
455 gas_price: Some(1_000_000_000),
456 ..Default::default()
457 };
458
459 let hash = eth_api
460 .send_transaction_request(tx_req)
461 .await
462 .expect("send_transaction should succeed");
463 let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
464
465 assert_eq!(pooled.transaction.gas_limit(), provided_gas_limit);
466 }
467
468 #[tokio::test]
469 async fn send_transaction_fills_missing_fee_fields() {
470 let signers = DevSigner::random_signers(1);
471 let address = signers[0].accounts()[0];
472 let accounts = AddressMap::from_iter([(
473 address,
474 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
475 )]);
476 let eth_api = mock_eth_api(accounts);
477 eth_api.signers().write().extend(signers);
478
479 let tx_req = TransactionRequest {
481 from: Some(address),
482 to: Some(address.into()),
483 value: Some(U256::from(1)),
484 ..Default::default()
485 };
486
487 let hash = eth_api
488 .send_transaction_request(tx_req)
489 .await
490 .expect("send_transaction should fill the missing fee fields");
491 let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
492 assert!(pooled.transaction.max_fee_per_gas() > 0);
493 }
494
495 #[tokio::test]
496 async fn send_transaction_rejects_mismatched_chain_id() {
497 let signers = DevSigner::random_signers(1);
498 let address = signers[0].accounts()[0];
499 let accounts = AddressMap::from_iter([(
500 address,
501 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
502 )]);
503 let eth_api = mock_eth_api(accounts);
504 eth_api.signers().write().extend(signers);
505
506 let tx_req = TransactionRequest {
508 from: Some(address),
509 to: Some(address.into()),
510 gas: Some(90_000),
511 gas_price: Some(1_000_000_000),
512 chain_id: Some(999),
513 ..Default::default()
514 };
515
516 let err = eth_api
517 .send_transaction_request(tx_req)
518 .await
519 .expect_err("a chain id that is not the node's must be rejected, not rewritten");
520 assert!(
521 err.to_string().contains("chainId does not match node's"),
522 "unexpected error: {err}"
523 );
524 assert!(eth_api.pool().is_empty(), "no transaction should have been submitted");
525 }
526
527 #[tokio::test]
528 async fn send_transaction_accepts_matching_chain_id() {
529 let signers = DevSigner::random_signers(1);
530 let address = signers[0].accounts()[0];
531 let accounts = AddressMap::from_iter([(
532 address,
533 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
534 )]);
535 let eth_api = mock_eth_api(accounts);
536 eth_api.signers().write().extend(signers);
537
538 let tx_req = TransactionRequest {
539 from: Some(address),
540 to: Some(address.into()),
541 gas: Some(90_000),
542 gas_price: Some(1_000_000_000),
543 chain_id: Some(1),
544 ..Default::default()
545 };
546
547 let hash = eth_api
548 .send_transaction_request(tx_req)
549 .await
550 .expect("a matching chain id must still be accepted");
551 let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
552 assert_eq!(pooled.transaction.chain_id(), Some(1));
553 }
554
555 #[tokio::test]
556 async fn test_fill_transaction_fills_chain_id() {
557 let address = Address::random();
558 let accounts = AddressMap::from_iter([(
559 address,
560 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)), )]);
562
563 let eth_api = mock_eth_api(accounts);
564
565 let tx_req = TransactionRequest {
566 from: Some(address),
567 to: Some(Address::random().into()),
568 gas: Some(21_000),
569 ..Default::default()
570 };
571
572 let filled =
573 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
574
575 assert!(filled.tx.chain_id().is_some());
577 }
578
579 #[tokio::test]
580 async fn test_fill_transaction_fills_nonce() {
581 let address = Address::random();
582 let nonce = 42u64;
583
584 let accounts = AddressMap::from_iter([(
585 address,
586 ExtendedAccount::new(nonce, U256::from(1_000_000_000_000_000_000u64)), )]);
588
589 let eth_api = mock_eth_api(accounts);
590
591 let tx_req = TransactionRequest {
592 from: Some(address),
593 to: Some(Address::random().into()),
594 value: Some(U256::from(1000)),
595 gas: Some(21_000),
596 ..Default::default()
597 };
598
599 let filled =
600 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
601
602 assert_eq!(filled.tx.nonce(), nonce);
603 }
604
605 #[tokio::test]
606 async fn test_fill_transaction_preserves_provided_fields() {
607 let address = Address::random();
608 let provided_nonce = 100u64;
609 let provided_gas_limit = 50_000u64;
610
611 let accounts = AddressMap::from_iter([(
612 address,
613 ExtendedAccount::new(42, U256::from(10_000_000_000_000_000_000u64)),
614 )]);
615
616 let eth_api = mock_eth_api(accounts);
617
618 let tx_req = TransactionRequest {
619 from: Some(address),
620 to: Some(Address::random().into()),
621 value: Some(U256::from(1000)),
622 nonce: Some(provided_nonce),
623 gas: Some(provided_gas_limit),
624 ..Default::default()
625 };
626
627 let filled =
628 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
629
630 assert_eq!(filled.tx.nonce(), provided_nonce);
632 assert_eq!(filled.tx.gas_limit(), provided_gas_limit);
633 }
634
635 #[tokio::test]
636 async fn test_fill_transaction_fills_all_missing_fields() {
637 let address = Address::random();
638
639 let balance = U256::from(100u128) * U256::from(1_000_000_000_000_000_000u128);
640 let accounts = AddressMap::from_iter([(address, ExtendedAccount::new(5, balance))]);
641
642 let eth_api = mock_eth_api(accounts);
643
644 let tx_req = TransactionRequest {
646 from: Some(address),
647 to: Some(Address::random().into()),
648 ..Default::default()
649 };
650
651 let filled =
652 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
653
654 assert!(filled.tx.is_eip1559());
655 }
656
657 #[tokio::test]
658 async fn test_fill_transaction_eip4844_blob_fee() {
659 let address = Address::random();
660 let accounts = AddressMap::from_iter([(
661 address,
662 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
663 )]);
664
665 let eth_api = mock_eth_api(accounts);
666
667 let mut builder = SidecarBuilder::<SimpleCoder>::new();
668 builder.ingest(b"dummy blob");
669
670 let tx_req = TransactionRequest {
672 from: Some(address),
673 to: Some(Address::random().into()),
674 sidecar: Some(BlobTransactionSidecarVariant::from(
675 builder.build::<BlobTransactionSidecar>().unwrap(),
676 )),
677 ..Default::default()
678 };
679
680 let filled =
681 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
682
683 assert!(
685 filled.tx.max_fee_per_blob_gas().is_some(),
686 "max_fee_per_blob_gas should be filled for blob tx"
687 );
688 assert!(
689 filled.tx.blob_versioned_hashes().is_some(),
690 "blob_versioned_hashes should be preserved"
691 );
692 }
693
694 #[tokio::test]
695 async fn test_fill_transaction_eip4844_preserves_blob_fee() {
696 let address = Address::random();
697 let accounts = AddressMap::from_iter([(
698 address,
699 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
700 )]);
701
702 let eth_api = mock_eth_api(accounts);
703
704 let provided_blob_fee = 5000000u128;
705
706 let mut builder = SidecarBuilder::<SimpleCoder>::new();
707 builder.ingest(b"dummy blob");
708
709 let tx_req = TransactionRequest {
711 from: Some(address),
712 to: Some(Address::random().into()),
713 transaction_type: Some(3), sidecar: Some(BlobTransactionSidecarVariant::from(
715 builder.build::<BlobTransactionSidecar>().unwrap(),
716 )),
717 max_fee_per_blob_gas: Some(provided_blob_fee), ..Default::default()
719 };
720
721 let filled =
722 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
723
724 assert_eq!(
726 filled.tx.max_fee_per_blob_gas(),
727 Some(provided_blob_fee),
728 "should preserve provided max_fee_per_blob_gas"
729 );
730 }
731
732 #[tokio::test]
733 async fn test_fill_transaction_non_blob_tx_no_blob_fee() {
734 let address = Address::random();
735 let accounts = AddressMap::from_iter([(
736 address,
737 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
738 )]);
739
740 let eth_api = mock_eth_api(accounts);
741
742 let tx_req = TransactionRequest {
744 from: Some(address),
745 to: Some(Address::random().into()),
746 transaction_type: Some(2), ..Default::default()
748 };
749
750 let filled =
751 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
752
753 assert!(
755 filled.tx.max_fee_per_blob_gas().is_none(),
756 "max_fee_per_blob_gas should not be set for non-blob tx"
757 );
758 }
759}