referrerpolicy=no-referrer-when-downgrade

pallet_revive_eth_rpc/
example.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17//! Example utilities
18use crate::{EthRpcClient, ReceiptInfo};
19use anyhow::Context;
20use pallet_revive::evm::*;
21use std::sync::Arc;
22
23/// Transaction type enum for specifying which type of transaction to send
24#[derive(Debug, Clone, Copy)]
25pub enum TransactionType {
26	Legacy,
27	Eip2930,
28	Eip1559,
29	Eip4844,
30}
31
32/// Transaction builder.
33pub struct TransactionBuilder<Client: EthRpcClient + Sync + Send> {
34	client: Arc<Client>,
35	signer: Account,
36	value: U256,
37	input: Bytes,
38	to: Option<H160>,
39	nonce: Option<U256>,
40	gas: Option<U256>,
41	mutate: Box<dyn FnOnce(&mut TransactionUnsigned)>,
42}
43
44#[derive(Debug)]
45pub struct SubmittedTransaction<Client: EthRpcClient + Sync + Send> {
46	tx: GenericTransaction,
47	hash: H256,
48	client: Arc<Client>,
49}
50
51impl<Client: EthRpcClient + Sync + Send> SubmittedTransaction<Client> {
52	/// Get the hash of the transaction.
53	pub fn hash(&self) -> H256 {
54		self.hash
55	}
56
57	/// The gas sent with the transaction.
58	pub fn gas(&self) -> U256 {
59		self.tx.gas.unwrap()
60	}
61
62	pub fn generic_transaction(&self) -> GenericTransaction {
63		self.tx.clone()
64	}
65
66	/// Wait for the receipt regardless of success or failure status.
67	pub async fn wait_for_receipt_any(&self) -> anyhow::Result<ReceiptInfo> {
68		let hash = self.hash();
69		for _ in 0..30 {
70			tokio::time::sleep(std::time::Duration::from_secs(2)).await;
71			if let Some(receipt) = self.client.get_transaction_receipt(hash).await? {
72				return Ok(receipt);
73			}
74		}
75		anyhow::bail!("Timeout, failed to get receipt for {hash:?}")
76	}
77
78	/// Wait for the receipt and assert the transaction succeeded.
79	pub async fn wait_for_receipt(&self) -> anyhow::Result<ReceiptInfo> {
80		let receipt = self.wait_for_receipt_any().await?;
81		if receipt.is_success() {
82			assert!(
83				self.gas() >= receipt.gas_used,
84				"Gas used {:?} should be less than or equal to gas limit {:?}",
85				receipt.gas_used,
86				self.gas()
87			);
88			Ok(receipt)
89		} else {
90			anyhow::bail!("Transaction failed receipt: {receipt:?}")
91		}
92	}
93}
94
95impl<Client: EthRpcClient + Send + Sync> TransactionBuilder<Client> {
96	pub fn new(client: Arc<Client>) -> Self {
97		Self {
98			client,
99			signer: Account::default(),
100			value: U256::zero(),
101			input: Bytes::default(),
102			to: None,
103			nonce: None,
104			gas: None,
105			mutate: Box::new(|_| {}),
106		}
107	}
108	/// Set the signer.
109	pub fn signer(mut self, signer: Account) -> Self {
110		self.signer = signer;
111		self
112	}
113
114	/// Set the value.
115	pub fn value(mut self, value: U256) -> Self {
116		self.value = value;
117		self
118	}
119
120	/// Set the input.
121	pub fn input(mut self, input: Vec<u8>) -> Self {
122		self.input = Bytes(input);
123		self
124	}
125
126	/// Set the destination.
127	pub fn to(mut self, to: H160) -> Self {
128		self.to = Some(to);
129		self
130	}
131
132	/// Set the nonce.
133	pub fn nonce(mut self, nonce: U256) -> Self {
134		self.nonce = Some(nonce);
135		self
136	}
137
138	/// Set the gas limit explicitly, skipping eth_estimateGas.
139	pub fn gas(mut self, gas: U256) -> Self {
140		self.gas = Some(gas);
141		self
142	}
143
144	/// Set a mutation function, that mutates the transaction before sending.
145	pub fn mutate(mut self, mutate: impl FnOnce(&mut TransactionUnsigned) + 'static) -> Self {
146		self.mutate = Box::new(mutate);
147		self
148	}
149
150	/// Call eth_call to get the result of a view function
151	pub async fn eth_call(self) -> anyhow::Result<Vec<u8>> {
152		let TransactionBuilder { client, signer, value, input, to, .. } = self;
153
154		let from = signer.address();
155		let result = client
156			.call(
157				GenericTransaction {
158					from: Some(from),
159					input: input.into(),
160					value: Some(value),
161					to,
162					..Default::default()
163				},
164				None,
165				None,
166			)
167			.await
168			.map_err(|e| anyhow::anyhow!("eth_call failed: {e}"))?;
169		Ok(result.0)
170	}
171
172	/// Send the transaction.
173	pub async fn send(self) -> anyhow::Result<SubmittedTransaction<Client>> {
174		self.send_with_type(TransactionType::Legacy).await
175	}
176
177	/// Send the transaction with a specific transaction type.
178	pub async fn send_with_type(
179		self,
180		tx_type: TransactionType,
181	) -> anyhow::Result<SubmittedTransaction<Client>> {
182		let TransactionBuilder { client, signer, value, input, to, nonce, gas, mutate } = self;
183
184		let from = signer.address();
185		let chain_id = client.chain_id().await?;
186		let gas_price = client.gas_price().await?;
187		let nonce = if let Some(nonce) = nonce {
188			nonce
189		} else {
190			client
191				.get_transaction_count(from, BlockTag::Latest.into())
192				.await
193				.with_context(|| "Failed to fetch account nonce")?
194		};
195
196		let gas = if let Some(gas) = gas {
197			gas
198		} else {
199			client
200				.estimate_gas(
201					GenericTransaction {
202						from: Some(from),
203						input: input.clone().into(),
204						value: Some(value),
205						gas_price: Some(gas_price),
206						to,
207						..Default::default()
208					},
209					None,
210				)
211				.await
212				.with_context(|| "Failed to fetch gas estimate")?
213		};
214
215		println!("Gas estimate: {gas:?}");
216
217		let mut unsigned_tx: TransactionUnsigned = match tx_type {
218			TransactionType::Legacy => TransactionLegacyUnsigned {
219				gas,
220				nonce,
221				to,
222				value,
223				input,
224				gas_price,
225				chain_id: Some(chain_id),
226				..Default::default()
227			}
228			.into(),
229			TransactionType::Eip2930 => Transaction2930Unsigned {
230				gas,
231				nonce,
232				to,
233				value,
234				input,
235				gas_price,
236				chain_id,
237				access_list: vec![],
238				r#type: TypeEip2930,
239			}
240			.into(),
241			TransactionType::Eip1559 => Transaction1559Unsigned {
242				gas,
243				nonce,
244				to,
245				value,
246				input,
247				gas_price,
248				max_fee_per_gas: gas_price,
249				max_priority_fee_per_gas: U256::zero(),
250				chain_id,
251				access_list: vec![],
252				r#type: TypeEip1559,
253			}
254			.into(),
255			TransactionType::Eip4844 => {
256				// For EIP-4844, we need a destination address (cannot be None for blob
257				// transactions)
258				let to = to.ok_or_else(|| {
259					anyhow::anyhow!("EIP-4844 transactions require a destination address")
260				})?;
261				let max_priority_fee_per_gas = gas_price / 10; // 10% of gas price as priority fee
262				Transaction4844Unsigned {
263					gas,
264					nonce,
265					to,
266					value,
267					input,
268					max_fee_per_gas: gas_price,
269					max_priority_fee_per_gas,
270					max_fee_per_blob_gas: gas_price, // Use gas_price as blob gas fee
271					chain_id,
272					access_list: vec![],
273					blob_versioned_hashes: vec![],
274					r#type: TypeEip4844,
275				}
276				.into()
277			},
278		};
279		mutate(&mut unsigned_tx);
280
281		let signed_tx = signer.sign_transaction(unsigned_tx);
282		let bytes = signed_tx.signed_payload();
283
284		let hash = client
285			.send_raw_transaction(bytes.into())
286			.await
287			.with_context(|| "send_raw_transaction failed")?;
288
289		Ok(SubmittedTransaction {
290			tx: GenericTransaction::from_signed(signed_tx, gas_price, Some(from)),
291			hash,
292			client,
293		})
294	}
295}
296
297#[test]
298fn test_dummy_payload_has_correct_len() {
299	let signer = Account::from(subxt_signer::eth::dev::ethan());
300	let unsigned_tx: TransactionUnsigned =
301		TransactionLegacyUnsigned { input: vec![42u8; 100].into(), ..Default::default() }.into();
302
303	let signed_tx = signer.sign_transaction(unsigned_tx.clone());
304	let signed_payload = signed_tx.signed_payload();
305	let unsigned_tx = signed_tx.unsigned();
306
307	let dummy_payload = unsigned_tx.dummy_signed_payload();
308	assert_eq!(dummy_payload.len(), signed_payload.len());
309}