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 c6ac3dca4e..e2d50ad9c0 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::{hd_wallet::WithdrawFrom, lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum, WithdrawFee}; /// Represents current status of the validator. #[derive(Default, Deserialize)] @@ -148,3 +149,16 @@ pub async fn validators_rpc( validators: validators.into_iter().map(jsonize_validator).collect(), }) } + +#[derive(Clone, Debug, Deserialize)] +pub struct DelegatePayload { + pub validator_address: String, + pub fee: 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 9573e6de4b..9a1339b965 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::{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; @@ -50,7 +51,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, QueryValidatorsResponse, Validator}; use cosmrs::tendermint::block::Height; use cosmrs::tendermint::chain::Id as ChainId; use cosmrs::tendermint::PublicKey; @@ -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 { @@ -785,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(), @@ -835,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 @@ -866,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 { @@ -876,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 { @@ -901,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)] @@ -945,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 { @@ -963,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( @@ -1030,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 { @@ -1041,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( @@ -1148,11 +1147,10 @@ impl TendermintCoin { .map_to_mm(|e| TendermintCoinRpcError::InvalidResponse(format!("balance is not u64, err {}", e))) } - #[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), WithdrawError> { + ) -> Result<(AccountId, Option), io::Error> { if let TendermintActivationPolicy::PublicKey(_) = self.activation_policy { return Ok((self.account_id.clone(), None)); } @@ -1162,28 +1160,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| 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| WithdrawError::InternalError(e.to_string()))? + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))? .to_derivation_path(path_to_coin) - .map_err(|e| WithdrawError::InternalError(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| WithdrawError::InternalError(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| WithdrawError::InternalError(e.to_string()))?; + .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| WithdrawError::InternalError(e.to_string()))?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; Ok((self.account_id.clone(), Some(*activated_key))) }, @@ -1192,14 +1190,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, + memo: &str, ) -> 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); @@ -1282,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); @@ -1297,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(); @@ -1329,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"; @@ -1348,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 @@ -1525,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 @@ -1536,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 @@ -1582,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 ); @@ -1592,7 +1590,7 @@ impl TendermintCoin { tx_payload.clone(), fee.clone(), timeout_height, - memo.clone(), + &memo, Duration::from_secs(timeout) ) .await @@ -1832,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?; @@ -1883,7 +1881,7 @@ impl TendermintCoin { self.activation_policy.activated_key(), msg_send, timeout_height, - TX_DEFAULT_MEMO.to_owned(), + TX_DEFAULT_MEMO, None, ) .await?; @@ -2124,6 +2122,180 @@ impl TendermintCoin { Ok(typed_response.validators) } + + 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()) + } + + /// 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> { + 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(not_sufficient(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(not_sufficient(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()))?; + + let (delegator_address, maybe_priv_key) = self + .extract_account_id_and_private_key(req.withdraw_from) + .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()) + .await?; + + let amount_u64 = if req.max { + balance_u64 + } else { + sat_from_big_decimal(&req.amount, self.decimals) + .map_err(|e| DelegationError::InternalError(e.to_string()))? + }; + + // 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 timeout_height = self + .current_block() + .compat() + .await + .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; + 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_priv_key, + msg_for_fee_prediction, + timeout_height, + &req.memo, + req.fee, + ) + .await?; + + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, self.decimals()); + + let fee = Fee::from_amount_and_gas( + Coin { + denom: self.denom.clone(), + amount: fee_amount_u64.into(), + }, + gas_limit, + ); + + 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(), + 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_priv_key, + msg_for_actual_tx, + &account_info, + fee, + timeout_height, + &req.memo, + ) + .map_to_mm(|e| DelegationError::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::StakingDelegation, + memo: Some(req.memo), + }) + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { @@ -2225,7 +2397,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_priv_key) = coin + .extract_account_id_and_private_key(req.from) + .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()) @@ -2288,10 +2462,10 @@ 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(), + &memo, req.fee, ) .await?; @@ -2351,7 +2525,7 @@ 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) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let internal_id = { @@ -2808,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!( @@ -2822,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 @@ -2869,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!( @@ -2883,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 @@ -3498,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 @@ -3509,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 { @@ -3538,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 e5cc90f895..e4576d2783 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_priv_key) = platform + .extract_account_id_and_private_key(req.from) + .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()) @@ -592,10 +594,10 @@ 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(), + &memo, req.fee, ) .await?; @@ -620,7 +622,7 @@ 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) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let internal_id = { diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 3dc95e7443..b170f08b81 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,111 @@ 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); - } - } + unrecognized => { + log::warn!( + "Found an unrecognized event '{unrecognized}' in transaction history processing." + ); + }, + }; } } 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 +627,7 @@ where }, token_id, }, + (TransferEventType::Delegate, _) => TransactionType::StakingDelegation, (_, Some(token_id)) => TransactionType::TokenTransfer(token_id), _ => TransactionType::StandardTransfer, } @@ -604,7 +648,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()])) }, } 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,