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