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