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::{BlobTransactionSidecar, Block, Header, Transaction};
132 use alloy_eips::{
133 eip2718::Encodable2718,
134 eip4844::{Blob, Bytes48},
135 };
136 use alloy_primitives::{bytes, map::AddressMap, Address, Bytes, Signature, U256};
137 use alloy_rpc_types_eth::request::TransactionRequest;
138 use reth_chainspec::{ChainSpec, ChainSpecBuilder};
139 use reth_ethereum_primitives::TransactionSigned;
140 use reth_evm::SenderRecoveryCache;
141 use reth_evm_ethereum::EthEvmConfig;
142 use reth_network_api::noop::NoopNetwork;
143 use reth_primitives_traits::SignedTransaction;
144 use reth_provider::{
145 test_utils::{ExtendedAccount, MockEthProvider},
146 ChainSpecProvider,
147 };
148 use reth_rpc_eth_api::node::RpcNodeCoreAdapter;
149 use reth_transaction_pool::{
150 test_utils::{testing_pool, TestPool, TransactionGenerator},
151 PoolPooledTx, TransactionOrigin, TransactionPool,
152 };
153
154 fn zero_blob_sidecar() -> BlobTransactionSidecar {
157 let mut point_at_infinity = [0; 48];
158 point_at_infinity[0] = 0xc0;
159 BlobTransactionSidecar::new(
160 vec![Blob::default()],
161 vec![Bytes48::from(point_at_infinity)],
162 vec![Bytes48::from(point_at_infinity)],
163 )
164 }
165 fn mock_eth_api(
166 accounts: AddressMap<ExtendedAccount>,
167 ) -> EthApi<
168 RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
169 EthRpcConverter<ChainSpec>,
170 > {
171 mock_eth_api_with_sync_timeout(accounts, Duration::from_secs(30))
172 }
173
174 fn mock_eth_api_with_sync_timeout(
175 accounts: AddressMap<ExtendedAccount>,
176 send_raw_transaction_sync_timeout: Duration,
177 ) -> EthApi<
178 RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
179 EthRpcConverter<ChainSpec>,
180 > {
181 mock_eth_api_builder(accounts)
182 .send_raw_transaction_sync_timeout(send_raw_transaction_sync_timeout)
183 .build()
184 }
185
186 fn mock_eth_api_builder(
187 accounts: AddressMap<ExtendedAccount>,
188 ) -> crate::EthApiBuilder<
189 RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
190 EthRpcConverter<ChainSpec>,
191 > {
192 let mock_provider = MockEthProvider::default()
193 .with_chain_spec(ChainSpecBuilder::mainnet().cancun_activated().build());
194 mock_provider.extend_accounts(accounts);
195
196 let evm_config = EthEvmConfig::new(mock_provider.chain_spec());
197 let pool = testing_pool();
198
199 let genesis_header = Header {
200 number: 0,
201 gas_limit: 30_000_000,
202 timestamp: 1,
203 excess_blob_gas: Some(0),
204 base_fee_per_gas: Some(1000000000),
205 blob_gas_used: Some(0),
206 ..Default::default()
207 };
208
209 let genesis_hash = B256::ZERO;
210 mock_provider.add_block(genesis_hash, Block::new(genesis_header, Default::default()));
211
212 EthApi::builder(mock_provider, pool, NoopNetwork::default(), evm_config)
213 }
214
215 fn raw_transfer_tx() -> Bytes {
216 bytes!("02f871018303579880850555633d1b82520894eee27662c2b8eba3cd936a23f039f3189633e4c887ad591c62bdaeb180c080a07ea72c68abfb8fca1bd964f0f99132ed9280261bdca3e549546c0205e800f7d0a05b4ef3039e9c9b9babc179a1878fb825b5aaf5aed2fa8744854150157b08d6f3")
218 }
219
220 #[tokio::test]
221 async fn raw_transaction_recovery_shares_sender_cache() {
222 let cache = SenderRecoveryCache::new(16);
223 let eth_api = mock_eth_api_builder(Default::default())
224 .sender_recovery_cache(Some(cache.clone()))
225 .build();
226 let mut generator = TransactionGenerator::new(rand::rng());
227
228 for (transaction, warm_cache) in [
229 (generator.transaction().into_legacy(), false),
230 (generator.transaction().nonce(1).into_eip1559(), true),
231 ] {
232 let raw = Bytes::from(transaction.encoded_2718());
233 let hash = *transaction.tx_hash();
234 let sender = transaction.try_recover().unwrap();
235 assert_eq!(cache.get(&hash), None);
236 if warm_cache {
237 let recovered =
238 eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap();
239 assert_eq!(recovered.signer(), sender);
240 assert_eq!(cache.get(&hash), Some(sender));
241 }
242
243 assert_eq!(eth_api.send_raw_transaction(raw.clone()).await.unwrap(), hash);
244 assert_eq!(cache.get(&hash), Some(sender));
245 let recovered =
246 eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap();
247 assert_eq!(recovered.signer(), sender);
248 assert_eq!(eth_api.recover_raw_pool_transaction(&raw).unwrap().sender(), sender);
249 }
250 }
251
252 #[tokio::test]
253 async fn raw_transaction_recovery_uses_pool_overrides() {
254 let raw = raw_transfer_tx();
255 for enabled in [false, true] {
256 let cache = SenderRecoveryCache::new(16);
257 let eth_api = mock_eth_api_builder(Default::default())
258 .sender_recovery_cache(enabled.then(|| cache.clone()))
259 .build();
260
261 for _ in 0..2 {
263 let transaction = eth_api.recover_raw_pool_transaction(&raw).unwrap();
264 assert_eq!(transaction.encoded_length(), raw.len());
265 assert_eq!(cache.get(transaction.hash()), enabled.then_some(transaction.sender()));
266 }
267 }
268 }
269
270 #[tokio::test]
271 async fn raw_transaction_recovery_preserves_errors() {
272 let invalid = TransactionSigned::new_unhashed(
273 alloy_consensus::TxLegacy::default().into(),
274 Signature::new(U256::ZERO, U256::ZERO, false),
275 );
276 let mut trailing = raw_transfer_tx().to_vec();
277 trailing.push(0);
278
279 for enabled in [false, true] {
280 let cache = SenderRecoveryCache::new(16);
281 let eth_api = mock_eth_api_builder(Default::default())
282 .sender_recovery_cache(enabled.then(|| cache.clone()))
283 .build();
284 let valid = eth_api.recover_raw_pool_transaction(&raw_transfer_tx()).unwrap();
285 assert_eq!(cache.get(valid.hash()), enabled.then_some(valid.sender()));
286 for (raw, expected) in [
287 (vec![], EthApiError::EmptyRawTransactionData),
288 (vec![2], EthApiError::FailedToDecodeSignedTransaction),
289 (trailing.clone(), EthApiError::FailedToDecodeSignedTransaction),
290 (invalid.encoded_2718(), EthApiError::InvalidTransactionSignature),
291 ] {
292 let err = eth_api.send_raw_transaction(raw.clone().into()).await.unwrap_err();
293 assert_eq!(err.to_string(), expected.to_string());
294 let err =
295 eth_api.recover_raw_transaction::<PoolPooledTx<TestPool>>(&raw).unwrap_err();
296 assert_eq!(err.to_string(), expected.to_string());
297 }
298 assert!(eth_api.pool().is_empty());
299 assert_eq!(cache.get(invalid.tx_hash()), None);
300 }
301 }
302
303 #[tokio::test]
304 async fn cached_raw_transaction_preserves_blob_sidecar() {
305 let cache = SenderRecoveryCache::new(16);
306 let provider = MockEthProvider::default();
307 let evm_config = EthEvmConfig::new(provider.chain_spec());
308 let eth_api = EthApi::builder(
309 provider,
310 reth_transaction_pool::noop::NoopTransactionPool::default(),
311 NoopNetwork::default(),
312 evm_config,
313 )
314 .sender_recovery_cache(Some(cache.clone()))
315 .build();
316 let sidecar = BlobTransactionSidecarVariant::from(zero_blob_sidecar());
317 let transaction = TransactionSigned::new_unhashed(
318 alloy_consensus::TxEip4844 {
319 blob_versioned_hashes: sidecar.versioned_hashes().collect(),
320 ..Default::default()
321 }
322 .into(),
323 Signature::test_signature(),
324 );
325 let hash = *transaction.tx_hash();
326 let sender = transaction.try_recover().unwrap();
327 let raw = transaction.try_into_pooled_eip4844(sidecar.clone()).unwrap().encoded_2718();
328
329 for _ in 0..2 {
330 let mut recovered = eth_api.recover_raw_pool_transaction(&raw).unwrap();
331 assert_eq!(recovered.sender(), sender);
332 assert_eq!(cache.get(&hash), Some(sender));
333 let EthBlobTransactionSidecar::Present(actual) = recovered.take_blob() else {
334 panic!("missing blob sidecar")
335 };
336 assert_eq!(actual.into_sidecar(), sidecar);
337 }
338 }
339
340 #[tokio::test]
341 async fn send_raw_transaction() {
342 let eth_api = mock_eth_api(Default::default());
343 let pool = eth_api.pool();
344
345 let tx_1 = raw_transfer_tx();
346
347 let tx_1_result = eth_api.send_raw_transaction(tx_1).await.unwrap();
348 assert_eq!(
349 pool.len(),
350 1,
351 "expect 1 transaction in the pool, but pool size is {}",
352 pool.len()
353 );
354
355 let tx_2 = bytes!("02f9043c018202b7843b9aca00850c807d37a08304d21d94ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b881bc16d674ec80000b903c43593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063e2d99f00000000000000000000000000000000000000000000000000000000000000030b000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000065717fe021ea67801d1088cc80099004b05b64600000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95fd5965fd1f1a6f0d4600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000428dca9537116148616a5a3e44035af17238fe9dc080a0c6ec1e41f5c0b9511c49b171ad4e04c6bb419c74d99fe9891d74126ec6e4e879a032069a753d7a2cfa158df95421724d24c0e9501593c09905abf3699b4a4405ce");
357
358 let tx_2_result = eth_api.send_raw_transaction(tx_2).await.unwrap();
359 assert_eq!(
360 pool.len(),
361 2,
362 "expect 2 transactions in the pool, but pool size is {}",
363 pool.len()
364 );
365
366 assert!(pool.get(&tx_1_result).is_some(), "tx1 not found in the pool");
367 assert!(pool.get(&tx_2_result).is_some(), "tx2 not found in the pool");
368 assert_eq!(pool.get(&tx_1_result).unwrap().origin, TransactionOrigin::Local);
369 assert_eq!(pool.get(&tx_2_result).unwrap().origin, TransactionOrigin::Local);
370 }
371
372 #[tokio::test]
373 async fn send_raw_transaction_sync_uses_request_timeout() {
374 let eth_api = mock_eth_api(Default::default());
375
376 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(1)).await.unwrap_err();
377
378 assert!(matches!(
379 err,
380 EthApiError::TransactionConfirmationTimeout { duration, .. }
381 if duration == Duration::from_millis(1)
382 ));
383 assert_eq!(eth_api.pool().len(), 1);
384 }
385
386 #[tokio::test]
387 async fn send_raw_transaction_sync_uses_configured_timeout_when_omitted() {
388 let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
389
390 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), None).await.unwrap_err();
391
392 assert!(matches!(
393 err,
394 EthApiError::TransactionConfirmationTimeout { duration, .. }
395 if duration == Duration::from_millis(1)
396 ));
397 assert_eq!(eth_api.pool().len(), 1);
398 }
399
400 #[tokio::test]
401 async fn send_raw_transaction_sync_uses_configured_timeout_when_zero() {
402 let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
403
404 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(0)).await.unwrap_err();
405
406 assert!(matches!(
407 err,
408 EthApiError::TransactionConfirmationTimeout { duration, .. }
409 if duration == Duration::from_millis(1)
410 ));
411 assert_eq!(eth_api.pool().len(), 1);
412 }
413
414 #[tokio::test]
415 async fn send_raw_transaction_sync_caps_request_timeout() {
416 let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
417
418 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(50)).await.unwrap_err();
419
420 assert!(matches!(
421 err,
422 EthApiError::TransactionConfirmationTimeout { duration, .. }
423 if duration == Duration::from_millis(1)
424 ));
425 assert_eq!(eth_api.pool().len(), 1);
426 }
427
428 #[tokio::test]
429 async fn send_transaction_preserves_provided_gas_limit() {
430 let signers = DevSigner::random_signers(1);
431 let address = signers[0].accounts()[0];
432 let accounts = AddressMap::from_iter([(
433 address,
434 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
435 )]);
436 let eth_api = mock_eth_api(accounts);
437 eth_api.signers().write().extend(signers);
438
439 let provided_gas_limit = 90_000;
440 let tx_req = TransactionRequest {
441 from: Some(address),
442 to: Some(address.into()),
443 gas: Some(provided_gas_limit),
444 gas_price: Some(1_000_000_000),
445 ..Default::default()
446 };
447
448 let hash = eth_api
449 .send_transaction_request(tx_req)
450 .await
451 .expect("send_transaction should succeed");
452 let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
453
454 assert_eq!(pooled.transaction.gas_limit(), provided_gas_limit);
455 }
456
457 #[tokio::test]
458 async fn send_transaction_fills_missing_fee_fields() {
459 let signers = DevSigner::random_signers(1);
460 let address = signers[0].accounts()[0];
461 let accounts = AddressMap::from_iter([(
462 address,
463 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
464 )]);
465 let eth_api = mock_eth_api(accounts);
466 eth_api.signers().write().extend(signers);
467
468 let tx_req = TransactionRequest {
470 from: Some(address),
471 to: Some(address.into()),
472 value: Some(U256::ONE),
473 ..Default::default()
474 };
475
476 let hash = eth_api
477 .send_transaction_request(tx_req)
478 .await
479 .expect("send_transaction should fill the missing fee fields");
480 let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
481 assert!(pooled.transaction.max_fee_per_gas() > 0);
482 }
483
484 #[tokio::test]
485 async fn send_transaction_rejects_mismatched_chain_id() {
486 let signers = DevSigner::random_signers(1);
487 let address = signers[0].accounts()[0];
488 let accounts = AddressMap::from_iter([(
489 address,
490 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
491 )]);
492 let eth_api = mock_eth_api(accounts);
493 eth_api.signers().write().extend(signers);
494
495 let tx_req = TransactionRequest {
497 from: Some(address),
498 to: Some(address.into()),
499 gas: Some(90_000),
500 gas_price: Some(1_000_000_000),
501 chain_id: Some(999),
502 ..Default::default()
503 };
504
505 let err = eth_api
506 .send_transaction_request(tx_req)
507 .await
508 .expect_err("a chain id that is not the node's must be rejected, not rewritten");
509 assert!(
510 err.to_string().contains("chainId does not match node's"),
511 "unexpected error: {err}"
512 );
513 assert!(eth_api.pool().is_empty(), "no transaction should have been submitted");
514 }
515
516 #[tokio::test]
517 async fn send_transaction_accepts_matching_chain_id() {
518 let signers = DevSigner::random_signers(1);
519 let address = signers[0].accounts()[0];
520 let accounts = AddressMap::from_iter([(
521 address,
522 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
523 )]);
524 let eth_api = mock_eth_api(accounts);
525 eth_api.signers().write().extend(signers);
526
527 let tx_req = TransactionRequest {
528 from: Some(address),
529 to: Some(address.into()),
530 gas: Some(90_000),
531 gas_price: Some(1_000_000_000),
532 chain_id: Some(1),
533 ..Default::default()
534 };
535
536 let hash = eth_api
537 .send_transaction_request(tx_req)
538 .await
539 .expect("a matching chain id must still be accepted");
540 let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
541 assert_eq!(pooled.transaction.chain_id(), Some(1));
542 }
543
544 #[tokio::test]
545 async fn test_fill_transaction_fills_chain_id() {
546 let address = Address::random();
547 let accounts = AddressMap::from_iter([(
548 address,
549 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)), )]);
551
552 let eth_api = mock_eth_api(accounts);
553
554 let tx_req = TransactionRequest {
555 from: Some(address),
556 to: Some(Address::random().into()),
557 gas: Some(21_000),
558 ..Default::default()
559 };
560
561 let filled =
562 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
563
564 assert!(filled.tx.chain_id().is_some());
566 }
567
568 #[tokio::test]
569 async fn test_fill_transaction_fills_nonce() {
570 let address = Address::random();
571 let nonce = 42u64;
572
573 let accounts = AddressMap::from_iter([(
574 address,
575 ExtendedAccount::new(nonce, U256::from(1_000_000_000_000_000_000u64)), )]);
577
578 let eth_api = mock_eth_api(accounts);
579
580 let tx_req = TransactionRequest {
581 from: Some(address),
582 to: Some(Address::random().into()),
583 value: Some(U256::from(1000)),
584 gas: Some(21_000),
585 ..Default::default()
586 };
587
588 let filled =
589 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
590
591 assert_eq!(filled.tx.nonce(), nonce);
592 }
593
594 #[tokio::test]
595 async fn test_fill_transaction_preserves_provided_fields() {
596 let address = Address::random();
597 let provided_nonce = 100u64;
598 let provided_gas_limit = 50_000u64;
599
600 let accounts = AddressMap::from_iter([(
601 address,
602 ExtendedAccount::new(42, U256::from(10_000_000_000_000_000_000u64)),
603 )]);
604
605 let eth_api = mock_eth_api(accounts);
606
607 let tx_req = TransactionRequest {
608 from: Some(address),
609 to: Some(Address::random().into()),
610 value: Some(U256::from(1000)),
611 nonce: Some(provided_nonce),
612 gas: Some(provided_gas_limit),
613 ..Default::default()
614 };
615
616 let filled =
617 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
618
619 assert_eq!(filled.tx.nonce(), provided_nonce);
621 assert_eq!(filled.tx.gas_limit(), provided_gas_limit);
622 }
623
624 #[tokio::test]
625 async fn test_fill_transaction_fills_all_missing_fields() {
626 let address = Address::random();
627
628 let balance = U256::from(100u128) * U256::from(1_000_000_000_000_000_000u128);
629 let accounts = AddressMap::from_iter([(address, ExtendedAccount::new(5, balance))]);
630
631 let eth_api = mock_eth_api(accounts);
632
633 let tx_req = TransactionRequest {
635 from: Some(address),
636 to: Some(Address::random().into()),
637 ..Default::default()
638 };
639
640 let filled =
641 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
642
643 assert!(filled.tx.is_eip1559());
644 }
645
646 #[tokio::test]
647 async fn test_fill_transaction_eip4844_blob_fee() {
648 let address = Address::random();
649 let accounts = AddressMap::from_iter([(
650 address,
651 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
652 )]);
653
654 let eth_api = mock_eth_api(accounts);
655
656 let tx_req = TransactionRequest {
658 from: Some(address),
659 to: Some(Address::random().into()),
660 sidecar: Some(BlobTransactionSidecarVariant::from(zero_blob_sidecar())),
661 ..Default::default()
662 };
663
664 let filled =
665 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
666
667 assert!(
669 filled.tx.max_fee_per_blob_gas().is_some(),
670 "max_fee_per_blob_gas should be filled for blob tx"
671 );
672 assert!(
673 filled.tx.blob_versioned_hashes().is_some(),
674 "blob_versioned_hashes should be preserved"
675 );
676 }
677
678 #[tokio::test]
679 async fn test_fill_transaction_eip4844_preserves_blob_fee() {
680 let address = Address::random();
681 let accounts = AddressMap::from_iter([(
682 address,
683 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
684 )]);
685
686 let eth_api = mock_eth_api(accounts);
687
688 let provided_blob_fee = 5000000u128;
689
690 let tx_req = TransactionRequest {
692 from: Some(address),
693 to: Some(Address::random().into()),
694 transaction_type: Some(3), sidecar: Some(BlobTransactionSidecarVariant::from(zero_blob_sidecar())),
696 max_fee_per_blob_gas: Some(provided_blob_fee), ..Default::default()
698 };
699
700 let filled =
701 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
702
703 assert_eq!(
705 filled.tx.max_fee_per_blob_gas(),
706 Some(provided_blob_fee),
707 "should preserve provided max_fee_per_blob_gas"
708 );
709 }
710
711 #[tokio::test]
712 async fn test_fill_transaction_non_blob_tx_no_blob_fee() {
713 let address = Address::random();
714 let accounts = AddressMap::from_iter([(
715 address,
716 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
717 )]);
718
719 let eth_api = mock_eth_api(accounts);
720
721 let tx_req = TransactionRequest {
723 from: Some(address),
724 to: Some(Address::random().into()),
725 transaction_type: Some(2), ..Default::default()
727 };
728
729 let filled =
730 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
731
732 assert!(
734 filled.tx.max_fee_per_blob_gas().is_none(),
735 "max_fee_per_blob_gas should not be set for non-blob tx"
736 );
737 }
738}