use std::ops::Mul;
use crate::{
errors::{Error, Result},
key,
wallet::{self, evm},
};
use ethers::{prelude::Eip1559TransactionRequest, utils::Units::Gwei};
use ethers_core::types::transaction::eip2718;
use ethers_providers::Middleware;
use lazy_static::lazy_static;
use primitive_types::{H160, H256, U256};
use tokio::time::Duration;
lazy_static! {
pub static ref URGENT_MAX_FEE_PER_GAS: U256 = {
let gwei = U256::from(10).checked_pow(Gwei.as_num().into()).unwrap();
U256::from(700).mul(gwei) };
pub static ref URGENT_MAX_PRIORITY_FEE_PER_GAS: U256 = {
let gwei = U256::from(10).checked_pow(Gwei.as_num().into()).unwrap();
U256::from(10).mul(gwei) };
}
impl<T, S> evm::Evm<T, S>
where
T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
S: ethers_signers::Signer + Clone,
S::Error: 'static,
{
#[must_use]
pub fn eip1559(&self) -> Tx<T, S> {
Tx::new(self)
}
}
#[derive(Clone, Debug)]
pub struct Tx<T, S>
where
T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
S: ethers_signers::Signer + Clone,
S::Error: 'static,
{
pub inner: wallet::evm::Evm<T, S>,
pub signer_nonce: Option<U256>,
pub max_priority_fee_per_gas: Option<U256>,
pub max_fee_per_gas: Option<U256>,
pub gas_limit: Option<U256>,
pub recipient: Option<H160>,
pub value: Option<U256>,
pub data: Option<Vec<u8>>,
pub check_receipt: bool,
pub check_acceptance: bool,
pub poll_initial_wait: Duration,
pub poll_interval: Duration,
pub poll_timeout: Duration,
pub dry_mode: bool,
}
impl<T, S> Tx<T, S>
where
T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
S: ethers_signers::Signer + Clone,
S::Error: 'static,
{
pub fn new(ev: &wallet::evm::Evm<T, S>) -> Self {
Self {
inner: ev.clone(),
signer_nonce: None,
max_priority_fee_per_gas: None,
max_fee_per_gas: None,
gas_limit: None,
recipient: None,
value: None,
data: None,
check_receipt: false,
check_acceptance: false,
poll_initial_wait: Duration::from_millis(500),
poll_interval: Duration::from_millis(700),
poll_timeout: Duration::from_secs(300),
dry_mode: false,
}
}
#[must_use]
pub fn signer_nonce(mut self, signer_nonce: impl Into<U256>) -> Self {
self.signer_nonce = Some(signer_nonce.into());
self
}
#[must_use]
pub fn max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: impl Into<U256>) -> Self {
self.max_priority_fee_per_gas = Some(max_priority_fee_per_gas.into());
self
}
#[must_use]
pub fn max_fee_per_gas(mut self, max_fee_per_gas: impl Into<U256>) -> Self {
self.max_fee_per_gas = Some(max_fee_per_gas.into());
self
}
#[must_use]
pub fn gas_limit(mut self, gas_limit: impl Into<U256>) -> Self {
self.gas_limit = Some(gas_limit.into());
self
}
#[must_use]
pub fn urgent(mut self) -> Self {
self.max_priority_fee_per_gas = Some(*URGENT_MAX_PRIORITY_FEE_PER_GAS);
self.max_fee_per_gas = Some(*URGENT_MAX_FEE_PER_GAS);
self
}
#[must_use]
pub fn recipient(mut self, to: impl Into<H160>) -> Self {
self.recipient = Some(to.into());
self
}
#[must_use]
pub fn value(mut self, value: impl Into<U256>) -> Self {
self.value = Some(value.into());
self
}
#[must_use]
pub fn data(mut self, data: impl Into<Vec<u8>>) -> Self {
self.data = Some(data.into());
self
}
#[must_use]
pub fn check_receipt(mut self, check_receipt: bool) -> Self {
self.check_receipt = check_receipt;
self
}
#[must_use]
pub fn check_acceptance(mut self, check_acceptance: bool) -> Self {
if check_acceptance {
self.check_receipt = true;
}
self.check_acceptance = check_acceptance;
self
}
#[must_use]
pub fn poll_initial_wait(mut self, poll_initial_wait: Duration) -> Self {
self.poll_initial_wait = poll_initial_wait;
self
}
#[must_use]
pub fn poll_interval(mut self, poll_interval: Duration) -> Self {
self.poll_interval = poll_interval;
self
}
#[must_use]
pub fn poll_timeout(mut self, poll_timeout: Duration) -> Self {
self.poll_timeout = poll_timeout;
self
}
#[must_use]
pub fn dry_mode(mut self, dry_mode: bool) -> Self {
self.dry_mode = dry_mode;
self
}
pub async fn submit(&mut self) -> Result<H256> {
let max_priority_fee_per_gas = if let Some(v) = self.max_priority_fee_per_gas {
format!("{} GWEI", super::wei_to_gwei(v))
} else {
"default".to_string()
};
let max_fee_per_gas = if let Some(v) = self.max_fee_per_gas {
format!("{} GWEI", super::wei_to_gwei(v))
} else {
"default".to_string()
};
log::info!(
"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 {}]",
self.inner.chain_id,
self.value,
self.inner.inner.h160_address,
self.recipient,
self.inner.chain_rpc_url,
self.gas_limit,
self.dry_mode,
);
let signer_nonce = if let Some(signer_nonce) = self.signer_nonce {
log::info!("using the existing signer nonce '{}'", signer_nonce);
signer_nonce
} else {
let fetched_nonce =
self.inner
.middleware
.initialize_nonce(None)
.await
.map_err(|e| {
Error::Other {
message: format!("failed initialize_nonce '{}'", e),
retryable: false,
}
})?;
log::info!("no signer nonce, thus fetched/cached '{}'", fetched_nonce);
self.signer_nonce = Some(fetched_nonce);
fetched_nonce
};
let mut tx_request = Eip1559TransactionRequest::new()
.from(ethers::prelude::H160::from(
self.inner.inner.h160_address.as_fixed_bytes(),
))
.chain_id(ethers::prelude::U64::from(self.inner.chain_id.as_u64()))
.nonce(ethers::prelude::U256::from(signer_nonce.as_u128()));
if let Some(to) = &self.recipient {
tx_request = tx_request.to(ethers::prelude::H160::from(to.as_fixed_bytes()));
}
if let Some(value) = &self.value {
let converted: ethers::prelude::U256 = value.into();
tx_request = tx_request.value(converted);
}
if let Some(max_priority_fee_per_gas) = &self.max_priority_fee_per_gas {
let converted: ethers::prelude::U256 = max_priority_fee_per_gas.into();
tx_request = tx_request.max_priority_fee_per_gas(converted);
}
if let Some(max_fee_per_gas) = &self.max_fee_per_gas {
let converted: ethers::prelude::U256 = max_fee_per_gas.into();
tx_request = tx_request.max_fee_per_gas(converted);
}
if let Some(gas_limit) = &self.gas_limit {
let converted: ethers::prelude::U256 = gas_limit.into();
tx_request = tx_request.gas(converted);
}
if let Some(data) = &self.data {
tx_request = tx_request.data(data.clone());
}
if self.dry_mode {
let gas_none = tx_request.gas.is_none();
let mut typed_tx: eip2718::TypedTransaction = tx_request.into();
if gas_none {
log::info!("dry-mode estimating gas");
let estimated_gas = self
.inner
.provider
.estimate_gas(&typed_tx, None)
.await
.map_err(|e| {
Error::API {
message: format!("failed estimate_gas '{}' for dry mode", e),
retryable: false,
}
})?;
log::info!(
"dry-mode caching estimated gas limit {} and updating 'gas' in typed tx",
estimated_gas
);
self.gas_limit = estimated_gas.into();
typed_tx.set_gas(estimated_gas);
};
let signature = self
.inner
.eth_signer
.sign_transaction(&typed_tx)
.await
.map_err(|e| {
Error::API {
message: format!("failed sign_transaction '{}' for dry-mode", e),
retryable: false,
}
})?;
let precomputed_tx_hash = typed_tx.hash(&signature);
log::info!(
"dry-mode pre-computed tx hash '0x{:x}'",
precomputed_tx_hash
);
return Ok(precomputed_tx_hash);
}
let pending_tx = self
.inner
.middleware
.send_transaction(tx_request, None)
.await
.map_err(|e| {
let mut retryable = false;
if e.to_string().contains("nonce too low")
|| e.to_string().contains("transaction underpriced")
|| e.to_string().contains("dropped from mempool")
{
log::warn!("tx submit failed with a retryable error; '{}'", e);
retryable = true;
}
Error::API {
message: format!("failed to send_transaction '{}'", e),
retryable,
}
})?;
let sent_tx_hash = H256(pending_tx.tx_hash().0);
if !self.check_receipt {
log::info!("sent tx '0x{:x}'", sent_tx_hash);
return Ok(sent_tx_hash);
}
log::info!("checking sent tx receipt '0x{:x}'", sent_tx_hash);
let tx_receipt = pending_tx.await.map_err(|e| {
Error::API {
message: format!("failed to wait for pending tx '{}'", e),
retryable: false,
}
})?;
if tx_receipt.is_none() {
return Err(Error::API {
message: "tx dropped from mempool or pending".to_string(),
retryable: true,
});
}
let tx_receipt = tx_receipt.unwrap();
let tx_hash = H256(tx_receipt.transaction_hash.0);
log::info!("confirmed sent tx receipt '0x{:x}'", tx_hash);
if !self.check_acceptance {
log::debug!("skipping checking acceptance for '0x{:x}'", tx_hash);
return Ok(tx_hash);
}
let tx = self
.inner
.middleware
.get_transaction(tx_receipt.transaction_hash)
.await
.map_err(|e| {
Error::API {
message: format!("failed eth_getTransactionByHash '{}'", e),
retryable: false,
}
})?;
if let Some(inner) = &tx {
if inner.hash() != sent_tx_hash {
return Err(Error::API {
message: format!(
"eth_getTransactionByHash returned unexpected tx hash '0x{:x}' (expected '0x{:x}')",
inner.hash(), sent_tx_hash
),
retryable: false,
});
}
if inner.hash() != tx_receipt.transaction_hash {
return Err(Error::API {
message: format!(
"eth_getTransactionByHash returned unexpected tx hash '0x{:x}' (expected '0x{:x}')",
inner.hash(), tx_receipt.transaction_hash
),
retryable: false,
});
}
} else {
log::warn!("transaction '0x{:x}' still pending", tx_hash);
return Err(Error::API {
message: "tx still pending".to_string(),
retryable: true,
});
}
log::info!("confirmed tx acceptance '0x{:x}'", tx_hash);
Ok(tx_hash)
}
}