From 36c50019b9047e27b761317bace5f7640d40fed9 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 14 Jan 2025 16:57:59 +0300 Subject: [PATCH 01/13] P.o.C implementation of cosmos delegation Signed-off-by: onur-ozkan --- .../coins/rpc_command/tendermint/staking.rs | 23 ++- mm2src/coins/tendermint/tendermint_coin.rs | 136 +++++++++++++++++- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 3 +- 3 files changed, 157 insertions(+), 5 deletions(-) diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index c6ac3dca4e..42f14cde0d 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -2,8 +2,9 @@ use common::{HttpStatusCode, PagingOptions, StatusCode}; use cosmrs::staking::{Commission, Description, Validator}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; +use mm2_number::BigDecimal; -use crate::{lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum}; +use crate::{lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum, TransactionDetails, WithdrawFee}; /// Represents current status of the validator. #[derive(Default, Deserialize)] @@ -148,3 +149,23 @@ pub async fn validators_rpc( validators: validators.into_iter().map(jsonize_validator).collect(), }) } + +#[derive(Clone, Deserialize)] +pub struct DelegationRPC { + pub coin: String, + pub validator_address: String, + #[serde(default)] + pub amount: BigDecimal, + #[serde(default)] + pub max: bool, + pub fee: Option, +} + +pub async fn delegation_rpc(ctx: MmArc, req: DelegationRPC) -> Result> { + match lp_coinfind_or_err(&ctx, &req.coin).await { + Ok(MmCoinEnum::Tendermint(coin)) => Ok(coin.delegate(req).await.unwrap()), + Ok(MmCoinEnum::TendermintToken(token)) => todo!(), + Ok(_) => todo!(), + Err(_) => todo!(), + } +} diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 9573e6de4b..d34cefffef 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -6,7 +6,7 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; -use crate::rpc_command::tendermint::staking::ValidatorStatus; +use crate::rpc_command::tendermint::staking::{DelegationRPC, ValidatorStatus}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -50,7 +50,7 @@ use cosmrs::proto::cosmos::staking::v1beta1::{QueryValidatorsRequest, use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; use cosmrs::proto::prost::{DecodeError, Message}; -use cosmrs::staking::{QueryValidatorsResponse, Validator}; +use cosmrs::staking::{MsgDelegate, MsgUndelegate, QueryValidatorsResponse, Validator}; use cosmrs::tendermint::block::Height; use cosmrs::tendermint::chain::Id as ChainId; use cosmrs::tendermint::PublicKey; @@ -105,7 +105,7 @@ const ABCI_REQUEST_PROVE: bool = false; /// 0.25 is good average gas price on atom and iris const DEFAULT_GAS_PRICE: f64 = 0.25; pub(super) const TIMEOUT_HEIGHT_DELTA: u64 = 100; -pub const GAS_LIMIT_DEFAULT: u64 = 125_000; +pub const GAS_LIMIT_DEFAULT: u64 = 225_000; pub const GAS_WANTED_BASE_VALUE: f64 = 50_000.; pub(crate) const TX_DEFAULT_MEMO: &str = ""; @@ -2124,6 +2124,136 @@ impl TendermintCoin { Ok(typed_response.validators) } + + pub(crate) async fn delegate(&self, req: DelegationRPC) -> MmResult { + let validator_address = + AccountId::from_str(&req.validator_address).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + + let (delegator_address, maybe_pk) = self.account_id_and_pk_for_withdraw(None)?; + + let (balance_denom, balance_dec) = self + .get_balance_as_unsigned_and_decimal(&delegator_address, &self.denom, self.decimals()) + .await?; + + let (amount_denom, amount_dec) = if req.max { + let amount_denom = balance_denom; + (amount_denom, big_decimal_from_sat_unsigned(amount_denom, self.decimals)) + } else { + (sat_from_big_decimal(&req.amount, self.decimals)?, req.amount.clone()) + }; + + if !self.is_tx_amount_enough(self.decimals, &amount_dec) { + // return MmError::err(WithdrawError::AmountTooLow { + // amount: amount_dec, + // threshold: coin.min_tx_amount(), + // }); + } + + let coin = Coin { + denom: self.denom.clone(), + amount: amount_denom.into(), + }; + + let msg = MsgDelegate { + delegator_address: delegator_address.clone(), + validator_address: validator_address.clone(), + amount: coin.clone(), + }; + + let current_block = self + .current_block() + .compat() + .await + .map_to_mm(WithdrawError::Transport)?; + + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + + let (_, gas_limit) = self.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); + + let fee_amount_u64 = self + .calculate_account_fee_amount_as_u64( + &delegator_address, + maybe_pk, + msg.clone().to_any().unwrap(), + timeout_height, + String::default(), + req.fee, + ) + .await?; + + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, self.decimals()); + + let fee_amount = Coin { + denom: self.denom.clone(), + amount: fee_amount_u64.into(), + }; + + let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); + + let (amount_denom, total_amount) = if req.max { + if balance_denom < fee_amount_u64 { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: self.ticker.clone(), + available: balance_dec, + required: fee_amount_dec, + }); + } + let amount_denom = balance_denom - fee_amount_u64; + (amount_denom, balance_dec) + } else { + let total = &req.amount + &fee_amount_dec; + if balance_dec < total { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: self.ticker.clone(), + available: balance_dec, + required: total, + }); + } + + (sat_from_big_decimal(&req.amount, self.decimals)?, total) + }; + + let msg = MsgDelegate { + delegator_address: delegator_address.clone(), + validator_address: validator_address.clone(), + amount: coin.clone(), + }; + let msg_payload = msg.to_any().unwrap(); + + let account_info = self.account_info(&delegator_address).await?; + + let tx = self + .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, "".into()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let internal_id = { + let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); + sha256(&hex_vec).to_vec().into() + }; + + Ok(TransactionDetails { + tx, + from: vec![delegator_address.to_string()], + to: vec![req.validator_address], + my_balance_change: &BigDecimal::default() - &total_amount, + spent_by_me: total_amount.clone(), + total_amount, + received_by_me: BigDecimal::default(), + block_height: 0, + timestamp: 0, + fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { + coin: self.ticker.clone(), + amount: fee_amount_dec, + uamount: fee_amount_u64, + gas_limit, + })), + coin: self.ticker.to_string(), + internal_id, + kmd_rewards: None, + transaction_type: TransactionType::StandardTransfer, + memo: None, + }) + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index fd9babd2c5..4c40fce965 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -23,7 +23,7 @@ use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; -use coins::rpc_command::tendermint::staking::validators_rpc; +use coins::rpc_command::tendermint::staking::{validators_rpc, delegation_rpc}; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, @@ -214,6 +214,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, stop_simple_market_maker_bot).await, "stop_version_stat_collection" => handle_mmrpc(ctx, request, stop_version_stat_collection).await, "tendermint_validators" => handle_mmrpc(ctx, request, validators_rpc).await, + "tendermint_delegation" => handle_mmrpc(ctx, request, delegation_rpc).await, "trade_preimage" => handle_mmrpc(ctx, request, trade_preimage_rpc).await, "trezor_connection_status" => handle_mmrpc(ctx, request, trezor_connection_status).await, "update_nft" => handle_mmrpc(ctx, request, update_nft).await, From 037a9dd44bdf50a17610dc1fe35d39b610ee7546 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 15 Jan 2025 12:59:11 +0300 Subject: [PATCH 02/13] improve delegate implementation significantly Signed-off-by: onur-ozkan --- .../coins/rpc_command/tendermint/staking.rs | 68 +++++++++++-- mm2src/coins/tendermint/tendermint_coin.rs | 99 +++++++++++-------- mm2src/coins/tendermint/tendermint_token.rs | 4 +- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 2 +- 4 files changed, 123 insertions(+), 50 deletions(-) diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 42f14cde0d..10d50b8777 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -4,7 +4,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; use mm2_number::BigDecimal; -use crate::{lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum, TransactionDetails, WithdrawFee}; +use crate::{hd_wallet::WithdrawFrom, lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum, + TransactionDetails, WithdrawFee}; /// Represents current status of the validator. #[derive(Default, Deserialize)] @@ -156,16 +157,71 @@ pub struct DelegationRPC { pub validator_address: String, #[serde(default)] pub amount: BigDecimal, + pub withdraw_from: Option, #[serde(default)] pub max: bool, pub fee: Option, + pub memo: Option, } -pub async fn delegation_rpc(ctx: MmArc, req: DelegationRPC) -> Result> { +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType, PartialEq)] +#[serde(tag = "error_type", content = "error_data")] +pub enum DelegationRPCError { + #[display(fmt = "Coin '{ticker}' could not be found in coins configuration.")] + CoinNotFound { ticker: String }, + #[display(fmt = "'{ticker}' is not a native staking token.")] + UnexpectedCoinType { ticker: String }, + #[display(fmt = "Invalid validator address '{}'", address)] + InvalidValidatorAddress { address: String }, + #[display( + fmt = "Not enough {} to withdraw: available {}, required at least {}", + coin, + available, + required + )] + NotSufficientBalance { + coin: String, + available: BigDecimal, + required: BigDecimal, + }, + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for DelegationRPCError { + fn status_code(&self) -> common::StatusCode { + match self { + DelegationRPCError::CoinNotFound { .. } => StatusCode::NOT_FOUND, + DelegationRPCError::UnexpectedCoinType { .. } + | DelegationRPCError::InvalidValidatorAddress { .. } + | DelegationRPCError::NotSufficientBalance { .. } => StatusCode::BAD_REQUEST, + DelegationRPCError::Transport(_) => StatusCode::SERVICE_UNAVAILABLE, + DelegationRPCError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for DelegationRPCError { + fn from(e: TendermintCoinRpcError) -> Self { + match e { + TendermintCoinRpcError::InvalidResponse(e) + | TendermintCoinRpcError::PerformError(e) + | TendermintCoinRpcError::RpcClientError(e) => DelegationRPCError::Transport(e), + TendermintCoinRpcError::Prost(e) | TendermintCoinRpcError::InternalError(e) => DelegationRPCError::InternalError(e), + TendermintCoinRpcError::UnexpectedAccountType { .. } => DelegationRPCError::InternalError( + "RPC client got an unexpected error 'TendermintCoinRpcError::UnexpectedAccountType', this isn't normal." + .into(), + ), + } + } +} + +pub async fn delegation_rpc(ctx: MmArc, req: DelegationRPC) -> Result> { match lp_coinfind_or_err(&ctx, &req.coin).await { - Ok(MmCoinEnum::Tendermint(coin)) => Ok(coin.delegate(req).await.unwrap()), - Ok(MmCoinEnum::TendermintToken(token)) => todo!(), - Ok(_) => todo!(), - Err(_) => todo!(), + Ok(MmCoinEnum::Tendermint(coin)) => coin.delegate(req).await, + Ok(_) => MmError::err(DelegationRPCError::UnexpectedCoinType { ticker: req.coin }), + Err(_) => MmError::err(DelegationRPCError::CoinNotFound { ticker: req.coin }), } } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index d34cefffef..65e00c38e5 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -6,7 +6,7 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; -use crate::rpc_command::tendermint::staking::{DelegationRPC, ValidatorStatus}; +use crate::rpc_command::tendermint::staking::{DelegationRPC, DelegationRPCError, ValidatorStatus}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -50,7 +50,7 @@ use cosmrs::proto::cosmos::staking::v1beta1::{QueryValidatorsRequest, use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; use cosmrs::proto::prost::{DecodeError, Message}; -use cosmrs::staking::{MsgDelegate, MsgUndelegate, QueryValidatorsResponse, Validator}; +use cosmrs::staking::{MsgDelegate, QueryValidatorsResponse, Validator}; use cosmrs::tendermint::block::Height; use cosmrs::tendermint::chain::Id as ChainId; use cosmrs::tendermint::PublicKey; @@ -105,7 +105,7 @@ const ABCI_REQUEST_PROVE: bool = false; /// 0.25 is good average gas price on atom and iris const DEFAULT_GAS_PRICE: f64 = 0.25; pub(super) const TIMEOUT_HEIGHT_DELTA: u64 = 100; -pub const GAS_LIMIT_DEFAULT: u64 = 225_000; +pub const GAS_LIMIT_DEFAULT: u64 = 125_000; pub const GAS_WANTED_BASE_VALUE: f64 = 50_000.; pub(crate) const TX_DEFAULT_MEMO: &str = ""; @@ -1152,7 +1152,7 @@ impl TendermintCoin { pub(super) fn account_id_and_pk_for_withdraw( &self, withdraw_from: Option, - ) -> Result<(AccountId, Option), WithdrawError> { + ) -> Result<(AccountId, Option), String> { if let TendermintActivationPolicy::PublicKey(_) = self.activation_policy { return Ok((self.account_id.clone(), None)); } @@ -1162,28 +1162,28 @@ impl TendermintCoin { let path_to_coin = self .activation_policy .path_to_coin_or_err() - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + .map_err(|e| e.to_string())?; let path_to_address = from .to_address_path(path_to_coin.coin_type()) - .map_err(|e| WithdrawError::InternalError(e.to_string()))? + .map_err(|e| e.to_string())? .to_derivation_path(path_to_coin) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + .map_err(|e| e.to_string())?; let priv_key = self .activation_policy .hd_wallet_derived_priv_key_or_err(&path_to_address) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + .map_err(|e| e.to_string())?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &self.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + let account_id = + account_id_from_privkey(priv_key.as_slice(), &self.account_prefix).map_err(|e| e.to_string())?; Ok((account_id, Some(priv_key))) }, None => { let activated_key = self .activation_policy .activated_key_or_err() - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + .map_err(|e| e.to_string())?; Ok((self.account_id.clone(), Some(*activated_key))) }, @@ -2125,33 +2125,30 @@ impl TendermintCoin { Ok(typed_response.validators) } - pub(crate) async fn delegate(&self, req: DelegationRPC) -> MmResult { + pub(crate) async fn delegate(&self, req: DelegationRPC) -> MmResult { let validator_address = - AccountId::from_str(&req.validator_address).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + AccountId::from_str(&req.validator_address).map_to_mm(|_| DelegationRPCError::InvalidValidatorAddress { + address: req.validator_address.clone(), + })?; - let (delegator_address, maybe_pk) = self.account_id_and_pk_for_withdraw(None)?; + let (delegator_address, maybe_pk) = self + .account_id_and_pk_for_withdraw(req.withdraw_from) + .map_err(DelegationRPCError::InternalError)?; - let (balance_denom, balance_dec) = self + let (balance_u64, balance_dec) = self .get_balance_as_unsigned_and_decimal(&delegator_address, &self.denom, self.decimals()) .await?; - let (amount_denom, amount_dec) = if req.max { - let amount_denom = balance_denom; - (amount_denom, big_decimal_from_sat_unsigned(amount_denom, self.decimals)) + let amount_u64 = if req.max { + balance_u64 } else { - (sat_from_big_decimal(&req.amount, self.decimals)?, req.amount.clone()) + sat_from_big_decimal(&req.amount, self.decimals) + .map_err(|e| DelegationRPCError::InternalError(e.to_string()))? }; - if !self.is_tx_amount_enough(self.decimals, &amount_dec) { - // return MmError::err(WithdrawError::AmountTooLow { - // amount: amount_dec, - // threshold: coin.min_tx_amount(), - // }); - } - let coin = Coin { denom: self.denom.clone(), - amount: amount_denom.into(), + amount: amount_u64.into(), }; let msg = MsgDelegate { @@ -2164,11 +2161,16 @@ impl TendermintCoin { .current_block() .compat() .await - .map_to_mm(WithdrawError::Transport)?; + .map_to_mm(DelegationRPCError::Transport)?; let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let (_, gas_limit) = self.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); + let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); + + // `delegate` uses more gas than the regular transactions + let gas_limit_default = (GAS_LIMIT_DEFAULT * 3) / 2; + + let (_, gas_limit) = self.gas_info_for_withdraw(&req.fee, gas_limit_default); let fee_amount_u64 = self .calculate_account_fee_amount_as_u64( @@ -2176,7 +2178,7 @@ impl TendermintCoin { maybe_pk, msg.clone().to_any().unwrap(), timeout_height, - String::default(), + memo.clone(), req.fee, ) .await?; @@ -2190,27 +2192,37 @@ impl TendermintCoin { let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); - let (amount_denom, total_amount) = if req.max { - if balance_denom < fee_amount_u64 { - return MmError::err(WithdrawError::NotSufficientBalance { + let (amount_u64, total_amount) = if req.max { + if balance_u64 < fee_amount_u64 { + return MmError::err(DelegationRPCError::NotSufficientBalance { coin: self.ticker.clone(), available: balance_dec, required: fee_amount_dec, }); } - let amount_denom = balance_denom - fee_amount_u64; - (amount_denom, balance_dec) + + let amount_u64 = balance_u64 - fee_amount_u64; + + (amount_u64, balance_dec) } else { let total = &req.amount + &fee_amount_dec; if balance_dec < total { - return MmError::err(WithdrawError::NotSufficientBalance { + return MmError::err(DelegationRPCError::NotSufficientBalance { coin: self.ticker.clone(), available: balance_dec, required: total, }); } - (sat_from_big_decimal(&req.amount, self.decimals)?, total) + let amount_u64 = sat_from_big_decimal(&req.amount, self.decimals) + .map_err(|e| DelegationRPCError::InternalError(e.to_string()))?; + + (amount_u64, total) + }; + + let coin = Coin { + denom: self.denom.clone(), + amount: amount_u64.into(), }; let msg = MsgDelegate { @@ -2218,13 +2230,14 @@ impl TendermintCoin { validator_address: validator_address.clone(), amount: coin.clone(), }; + let msg_payload = msg.to_any().unwrap(); let account_info = self.account_info(&delegator_address).await?; let tx = self - .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, "".into()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone()) + .map_to_mm(|e| DelegationRPCError::InternalError(e.to_string()))?; let internal_id = { let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); @@ -2250,8 +2263,8 @@ impl TendermintCoin { coin: self.ticker.to_string(), internal_id, kmd_rewards: None, - transaction_type: TransactionType::StandardTransfer, - memo: None, + transaction_type: TransactionType::StakingDelegation, + memo: Some(memo), }) } } @@ -2355,7 +2368,9 @@ impl MmCoin for TendermintCoin { let is_ibc_transfer = to_address.prefix() != coin.account_prefix || req.ibc_source_channel.is_some(); - let (account_id, maybe_pk) = coin.account_id_and_pk_for_withdraw(req.from)?; + let (account_id, maybe_pk) = coin + .account_id_and_pk_for_withdraw(req.from) + .map_err(WithdrawError::InternalError)?; let (balance_denom, balance_dec) = coin .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index e5cc90f895..058b6be462 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -511,7 +511,9 @@ impl MmCoin for TendermintToken { let is_ibc_transfer = to_address.prefix() != platform.account_prefix || req.ibc_source_channel.is_some(); - let (account_id, maybe_pk) = platform.account_id_and_pk_for_withdraw(req.from)?; + let (account_id, maybe_pk) = platform + .account_id_and_pk_for_withdraw(req.from) + .map_err(WithdrawError::InternalError)?; let (base_denom_balance, base_denom_balance_dec) = platform .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 4c40fce965..5bead769e9 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -23,7 +23,7 @@ use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; -use coins::rpc_command::tendermint::staking::{validators_rpc, delegation_rpc}; +use coins::rpc_command::tendermint::staking::{delegation_rpc, validators_rpc}; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, From 401255055ed7589b374e6f48aaef667986dd7b6f Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 15 Jan 2025 17:45:18 +0300 Subject: [PATCH 03/13] consolidate QTUM and Cosmos staking Signed-off-by: onur-ozkan --- mm2src/coins/hd_wallet/withdraw_ops.rs | 2 +- mm2src/coins/lp_coins.rs | 30 +++++--- .../coins/rpc_command/tendermint/staking.rs | 69 +------------------ mm2src/coins/tendermint/tendermint_coin.rs | 55 ++++++++------- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 3 +- 5 files changed, 54 insertions(+), 105 deletions(-) diff --git a/mm2src/coins/hd_wallet/withdraw_ops.rs b/mm2src/coins/hd_wallet/withdraw_ops.rs index 7f1aa8b19c..b7bd7e04da 100644 --- a/mm2src/coins/hd_wallet/withdraw_ops.rs +++ b/mm2src/coins/hd_wallet/withdraw_ops.rs @@ -10,7 +10,7 @@ type HDCoinPubKey = <<<::HDWallet as HDWalletOps>::HDAccount as HDAccountOps>::HDAddress as HDAddressOps>::Pubkey; /// Represents the source of the funds for a withdrawal operation. -#[derive(Clone, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum WithdrawFrom { /// The address id of the sender address which is specified by the account id, chain, and address id. diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 16ade17a6b..ce23c3bfbe 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2135,6 +2135,7 @@ pub struct WithdrawRequest { #[serde(tag = "type")] pub enum StakingDetails { Qtum(QtumDelegationRequest), + Cosmos(Box), } #[allow(dead_code)] @@ -4878,17 +4879,26 @@ pub async fn get_staking_infos(ctx: MmArc, req: GetStakingInfosRequest) -> Staki pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationResult { let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - // Need to find a way to do a proper dispatch - let coin_concrete = match coin { - MmCoinEnum::QtumCoin(qtum) => qtum, - _ => { - return MmError::err(DelegationError::CoinDoesntSupportDelegation { - coin: coin.ticker().to_string(), - }) - }, - }; + match req.staking_details { - StakingDetails::Qtum(qtum_staking) => coin_concrete.add_delegation(qtum_staking).compat().await, + StakingDetails::Qtum(req) => { + let MmCoinEnum::QtumCoin(qtum) = coin else { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }); + }; + + qtum.add_delegation(req).compat().await + }, + StakingDetails::Cosmos(req) => { + let MmCoinEnum::Tendermint(tendermint) = coin else { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }); + }; + + tendermint.add_delegate(*req).await + }, } } diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 10d50b8777..85c2a9d704 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -4,8 +4,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; use mm2_number::BigDecimal; -use crate::{hd_wallet::WithdrawFrom, lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum, - TransactionDetails, WithdrawFee}; +use crate::{hd_wallet::WithdrawFrom, lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum, WithdrawFee}; /// Represents current status of the validator. #[derive(Default, Deserialize)] @@ -151,8 +150,8 @@ pub async fn validators_rpc( }) } -#[derive(Clone, Deserialize)] -pub struct DelegationRPC { +#[derive(Clone, Debug, Deserialize)] +pub struct DelegatePayload { pub coin: String, pub validator_address: String, #[serde(default)] @@ -163,65 +162,3 @@ pub struct DelegationRPC { pub fee: Option, pub memo: Option, } - -#[derive(Clone, Debug, Display, Serialize, SerializeErrorType, PartialEq)] -#[serde(tag = "error_type", content = "error_data")] -pub enum DelegationRPCError { - #[display(fmt = "Coin '{ticker}' could not be found in coins configuration.")] - CoinNotFound { ticker: String }, - #[display(fmt = "'{ticker}' is not a native staking token.")] - UnexpectedCoinType { ticker: String }, - #[display(fmt = "Invalid validator address '{}'", address)] - InvalidValidatorAddress { address: String }, - #[display( - fmt = "Not enough {} to withdraw: available {}, required at least {}", - coin, - available, - required - )] - NotSufficientBalance { - coin: String, - available: BigDecimal, - required: BigDecimal, - }, - #[display(fmt = "Transport error: {}", _0)] - Transport(String), - #[display(fmt = "Internal error: {}", _0)] - InternalError(String), -} - -impl HttpStatusCode for DelegationRPCError { - fn status_code(&self) -> common::StatusCode { - match self { - DelegationRPCError::CoinNotFound { .. } => StatusCode::NOT_FOUND, - DelegationRPCError::UnexpectedCoinType { .. } - | DelegationRPCError::InvalidValidatorAddress { .. } - | DelegationRPCError::NotSufficientBalance { .. } => StatusCode::BAD_REQUEST, - DelegationRPCError::Transport(_) => StatusCode::SERVICE_UNAVAILABLE, - DelegationRPCError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for DelegationRPCError { - fn from(e: TendermintCoinRpcError) -> Self { - match e { - TendermintCoinRpcError::InvalidResponse(e) - | TendermintCoinRpcError::PerformError(e) - | TendermintCoinRpcError::RpcClientError(e) => DelegationRPCError::Transport(e), - TendermintCoinRpcError::Prost(e) | TendermintCoinRpcError::InternalError(e) => DelegationRPCError::InternalError(e), - TendermintCoinRpcError::UnexpectedAccountType { .. } => DelegationRPCError::InternalError( - "RPC client got an unexpected error 'TendermintCoinRpcError::UnexpectedAccountType', this isn't normal." - .into(), - ), - } - } -} - -pub async fn delegation_rpc(ctx: MmArc, req: DelegationRPC) -> Result> { - match lp_coinfind_or_err(&ctx, &req.coin).await { - Ok(MmCoinEnum::Tendermint(coin)) => coin.delegate(req).await, - Ok(_) => MmError::err(DelegationRPCError::UnexpectedCoinType { ticker: req.coin }), - Err(_) => MmError::err(DelegationRPCError::CoinNotFound { ticker: req.coin }), - } -} diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 65e00c38e5..d167ef6733 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -6,7 +6,7 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; -use crate::rpc_command::tendermint::staking::{DelegationRPC, DelegationRPCError, ValidatorStatus}; +use crate::rpc_command::tendermint::staking::{DelegatePayload, ValidatorStatus}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -15,20 +15,21 @@ use crate::tendermint::ibc::IBC_OUT_SOURCE_PORT; use crate::utxo::sat_from_big_decimal; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, - CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, - HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, - PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicy, - PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, - RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, RpcCommonOps, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, - SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, ToBytes, TradeFee, - TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, - TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, - TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, - ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; + CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DelegationError, DexFee, FeeApproxStage, + FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, + NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, + PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, + RawTransactionRequest, RawTransactionRes, RawTransactionResult, RefundError, RefundPaymentArgs, + RefundResult, RpcCommonOps, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, + SendPaymentArgs, SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, + TakerSwapMakerCoin, ToBytes, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TransactionData, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, + TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, + VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; use bip32::DerivationPath; @@ -457,6 +458,10 @@ impl From for WithdrawError { fn from(err: TendermintCoinRpcError) -> Self { WithdrawError::Transport(err.to_string()) } } +impl From for DelegationError { + fn from(err: TendermintCoinRpcError) -> Self { DelegationError::Transport(err.to_string()) } +} + impl From for BalanceError { fn from(err: TendermintCoinRpcError) -> Self { match err { @@ -2125,15 +2130,13 @@ impl TendermintCoin { Ok(typed_response.validators) } - pub(crate) async fn delegate(&self, req: DelegationRPC) -> MmResult { + pub(crate) async fn add_delegate(&self, req: DelegatePayload) -> MmResult { let validator_address = - AccountId::from_str(&req.validator_address).map_to_mm(|_| DelegationRPCError::InvalidValidatorAddress { - address: req.validator_address.clone(), - })?; + AccountId::from_str(&req.validator_address).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; let (delegator_address, maybe_pk) = self .account_id_and_pk_for_withdraw(req.withdraw_from) - .map_err(DelegationRPCError::InternalError)?; + .map_err(DelegationError::InternalError)?; let (balance_u64, balance_dec) = self .get_balance_as_unsigned_and_decimal(&delegator_address, &self.denom, self.decimals()) @@ -2143,7 +2146,7 @@ impl TendermintCoin { balance_u64 } else { sat_from_big_decimal(&req.amount, self.decimals) - .map_err(|e| DelegationRPCError::InternalError(e.to_string()))? + .map_err(|e| DelegationError::InternalError(e.to_string()))? }; let coin = Coin { @@ -2161,7 +2164,7 @@ impl TendermintCoin { .current_block() .compat() .await - .map_to_mm(DelegationRPCError::Transport)?; + .map_to_mm(DelegationError::Transport)?; let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; @@ -2194,7 +2197,7 @@ impl TendermintCoin { let (amount_u64, total_amount) = if req.max { if balance_u64 < fee_amount_u64 { - return MmError::err(DelegationRPCError::NotSufficientBalance { + return MmError::err(DelegationError::NotSufficientBalance { coin: self.ticker.clone(), available: balance_dec, required: fee_amount_dec, @@ -2207,7 +2210,7 @@ impl TendermintCoin { } else { let total = &req.amount + &fee_amount_dec; if balance_dec < total { - return MmError::err(DelegationRPCError::NotSufficientBalance { + return MmError::err(DelegationError::NotSufficientBalance { coin: self.ticker.clone(), available: balance_dec, required: total, @@ -2215,7 +2218,7 @@ impl TendermintCoin { } let amount_u64 = sat_from_big_decimal(&req.amount, self.decimals) - .map_err(|e| DelegationRPCError::InternalError(e.to_string()))?; + .map_err(|e| DelegationError::InternalError(e.to_string()))?; (amount_u64, total) }; @@ -2237,7 +2240,7 @@ impl TendermintCoin { let tx = self .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone()) - .map_to_mm(|e| DelegationRPCError::InternalError(e.to_string()))?; + .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; let internal_id = { let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 5bead769e9..fd9babd2c5 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -23,7 +23,7 @@ use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; -use coins::rpc_command::tendermint::staking::{delegation_rpc, validators_rpc}; +use coins::rpc_command::tendermint::staking::validators_rpc; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, @@ -214,7 +214,6 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, stop_simple_market_maker_bot).await, "stop_version_stat_collection" => handle_mmrpc(ctx, request, stop_version_stat_collection).await, "tendermint_validators" => handle_mmrpc(ctx, request, validators_rpc).await, - "tendermint_delegation" => handle_mmrpc(ctx, request, delegation_rpc).await, "trade_preimage" => handle_mmrpc(ctx, request, trade_preimage_rpc).await, "trezor_connection_status" => handle_mmrpc(ctx, request, trezor_connection_status).await, "update_nft" => handle_mmrpc(ctx, request, update_nft).await, From c99ca550d04033199f7585f4041806684b164d6a Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 15 Jan 2025 18:07:14 +0300 Subject: [PATCH 04/13] remove `coin` field from `DelegatePayload` Signed-off-by: onur-ozkan --- mm2src/coins/rpc_command/tendermint/staking.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 85c2a9d704..187eaa1d26 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -152,13 +152,12 @@ pub async fn validators_rpc( #[derive(Clone, Debug, Deserialize)] pub struct DelegatePayload { - pub coin: String, pub validator_address: String, + pub fee: Option, + pub memo: Option, + pub withdraw_from: Option, #[serde(default)] pub amount: BigDecimal, - pub withdraw_from: Option, #[serde(default)] pub max: bool, - pub fee: Option, - pub memo: Option, } From d2a611113d7b6ca740f474acafc29e01a243f1ff Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 15 Jan 2025 19:54:47 +0300 Subject: [PATCH 05/13] minor readibility improvements Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 77 +++++++++++++--------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index d167ef6733..39f5c77825 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2131,6 +2131,21 @@ impl TendermintCoin { } pub(crate) async fn add_delegate(&self, req: DelegatePayload) -> MmResult { + fn generate_message( + delegator_address: AccountId, + validator_address: AccountId, + denom: Denom, + amount: u128, + ) -> Result { + MsgDelegate { + delegator_address, + validator_address, + amount: Coin { denom, amount }, + } + .to_any() + .map_err(|e| e.to_string()) + } + let validator_address = AccountId::from_str(&req.validator_address).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; @@ -2149,16 +2164,14 @@ impl TendermintCoin { .map_err(|e| DelegationError::InternalError(e.to_string()))? }; - let coin = Coin { - denom: self.denom.clone(), - amount: amount_u64.into(), - }; - - let msg = MsgDelegate { - delegator_address: delegator_address.clone(), - validator_address: validator_address.clone(), - amount: coin.clone(), - }; + // This is used for transaction simulation so we can predict the best possible fee amount. + let msg_for_fee_prediction = generate_message( + delegator_address.clone(), + validator_address.clone(), + self.denom.clone(), + amount_u64.into(), + ) + .map_err(DelegationError::InternalError)?; let current_block = self .current_block() @@ -2172,14 +2185,13 @@ impl TendermintCoin { // `delegate` uses more gas than the regular transactions let gas_limit_default = (GAS_LIMIT_DEFAULT * 3) / 2; - let (_, gas_limit) = self.gas_info_for_withdraw(&req.fee, gas_limit_default); let fee_amount_u64 = self .calculate_account_fee_amount_as_u64( &delegator_address, maybe_pk, - msg.clone().to_any().unwrap(), + msg_for_fee_prediction, timeout_height, memo.clone(), req.fee, @@ -2188,12 +2200,13 @@ impl TendermintCoin { let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, self.decimals()); - let fee_amount = Coin { - denom: self.denom.clone(), - amount: fee_amount_u64.into(), - }; - - let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); + let fee = Fee::from_amount_and_gas( + Coin { + denom: self.denom.clone(), + amount: fee_amount_u64.into(), + }, + gas_limit, + ); let (amount_u64, total_amount) = if req.max { if balance_u64 < fee_amount_u64 { @@ -2223,23 +2236,25 @@ impl TendermintCoin { (amount_u64, total) }; - let coin = Coin { - denom: self.denom.clone(), - amount: amount_u64.into(), - }; - - let msg = MsgDelegate { - delegator_address: delegator_address.clone(), - validator_address: validator_address.clone(), - amount: coin.clone(), - }; - - let msg_payload = msg.to_any().unwrap(); + let msg_for_actual_tx = generate_message( + delegator_address.clone(), + validator_address.clone(), + self.denom.clone(), + amount_u64.into(), + ) + .map_err(DelegationError::InternalError)?; let account_info = self.account_info(&delegator_address).await?; let tx = self - .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone()) + .any_to_transaction_data( + maybe_pk, + msg_for_actual_tx, + &account_info, + fee, + timeout_height, + memo.clone(), + ) .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; let internal_id = { From 15948b00c2c340b5613f17b207fa8e3d0f7903eb Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 16 Jan 2025 10:49:08 +0300 Subject: [PATCH 06/13] support `delegate` transactions in tx_history Signed-off-by: onur-ozkan --- .../tendermint/tendermint_tx_history_v2.rs | 171 +++++++++++------- 1 file changed, 108 insertions(+), 63 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 3dc95e7443..1dff74d3d2 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -37,6 +37,7 @@ const CLAIM_HTLC_EVENT: &str = "claim_htlc"; const IBC_SEND_EVENT: &str = "ibc_transfer"; const IBC_RECEIVE_EVENT: &str = "fungible_token_packet"; const IBC_NFT_RECEIVE_EVENT: &str = "non_fungible_token_packet"; +const DELEGATE_EVENT: &str = "delegate"; const ACCEPTED_EVENTS: &[&str] = &[ TRANSFER_EVENT, @@ -45,6 +46,7 @@ const ACCEPTED_EVENTS: &[&str] = &[ IBC_SEND_EVENT, IBC_RECEIVE_EVENT, IBC_NFT_RECEIVE_EVENT, + DELEGATE_EVENT, ]; const RECEIVER_TAG_KEY: &str = "receiver"; @@ -56,6 +58,12 @@ const RECIPIENT_TAG_KEY_BASE64: &str = "cmVjaXBpZW50"; const SENDER_TAG_KEY: &str = "sender"; const SENDER_TAG_KEY_BASE64: &str = "c2VuZGVy"; +const DELEGATOR_TAG_KEY: &str = "delegator"; +const DELEGATOR_TAG_KEY_BASE64: &str = "ZGVsZWdhdG9y"; + +const VALIDATOR_TAG_KEY: &str = "validator"; +const VALIDATOR_TAG_KEY_BASE64: &str = "dmFsaWRhdG9y"; + const AMOUNT_TAG_KEY: &str = "amount"; const AMOUNT_TAG_KEY_BASE64: &str = "YW1vdW50"; @@ -403,6 +411,7 @@ where ClaimHtlc, IBCSend, IBCReceive, + Delegate, } #[derive(Clone)] @@ -470,77 +479,109 @@ where let mut transfer_details_list: Vec = vec![]; for event in tx_events.iter() { - if event.kind.as_str() == TRANSFER_EVENT { - let amount_with_denoms = some_or_continue!(get_value_from_event_attributes( - &event.attributes, - AMOUNT_TAG_KEY, - AMOUNT_TAG_KEY_BASE64 - )); - - let amount_with_denoms = amount_with_denoms.split(','); - - for amount_with_denom in amount_with_denoms { - let extracted_amount: String = - amount_with_denom.chars().take_while(|c| c.is_numeric()).collect(); - let denom = &amount_with_denom[extracted_amount.len()..]; - let amount = some_or_continue!(extracted_amount.parse().ok()); - - let from = some_or_continue!(get_value_from_event_attributes( - &event.attributes, - SENDER_TAG_KEY, - SENDER_TAG_KEY_BASE64 - )); - - let to = some_or_continue!(get_value_from_event_attributes( - &event.attributes, - RECIPIENT_TAG_KEY, - RECIPIENT_TAG_KEY_BASE64, - )); - - let mut tx_details = TransferDetails { - from, - to, - denom: denom.to_owned(), - amount, - // Default is Standard, can be changed later in read_real_htlc_addresses - transfer_event_type: TransferEventType::default(), - }; + let amount_with_denoms = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + AMOUNT_TAG_KEY, + AMOUNT_TAG_KEY_BASE64 + )); - // For HTLC transactions, the sender and receiver addresses in the "transfer" event will be incorrect. - // Use `read_real_htlc_addresses` to handle them properly. - if let Some(htlc_event) = tx_events - .iter() - .find(|e| [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&e.kind.as_str())) - { - read_real_htlc_addresses(&mut tx_details, htlc_event); - } - // For IBC transactions, the sender and receiver addresses in the "transfer" event will be incorrect. - // Use `read_real_ibc_addresses` to handle them properly. - else if let Some(ibc_event) = tx_events.iter().find(|e| { - [IBC_SEND_EVENT, IBC_RECEIVE_EVENT, IBC_NFT_RECEIVE_EVENT].contains(&e.kind.as_str()) - }) { - read_real_ibc_addresses(&mut tx_details, ibc_event); - } + let amount_with_denoms = amount_with_denoms.split(','); + for amount_with_denom in amount_with_denoms { + let extracted_amount: String = amount_with_denom.chars().take_while(|c| c.is_numeric()).collect(); + let denom = &amount_with_denom[extracted_amount.len()..]; + let amount = some_or_continue!(extracted_amount.parse().ok()); + + match event.kind.as_str() { + TRANSFER_EVENT => { + let from = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + SENDER_TAG_KEY, + SENDER_TAG_KEY_BASE64 + )); + + let to = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + RECIPIENT_TAG_KEY, + RECIPIENT_TAG_KEY_BASE64, + )); + + let mut tx_details = TransferDetails { + from, + to, + denom: denom.to_owned(), + amount, + // Default is Standard, can be changed later in read_real_htlc_addresses + transfer_event_type: TransferEventType::default(), + }; + + // For HTLC transactions, the sender and receiver addresses in the "transfer" event will be incorrect. + // Use `read_real_htlc_addresses` to handle them properly. + if let Some(htlc_event) = tx_events + .iter() + .find(|e| [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&e.kind.as_str())) + { + read_real_htlc_addresses(&mut tx_details, htlc_event); + } + // For IBC transactions, the sender and receiver addresses in the "transfer" event will be incorrect. + // Use `read_real_ibc_addresses` to handle them properly. + else if let Some(ibc_event) = tx_events.iter().find(|e| { + [IBC_SEND_EVENT, IBC_RECEIVE_EVENT, IBC_NFT_RECEIVE_EVENT].contains(&e.kind.as_str()) + }) { + read_real_ibc_addresses(&mut tx_details, ibc_event); + } + + handle_new_transfer_event(&mut transfer_details_list, tx_details); + }, - // sum the amounts coins and pairs are same - let mut duplicated_details = transfer_details_list.iter_mut().find(|details| { - details.from == tx_details.from - && details.to == tx_details.to - && details.denom == tx_details.denom - }); + DELEGATE_EVENT => { + let from = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + DELEGATOR_TAG_KEY, + DELEGATOR_TAG_KEY_BASE64, + )); + + let to = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + VALIDATOR_TAG_KEY, + VALIDATOR_TAG_KEY_BASE64, + )); + + let tx_details = TransferDetails { + from, + to, + denom: denom.to_owned(), + amount, + transfer_event_type: TransferEventType::Delegate, + }; + + handle_new_transfer_event(&mut transfer_details_list, tx_details); + }, - if let Some(duplicated_details) = &mut duplicated_details { - duplicated_details.amount += tx_details.amount; - } else { - transfer_details_list.push(tx_details); - } - } + _ => { + todo!() + }, + }; } } transfer_details_list } + fn handle_new_transfer_event(transfer_details_list: &mut Vec, new_transfer: TransferDetails) { + let mut existing_transfer = transfer_details_list.iter_mut().find(|details| { + details.from == new_transfer.from + && details.to == new_transfer.to + && details.denom == new_transfer.denom + }); + + if let Some(existing_transfer) = &mut existing_transfer { + // Handle multi-amount transfer events + existing_transfer.amount += new_transfer.amount; + } else { + transfer_details_list.push(new_transfer); + } + } + fn get_transfer_details(tx_events: Vec, fee_amount_with_denom: String) -> Vec { // Filter out irrelevant events let mut events: Vec<&Event> = tx_events @@ -584,6 +625,7 @@ where }, token_id, }, + (TransferEventType::Delegate, _) => TransactionType::StakingDelegation, (_, Some(token_id)) => TransactionType::TokenTransfer(token_id), _ => TransactionType::StandardTransfer, } @@ -604,7 +646,10 @@ where } }, TransferEventType::ClaimHtlc => Some((vec![my_address], vec![])), - TransferEventType::Standard | TransferEventType::IBCSend | TransferEventType::IBCReceive => { + TransferEventType::Standard + | TransferEventType::IBCSend + | TransferEventType::IBCReceive + | TransferEventType::Delegate => { Some((vec![transfer_details.from.clone()], vec![transfer_details.to.clone()])) }, } From c42fc6ede4cfd0b9b473c1193bbab238943e7200 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 16 Jan 2025 10:51:46 +0300 Subject: [PATCH 07/13] warn on unrecognized events Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_tx_history_v2.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 1dff74d3d2..b170f08b81 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -557,8 +557,10 @@ where handle_new_transfer_event(&mut transfer_details_list, tx_details); }, - _ => { - todo!() + unrecognized => { + log::warn!( + "Found an unrecognized event '{unrecognized}' in transaction history processing." + ); }, }; } From 6e5e145519f7305661087175071093a29a59557b Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 16 Jan 2025 11:17:33 +0300 Subject: [PATCH 08/13] rename pk to priv_key Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 29 +++++++++++++-------- mm2src/coins/tendermint/tendermint_token.rs | 15 ++++++++--- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 39f5c77825..b7dc13f3fc 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -1154,7 +1154,7 @@ impl TendermintCoin { } #[allow(clippy::result_large_err)] - pub(super) fn account_id_and_pk_for_withdraw( + pub(super) fn extract_account_id_and_private_key( &self, withdraw_from: Option, ) -> Result<(AccountId, Option), String> { @@ -1197,14 +1197,14 @@ impl TendermintCoin { pub(super) fn any_to_transaction_data( &self, - maybe_pk: Option, + maybe_priv_key: Option, message: Any, account_info: &BaseAccount, fee: Fee, timeout_height: u64, memo: String, ) -> Result { - if let Some(priv_key) = maybe_pk { + if let Some(priv_key) = maybe_priv_key { let tx_raw = self.any_to_signed_raw_tx(&priv_key, account_info, message, fee, timeout_height, memo)?; let tx_bytes = tx_raw.to_bytes()?; let hash = sha256(&tx_bytes); @@ -2149,8 +2149,8 @@ impl TendermintCoin { let validator_address = AccountId::from_str(&req.validator_address).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; - let (delegator_address, maybe_pk) = self - .account_id_and_pk_for_withdraw(req.withdraw_from) + let (delegator_address, maybe_priv_key) = self + .extract_account_id_and_private_key(req.withdraw_from) .map_err(DelegationError::InternalError)?; let (balance_u64, balance_dec) = self @@ -2190,7 +2190,7 @@ impl TendermintCoin { let fee_amount_u64 = self .calculate_account_fee_amount_as_u64( &delegator_address, - maybe_pk, + maybe_priv_key, msg_for_fee_prediction, timeout_height, memo.clone(), @@ -2248,7 +2248,7 @@ impl TendermintCoin { let tx = self .any_to_transaction_data( - maybe_pk, + maybe_priv_key, msg_for_actual_tx, &account_info, fee, @@ -2386,8 +2386,8 @@ impl MmCoin for TendermintCoin { let is_ibc_transfer = to_address.prefix() != coin.account_prefix || req.ibc_source_channel.is_some(); - let (account_id, maybe_pk) = coin - .account_id_and_pk_for_withdraw(req.from) + let (account_id, maybe_priv_key) = coin + .extract_account_id_and_private_key(req.from) .map_err(WithdrawError::InternalError)?; let (balance_denom, balance_dec) = coin @@ -2451,7 +2451,7 @@ impl MmCoin for TendermintCoin { let fee_amount_u64 = coin .calculate_account_fee_amount_as_u64( &account_id, - maybe_pk, + maybe_priv_key, msg_payload.clone(), timeout_height, memo.clone(), @@ -2514,7 +2514,14 @@ impl MmCoin for TendermintCoin { let account_info = coin.account_info(&account_id).await?; let tx = coin - .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone()) + .any_to_transaction_data( + maybe_priv_key, + msg_payload, + &account_info, + fee, + timeout_height, + memo.clone(), + ) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let internal_id = { diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 058b6be462..2880688e9b 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -511,8 +511,8 @@ impl MmCoin for TendermintToken { let is_ibc_transfer = to_address.prefix() != platform.account_prefix || req.ibc_source_channel.is_some(); - let (account_id, maybe_pk) = platform - .account_id_and_pk_for_withdraw(req.from) + let (account_id, maybe_priv_key) = platform + .extract_account_id_and_private_key(req.from) .map_err(WithdrawError::InternalError)?; let (base_denom_balance, base_denom_balance_dec) = platform @@ -594,7 +594,7 @@ impl MmCoin for TendermintToken { let fee_amount_u64 = platform .calculate_account_fee_amount_as_u64( &account_id, - maybe_pk, + maybe_priv_key, msg_payload.clone(), timeout_height, memo.clone(), @@ -622,7 +622,14 @@ impl MmCoin for TendermintToken { let account_info = platform.account_info(&account_id).await?; let tx = platform - .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone()) + .any_to_transaction_data( + maybe_priv_key, + msg_payload, + &account_info, + fee, + timeout_height, + memo.clone(), + ) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let internal_id = { From 1dd2b7365905a5cd544b377ed14906bd402930b1 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 16 Jan 2025 13:46:13 +0300 Subject: [PATCH 09/13] add docker test Signed-off-by: onur-ozkan --- .../tests/docker_tests/tendermint_tests.rs | 39 ++++++++++++++++++- mm2src/mm2_test_helpers/src/for_tests.rs | 30 ++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index c602c93662..79d1247401 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -5,9 +5,10 @@ use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_ enable_tendermint_token, enable_tendermint_without_balance, get_tendermint_my_tx_history, ibc_withdraw, iris_ibc_nucleus_testnet_conf, my_balance, nucleus_testnet_conf, orderbook, orderbook_v2, send_raw_transaction, - set_price, tendermint_validators, withdraw_v1, MarketMakerIt, Mm2TestConf}; + set_price, tendermint_add_delegation, tendermint_validators, withdraw_v1, + MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, OrderbookAddress, OrderbookV2Response, RpcV2Response, - TendermintActivationResult, TransactionDetails}; + TendermintActivationResult, TransactionDetails, TransactionType}; use serde_json::json; use std::collections::HashSet; use std::iter::FromIterator; @@ -677,6 +678,40 @@ fn test_tendermint_validators_rpc() { assert_eq!(validators_raw_response["result"]["validators"][0]["jailed"], false); } +#[test] +fn test_tendermint_add_delegation() { + const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; + const VALIDATOR_ADDRESS: &str = "nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu"; + + let coins = json!([nucleus_testnet_conf()]); + let coin_ticker = coins[0]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode(TENDERMINT_TEST_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_tendermint( + &mm, + coin_ticker, + &[], + NUCLEUS_TESTNET_RPC_URLS, + false, + )); + + log!( + "Activation with assets {}", + serde_json::to_string(&activation_res).unwrap() + ); + + let tx_details = block_on(tendermint_add_delegation(&mm, coin_ticker, VALIDATOR_ADDRESS, "0.5")); + + assert_eq!(tx_details.to, vec![VALIDATOR_ADDRESS.to_owned()]); + assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); + assert_eq!(tx_details.transaction_type, TransactionType::StakingDelegation); + + let send_raw_tx = block_on(send_raw_transaction(&mm, coin_ticker, &tx_details.tx_hex)); + log!("Send raw tx {}", serde_json::to_string(&send_raw_tx).unwrap()); +} + mod swap { use super::*; diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index b367c4653c..d1db770ea2 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -3118,6 +3118,36 @@ pub async fn tendermint_validators( json::from_str(&response.1).unwrap() } +pub async fn tendermint_add_delegation( + mm: &MarketMakerIt, + coin: &str, + validator_address: &str, + amount: &str, +) -> TransactionDetails { + let rpc_endpoint = "add_delegation"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "coin": coin, + "staking_details": { + "type": "Cosmos", + "validator_address": validator_address, + "amount": amount, + } + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let response = mm.rpc(&request).await.unwrap(); + assert_eq!(response.0, StatusCode::OK, "{rpc_endpoint} failed: {}", response.1); + log!("{rpc_endpoint} response {}", response.1); + + let json: Json = json::from_str(&response.1).unwrap(); + json::from_value(json["result"].clone()).unwrap() +} + pub async fn init_utxo_electrum( mm: &MarketMakerIt, coin: &str, From 0f08e053515ce3360db5a7199b590ebf21fc6af6 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 20 Jan 2025 13:04:10 +0300 Subject: [PATCH 10/13] improve amount calculation slightly Signed-off-by: onur-ozkan --- .../coins/rpc_command/tendermint/staking.rs | 3 +- mm2src/coins/tendermint/tendermint_coin.rs | 92 +++++++++++-------- 2 files changed, 58 insertions(+), 37 deletions(-) diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 187eaa1d26..e2d50ad9c0 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -154,9 +154,10 @@ pub async fn validators_rpc( pub struct DelegatePayload { pub validator_address: String, pub fee: Option, - pub memo: Option, pub withdraw_from: Option, #[serde(default)] + pub memo: String, + #[serde(default)] pub amount: BigDecimal, #[serde(default)] pub max: bool, diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index b7dc13f3fc..9db16d1bc4 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2146,6 +2146,47 @@ impl TendermintCoin { .map_err(|e| e.to_string()) } + /// Calculates the send and total amounts. + /// + /// The send amount is what the receiver receives, while the total amount is what sender + /// pays including the transaction fee. + fn calc_send_and_total_amount( + coin: &TendermintCoin, + balance_u64: u64, + balance_decimal: BigDecimal, + fee_u64: u64, + fee_decimal: BigDecimal, + request_amount: BigDecimal, + is_max: bool, + ) -> Result<(u64, BigDecimal), DelegationError> { + if is_max { + if balance_u64 < fee_u64 { + return Err(DelegationError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: balance_decimal, + required: fee_decimal, + }); + } + + let amount_u64 = balance_u64 - fee_u64; + return Ok((amount_u64, balance_decimal)); + } + + let total = &request_amount + &fee_decimal; + if balance_decimal < total { + return Err(DelegationError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: balance_decimal, + required: total, + }); + } + + let amount_u64 = sat_from_big_decimal(&request_amount, coin.decimals) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + Ok((amount_u64, total)) + } + let validator_address = AccountId::from_str(&req.validator_address).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; @@ -2173,15 +2214,12 @@ impl TendermintCoin { ) .map_err(DelegationError::InternalError)?; - let current_block = self + let timeout_height = self .current_block() .compat() .await - .map_to_mm(DelegationError::Transport)?; - - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - - let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); + .map_to_mm(DelegationError::Transport)? + + TIMEOUT_HEIGHT_DELTA; // `delegate` uses more gas than the regular transactions let gas_limit_default = (GAS_LIMIT_DEFAULT * 3) / 2; @@ -2193,7 +2231,7 @@ impl TendermintCoin { maybe_priv_key, msg_for_fee_prediction, timeout_height, - memo.clone(), + req.memo.clone(), req.fee, ) .await?; @@ -2208,33 +2246,15 @@ impl TendermintCoin { gas_limit, ); - let (amount_u64, total_amount) = if req.max { - if balance_u64 < fee_amount_u64 { - return MmError::err(DelegationError::NotSufficientBalance { - coin: self.ticker.clone(), - available: balance_dec, - required: fee_amount_dec, - }); - } - - let amount_u64 = balance_u64 - fee_amount_u64; - - (amount_u64, balance_dec) - } else { - let total = &req.amount + &fee_amount_dec; - if balance_dec < total { - return MmError::err(DelegationError::NotSufficientBalance { - coin: self.ticker.clone(), - available: balance_dec, - required: total, - }); - } - - let amount_u64 = sat_from_big_decimal(&req.amount, self.decimals) - .map_err(|e| DelegationError::InternalError(e.to_string()))?; - - (amount_u64, total) - }; + let (amount_u64, total_amount) = calc_send_and_total_amount( + self, + balance_u64, + balance_dec, + fee_amount_u64, + fee_amount_dec.clone(), + req.amount, + req.max, + )?; let msg_for_actual_tx = generate_message( delegator_address.clone(), @@ -2253,7 +2273,7 @@ impl TendermintCoin { &account_info, fee, timeout_height, - memo.clone(), + req.memo.clone(), ) .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; @@ -2282,7 +2302,7 @@ impl TendermintCoin { internal_id, kmd_rewards: None, transaction_type: TransactionType::StakingDelegation, - memo: Some(memo), + memo: Some(req.memo), }) } } From 8d44f100980c3f0ff98e4facc31827ecabdb2839 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 23 Jan 2025 21:48:39 +0300 Subject: [PATCH 11/13] create helper closure for balance error Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 9db16d1bc4..81d9dca895 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -1153,7 +1153,6 @@ impl TendermintCoin { .map_to_mm(|e| TendermintCoinRpcError::InvalidResponse(format!("balance is not u64, err {}", e))) } - #[allow(clippy::result_large_err)] pub(super) fn extract_account_id_and_private_key( &self, withdraw_from: Option, @@ -2159,13 +2158,15 @@ impl TendermintCoin { request_amount: BigDecimal, is_max: bool, ) -> Result<(u64, BigDecimal), DelegationError> { + let not_sufficient = |required| DelegationError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: balance_decimal.clone(), + required, + }; + if is_max { if balance_u64 < fee_u64 { - return Err(DelegationError::NotSufficientBalance { - coin: coin.ticker.clone(), - available: balance_decimal, - required: fee_decimal, - }); + return Err(not_sufficient(fee_decimal)); } let amount_u64 = balance_u64 - fee_u64; @@ -2174,11 +2175,7 @@ impl TendermintCoin { let total = &request_amount + &fee_decimal; if balance_decimal < total { - return Err(DelegationError::NotSufficientBalance { - coin: coin.ticker.clone(), - available: balance_decimal, - required: total, - }); + return Err(not_sufficient(total)); } let amount_u64 = sat_from_big_decimal(&request_amount, coin.decimals) From a08dbcb9f79abf54a5f91c1bbbe0d37405b68e23 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 27 Jan 2025 11:38:07 +0300 Subject: [PATCH 12/13] use `io::Error` Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 20 ++++++++++---------- mm2src/coins/tendermint/tendermint_token.rs | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 81d9dca895..a040196322 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -1156,7 +1156,7 @@ impl TendermintCoin { pub(super) fn extract_account_id_and_private_key( &self, withdraw_from: Option, - ) -> Result<(AccountId, Option), String> { + ) -> Result<(AccountId, Option), io::Error> { if let TendermintActivationPolicy::PublicKey(_) = self.activation_policy { return Ok((self.account_id.clone(), None)); } @@ -1166,28 +1166,28 @@ impl TendermintCoin { let path_to_coin = self .activation_policy .path_to_coin_or_err() - .map_err(|e| e.to_string())?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; let path_to_address = from .to_address_path(path_to_coin.coin_type()) - .map_err(|e| e.to_string())? + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))? .to_derivation_path(path_to_coin) - .map_err(|e| e.to_string())?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; let priv_key = self .activation_policy .hd_wallet_derived_priv_key_or_err(&path_to_address) - .map_err(|e| e.to_string())?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; - let account_id = - account_id_from_privkey(priv_key.as_slice(), &self.account_prefix).map_err(|e| e.to_string())?; + let account_id = account_id_from_privkey(priv_key.as_slice(), &self.account_prefix) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?; Ok((account_id, Some(priv_key))) }, None => { let activated_key = self .activation_policy .activated_key_or_err() - .map_err(|e| e.to_string())?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; Ok((self.account_id.clone(), Some(*activated_key))) }, @@ -2189,7 +2189,7 @@ impl TendermintCoin { let (delegator_address, maybe_priv_key) = self .extract_account_id_and_private_key(req.withdraw_from) - .map_err(DelegationError::InternalError)?; + .map_err(|e| DelegationError::InternalError(e.to_string()))?; let (balance_u64, balance_dec) = self .get_balance_as_unsigned_and_decimal(&delegator_address, &self.denom, self.decimals()) @@ -2405,7 +2405,7 @@ impl MmCoin for TendermintCoin { let (account_id, maybe_priv_key) = coin .extract_account_id_and_private_key(req.from) - .map_err(WithdrawError::InternalError)?; + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; let (balance_denom, balance_dec) = coin .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 2880688e9b..30ff3992a2 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -513,7 +513,7 @@ impl MmCoin for TendermintToken { let (account_id, maybe_priv_key) = platform .extract_account_id_and_private_key(req.from) - .map_err(WithdrawError::InternalError)?; + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; let (base_denom_balance, base_denom_balance_dec) = platform .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) From e3d4c8ca740a887319c6c7d9ca4d14e4672f1224 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 27 Jan 2025 09:23:16 +0000 Subject: [PATCH 13/13] use memo by ref Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 100 +++++++------------- mm2src/coins/tendermint/tendermint_token.rs | 11 +-- 2 files changed, 38 insertions(+), 73 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index a040196322..9a1339b965 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -790,7 +790,7 @@ impl TendermintCoin { priv_key: &Secp256k1Secret, tx_payload: Any, timeout_height: u64, - memo: String, + memo: &str, ) -> cosmrs::Result> { let fee_amount = Coin { denom: self.denom.clone(), @@ -840,7 +840,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, timeout: Duration, ) -> Result<(String, Raw), TransactionErr> { // As there wouldn't be enough time to process the data, to mitigate potential edge problems (such as attempting to send transaction @@ -871,7 +871,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, ) -> Result<(String, Raw), TransactionErr> { let mut account_info = try_tx_s!(self.account_info(&self.account_id).await); let (tx_id, tx_raw) = loop { @@ -881,7 +881,7 @@ impl TendermintCoin { tx_payload.clone(), fee.clone(), timeout_height, - memo.clone(), + memo, )); match self.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await { @@ -906,7 +906,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, timeout: Duration, ) -> Result<(String, Raw), TransactionErr> { #[derive(Deserialize)] @@ -950,7 +950,7 @@ impl TendermintCoin { &self, msg: Any, timeout_height: u64, - memo: String, + memo: &str, withdraw_fee: Option, ) -> MmResult { let Ok(activated_priv_key) = self.activation_policy.activated_key_or_err() else { @@ -968,13 +968,7 @@ impl TendermintCoin { let mut account_info = self.account_info(&self.account_id).await?; let (response, raw_response) = loop { let tx_bytes = self - .gen_simulated_tx( - &account_info, - activated_priv_key, - msg.clone(), - timeout_height, - memo.clone(), - ) + .gen_simulated_tx(&account_info, activated_priv_key, msg.clone(), timeout_height, memo) .map_to_mm(|e| TendermintCoinRpcError::InternalError(format!("{}", e)))?; let request = AbciRequest::new( @@ -1035,7 +1029,7 @@ impl TendermintCoin { priv_key: Option, msg: Any, timeout_height: u64, - memo: String, + memo: &str, withdraw_fee: Option, ) -> MmResult { let Some(priv_key) = priv_key else { @@ -1046,7 +1040,7 @@ impl TendermintCoin { let mut account_info = self.account_info(account_id).await?; let (response, raw_response) = loop { let tx_bytes = self - .gen_simulated_tx(&account_info, &priv_key, msg.clone(), timeout_height, memo.clone()) + .gen_simulated_tx(&account_info, &priv_key, msg.clone(), timeout_height, memo) .map_to_mm(|e| TendermintCoinRpcError::InternalError(format!("{}", e)))?; let request = AbciRequest::new( @@ -1201,7 +1195,7 @@ impl TendermintCoin { account_info: &BaseAccount, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, ) -> Result { if let Some(priv_key) = maybe_priv_key { let tx_raw = self.any_to_signed_raw_tx(&priv_key, account_info, message, fee, timeout_height, memo)?; @@ -1286,7 +1280,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, ) -> cosmrs::Result { let signkey = SigningKey::from_slice(priv_key.as_slice())?; let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); @@ -1301,7 +1295,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, ) -> cosmrs::Result { let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); let pubkey = self.activation_policy.public_key()?.into(); @@ -1333,7 +1327,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, ) -> cosmrs::Result { const MSG_SEND_TYPE_URL: &str = "/cosmos.bank.v1beta1.MsgSend"; const LEDGER_MSG_SEND_TYPE_URL: &str = "cosmos-sdk/MsgSend"; @@ -1352,7 +1346,7 @@ impl TendermintCoin { let msg_send = MsgSend::from_any(&tx_payload)?; let timeout_height = u32::try_from(timeout_height)?; let original_tx_type_url = tx_payload.type_url.clone(); - let body_bytes = tx::Body::new(vec![tx_payload], &memo, timeout_height).into_bytes()?; + let body_bytes = tx::Body::new(vec![tx_payload], memo, timeout_height).into_bytes()?; let amount: Vec = msg_send .amount @@ -1529,7 +1523,7 @@ impl TendermintCoin { coin.calculate_fee( create_htlc_tx.msg_payload.clone(), timeout_height, - TX_DEFAULT_MEMO.to_owned(), + TX_DEFAULT_MEMO, None ) .await @@ -1540,7 +1534,7 @@ impl TendermintCoin { create_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO, Duration::from_secs(time_lock_duration), ) .await @@ -1586,7 +1580,7 @@ impl TendermintCoin { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; let fee = try_tx_s!( - coin.calculate_fee(tx_payload.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned(), None) + coin.calculate_fee(tx_payload.clone(), timeout_height, TX_DEFAULT_MEMO, None) .await ); @@ -1596,7 +1590,7 @@ impl TendermintCoin { tx_payload.clone(), fee.clone(), timeout_height, - memo.clone(), + &memo, Duration::from_secs(timeout) ) .await @@ -1836,7 +1830,7 @@ impl TendermintCoin { self.activation_policy.activated_key(), create_htlc_tx.msg_payload.clone(), timeout_height, - TX_DEFAULT_MEMO.to_owned(), + TX_DEFAULT_MEMO, None, ) .await?; @@ -1887,7 +1881,7 @@ impl TendermintCoin { self.activation_policy.activated_key(), msg_send, timeout_height, - TX_DEFAULT_MEMO.to_owned(), + TX_DEFAULT_MEMO, None, ) .await?; @@ -2228,7 +2222,7 @@ impl TendermintCoin { maybe_priv_key, msg_for_fee_prediction, timeout_height, - req.memo.clone(), + &req.memo, req.fee, ) .await?; @@ -2270,7 +2264,7 @@ impl TendermintCoin { &account_info, fee, timeout_height, - req.memo.clone(), + &req.memo, ) .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; @@ -2471,7 +2465,7 @@ impl MmCoin for TendermintCoin { maybe_priv_key, msg_payload.clone(), timeout_height, - memo.clone(), + &memo, req.fee, ) .await?; @@ -2531,14 +2525,7 @@ impl MmCoin for TendermintCoin { let account_info = coin.account_info(&account_id).await?; let tx = coin - .any_to_transaction_data( - maybe_priv_key, - msg_payload, - &account_info, - fee, - timeout_height, - memo.clone(), - ) + .any_to_transaction_data(maybe_priv_key, msg_payload, &account_info, fee, timeout_height, &memo) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let internal_id = { @@ -2995,13 +2982,8 @@ impl SwapOps for TendermintCoin { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; let fee = try_tx_s!( - self.calculate_fee( - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.to_owned(), - None - ) - .await + self.calculate_fee(claim_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO, None) + .await ); let (_tx_id, tx_raw) = try_tx_s!( @@ -3009,7 +2991,7 @@ impl SwapOps for TendermintCoin { claim_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO, Duration::from_secs(timeout), ) .await @@ -3056,13 +3038,8 @@ impl SwapOps for TendermintCoin { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; let fee = try_tx_s!( - self.calculate_fee( - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - None - ) - .await + self.calculate_fee(claim_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO, None) + .await ); let (tx_id, tx_raw) = try_tx_s!( @@ -3070,7 +3047,7 @@ impl SwapOps for TendermintCoin { claim_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO, Duration::from_secs(timeout), ) .await @@ -3685,7 +3662,7 @@ pub mod tendermint_coin_tests { coin.calculate_fee( create_htlc_tx.msg_payload.clone(), timeout_height, - TX_DEFAULT_MEMO.to_owned(), + TX_DEFAULT_MEMO, None, ) .await @@ -3696,7 +3673,7 @@ pub mod tendermint_coin_tests { create_htlc_tx.msg_payload.clone(), fee, timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO, Duration::from_secs(20), ); block_on(async { @@ -3725,21 +3702,16 @@ pub mod tendermint_coin_tests { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; let fee = block_on(async { - coin.calculate_fee( - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.to_owned(), - None, - ) - .await - .unwrap() + coin.calculate_fee(claim_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO, None) + .await + .unwrap() }); let send_tx_fut = coin.common_send_raw_tx_bytes( claim_htlc_tx.msg_payload, fee, timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO, Duration::from_secs(30), ); diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 30ff3992a2..e4576d2783 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -597,7 +597,7 @@ impl MmCoin for TendermintToken { maybe_priv_key, msg_payload.clone(), timeout_height, - memo.clone(), + &memo, req.fee, ) .await?; @@ -622,14 +622,7 @@ impl MmCoin for TendermintToken { let account_info = platform.account_info(&account_id).await?; let tx = platform - .any_to_transaction_data( - maybe_priv_key, - msg_payload, - &account_info, - fee, - timeout_height, - memo.clone(), - ) + .any_to_transaction_data(maybe_priv_key, msg_payload, &account_info, fee, timeout_height, &memo) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let internal_id = {