use ibc_proto::cosmos::tx::v1beta1::{Fee, Tx};
use ibc_proto::google::protobuf::Any;
use ibc_relayer_types::core::ics24_host::identifier::ChainId;
use tendermint_rpc::Url;
use tonic::codegen::http::Uri;
use tracing::{debug, error, span, warn, Level};
use crate::chain::cosmos::encode::sign_tx;
use crate::chain::cosmos::gas::gas_amount_to_fee;
use crate::chain::cosmos::simulate::send_tx_simulate;
use crate::chain::cosmos::types::account::Account;
use crate::chain::cosmos::types::config::TxConfig;
use crate::chain::cosmos::types::gas::GasConfig;
use crate::config::types::Memo;
use crate::error::Error;
use crate::keyring::Secp256k1KeyPair;
use crate::telemetry;
use crate::util::pretty::PrettyFee;
pub enum EstimatedGas {
Simulated(u64),
Default(u64),
}
impl EstimatedGas {
pub fn get_amount(&self) -> u64 {
match self {
Self::Simulated(amount) | Self::Default(amount) => *amount,
}
}
}
pub async fn estimate_tx_fees(
config: &TxConfig,
key_pair: &Secp256k1KeyPair,
account: &Account,
tx_memo: &Memo,
messages: &[Any],
) -> Result<(Fee, EstimatedGas), Error> {
let gas_config = &config.gas_config;
debug!(
"max fee, for use in tx simulation: {}",
PrettyFee(&gas_config.max_fee)
);
let signed_tx = sign_tx(
config,
key_pair,
account,
tx_memo,
messages,
&gas_config.max_fee,
)?;
let tx = Tx {
body: Some(signed_tx.body),
auth_info: Some(signed_tx.auth_info),
signatures: signed_tx.signatures,
};
let estimated_fee_and_gas = estimate_fee_with_tx(
gas_config,
&config.grpc_address,
&config.rpc_address,
&config.chain_id,
tx,
account,
)
.await?;
Ok(estimated_fee_and_gas)
}
async fn estimate_fee_with_tx(
gas_config: &GasConfig,
grpc_address: &Uri,
rpc_address: &Url,
chain_id: &ChainId,
tx: Tx,
account: &Account,
) -> Result<(Fee, EstimatedGas), Error> {
let estimated_gas = {
crate::time!(
"estimate_gas_with_tx",
{
"src_chain": chain_id,
}
);
estimate_gas_with_tx(gas_config, grpc_address, tx, account).await
}?;
let estimated_gas_amount = estimated_gas.get_amount();
if estimated_gas_amount > gas_config.max_gas {
debug!(
id = %chain_id, estimated = ?estimated_gas_amount, max = ?gas_config.max_gas,
"send_tx: estimated gas is higher than max gas"
);
return Err(Error::tx_simulate_gas_estimate_exceeded(
chain_id.clone(),
estimated_gas_amount,
gas_config.max_gas,
));
}
let adjusted_fee =
gas_amount_to_fee(gas_config, estimated_gas_amount, chain_id, rpc_address).await;
debug!(
id = %chain_id,
"send_tx: using {} gas, fee {}",
estimated_gas_amount,
PrettyFee(&adjusted_fee)
);
Ok((adjusted_fee, estimated_gas))
}
async fn estimate_gas_with_tx(
gas_config: &GasConfig,
grpc_address: &Uri,
tx: Tx,
_account: &Account,
) -> Result<EstimatedGas, Error> {
let simulated_gas = send_tx_simulate(grpc_address, tx)
.await
.map(|sr| sr.gas_info);
let _span = span!(Level::ERROR, "estimate_gas").entered();
match simulated_gas {
Ok(Some(gas_info)) => {
debug!(
"tx simulation successful, gas amount used: {:?}",
gas_info.gas_used
);
Ok(EstimatedGas::Simulated(gas_info.gas_used))
}
Ok(None) => {
warn!(
"tx simulation successful but no gas amount used was returned, falling back on default gas: {}",
gas_config.default_gas
);
Ok(EstimatedGas::Default(gas_config.default_gas))
}
Err(e) if can_recover_from_simulation_failure(&e) => {
warn!(
"failed to simulate tx, falling back on default gas because the error is potentially recoverable: {}",
e.detail()
);
telemetry!(
simulate_errors,
&_account.address.to_string(),
true,
get_error_text(&e),
);
Ok(EstimatedGas::Default(gas_config.default_gas))
}
Err(e) => {
error!(
"failed to simulate tx. propagating error to caller: {}",
e.detail()
);
telemetry!(
simulate_errors,
&_account.address.to_string(),
false,
get_error_text(&e),
);
Err(e)
}
}
}
fn can_recover_from_simulation_failure(e: &Error) -> bool {
use crate::error::ErrorDetail::*;
match e.detail() {
GrpcStatus(detail) => {
detail.is_client_state_height_too_low()
|| detail.is_account_sequence_mismatch_that_can_be_ignored()
|| detail.is_out_of_order_packet_sequence_error()
|| detail.is_empty_tx_error()
}
_ => false,
}
}
fn get_error_text(e: &Error) -> String {
use crate::error::ErrorDetail::*;
match e.detail() {
GrpcStatus(detail) => detail.status.code().to_string(),
detail => detail.to_string(),
}
}