1use crate::{EthRpcClient, ReceiptInfo};
19use anyhow::Context;
20use pallet_revive::evm::*;
21use pallet_revive_types::runtime_api::GenericTransactionV1;
22use std::sync::Arc;
23
24#[derive(Debug, Clone, Copy)]
26pub enum TransactionType {
27 Legacy,
28 Eip2930,
29 Eip1559,
30 Eip4844,
31}
32
33pub 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 pub fn hash(&self) -> H256 {
55 self.hash
56 }
57
58 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 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 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 pub fn signer(mut self, signer: Account) -> Self {
116 self.signer = signer;
117 self
118 }
119
120 pub fn value(mut self, value: U256) -> Self {
122 self.value = value;
123 self
124 }
125
126 pub fn input(mut self, input: Vec<u8>) -> Self {
128 self.input = Bytes(input);
129 self
130 }
131
132 pub fn to(mut self, to: H160) -> Self {
134 self.to = Some(to);
135 self
136 }
137
138 pub fn nonce(mut self, nonce: U256) -> Self {
140 self.nonce = Some(nonce);
141 self
142 }
143
144 pub fn gas(mut self, gas: U256) -> Self {
146 self.gas = Some(gas);
147 self
148 }
149
150 pub fn mutate(mut self, mutate: impl FnOnce(&mut TransactionUnsigned) + 'static) -> Self {
152 self.mutate = Box::new(mutate);
153 self
154 }
155
156 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 pub async fn send(self) -> anyhow::Result<SubmittedTransaction<Client>> {
180 self.send_with_type(TransactionType::Legacy).await
181 }
182
183 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 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; 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, 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}