Skip to main content

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}