avalanche_types/wallet/evm/eip1559.rs
1use std::ops::Mul;
2
3use crate::{
4 errors::{Error, Result},
5 key,
6 wallet::{self, evm},
7};
8use ethers::{prelude::Eip1559TransactionRequest, utils::Units::Gwei};
9use ethers_core::types::transaction::eip2718;
10use ethers_providers::Middleware;
11use lazy_static::lazy_static;
12use primitive_types::{H160, H256, U256};
13use tokio::time::Duration;
14
15// With EIP-1559, the fees are: units of gas used * (base fee + priority fee).
16// The expensive but highly guaranteed way of getting transaction in is:
17// set very high "max_fee_per_gas" and very low "max_priority_fee_per_gas".
18// For example, set "max_fee_per_gas" 500 GWEI and "max_priority_fee_per_gas" 10 GWEI.
19// If the base fee is 25 GWEI, it will only cost: units of gas used * (25 + 10).
20// If the base fee is 200 GWEI, it will only cost: units of gas used * (200 + 10).
21// Therefore, we can set the "max_fee_per_gas" to the actual maximum
22// we are willing to pay without manual intervention.
23// ref. <https://docs.avax.network/quickstart/adjusting-gas-price-during-high-network-activity>
24lazy_static! {
25 pub static ref URGENT_MAX_FEE_PER_GAS: U256 = {
26 let gwei = U256::from(10).checked_pow(Gwei.as_num().into()).unwrap();
27 U256::from(700).mul(gwei) // 700 GWEI
28 };
29 pub static ref URGENT_MAX_PRIORITY_FEE_PER_GAS: U256 = {
30 let gwei = U256::from(10).checked_pow(Gwei.as_num().into()).unwrap();
31 U256::from(10).mul(gwei) // 10 GWEI
32 };
33}
34
35impl<T, S> evm::Evm<T, S>
36where
37 T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
38 S: ethers_signers::Signer + Clone,
39 S::Error: 'static,
40{
41 #[must_use]
42 pub fn eip1559(&self) -> Tx<T, S> {
43 Tx::new(self)
44 }
45}
46/// Represents an EIP-1559 Ethereum transaction (dynamic fee transaction in coreth/subnet-evm).
47/// ref. <https://ethereum.org/en/developers/docs/transactions>
48/// ref. <https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md>
49/// ref. <https://github.com/gakonst/ethers-rs/blob/master/ethers-core/src/types/transaction/eip1559.rs>
50/// ref. <https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendrawtransaction>
51/// ref. <https://pkg.go.dev/github.com/ava-labs/subnet-evm/core/types#DynamicFeeTx>
52#[derive(Clone, Debug)]
53pub struct Tx<T, S>
54where
55 T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
56 S: ethers_signers::Signer + Clone,
57 S::Error: 'static,
58{
59 pub inner: wallet::evm::Evm<T, S>,
60
61 /// Sequence number originated from this account to prevent message replay attack
62 /// ref. <https://eips.ethereum.org/EIPS/eip-155>
63 ///
64 /// Must keep track of nonces when creating transactions programmatically.
65 /// If two transactions were transmitted with the same nonce,
66 /// only one will be confirmed and the other will be rejected.
67 ///
68 /// Note that nonce increments regardless whether a transaction execution succeeds or not.
69 /// The nonce increments when the transaction is included in the block, but
70 /// its execution can fail and still pays the gas.
71 ///
72 /// None for automatically fetching the next available nonce.
73 pub signer_nonce: Option<U256>,
74
75 /// Maximum transaction fee as a premium.
76 /// Maps to subnet-evm DynamicFeeTx "GasTipCap".
77 /// ref. <https://ethereum.org/en/developers/docs/gas/>
78 pub max_priority_fee_per_gas: Option<U256>,
79
80 /// Maximum amount that the originator is willing to pay for this transaction.
81 /// Maps to subnet-evm DynamicFeeTx "GasFeeCap".
82 /// ref. <https://ethereum.org/en/developers/docs/gas/>
83 ///
84 /// With EIP-1559, the fees are: units of gas used * (base fee + priority fee).
85 /// The expensive but highly guaranteed way of getting transaction in is:
86 /// set very high "max_fee_per_gas" and very low "max_priority_fee_per_gas".
87 /// For example, set "max_fee_per_gas" 500 GWEI and "max_priority_fee_per_gas" 10 GWEI.
88 /// If the base fee is 25 GWEI, it will only cost: units of gas used * (25 + 10).
89 /// If the base fee is 200 GWEI, it will only cost: units of gas used * (200 + 10).
90 /// Therefore, we can set the "max_fee_per_gas" to the actual maximum
91 /// we are willing to pay without manual intervention.
92 /// ref. <https://docs.avax.network/quickstart/adjusting-gas-price-during-high-network-activity>
93 pub max_fee_per_gas: Option<U256>,
94
95 /// Maximum amount of gas that the originator is willing to buy.
96 /// Maximum amount of gas that can be consumed by this transaction.
97 /// Think of it as a fuel tank capacity for this specific transaction.
98 /// The standard gas limit on Ethereum is 21,000 units (e.g., ETH transfer).
99 /// If a user puts a gas limit of 30,000 for a simple ETH transfer,
100 /// the EVM would only consume 21,000 units, and the user would get back the
101 /// remaining 10,000. If the user puts too low gas limit, the EVM would revert
102 /// the change (execution failure).
103 ///
104 /// Before EIP-1559, if a transaction used up all gas units and the current
105 /// gas price is 200 GWEI, this transaction fee can cost up to 21,000 * 200
106 /// which is 4,200,000 gwei or 0.0042 ETH.
107 /// That is, the fees are: Gas units (limit) * Gas price per unit.
108 ///
109 /// With EIP-1559, the fees are: units of gas used * (base fee + priority fee).
110 /// The base fee is set by the protocol (via chain fee configuration).
111 /// The priority fee is set by the user (via "max_priority_fee_per_gas").
112 ///
113 /// In addition, the user can also set "max_fee_per_gas" for the transaction.
114 /// The surplus from the max fee and the actual fee is refunded to the user.
115 /// For instance, the refunds are: max fee - (base fee + priority fee).
116 /// The "max_fee_per_gas" can limit the maximum amount to pay for the transaction.
117 /// ref. <https://ethereum.org/en/developers/docs/gas/>
118 ///
119 /// This is different than "gas limit" in the chain fee configuration.
120 /// Which is the maximum amount of gas that can be consumed per block (e.g., 8-million GWEI).
121 /// ref. <https://pkg.go.dev/github.com/ava-labs/subnet-evm/params#pkg-variables>
122 pub gas_limit: Option<U256>,
123
124 /// If the recipient is an externally-owned account, the transaction will transfer the "value".
125 /// If the recipient is a contract account/address, the transaction will execute the contract code.
126 /// If the recipient is None, the transaction is for contract creation.
127 /// The contract address is created based on the signer address and transaction nonce.
128 pub recipient: Option<H160>,
129
130 /// Transfer amount value.
131 pub value: Option<U256>,
132
133 /// Arbitrary data.
134 pub data: Option<Vec<u8>>,
135
136 /// Set "true" to check whether a transaction is confirmed using "eth_getTransactionReceipt".
137 /// If false, returns the transaction Id immediately after signing and sending the transaction.
138 /// The transaction may still be pending.
139 /// ref. <https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionreceipt>
140 pub check_receipt: bool,
141
142 /// Set "true" to poll transfer status after issuance for its acceptance
143 /// by calling "eth_getTransactionByHash".
144 /// ref. <https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionbyhash>
145 pub check_acceptance: bool,
146
147 /// Initial wait duration before polling for acceptance.
148 pub poll_initial_wait: Duration,
149 /// Wait between each poll intervals for acceptance.
150 pub poll_interval: Duration,
151 /// Maximum duration for polling.
152 pub poll_timeout: Duration,
153
154 /// Set to true to return transaction Id for "issue" in dry mode.
155 pub dry_mode: bool,
156}
157
158impl<T, S> Tx<T, S>
159where
160 T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
161 S: ethers_signers::Signer + Clone,
162 S::Error: 'static,
163{
164 pub fn new(ev: &wallet::evm::Evm<T, S>) -> Self {
165 Self {
166 inner: ev.clone(),
167
168 signer_nonce: None,
169
170 max_priority_fee_per_gas: None,
171 max_fee_per_gas: None,
172 gas_limit: None,
173
174 recipient: None,
175 value: None,
176 data: None,
177
178 check_receipt: false,
179 check_acceptance: false,
180
181 poll_initial_wait: Duration::from_millis(500),
182 poll_interval: Duration::from_millis(700),
183 poll_timeout: Duration::from_secs(300),
184
185 dry_mode: false,
186 }
187 }
188
189 #[must_use]
190 pub fn signer_nonce(mut self, signer_nonce: impl Into<U256>) -> Self {
191 self.signer_nonce = Some(signer_nonce.into());
192 self
193 }
194
195 /// Same as "GasTipCap" in subnet-evm.
196 #[must_use]
197 pub fn max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: impl Into<U256>) -> Self {
198 self.max_priority_fee_per_gas = Some(max_priority_fee_per_gas.into());
199 self
200 }
201
202 /// Same as "GasFeeCap" in subnet-evm.
203 #[must_use]
204 pub fn max_fee_per_gas(mut self, max_fee_per_gas: impl Into<U256>) -> Self {
205 self.max_fee_per_gas = Some(max_fee_per_gas.into());
206 self
207 }
208
209 #[must_use]
210 pub fn gas_limit(mut self, gas_limit: impl Into<U256>) -> Self {
211 self.gas_limit = Some(gas_limit.into());
212 self
213 }
214
215 /// Overwrites all gas and fee parameters to mark this transaction as urgent.
216 #[must_use]
217 pub fn urgent(mut self) -> Self {
218 self.max_priority_fee_per_gas = Some(*URGENT_MAX_PRIORITY_FEE_PER_GAS);
219 self.max_fee_per_gas = Some(*URGENT_MAX_FEE_PER_GAS);
220 self
221 }
222
223 #[must_use]
224 pub fn recipient(mut self, to: impl Into<H160>) -> Self {
225 self.recipient = Some(to.into());
226 self
227 }
228
229 #[must_use]
230 pub fn value(mut self, value: impl Into<U256>) -> Self {
231 self.value = Some(value.into());
232 self
233 }
234
235 #[must_use]
236 pub fn data(mut self, data: impl Into<Vec<u8>>) -> Self {
237 self.data = Some(data.into());
238 self
239 }
240
241 /// Sets the check receipt boolean flag.
242 #[must_use]
243 pub fn check_receipt(mut self, check_receipt: bool) -> Self {
244 self.check_receipt = check_receipt;
245 self
246 }
247
248 /// Sets the check acceptance boolean flag.
249 /// If "true", overwrites "check_receipt" with "true".
250 /// If "false", does not overwrite "check_receipt" with "false".
251 #[must_use]
252 pub fn check_acceptance(mut self, check_acceptance: bool) -> Self {
253 if check_acceptance {
254 self.check_receipt = true;
255 }
256 self.check_acceptance = check_acceptance;
257 self
258 }
259
260 /// Sets the initial poll wait time.
261 #[must_use]
262 pub fn poll_initial_wait(mut self, poll_initial_wait: Duration) -> Self {
263 self.poll_initial_wait = poll_initial_wait;
264 self
265 }
266
267 /// Sets the poll wait time between intervals.
268 #[must_use]
269 pub fn poll_interval(mut self, poll_interval: Duration) -> Self {
270 self.poll_interval = poll_interval;
271 self
272 }
273
274 /// Sets the poll timeout.
275 #[must_use]
276 pub fn poll_timeout(mut self, poll_timeout: Duration) -> Self {
277 self.poll_timeout = poll_timeout;
278 self
279 }
280
281 /// Sets the dry mode boolean flag.
282 #[must_use]
283 pub fn dry_mode(mut self, dry_mode: bool) -> Self {
284 self.dry_mode = dry_mode;
285 self
286 }
287
288 /// Issues the transaction and returns the transaction Id.
289 /// ref. "coreth,subnet-evm/internal/ethapi.SubmitTransaction"
290 pub async fn submit(&mut self) -> Result<H256> {
291 let max_priority_fee_per_gas = if let Some(v) = self.max_priority_fee_per_gas {
292 format!("{} GWEI", super::wei_to_gwei(v))
293 } else {
294 "default".to_string()
295 };
296 let max_fee_per_gas = if let Some(v) = self.max_fee_per_gas {
297 format!("{} GWEI", super::wei_to_gwei(v))
298 } else {
299 "default".to_string()
300 };
301
302 log::info!(
303 "submit tx [chain Id {}, value {:?}, from {}, recipient {:?}, chain RPC URL {}, max_priority_fee_per_gas {max_priority_fee_per_gas}, max_fee_per_gas {max_fee_per_gas}, gas_limit {:?}, dry_mode {}]",
304 self.inner.chain_id,
305 self.value,
306 self.inner.inner.h160_address,
307 self.recipient,
308 self.inner.chain_rpc_url,
309 self.gas_limit,
310 self.dry_mode,
311 );
312
313 let signer_nonce = if let Some(signer_nonce) = self.signer_nonce {
314 log::info!("using the existing signer nonce '{}'", signer_nonce);
315 signer_nonce
316 } else {
317 let fetched_nonce =
318 self.inner
319 .middleware
320 .initialize_nonce(None)
321 .await
322 .map_err(|e| {
323 // TODO: check retryable
324 Error::Other {
325 message: format!("failed initialize_nonce '{}'", e),
326 retryable: false,
327 }
328 })?;
329
330 log::info!("no signer nonce, thus fetched/cached '{}'", fetched_nonce);
331 self.signer_nonce = Some(fetched_nonce);
332
333 fetched_nonce
334 };
335
336 // "from" itself is not RLP-encoded field
337 // "from" can be simply derived from signature and transaction hash
338 // when the RPC decodes the raw transaction
339 // ref. <https://github.com/gakonst/ethers-rs/blob/master/ethers-core/src/types/transaction/eip1559.rs>
340 // ref. <https://eips.ethereum.org/EIPS/eip-1559>
341 // ref. <https://github.com/gakonst/ethers-rs/blob/master/ethers-core/src/types/transaction/eip2718.rs>
342 // ref. <https://eips.ethereum.org/EIPS/eip-2718>
343 let mut tx_request = Eip1559TransactionRequest::new()
344 .from(ethers::prelude::H160::from(
345 self.inner.inner.h160_address.as_fixed_bytes(),
346 ))
347 .chain_id(ethers::prelude::U64::from(self.inner.chain_id.as_u64()))
348 .nonce(ethers::prelude::U256::from(signer_nonce.as_u128()));
349
350 if let Some(to) = &self.recipient {
351 tx_request = tx_request.to(ethers::prelude::H160::from(to.as_fixed_bytes()));
352 }
353
354 if let Some(value) = &self.value {
355 let converted: ethers::prelude::U256 = value.into();
356 tx_request = tx_request.value(converted);
357 }
358
359 if let Some(max_priority_fee_per_gas) = &self.max_priority_fee_per_gas {
360 let converted: ethers::prelude::U256 = max_priority_fee_per_gas.into();
361 tx_request = tx_request.max_priority_fee_per_gas(converted);
362 }
363
364 if let Some(max_fee_per_gas) = &self.max_fee_per_gas {
365 let converted: ethers::prelude::U256 = max_fee_per_gas.into();
366 tx_request = tx_request.max_fee_per_gas(converted);
367 }
368
369 if let Some(gas_limit) = &self.gas_limit {
370 let converted: ethers::prelude::U256 = gas_limit.into();
371 tx_request = tx_request.gas(converted);
372 }
373
374 if let Some(data) = &self.data {
375 tx_request = tx_request.data(data.clone());
376 }
377
378 if self.dry_mode {
379 // note that the tx hash is only same iff there's no other worker
380 // signing/sending the transaction using the same key
381 // because tx hash differs for different nonces, different gas
382 // if other workers have used the same key (thus incremented the nonce)
383 // the hash returned from dry mode will be different
384 // ref. "ethers-middleware/signer" "send_transaction"
385 let gas_none = tx_request.gas.is_none();
386 let mut typed_tx: eip2718::TypedTransaction = tx_request.into();
387 if gas_none {
388 log::info!("dry-mode estimating gas");
389 let estimated_gas = self
390 .inner
391 .provider
392 .estimate_gas(&typed_tx, None)
393 .await
394 .map_err(|e| {
395 // TODO: check retryable
396 Error::API {
397 message: format!("failed estimate_gas '{}' for dry mode", e),
398 retryable: false,
399 }
400 })?;
401
402 log::info!(
403 "dry-mode caching estimated gas limit {} and updating 'gas' in typed tx",
404 estimated_gas
405 );
406 self.gas_limit = estimated_gas.into();
407
408 typed_tx.set_gas(estimated_gas);
409 };
410
411 let signature = self
412 .inner
413 .eth_signer
414 .sign_transaction(&typed_tx)
415 .await
416 .map_err(|e| {
417 // TODO: check retryable
418 Error::API {
419 message: format!("failed sign_transaction '{}' for dry-mode", e),
420 retryable: false,
421 }
422 })?;
423 let precomputed_tx_hash = typed_tx.hash(&signature);
424
425 log::info!(
426 "dry-mode pre-computed tx hash '0x{:x}'",
427 precomputed_tx_hash
428 );
429 return Ok(precomputed_tx_hash);
430 }
431
432 let pending_tx = self
433 .inner
434 .middleware
435 .send_transaction(tx_request, None)
436 .await
437 .map_err(|e| {
438 // e.g., 'Custom { kind: Other, error: "failed to send_transaction '(code: -32000, message: nonce too low: address 0xaa3033DB04bE0C31967bfC9D0D01bF04a0038526 current nonce (1562) > tx nonce (1561), data: None)'" }'
439 // e.g., 'Custom { kind: Other, error: "failed to send_transaction '(code: -32000, message: replacement transaction underpriced, data: None)'" }'
440 let mut retryable = false;
441 if e.to_string().contains("nonce too low")
442 || e.to_string().contains("transaction underpriced")
443 || e.to_string().contains("dropped from mempool")
444 {
445 log::warn!("tx submit failed with a retryable error; '{}'", e);
446 retryable = true;
447 }
448 Error::API {
449 message: format!("failed to send_transaction '{}'", e),
450 retryable,
451 }
452 })?;
453 let sent_tx_hash = H256(pending_tx.tx_hash().0);
454 if !self.check_receipt {
455 log::info!("sent tx '0x{:x}'", sent_tx_hash);
456 return Ok(sent_tx_hash);
457 }
458
459 // blocks until "eth_getTransactionReceipt" returns
460 // thus this tx is confirmed (not pending)
461 log::info!("checking sent tx receipt '0x{:x}'", sent_tx_hash);
462 let tx_receipt = pending_tx.await.map_err(|e| {
463 // TODO: check retryable
464 Error::API {
465 message: format!("failed to wait for pending tx '{}'", e),
466 retryable: false,
467 }
468 })?;
469
470 // "receipt is not available for pending transactions"
471 // ref. <https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionreceipt>
472 if tx_receipt.is_none() {
473 return Err(Error::API {
474 message: "tx dropped from mempool or pending".to_string(),
475 retryable: true,
476 });
477 }
478
479 let tx_receipt = tx_receipt.unwrap();
480 let tx_hash = H256(tx_receipt.transaction_hash.0);
481 log::info!("confirmed sent tx receipt '0x{:x}'", tx_hash);
482
483 if !self.check_acceptance {
484 log::debug!("skipping checking acceptance for '0x{:x}'", tx_hash);
485 return Ok(tx_hash);
486 }
487
488 // calls "eth_getTransactionByHash"; None when the transaction is pending
489 let tx = self
490 .inner
491 .middleware
492 .get_transaction(tx_receipt.transaction_hash)
493 .await
494 .map_err(|e| {
495 // TODO: check retryable
496 Error::API {
497 message: format!("failed eth_getTransactionByHash '{}'", e),
498 retryable: false,
499 }
500 })?;
501
502 // serde_json::to_string(&tx).unwrap()
503 if let Some(inner) = &tx {
504 if inner.hash() != sent_tx_hash {
505 return Err(Error::API {
506 message: format!(
507 "eth_getTransactionByHash returned unexpected tx hash '0x{:x}' (expected '0x{:x}')",
508 inner.hash(), sent_tx_hash
509 ),
510 retryable: false,
511 });
512 }
513 if inner.hash() != tx_receipt.transaction_hash {
514 return Err(Error::API {
515 message: format!(
516 "eth_getTransactionByHash returned unexpected tx hash '0x{:x}' (expected '0x{:x}')",
517 inner.hash(), tx_receipt.transaction_hash
518 ),
519 retryable: false,
520 });
521 }
522 } else {
523 log::warn!("transaction '0x{:x}' still pending", tx_hash);
524 return Err(Error::API {
525 message: "tx still pending".to_string(),
526 retryable: true,
527 });
528 }
529
530 log::info!("confirmed tx acceptance '0x{:x}'", tx_hash);
531 Ok(tx_hash)
532 }
533}