1use std::time::Duration;
4
5use crate::EthApi;
6use alloy_consensus::BlobTransactionValidationError;
7use alloy_eips::{eip7594::BlobTransactionSidecarVariant, BlockId, Typed2718};
8use alloy_primitives::{hex, Bytes, B256};
9use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
10use reth_primitives_traits::AlloyBlockHeader;
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, utils::recover_raw_transaction, EthApiError};
17use reth_storage_api::BlockReaderIdExt;
18use reth_transaction_pool::{
19 error::Eip4844PoolTransactionError, AddedTransactionOutcome, EthBlobTransactionSidecar,
20 EthPoolTransaction, PoolTransaction, TransactionPool,
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_raw_transaction(&self, tx: Bytes) -> Result<B256, Self::Error> {
43 let recovered = recover_raw_transaction(&tx)?;
44
45 let mut pool_transaction =
46 <Self::Pool as TransactionPool>::Transaction::from_pooled(recovered);
47
48 if pool_transaction.is_eip4844() {
51 let EthBlobTransactionSidecar::Present(sidecar) = pool_transaction.take_blob() else {
52 return Err(EthApiError::PoolError(RpcPoolError::Eip4844(
53 Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
54 )));
55 };
56
57 let sidecar = match sidecar {
58 BlobTransactionSidecarVariant::Eip4844(sidecar) => {
59 let latest = self
60 .provider()
61 .latest_header()?
62 .ok_or(EthApiError::HeaderNotFound(BlockId::latest()))?;
63 if self
65 .provider()
66 .chain_spec()
67 .is_osaka_active_at_timestamp(latest.timestamp().saturating_add(12))
68 {
69 BlobTransactionSidecarVariant::Eip7594(
70 self.blob_sidecar_converter().convert(sidecar).await.ok_or_else(
71 || {
72 RpcPoolError::Eip4844(
73 Eip4844PoolTransactionError::InvalidEip4844Blob(
74 BlobTransactionValidationError::InvalidProof,
75 ),
76 )
77 },
78 )?,
79 )
80 } else {
81 BlobTransactionSidecarVariant::Eip4844(sidecar)
82 }
83 }
84 sidecar => sidecar,
85 };
86
87 pool_transaction =
88 EthPoolTransaction::try_from_eip4844(pool_transaction.into_consensus(), sidecar)
89 .ok_or_else(|| {
90 RpcPoolError::Eip4844(
91 Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
92 )
93 })?;
94 }
95
96 if let Some(client) = self.raw_tx_forwarder() {
98 tracing::debug!(target: "rpc::eth", hash = %pool_transaction.hash(), "forwarding raw transaction to forwarder");
99 let rlp_hex = hex::encode_prefixed(&tx);
100
101 self.broadcast_raw_transaction(tx);
103
104 let hash =
105 client.request("eth_sendRawTransaction", (rlp_hex,)).await.inspect_err(|err| {
106 tracing::debug!(target: "rpc::eth", %err, hash=% *pool_transaction.hash(), "failed to forward raw transaction");
107 }).map_err(EthApiError::other)?;
108
109 let _ = self.inner.add_pool_transaction(pool_transaction).await;
111
112 return Ok(hash);
113 }
114
115 self.broadcast_raw_transaction(tx);
117
118 let AddedTransactionOutcome { hash, .. } =
120 self.inner.add_pool_transaction(pool_transaction).await?;
121
122 Ok(hash)
123 }
124}
125
126impl<N, Rpc> LoadTransaction for EthApi<N, Rpc>
127where
128 N: RpcNodeCore,
129 EthApiError: FromEvmError<N::Evm>,
130 Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
131{
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::eth::helpers::types::EthRpcConverter;
138 use alloy_consensus::{Block, Header, SidecarBuilder, SimpleCoder, Transaction};
139 use alloy_primitives::{Address, 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::test_utils::{testing_pool, TestPool};
150 use std::collections::HashMap;
151
152 fn mock_eth_api(
153 accounts: HashMap<Address, ExtendedAccount>,
154 ) -> EthApi<
155 RpcNodeCoreAdapter<MockEthProvider, TestPool, NoopNetwork, EthEvmConfig>,
156 EthRpcConverter<ChainSpec>,
157 > {
158 let mock_provider = MockEthProvider::default()
159 .with_chain_spec(ChainSpecBuilder::mainnet().cancun_activated().build());
160 mock_provider.extend_accounts(accounts);
161
162 let evm_config = EthEvmConfig::new(mock_provider.chain_spec());
163 let pool = testing_pool();
164
165 let genesis_header = Header {
166 number: 0,
167 gas_limit: 30_000_000,
168 timestamp: 1,
169 excess_blob_gas: Some(0),
170 base_fee_per_gas: Some(1000000000),
171 blob_gas_used: Some(0),
172 ..Default::default()
173 };
174
175 let genesis_hash = B256::ZERO;
176 mock_provider.add_block(genesis_hash, Block::new(genesis_header, Default::default()));
177
178 EthApi::builder(mock_provider, pool, NoopNetwork::default(), evm_config).build()
179 }
180
181 #[tokio::test]
182 async fn send_raw_transaction() {
183 let eth_api = mock_eth_api(Default::default());
184 let pool = eth_api.pool();
185
186 let tx_1 = Bytes::from(hex!(
188 "02f871018303579880850555633d1b82520894eee27662c2b8eba3cd936a23f039f3189633e4c887ad591c62bdaeb180c080a07ea72c68abfb8fca1bd964f0f99132ed9280261bdca3e549546c0205e800f7d0a05b4ef3039e9c9b9babc179a1878fb825b5aaf5aed2fa8744854150157b08d6f3"
189 ));
190
191 let tx_1_result = eth_api.send_raw_transaction(tx_1).await.unwrap();
192 assert_eq!(
193 pool.len(),
194 1,
195 "expect 1 transaction in the pool, but pool size is {}",
196 pool.len()
197 );
198
199 let tx_2 = Bytes::from(hex!(
201 "02f9043c018202b7843b9aca00850c807d37a08304d21d94ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b881bc16d674ec80000b903c43593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063e2d99f00000000000000000000000000000000000000000000000000000000000000030b000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000065717fe021ea67801d1088cc80099004b05b64600000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95fd5965fd1f1a6f0d4600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000428dca9537116148616a5a3e44035af17238fe9dc080a0c6ec1e41f5c0b9511c49b171ad4e04c6bb419c74d99fe9891d74126ec6e4e879a032069a753d7a2cfa158df95421724d24c0e9501593c09905abf3699b4a4405ce"
202 ));
203
204 let tx_2_result = eth_api.send_raw_transaction(tx_2).await.unwrap();
205 assert_eq!(
206 pool.len(),
207 2,
208 "expect 2 transactions in the pool, but pool size is {}",
209 pool.len()
210 );
211
212 assert!(pool.get(&tx_1_result).is_some(), "tx1 not found in the pool");
213 assert!(pool.get(&tx_2_result).is_some(), "tx2 not found in the pool");
214 }
215
216 #[tokio::test]
217 async fn test_fill_transaction_fills_chain_id() {
218 let address = Address::random();
219 let accounts = HashMap::from([(
220 address,
221 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)), )]);
223
224 let eth_api = mock_eth_api(accounts);
225
226 let tx_req = TransactionRequest {
227 from: Some(address),
228 to: Some(Address::random().into()),
229 gas: Some(21_000),
230 ..Default::default()
231 };
232
233 let filled =
234 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
235
236 assert!(filled.tx.chain_id().is_some());
238 }
239
240 #[tokio::test]
241 async fn test_fill_transaction_fills_nonce() {
242 let address = Address::random();
243 let nonce = 42u64;
244
245 let accounts = HashMap::from([(
246 address,
247 ExtendedAccount::new(nonce, U256::from(1_000_000_000_000_000_000u64)), )]);
249
250 let eth_api = mock_eth_api(accounts);
251
252 let tx_req = TransactionRequest {
253 from: Some(address),
254 to: Some(Address::random().into()),
255 value: Some(U256::from(1000)),
256 gas: Some(21_000),
257 ..Default::default()
258 };
259
260 let filled =
261 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
262
263 assert_eq!(filled.tx.nonce(), nonce);
264 }
265
266 #[tokio::test]
267 async fn test_fill_transaction_preserves_provided_fields() {
268 let address = Address::random();
269 let provided_nonce = 100u64;
270 let provided_gas_limit = 50_000u64;
271
272 let accounts = HashMap::from([(
273 address,
274 ExtendedAccount::new(42, U256::from(10_000_000_000_000_000_000u64)),
275 )]);
276
277 let eth_api = mock_eth_api(accounts);
278
279 let tx_req = TransactionRequest {
280 from: Some(address),
281 to: Some(Address::random().into()),
282 value: Some(U256::from(1000)),
283 nonce: Some(provided_nonce),
284 gas: Some(provided_gas_limit),
285 ..Default::default()
286 };
287
288 let filled =
289 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
290
291 assert_eq!(filled.tx.nonce(), provided_nonce);
293 assert_eq!(filled.tx.gas_limit(), provided_gas_limit);
294 }
295
296 #[tokio::test]
297 async fn test_fill_transaction_fills_all_missing_fields() {
298 let address = Address::random();
299
300 let balance = U256::from(100u128) * U256::from(1_000_000_000_000_000_000u128);
301 let accounts = HashMap::from([(address, ExtendedAccount::new(5, balance))]);
302
303 let eth_api = mock_eth_api(accounts);
304
305 let tx_req = TransactionRequest {
307 from: Some(address),
308 to: Some(Address::random().into()),
309 ..Default::default()
310 };
311
312 let filled =
313 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
314
315 assert!(filled.tx.is_eip1559());
316 }
317
318 #[tokio::test]
319 async fn test_fill_transaction_eip4844_blob_fee() {
320 let address = Address::random();
321 let accounts = HashMap::from([(
322 address,
323 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
324 )]);
325
326 let eth_api = mock_eth_api(accounts);
327
328 let mut builder = SidecarBuilder::<SimpleCoder>::new();
329 builder.ingest(b"dummy blob");
330
331 let tx_req = TransactionRequest {
333 from: Some(address),
334 to: Some(Address::random().into()),
335 sidecar: Some(builder.build().unwrap()),
336 ..Default::default()
337 };
338
339 let filled =
340 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
341
342 assert!(
344 filled.tx.max_fee_per_blob_gas().is_some(),
345 "max_fee_per_blob_gas should be filled for blob tx"
346 );
347 assert!(
348 filled.tx.blob_versioned_hashes().is_some(),
349 "blob_versioned_hashes should be preserved"
350 );
351 }
352
353 #[tokio::test]
354 async fn test_fill_transaction_eip4844_preserves_blob_fee() {
355 let address = Address::random();
356 let accounts = HashMap::from([(
357 address,
358 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
359 )]);
360
361 let eth_api = mock_eth_api(accounts);
362
363 let provided_blob_fee = 5000000u128;
364
365 let mut builder = SidecarBuilder::<SimpleCoder>::new();
366 builder.ingest(b"dummy blob");
367
368 let tx_req = TransactionRequest {
370 from: Some(address),
371 to: Some(Address::random().into()),
372 transaction_type: Some(3), sidecar: Some(builder.build().unwrap()),
374 max_fee_per_blob_gas: Some(provided_blob_fee), ..Default::default()
376 };
377
378 let filled =
379 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
380
381 assert_eq!(
383 filled.tx.max_fee_per_blob_gas(),
384 Some(provided_blob_fee),
385 "should preserve provided max_fee_per_blob_gas"
386 );
387 }
388
389 #[tokio::test]
390 async fn test_fill_transaction_non_blob_tx_no_blob_fee() {
391 let address = Address::random();
392 let accounts = HashMap::from([(
393 address,
394 ExtendedAccount::new(0, U256::from(10_000_000_000_000_000_000u64)),
395 )]);
396
397 let eth_api = mock_eth_api(accounts);
398
399 let tx_req = TransactionRequest {
401 from: Some(address),
402 to: Some(Address::random().into()),
403 transaction_type: Some(2), ..Default::default()
405 };
406
407 let filled =
408 eth_api.fill_transaction(tx_req).await.expect("fill_transaction should succeed");
409
410 assert!(
412 filled.tx.max_fee_per_blob_gas().is_none(),
413 "max_fee_per_blob_gas should not be set for non-blob tx"
414 );
415 }
416}