From fdd34863ba79b584dc3a63a802cff196b4fa9836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 11 Aug 2021 12:01:53 +0100 Subject: [PATCH] Feature/nymd client fee handling (#730) * Calculating gas fees * Ability to set custom fees * Added extra test * Removed commented code * Removed needless borrow --- .../client-libs/validator-client/src/error.rs | 4 + .../validator-client/src/nymd/fee_helpers.rs | 88 +++++++++++++++++++ .../validator-client/src/nymd/gas_price.rs | 76 ++++++++++++++++ .../validator-client/src/nymd/mod.rs | 37 +++++--- common/config/src/defaults.rs | 3 + 5 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 common/client-libs/validator-client/src/nymd/fee_helpers.rs create mode 100644 common/client-libs/validator-client/src/nymd/gas_price.rs diff --git a/common/client-libs/validator-client/src/error.rs b/common/client-libs/validator-client/src/error.rs index 16690573de..7b9903734f 100644 --- a/common/client-libs/validator-client/src/error.rs +++ b/common/client-libs/validator-client/src/error.rs @@ -137,6 +137,10 @@ pub enum ValidatorClientError { code: u32, raw_log: String, }, + + #[cfg(feature = "nymd-client")] + #[error("The provided gas price is malformed")] + MalformedGasPrice, } #[cfg(feature = "nymd-client")] diff --git a/common/client-libs/validator-client/src/nymd/fee_helpers.rs b/common/client-libs/validator-client/src/nymd/fee_helpers.rs new file mode 100644 index 0000000000..f7dc0bc7ef --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/fee_helpers.rs @@ -0,0 +1,88 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nymd::GasPrice; +use cosmos_sdk::tx::{Fee, Gas}; +use cosmos_sdk::Coin; +use cosmwasm_std::Uint128; + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum Operation { + Upload, + Init, + Migrate, + ChangeAdmin, + Send, + + BondMixnode, +} + +pub(crate) fn calculate_fee(gas_price: &GasPrice, gas_limit: Gas) -> Coin { + let limit_uint128 = Uint128::from(gas_limit.value()); + let amount = gas_price.amount * limit_uint128; + assert!(amount.u128() <= u64::MAX as u128); + Coin { + denom: gas_price.denom.clone(), + amount: (amount.u128() as u64).into(), + } +} + +impl Operation { + pub(crate) fn default_gas_limit(&self) -> Gas { + match self { + Operation::Upload => 2_500_000u64.into(), + Operation::Init => 500_000u64.into(), + Operation::Migrate => 200_000u64.into(), + Operation::ChangeAdmin => 80_000u64.into(), + Operation::Send => 80_000u64.into(), + + Operation::BondMixnode => 175_000u64.into(), + } + } + + pub(crate) fn determine_fee(&self, gas_price: &GasPrice, gas_limit: Option) -> Fee { + // we need to know 2 of the following 3 parameters (the third one is being implicit) in order to construct Fee: + // (source: https://docs.cosmos.network/v0.42/basics/gas-fees.html) + // - gas price + // - gas limit + // - fees + let gas_limit = gas_limit.unwrap_or_else(|| self.default_gas_limit()); + let fee = calculate_fee(gas_price, gas_limit); + Fee::from_amount_and_gas(fee, gas_limit) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn calculating_fee() { + let expected = Coin { + denom: "upunk".parse().unwrap(), + amount: 1000u64.into(), + }; + let gas_price = "1upunk".parse().unwrap(); + let gas_limit = 1000u64.into(); + + assert_eq!(expected, calculate_fee(&gas_price, gas_limit)); + + let expected = Coin { + denom: "upunk".parse().unwrap(), + amount: 50u64.into(), + }; + let gas_price = "0.05upunk".parse().unwrap(); + let gas_limit = 1000u64.into(); + + assert_eq!(expected, calculate_fee(&gas_price, gas_limit)); + + let expected = Coin { + denom: "upunk".parse().unwrap(), + amount: 100000u64.into(), + }; + let gas_price = "100upunk".parse().unwrap(); + let gas_limit = 1000u64.into(); + + assert_eq!(expected, calculate_fee(&gas_price, gas_limit)) + } +} diff --git a/common/client-libs/validator-client/src/nymd/gas_price.rs b/common/client-libs/validator-client/src/nymd/gas_price.rs new file mode 100644 index 0000000000..bba160071f --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/gas_price.rs @@ -0,0 +1,76 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ValidatorClientError; +use config::defaults; +use cosmos_sdk::Denom; +use cosmwasm_std::Decimal; +use std::str::FromStr; + +/// A gas price, i.e. the price of a single unit of gas. This is typically a fraction of +/// the smallest fee token unit, such as 0.012utoken. +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct GasPrice { + // I really hate the combination of cosmwasm Decimal with cosmos-sdk Denom, + // but cosmos-sdk's Decimal is too basic for our needs + pub amount: Decimal, + + pub denom: Denom, +} + +impl FromStr for GasPrice { + type Err = ValidatorClientError; + + fn from_str(s: &str) -> Result { + let possible_amount = s + .chars() + .take_while(|c| c.is_ascii_digit() || c == &'.') + .collect::(); + let amount_len = possible_amount.len(); + let amount = possible_amount + .parse() + .map_err(|_| ValidatorClientError::MalformedGasPrice)?; + let possible_denom = s.chars().skip(amount_len).collect::(); + let denom = possible_denom + .parse() + .map_err(|_| ValidatorClientError::MalformedGasPrice)?; + + Ok(GasPrice { amount, denom }) + } +} + +impl Default for GasPrice { + fn default() -> Self { + format!("{}{}", defaults::GAS_PRICE_AMOUNT, defaults::DENOM) + .parse() + .unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gas_price_parsing() { + assert_eq!( + GasPrice { + amount: "0.025".parse().unwrap(), + denom: "upunk".parse().unwrap() + }, + "0.025upunk".parse().unwrap() + ); + + assert_eq!( + GasPrice { + amount: "123".parse().unwrap(), + denom: "upunk".parse().unwrap() + }, + "123upunk".parse().unwrap() + ); + + assert!(".25upunk".parse::().is_err()); + assert!("0.025 upunk".parse::().is_err()); + assert!("0.025UPUNK".parse::().is_err()); + } +} diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 14b21e5a59..4d7b61b699 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -6,23 +6,30 @@ use crate::nymd::cosmwasm_client::client::CosmWasmClient; use crate::nymd::cosmwasm_client::signing_client; use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient; use crate::nymd::cosmwasm_client::types::ExecuteResult; +use crate::nymd::fee_helpers::Operation; +pub use crate::nymd::gas_price::GasPrice; use crate::nymd::wallet::DirectSecp256k1HdWallet; use crate::ValidatorClientError; use cosmos_sdk::rpc::{Error as TendermintRpcError, HttpClient, HttpClientUrl}; -use cosmos_sdk::tx::Fee; +use cosmos_sdk::tx::Gas; use cosmos_sdk::Coin as CosmosCoin; use cosmos_sdk::{AccountId, Denom}; use cosmwasm_std::Coin; use mixnet_contract::LayerDistribution; +use std::collections::HashMap; use std::convert::TryInto; pub mod cosmwasm_client; +pub(crate) mod fee_helpers; +pub mod gas_price; pub mod wallet; pub struct NymdClient { client: C, contract_address: AccountId, client_address: Option>, + gas_price: GasPrice, + custom_gas_limits: HashMap, } impl NymdClient { @@ -37,6 +44,8 @@ impl NymdClient { client: HttpClient::new(endpoint)?, contract_address, client_address: None, + gas_price: Default::default(), + custom_gas_limits: Default::default(), }) } } @@ -61,6 +70,8 @@ impl NymdClient { client: signing_client::Client::connect_with_signer(endpoint, signer)?, contract_address, client_address: Some(client_address), + gas_price: Default::default(), + custom_gas_limits: Default::default(), }) } @@ -83,11 +94,21 @@ impl NymdClient { client: signing_client::Client::connect_with_signer(endpoint, wallet)?, contract_address, client_address: Some(client_address), + gas_price: Default::default(), + custom_gas_limits: Default::default(), }) } } impl NymdClient { + pub fn set_gas_price(&mut self, gas_price: GasPrice) { + self.gas_price = gas_price + } + + pub fn set_custom_gas_limit(&mut self, operation: Operation, limit: Gas) { + self.custom_gas_limits.insert(operation, limit); + } + pub fn address(&self) -> &AccountId where C: SigningCosmWasmClient, @@ -97,7 +118,7 @@ impl NymdClient { } // now the question is as follows: will denom always be in the format of `u{prefix}`? - fn denom(&self) -> Denom { + pub fn denom(&self) -> Denom { format!("u{}", self.contract_address.prefix()) .parse() .unwrap() @@ -114,16 +135,12 @@ impl NymdClient { where C: SigningCosmWasmClient + Sync, { - // TODO: will need to create some nice fee table, for now, for test sake, just use hardcoded value - let fee_amount = CosmosCoin { - amount: 100_0000u64.into(), - denom: self.denom(), - }; - let gas = 100_0000; - let fee = Fee::from_amount_and_gas(fee_amount, gas); + let fee = Operation::BondMixnode.determine_fee( + &self.gas_price, + self.custom_gas_limits.get(&Operation::BondMixnode).cloned(), + ); let req = ExecuteMsg::BondMixnode { mix_node: mixnode }; - self.client .execute( self.address(), diff --git a/common/config/src/defaults.rs b/common/config/src/defaults.rs index 00e67e74f7..73a76103c6 100644 --- a/common/config/src/defaults.rs +++ b/common/config/src/defaults.rs @@ -11,6 +11,9 @@ pub const NETWORK_MONITOR_ADDRESS: &str = "punk1v9qauwdq5terag6uvfsdytcs2d0sdmfd pub const COSMOS_DERIVATION_PATH: &str = "m/44'/118'/0'/0/0"; pub const BECH32_PREFIX: &str = "punk"; pub const DENOM: &str = "upunk"; +// as set by validators in their configs +// (note that the 'amount' postfix is relevant here as the full gas price also includes denom) +pub const GAS_PRICE_AMOUNT: f64 = 0.025; pub const DEFAULT_MIX_LISTENING_PORT: u16 = 1789;