1use std::time::Duration;
4
5use crate::EthApi;
6use alloy_consensus::BlobTransactionValidationError;
7use alloy_eips::{eip7594::BlobTransactionSidecarVariant, BlockId, Typed2718};
8use alloy_primitives::{hex, B256};
9use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
10use reth_primitives_traits::{AlloyBlockHeader, WithEncoded};
11use reth_rpc_convert::RpcConvert;
12use reth_rpc_eth_api::{
13 helpers::{spec::SignersForRpc, EthTransactions, LoadTransaction},
14 FromEvmError, RpcNodeCore,
15};
16use reth_rpc_eth_types::{error::RpcPoolError, EthApiError};
17use reth_storage_api::BlockReaderIdExt;
18use reth_transaction_pool::{
19 error::Eip4844PoolTransactionError, AddedTransactionOutcome, EthBlobTransactionSidecar,
20 EthPoolTransaction, PoolTransaction, PoolTx,
21};
22
23impl<N, Rpc> EthTransactions for EthApi<N, Rpc>
24where
25 N: RpcNodeCore,
26 EthApiError: FromEvmError<N::Evm>,
27 Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
28{
29 #[inline]
30 fn signers(&self) -> &SignersForRpc<Self::Provider, Self::NetworkTypes> {
31 self.inner.signers()
32 }
33
34 #[inline]
35 fn send_raw_transaction_sync_timeout(&self) -> Duration {
36 self.inner.send_raw_transaction_sync_timeout()
37 }
38
39 async fn send_pool_transaction(
40 &self,
41 origin: reth_transaction_pool::TransactionOrigin,
42 tx: WithEncoded<PoolTx<Self::Pool>>,
43 ) -> Result<B256, Self::Error> {
44 let (tx, mut pool_transaction) = tx.split();
45
46 if self.inner.force_blob_sidecar_upcasting() && pool_transaction.is_eip4844() {
49 let EthBlobTransactionSidecar::Present(sidecar) = pool_transaction.take_blob() else {
50 return Err(EthApiError::PoolError(RpcPoolError::Eip4844(
51 Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
52 )));
53 };
54 let sidecar = sidecar.into_sidecar();
55
56 let sidecar = match sidecar {
57 BlobTransactionSidecarVariant::Eip4844(sidecar) => {
58 let latest = self
59 .provider()
60 .latest_header()?
61 .ok_or(EthApiError::HeaderNotFound(BlockId::latest()))?;
62 if self
64 .provider()
65 .chain_spec()
66 .is_osaka_active_at_timestamp(latest.timestamp().saturating_add(12))
67 {
68 BlobTransactionSidecarVariant::Eip7594(
69 self.blob_sidecar_converter().convert(sidecar).await.ok_or_else(
70 || {
71 RpcPoolError::Eip4844(
72 Eip4844PoolTransactionError::InvalidEip4844Blob(
73 BlobTransactionValidationError::InvalidProof,
74 ),
75 )
76 },
77 )?,
78 )
79 } else {
80 BlobTransactionSidecarVariant::Eip4844(sidecar)
81 }
82 }
83 sidecar => sidecar,
84 };
85
86 pool_transaction =
87 EthPoolTransaction::try_from_eip4844(pool_transaction.into_consensus(), sidecar)
88 .ok_or_else(|| {
89 RpcPoolError::Eip4844(
90 Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
91 )
92 })?;
93 }
94
95 if let Some(client) = self.raw_tx_forwarder() {
97 tracing::debug!(target: "rpc::eth", hash = %pool_transaction.hash(), "forwarding raw transaction to forwarder");
98 let rlp_hex = hex::encode_prefixed(&tx);
99
100 self.broadcast_raw_transaction(tx);
102
103 let hash =
104 client.request("eth_sendRawTransaction", (rlp_hex,)).await.inspect_err(|err| {
105 tracing::debug!(target: "rpc::eth", %err, hash=% *pool_transaction.hash(), "failed to forward raw transaction");
106 }).map_err(EthApiError::other)?;
107
108 let _ = self.inner.add_pool_transaction(origin, pool_transaction).await;
110
111 return Ok(hash);
112 }
113
114 self.broadcast_raw_transaction(tx);
116
117 let AddedTransactionOutcome { hash, .. } =
118 self.inner.add_pool_transaction(origin, pool_transaction).await?;
119
120 Ok(hash)
121 }
122}
123
124impl<N, Rpc> LoadTransaction for EthApi<N, Rpc>
125where
126 N: RpcNodeCore,
127 EthApiError: FromEvmError<N::Evm>,
128 Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
129{
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::eth::helpers::{signer::DevSigner, types::EthRpcConverter};
136 use alloy_consensus::{
137 BlobTransactionSidecar, Block, Header, SidecarBuilder, SimpleCoder, Transaction,
138 };
139 use alloy_primitives::{map::AddressMap, Address, Bytes, U256};
140 use alloy_rpc_types_eth::request::TransactionRequest;
141 use reth_chainspec::{ChainSpec, ChainSpecBuilder};
142 use reth_evm_ethereum::EthEvmConfig;
143 use reth_network_api::noop::NoopNetwork;
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},
151 TransactionOrigin, TransactionPool,
152 };
153
154 fn mock_eth_api(
155 accounts: AddressMap<ExtendedAccount>,
156 ) -> EthApi<
157 RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
158 EthRpcConverter<ChainSpec>,
159 > {
160 mock_eth_api_with_sync_timeout(accounts, Duration::from_secs(30))
161 }
162
163 fn mock_eth_api_with_sync_timeout(
164 accounts: AddressMap<ExtendedAccount>,
165 send_raw_transaction_sync_timeout: Duration,
166 ) -> EthApi<
167 RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
168 EthRpcConverter<ChainSpec>,
169 > {
170 let mock_provider = MockEthProvider::default()
171 .with_chain_spec(ChainSpecBuilder::mainnet().cancun_activated().build());
172 mock_provider.extend_accounts(accounts);
173
174 let evm_config = EthEvmConfig::new(mock_provider.chain_spec());
175 let pool = testing_pool();
176
177 let genesis_header = Header {
178 number: 0,
179 gas_limit: 30_000_000,
180 timestamp: 1,
181 excess_blob_gas: Some(0),
182 base_fee_per_gas: Some(1000000000),
183 blob_gas_used: Some(0),
184 ..Default::default()
185 };
186
187 let genesis_hash = B256::ZERO;
188 mock_provider.add_block(genesis_hash, Block::new(genesis_header, Default::default()));
189
190 EthApi::builder(mock_provider, pool, NoopNetwork::default(), evm_config)
191 .send_raw_transaction_sync_timeout(send_raw_transaction_sync_timeout)
192 .build()
193 }
194
195 fn raw_transfer_tx() -> Bytes {
196 Bytes::from(hex!(
198 "02f871018303579880850555633d1b82520894eee27662c2b8eba3cd936a23f039f3189633e4c887ad591c62bdaeb180c080a07ea72c68abfb8fca1bd964f0f99132ed9280261bdca3e549546c0205e800f7d0a05b4ef3039e9c9b9babc179a1878fb825b5aaf5aed2fa8744854150157b08d6f3"
199 ))
200 }
201
202 #[tokio::test]
203 async fn send_raw_transaction() {
204 let eth_api = mock_eth_api(Default::default());
205 let pool = eth_api.pool();
206
207 let tx_1 = raw_transfer_tx();
208
209 let tx_1_result = eth_api.send_raw_transaction(tx_1).await.unwrap();
210 assert_eq!(
211 pool.len(),
212 1,
213 "expect 1 transaction in the pool, but pool size is {}",
214 pool.len()
215 );
216
217 let tx_2 = Bytes::from(hex!(
219 "02f9043c018202b7843b9aca00850c807d37a08304d21d94ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b881bc16d674ec80000b903c43593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063e2d99f00000000000000000000000000000000000000000000000000000000000000030b000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000065717fe021ea67801d1088cc80099004b05b64600000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95fd5965fd1f1a6f0d4600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000428dca9537116148616a5a3e44035af17238fe9dc080a0c6ec1e41f5c0b9511c49b171ad4e04c6bb419c74d99fe9891d74126ec6e4e879a032069a753d7a2cfa158df95421724d24c0e9501593c09905abf3699b4a4405ce"
220 ));
221
222 let tx_2_result = eth_api.send_raw_transaction(tx_2).await.unwrap();
223 assert_eq!(
224 pool.len(),
225 2,
226 "expect 2 transactions in the pool, but pool size is {}",
227 pool.len()
228 );
229
230 assert!(pool.get(&tx_1_result).is_some(), "tx1 not found in the pool");
231 assert!(pool.get(&tx_2_result).is_some(), "tx2 not found in the pool");
232 assert_eq!(pool.get(&tx_1_result).unwrap().origin, TransactionOrigin::Local);
233 assert_eq!(pool.get(&tx_2_result).unwrap().origin, TransactionOrigin::Local);
234 }
235
236 #[tokio::test]
237 async fn send_raw_transaction_sync_uses_request_timeout() {
238 let eth_api = mock_eth_api(Default::default());
239
240 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(1)).await.unwrap_err();
241
242 assert!(matches!(
243 err,
244 EthApiError::TransactionConfirmationTimeout { duration, .. }
245 if duration == Duration::from_millis(1)
246 ));
247 assert_eq!(eth_api.pool().len(), 1);
248 }
249
250 #[tokio::test]
251 async fn send_raw_transaction_sync_uses_configured_timeout_when_omitted() {
252 let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
253
254 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), None).await.unwrap_err();
255
256 assert!(matches!(
257 err,
258 EthApiError::TransactionConfirmationTimeout { duration, .. }
259 if duration == Duration::from_millis(1)
260 ));
261 assert_eq!(eth_api.pool().len(), 1);
262 }
263
264 #[tokio::test]
265 async fn send_raw_transaction_sync_uses_configured_timeout_when_zero() {
266 let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
267
268 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(0)).await.unwrap_err();
269
270 assert!(matches!(
271 err,
272 EthApiError::TransactionConfirmationTimeout { duration, .. }
273 if duration == Duration::from_millis(1)
274 ));
275 assert_eq!(eth_api.pool().len(), 1);
276 }
277
278 #[tokio::test]
279 async fn send_raw_transaction_sync_caps_request_timeout() {
280 let eth_api = mock_eth_api_with_sync_timeout(Default::default(), Duration::from_millis(1));
281
282 let err = eth_api.send_raw_transaction_sync(raw_transfer_tx(), Some(50)).await.unwrap_err();
283
284 assert!(matches!(
285 err,
286 EthApiError::TransactionConfirmationTimeout { duration, .. }
287 if duration == Duration::from_millis(1)
288 ));
289 assert_eq!(eth_api.pool().len(), 1);
290 }
291
292 #[tokio::test]
293 async fn send_transaction_preserves_provided_gas_limit() {
294 let signers = DevSigner::random_signers(1);
295 let address = signers[0].accounts()[0];
296 let accounts = AddressMap::from_iter([(
297 address,
298 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
299 )]);
300 let eth_api = mock_eth_api(accounts);
301 eth_api.signers().write().extend(signers);
302
303 let provided_gas_limit = 90_000;
304 let tx_req = TransactionRequest {
305 from: Some(address),
306 to: Some(address.into()),
307 gas: Some(provided_gas_limit),
308 gas_price: Some(1_000_000_000),
309 ..Default::default()
310 };
311
312 let hash = eth_api
313 .send_transaction_request(tx_req)
314 .await
315 .expect("send_transaction should succeed");
316 let pooled = eth_api.pool().get(&hash).expect("transaction should be in the pool");
317
318 assert_eq!(pooled.transaction.gas_limit(), provided_gas_limit);
319 }
320
321 #[tokio::test]
322 async fn test_fill_transaction_fills_chain_id() {
323 let address = Address::random();
324 let accounts = AddressMap::from_iter([(
325 address,
326 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)), )]);
328
329 let eth_api = mock_eth_api(accounts);
330
331 let tx_req = TransactionRequest {
332 from: Some(address),
333 to: Some(Address::random().into()),
334 gas: Some(21_000),
335 ..Default::default()
336 };
337
338 let filled =
339 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
340
341 assert!(filled.tx.chain_id().is_some());
343 }
344
345 #[tokio::test]
346 async fn test_fill_transaction_fills_nonce() {
347 let address = Address::random();
348 let nonce = 42u64;
349
350 let accounts = AddressMap::from_iter([(
351 address,
352 ExtendedAccount::new(nonce, U256::from(1_000_000_000_000_000_000u64)), )]);
354
355 let eth_api = mock_eth_api(accounts);
356
357 let tx_req = TransactionRequest {
358 from: Some(address),
359 to: Some(Address::random().into()),
360 value: Some(U256::from(1000)),
361 gas: Some(21_000),
362 ..Default::default()
363 };
364
365 let filled =
366 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
367
368 assert_eq!(filled.tx.nonce(), nonce);
369 }
370
371 #[tokio::test]
372 async fn test_fill_transaction_preserves_provided_fields() {
373 let address = Address::random();
374 let provided_nonce = 100u64;
375 let provided_gas_limit = 50_000u64;
376
377 let accounts = AddressMap::from_iter([(
378 address,
379 ExtendedAccount::new(42, U256::from(10_000_000_000_000_000_000u64)),
380 )]);
381
382 let eth_api = mock_eth_api(accounts);
383
384 let tx_req = TransactionRequest {
385 from: Some(address),
386 to: Some(Address::random().into()),
387 value: Some(U256::from(1000)),
388 nonce: Some(provided_nonce),
389 gas: Some(provided_gas_limit),
390 ..Default::default()
391 };
392
393 let filled =
394 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
395
396 assert_eq!(filled.tx.nonce(), provided_nonce);
398 assert_eq!(filled.tx.gas_limit(), provided_gas_limit);
399 }
400
401 #[tokio::test]
402 async fn test_fill_transaction_fills_all_missing_fields() {
403 let address = Address::random();
404
405 let balance = U256::from(100u128) * U256::from(1_000_000_000_000_000_000u128);
406 let accounts = AddressMap::from_iter([(address, ExtendedAccount::new(5, balance))]);
407
408 let eth_api = mock_eth_api(accounts);
409
410 let tx_req = TransactionRequest {
412 from: Some(address),
413 to: Some(Address::random().into()),
414 ..Default::default()
415 };
416
417 let filled =
418 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
419
420 assert!(filled.tx.is_eip1559());
421 }
422
423 #[tokio::test]
424 async fn test_fill_transaction_eip4844_blob_fee() {
425 let address = Address::random();
426 let accounts = AddressMap::from_iter([(
427 address,
428 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
429 )]);
430
431 let eth_api = mock_eth_api(accounts);
432
433 let mut builder = SidecarBuilder::<SimpleCoder>::new();
434 builder.ingest(b"dummy blob");
435
436 let tx_req = TransactionRequest {
438 from: Some(address),
439 to: Some(Address::random().into()),
440 sidecar: Some(BlobTransactionSidecarVariant::from(
441 builder.build::<BlobTransactionSidecar>().unwrap(),
442 )),
443 ..Default::default()
444 };
445
446 let filled =
447 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
448
449 assert!(
451 filled.tx.max_fee_per_blob_gas().is_some(),
452 "max_fee_per_blob_gas should be filled for blob tx"
453 );
454 assert!(
455 filled.tx.blob_versioned_hashes().is_some(),
456 "blob_versioned_hashes should be preserved"
457 );
458 }
459
460 #[tokio::test]
461 async fn test_fill_transaction_eip4844_preserves_blob_fee() {
462 let address = Address::random();
463 let accounts = AddressMap::from_iter([(
464 address,
465 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
466 )]);
467
468 let eth_api = mock_eth_api(accounts);
469
470 let provided_blob_fee = 5000000u128;
471
472 let mut builder = SidecarBuilder::<SimpleCoder>::new();
473 builder.ingest(b"dummy blob");
474
475 let tx_req = TransactionRequest {
477 from: Some(address),
478 to: Some(Address::random().into()),
479 transaction_type: Some(3), sidecar: Some(BlobTransactionSidecarVariant::from(
481 builder.build::<BlobTransactionSidecar>().unwrap(),
482 )),
483 max_fee_per_blob_gas: Some(provided_blob_fee), ..Default::default()
485 };
486
487 let filled =
488 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
489
490 assert_eq!(
492 filled.tx.max_fee_per_blob_gas(),
493 Some(provided_blob_fee),
494 "should preserve provided max_fee_per_blob_gas"
495 );
496 }
497
498 #[tokio::test]
499 async fn test_fill_transaction_non_blob_tx_no_blob_fee() {
500 let address = Address::random();
501 let accounts = AddressMap::from_iter([(
502 address,
503 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
504 )]);
505
506 let eth_api = mock_eth_api(accounts);
507
508 let tx_req = TransactionRequest {
510 from: Some(address),
511 to: Some(Address::random().into()),
512 transaction_type: Some(2), ..Default::default()
514 };
515
516 let filled =
517 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
518
519 assert!(
521 filled.tx.max_fee_per_blob_gas().is_none(),
522 "max_fee_per_blob_gas should not be set for non-blob tx"
523 );
524 }
525}