From 26a54e6d1c72afacec18082616609feac3f0ccd6 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 10 Feb 2025 11:36:29 +0000 Subject: [PATCH 01/18] add new RPC `claim_staking_rewards` Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 29 +++++++++++++++++++ .../coins/rpc_command/tendermint/staking.rs | 8 +++++ .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 3 +- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 344d9d8306..d15b13a623 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2164,6 +2164,19 @@ pub struct RemoveDelegateRequest { pub staking_details: Option, } +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum ClaimingDetails { + Cosmos(rpc_command::tendermint::staking::ClaimRewardsPayload), +} + +#[allow(dead_code)] +#[derive(Deserialize)] +pub struct ClaimStakingRewardsRequest { + pub coin: String, + pub claiming_details: ClaimingDetails, +} + #[derive(Deserialize)] pub struct GetStakingInfosRequest { pub coin: String, @@ -4930,6 +4943,22 @@ pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationRe } } +pub async fn claim_staking_rewards(ctx: MmArc, req: ClaimStakingRewardsRequest) -> DelegationResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + + match req.claiming_details { + ClaimingDetails::Cosmos(req) => { + let MmCoinEnum::Tendermint(tendermint) = coin else { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }); + }; + + tendermint.claim_staking_rewards(req).await + }, + } +} + pub async fn send_raw_transaction(ctx: MmArc, req: Json) -> Result>, String> { let ticker = try_s!(req["coin"].as_str().ok_or("No 'coin' field")).to_owned(); let coin = match lp_coinfind(&ctx, &ticker).await { diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 3ca77b0295..2761b6a2ab 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -162,3 +162,11 @@ pub struct DelegationPayload { #[serde(default)] pub max: bool, } + +#[derive(Clone, Debug, Deserialize)] +pub struct ClaimRewardsPayload { + pub validator_address: String, + pub fee: Option, + #[serde(default)] + pub memo: String, +} diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index f72b2be399..a7adb61264 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -48,7 +48,7 @@ use coins::utxo::utxo_standard::UtxoStandardCoin; use coins::z_coin::ZCoin; use coins::{add_delegation, get_my_address, get_raw_transaction, get_staking_infos, get_swap_transaction_fee_policy, nft, remove_delegation, set_swap_transaction_fee_policy, sign_message, sign_raw_transaction, - verify_message, withdraw}; + verify_message, withdraw, claim_staking_rewards}; use coins_activation::{cancel_init_l2, cancel_init_platform_coin_with_tokens, cancel_init_standalone_coin, cancel_init_token, enable_platform_coin_with_tokens, enable_token, init_l2, init_l2_status, init_l2_user_action, init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, @@ -176,6 +176,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, approve_token_rpc).await, "get_token_allowance" => handle_mmrpc(ctx, request, get_token_allowance_rpc).await, "best_orders" => handle_mmrpc(ctx, request, best_orders_rpc_v2).await, + "claim_staking_rewards" => handle_mmrpc(ctx, request, claim_staking_rewards).await, "clear_nft_db" => handle_mmrpc(ctx, request, clear_nft_db).await, "enable_bch_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, "enable_slp" => handle_mmrpc(ctx, request, enable_token::).await, From 9a8c8291af2be43a724dd6b8d6dccf42fae184b2 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 10 Feb 2025 11:36:49 +0000 Subject: [PATCH 02/18] implement initial version of claiming rewards Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 98 +++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index cf20c37e81..c65fdedd1b 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::{DelegationPayload, ValidatorStatus}; +use crate::rpc_command::tendermint::staking::{ClaimRewardsPayload, DelegationPayload, ValidatorStatus}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -39,6 +39,7 @@ use common::log::{debug, warn}; use common::{get_utc_timestamp, now_sec, Future01CompatExt, PagingOptions, DEX_FEE_ADDR_PUBKEY}; use cosmrs::bank::MsgSend; use cosmrs::crypto::secp256k1::SigningKey; +use cosmrs::distribution::MsgWithdrawDelegatorReward; use cosmrs::proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest, QueryAccountResponse}; use cosmrs::proto::cosmos::bank::v1beta1::{MsgSend as MsgSendProto, QueryBalanceRequest, QueryBalanceResponse}; use cosmrs::proto::cosmos::base::query::v1beta1::PageRequest; @@ -2472,6 +2473,101 @@ impl TendermintCoin { Ok((big_decimal_from_sat_unsigned(uamount, self.decimals()), uamount)) } + + pub(crate) async fn claim_staking_rewards( + &self, + req: ClaimRewardsPayload, + ) -> MmResult { + let (delegator_address, maybe_priv_key) = self + .extract_account_id_and_private_key(None) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + let validator_address = + AccountId::from_str(&req.validator_address).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; + + let msg = MsgWithdrawDelegatorReward { + delegator_address: delegator_address.clone(), + validator_address: validator_address.clone(), + } + .to_any() + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + let timeout_height = self + .current_block() + .compat() + .await + .map_to_mm(DelegationError::Transport)? + + TIMEOUT_HEIGHT_DELTA; + + // This uses more gas than any other transactions + let gas_limit_default = GAS_LIMIT_DEFAULT * 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.clone(), + timeout_height, + &req.memo, + req.fee, + ) + .await?; + + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, self.decimals()); + + let my_balance = self.my_balance().compat().await?.spendable; + + if fee_amount_dec > my_balance { + return MmError::err(DelegationError::NotSufficientBalance { + coin: self.ticker.clone(), + available: my_balance, + required: fee_amount_dec, + }); + } + + let fee = Fee::from_amount_and_gas( + Coin { + denom: self.denom.clone(), + amount: fee_amount_u64.into(), + }, + gas_limit, + ); + + let account_info = self.account_info(&delegator_address).await?; + + let tx = self + .any_to_transaction_data(maybe_priv_key, msg, &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().map_or_else(Vec::new, |h| h.to_vec()); + sha256(&hex_vec).to_vec().into() + }; + + Ok(TransactionDetails { + tx, + from: vec![delegator_address.to_string()], + to: vec![], // We just pay the transaction fee for undelegation + my_balance_change: &BigDecimal::default() - &fee_amount_dec, + spent_by_me: fee_amount_dec.clone(), + total_amount: fee_amount_dec.clone(), + 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::RemoveDelegation, + memo: Some(req.memo), + }) + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { From 5eb3ba039de0670f1847c5a170665d06fa01ac7c Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 11 Feb 2025 07:16:09 +0000 Subject: [PATCH 03/18] add todo Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index c65fdedd1b..f3e86d5365 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2492,6 +2492,8 @@ impl TendermintCoin { .to_any() .map_err(|e| DelegationError::InternalError(e.to_string()))?; + let reward_amount = todo!(); + let timeout_height = self .current_block() .compat() @@ -2547,8 +2549,8 @@ impl TendermintCoin { Ok(TransactionDetails { tx, - from: vec![delegator_address.to_string()], - to: vec![], // We just pay the transaction fee for undelegation + from: vec![validator_address.to_string()], + to: vec![delegator_address.to_string()], my_balance_change: &BigDecimal::default() - &fee_amount_dec, spent_by_me: fee_amount_dec.clone(), total_amount: fee_amount_dec.clone(), From 8ccee689465256308fa2504835bed7eb73726583 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 11 Feb 2025 07:22:21 +0000 Subject: [PATCH 04/18] add new transaction type `ClaimDelegationRewards` Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 1 + mm2src/coins/my_tx_history_v2.rs | 1 + mm2src/coins/tendermint/tendermint_coin.rs | 2 +- mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs | 6 +++--- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index d15b13a623..071cd5fc31 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2303,6 +2303,7 @@ impl KmdRewardsDetails { pub enum TransactionType { StakingDelegation, RemoveDelegation, + ClaimDelegationRewards, #[default] StandardTransfer, TokenTransfer(BytesJson), diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index f7348a5e78..bc60f8fc74 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -235,6 +235,7 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T }, TransactionType::StakingDelegation | TransactionType::RemoveDelegation + | TransactionType::ClaimDelegationRewards | TransactionType::FeeForTokenTx | TransactionType::StandardTransfer | TransactionType::NftTransfer diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index f3e86d5365..3d086204e6 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2566,7 +2566,7 @@ impl TendermintCoin { coin: self.ticker.to_string(), internal_id, kmd_rewards: None, - transaction_type: TransactionType::RemoveDelegation, + transaction_type: TransactionType::ClaimDelegationRewards, memo: Some(req.memo), }) } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index a7adb61264..c267471def 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -46,9 +46,9 @@ use coins::utxo::qtum::QtumCoin; use coins::utxo::slp::SlpToken; use coins::utxo::utxo_standard::UtxoStandardCoin; use coins::z_coin::ZCoin; -use coins::{add_delegation, get_my_address, get_raw_transaction, get_staking_infos, get_swap_transaction_fee_policy, - nft, remove_delegation, set_swap_transaction_fee_policy, sign_message, sign_raw_transaction, - verify_message, withdraw, claim_staking_rewards}; +use coins::{add_delegation, claim_staking_rewards, get_my_address, get_raw_transaction, get_staking_infos, + get_swap_transaction_fee_policy, nft, remove_delegation, set_swap_transaction_fee_policy, sign_message, + sign_raw_transaction, verify_message, withdraw}; use coins_activation::{cancel_init_l2, cancel_init_platform_coin_with_tokens, cancel_init_standalone_coin, cancel_init_token, enable_platform_coin_with_tokens, enable_token, init_l2, init_l2_status, init_l2_user_action, init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, From 3d827330550c784a85050cf0bc74aa6c7aea6ea3 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 11 Feb 2025 13:37:48 +0000 Subject: [PATCH 05/18] handle reward amounts Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 56 ++++++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 3d086204e6..8ad3a18916 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -46,6 +46,7 @@ use cosmrs::proto::cosmos::base::query::v1beta1::PageRequest; use cosmrs::proto::cosmos::base::tendermint::v1beta1::{GetBlockByHeightRequest, GetBlockByHeightResponse, GetLatestBlockRequest, GetLatestBlockResponse}; use cosmrs::proto::cosmos::base::v1beta1::Coin as CoinProto; +use cosmrs::proto::cosmos::distribution::v1beta1::{QueryDelegationRewardsRequest, QueryDelegationRewardsResponse}; use cosmrs::proto::cosmos::staking::v1beta1::{QueryDelegationRequest, QueryDelegationResponse, QueryValidatorsRequest, QueryValidatorsResponse as QueryValidatorsResponseProto}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, @@ -2474,6 +2475,53 @@ impl TendermintCoin { Ok((big_decimal_from_sat_unsigned(uamount, self.decimals()), uamount)) } + pub(crate) async fn get_delegation_reward_amount( + &self, + validator_addr: &AccountId, // keep this as `AccountId` to make it pre-validated + ) -> MmResult { + let delegator_address = self + .my_address() + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + let validator_address = validator_addr.to_string(); + + let query_payload = QueryDelegationRewardsRequest { + delegator_address, + validator_address, + }; + + let path = "/cosmos.distribution.v1beta1.Query/DelegationRewards"; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(path.to_owned()), + query_payload.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .map_err(|e| DelegationError::Transport(e.to_string())) + .await?; + + let decoded_response = QueryDelegationRewardsResponse::decode(raw_response.value.as_slice()) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + let denom = self.denom.to_string(); + let mut amount = BigDecimal::from(0); + for reward in decoded_response.rewards { + if denom == reward.denom { + // TODO: separate and add coverage + let raw = + BigDecimal::from_str(&reward.amount).map_err(|e| DelegationError::InternalError(e.to_string()))?; + let scale = BigDecimal::from(1_000_000_000_000_000_000u64); + let reward_amount = (raw / scale) / BigDecimal::from(10u64.pow(self.decimals as u32)); + + amount += reward_amount; + } + } + + Ok(amount) + } pub(crate) async fn claim_staking_rewards( &self, req: ClaimRewardsPayload, @@ -2492,7 +2540,7 @@ impl TendermintCoin { .to_any() .map_err(|e| DelegationError::InternalError(e.to_string()))?; - let reward_amount = todo!(); + let reward_amount = self.get_delegation_reward_amount(&validator_address).await?; let timeout_height = self .current_block() @@ -2551,10 +2599,10 @@ impl TendermintCoin { tx, from: vec![validator_address.to_string()], to: vec![delegator_address.to_string()], - my_balance_change: &BigDecimal::default() - &fee_amount_dec, + my_balance_change: &reward_amount - &fee_amount_dec, spent_by_me: fee_amount_dec.clone(), - total_amount: fee_amount_dec.clone(), - received_by_me: BigDecimal::default(), + total_amount: reward_amount.clone(), + received_by_me: reward_amount, block_height: 0, timestamp: 0, fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { From a1fe42b4f259e286c2faf3eb42a97cc3b24a8f08 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 12 Feb 2025 06:35:01 +0000 Subject: [PATCH 06/18] improve error handling Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 8 ++++++++ mm2src/coins/rpc_command/tendermint/staking.rs | 2 ++ mm2src/coins/tendermint/tendermint_coin.rs | 18 +++++++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 071cd5fc31..f84bf12540 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2803,6 +2803,14 @@ pub enum DelegationError { available: BigDecimal, requested: BigDecimal, }, + #[display( + fmt = "Fee ({}) exceeds reward ({}) which makes this unprofitable. Set 'force' to true in the request to bypass this check.", + fee, + reward + )] + UnprofitableReward { reward: BigDecimal, fee: BigDecimal }, + #[display(fmt = "There is no reward for {} to claim.", coin)] + NothingToClaim { coin: String }, #[display(fmt = "{}", _0)] CannotInteractWithSmartContract(String), #[from_stringify("ScriptHashTypeNotSupported")] diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 2761b6a2ab..ac61d4e6f4 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -169,4 +169,6 @@ pub struct ClaimRewardsPayload { pub fee: Option, #[serde(default)] pub memo: String, + #[serde(default)] + pub force: bool, } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 8ad3a18916..07ecbe6db0 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -98,6 +98,7 @@ const ABCI_GET_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTx"; const ABCI_GET_TXS_EVENT_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTxsEvent"; const ABCI_VALIDATORS_PATH: &str = "/cosmos.staking.v1beta1.Query/Validators"; const ABCI_DELEGATION_PATH: &str = "/cosmos.staking.v1beta1.Query/Delegation"; +const ABCI_DELEGATION_REWARDS_PATH: &str = "/cosmos.distribution.v1beta1.Query/DelegationRewards"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -2489,13 +2490,11 @@ impl TendermintCoin { validator_address, }; - let path = "/cosmos.distribution.v1beta1.Query/DelegationRewards"; - let raw_response = self .rpc_client() .await? .abci_query( - Some(path.to_owned()), + Some(ABCI_DELEGATION_REWARDS_PATH.to_owned()), query_payload.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -2542,6 +2541,12 @@ impl TendermintCoin { let reward_amount = self.get_delegation_reward_amount(&validator_address).await?; + if reward_amount == BigDecimal::from(0) { + return MmError::err(DelegationError::NothingToClaim { + coin: self.ticker.clone(), + }); + } + let timeout_height = self .current_block() .compat() @@ -2576,6 +2581,13 @@ impl TendermintCoin { }); } + if !req.force && fee_amount_dec > reward_amount { + return MmError::err(DelegationError::UnprofitableReward { + reward: reward_amount.clone(), + fee: fee_amount_dec.clone(), + }); + } + let fee = Fee::from_amount_and_gas( Coin { denom: self.denom.clone(), From 926600a2789b811b860b163ebea7bfad29357aa9 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 12 Feb 2025 07:14:39 +0000 Subject: [PATCH 07/18] make decimal parsing testable Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 38 ++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 07ecbe6db0..a4535a81f8 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -45,7 +45,7 @@ use cosmrs::proto::cosmos::bank::v1beta1::{MsgSend as MsgSendProto, QueryBalance use cosmrs::proto::cosmos::base::query::v1beta1::PageRequest; use cosmrs::proto::cosmos::base::tendermint::v1beta1::{GetBlockByHeightRequest, GetBlockByHeightResponse, GetLatestBlockRequest, GetLatestBlockResponse}; -use cosmrs::proto::cosmos::base::v1beta1::Coin as CoinProto; +use cosmrs::proto::cosmos::base::v1beta1::{Coin as CoinProto, DecCoin}; use cosmrs::proto::cosmos::distribution::v1beta1::{QueryDelegationRewardsRequest, QueryDelegationRewardsResponse}; use cosmrs::proto::cosmos::staking::v1beta1::{QueryDelegationRequest, QueryDelegationResponse, QueryValidatorsRequest, QueryValidatorsResponse as QueryValidatorsResponseProto}; @@ -72,6 +72,7 @@ use keys::{KeyPair, Public}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; use mm2_git::{FileMetadata, GitController, GithubClient, RepositoryOperations, GITHUB_API_URI}; +use mm2_number::bigdecimal::ParseBigDecimalError; use mm2_number::MmNumber; use mm2_p2p::p2p_ctx::P2PContext; use parking_lot::Mutex as PaMutex; @@ -2509,11 +2510,8 @@ impl TendermintCoin { let mut amount = BigDecimal::from(0); for reward in decoded_response.rewards { if denom == reward.denom { - // TODO: separate and add coverage - let raw = - BigDecimal::from_str(&reward.amount).map_err(|e| DelegationError::InternalError(e.to_string()))?; - let scale = BigDecimal::from(1_000_000_000_000_000_000u64); - let reward_amount = (raw / scale) / BigDecimal::from(10u64.pow(self.decimals as u32)); + let reward_amount = extract_big_decimal_from_dec_coin(&reward, self.decimals as u32) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; amount += reward_amount; } @@ -3852,6 +3850,13 @@ pub async fn get_ibc_transfer_channels( }) } +fn extract_big_decimal_from_dec_coin(dec_coin: &DecCoin, decimals: u32) -> Result { + let raw = BigDecimal::from_str(&dec_coin.amount)?; + // `DecCoin` represents decimal numbers as integer-like strings where the last 18 digits are the decimal part. + let scale = BigDecimal::from(1_000_000_000_000_000_000u64) * BigDecimal::from(10u64.pow(decimals)); + Ok(raw / scale) +} + fn parse_expected_sequence_number(e: &str) -> MmResult { if let Some(sequence) = SEQUENCE_PARSER_REGEX.captures(e).and_then(|c| c.get(1)) { let account_sequence = @@ -4769,4 +4774,25 @@ pub mod tendermint_coin_tests { assert!(parse_expected_sequence_number("").is_err()); assert!(parse_expected_sequence_number("check_tx log: account sequence mismatch, expected").is_err()); } + + #[test] + fn test_extract_big_decimal_from_dec_coin() { + let dec_coin = DecCoin { + denom: "".into(), + amount: "232503485176823921544000".into(), + }; + + let expected = BigDecimal::from_str("0.232503485176823921544").unwrap(); + let actual = extract_big_decimal_from_dec_coin(&dec_coin, 6).unwrap(); + assert_eq!(expected, actual); + + let dec_coin = DecCoin { + denom: "".into(), + amount: "1000000000000000000000000".into(), + }; + + let expected = BigDecimal::from(1); + let actual = extract_big_decimal_from_dec_coin(&dec_coin, 6).unwrap(); + assert_eq!(expected, actual); + } } From 9b9e0d261345beb3b6d4834b6cdd1a2463c12562 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 12 Feb 2025 07:22:22 +0000 Subject: [PATCH 08/18] reduce gas price Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index a4535a81f8..352ce9c97f 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2552,8 +2552,8 @@ impl TendermintCoin { .map_to_mm(DelegationError::Transport)? + TIMEOUT_HEIGHT_DELTA; - // This uses more gas than any other transactions - let gas_limit_default = GAS_LIMIT_DEFAULT * 2; + // This 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 From 52076fc719c93d33bde77d130236d08dd7b6b387 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 12 Feb 2025 07:38:15 +0000 Subject: [PATCH 09/18] add test coverage Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 352ce9c97f..cb59a1f06b 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -4795,4 +4795,53 @@ pub mod tendermint_coin_tests { let actual = extract_big_decimal_from_dec_coin(&dec_coin, 6).unwrap(); assert_eq!(expected, actual); } + + #[test] + fn test_claim_staking_rewards() { + let nodes = vec![RpcNode::for_test(IRIS_TESTNET_RPC_URL)]; + let protocol_conf = get_iris_protocol(); + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); + + let coin = block_on(TendermintCoin::init( + &ctx, + "IRIS-TEST".to_string(), + conf, + protocol_conf, + nodes, + false, + activation_policy, + false, + )) + .unwrap(); + + let validator_address = "iva1svannhv2zaxefq83m7treg078udfk37lpjufkw"; + let memo = "test".to_owned(); + let req = ClaimRewardsPayload { + validator_address: validator_address.to_owned(), + fee: None, + memo: memo.clone(), + force: false, + }; + let reward_amount = + block_on(coin.get_delegation_reward_amount(&AccountId::from_str(validator_address).unwrap())).unwrap(); + let res = block_on(coin.claim_staking_rewards(req)).unwrap(); + + assert_eq!(vec![validator_address], res.from); + assert_eq!(vec![coin.account_id.to_string()], res.to); + assert_eq!(TransactionType::ClaimDelegationRewards, res.transaction_type); + assert_eq!(Some(memo), res.memo); + assert_eq!(reward_amount, res.total_amount); + assert_eq!(reward_amount, res.received_by_me); + // tx fee must be taken into account + assert!(reward_amount > res.my_balance_change); + } } From 718da9dfb505a7bab572c32ffe8b45613e53a9f7 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 12 Feb 2025 07:40:31 +0000 Subject: [PATCH 10/18] remove dead_code annotations Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index f84bf12540..2c059bae4f 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2150,14 +2150,12 @@ pub enum StakingDetails { Cosmos(Box), } -#[allow(dead_code)] #[derive(Deserialize)] pub struct AddDelegateRequest { pub coin: String, pub staking_details: StakingDetails, } -#[allow(dead_code)] #[derive(Deserialize)] pub struct RemoveDelegateRequest { pub coin: String, @@ -2170,7 +2168,6 @@ pub enum ClaimingDetails { Cosmos(rpc_command::tendermint::staking::ClaimRewardsPayload), } -#[allow(dead_code)] #[derive(Deserialize)] pub struct ClaimStakingRewardsRequest { pub coin: String, From 970db407cdd2e175d21db3d1615ed4b538113974 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 13 Feb 2025 06:57:54 +0000 Subject: [PATCH 11/18] implement tx history support for claiming rewards Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 1 + .../tendermint/tendermint_tx_history_v2.rs | 58 ++++++++++++++++--- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index cb59a1f06b..465d23ed67 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2519,6 +2519,7 @@ impl TendermintCoin { Ok(amount) } + pub(crate) async fn claim_staking_rewards( &self, req: ClaimRewardsPayload, diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 22caa0554b..389363e53f 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -39,8 +39,10 @@ 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 UNDELEGATE_EVENT: &str = "unbond"; +const WITHDRAW_REWARDS_EVENT: &str = "withdraw_rewards"; const ACCEPTED_EVENTS: &[&str] = &[ TRANSFER_EVENT, @@ -51,6 +53,7 @@ const ACCEPTED_EVENTS: &[&str] = &[ IBC_NFT_RECEIVE_EVENT, DELEGATE_EVENT, UNDELEGATE_EVENT, + WITHDRAW_REWARDS_EVENT, ]; const RECEIVER_TAG_KEY: &str = "receiver"; @@ -418,6 +421,7 @@ where IBCReceive, Delegate, Undelegate, + ClaimRewards, } #[derive(Clone)] @@ -581,6 +585,30 @@ where handle_new_transfer_event(&mut transfer_details_list, tx_details); }, + WITHDRAW_REWARDS_EVENT => { + let to = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + DELEGATOR_TAG_KEY, + DELEGATOR_TAG_KEY_BASE64, + )); + + let from = 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::ClaimRewards, + }; + + handle_new_transfer_event(&mut transfer_details_list, tx_details); + }, + unrecognized => { log::warn!( "Found an unrecognized event '{unrecognized}' in transaction history processing." @@ -609,6 +637,21 @@ where } fn get_transfer_details(tx_events: Vec, fee_amount_with_denom: String) -> Vec { + // We only interested `DELEGATE_EVENT` events for delegation transactions. + if let Some(delegate_event) = tx_events.iter().find(|e| e.kind == DELEGATE_EVENT) { + return parse_transfer_values_from_events(vec![delegate_event]); + }; + + // We only interested `UNDELEGATE_EVENT` events for undelegation transactions. + if let Some(undelegate_event) = tx_events.iter().find(|e| e.kind == UNDELEGATE_EVENT) { + return parse_transfer_values_from_events(vec![undelegate_event]); + }; + + // We only interested `WITHDRAW_REWARDS_EVENT` events for withdraw reward transactions. + if let Some(withdraw_rewards_event) = tx_events.iter().find(|e| e.kind == WITHDRAW_REWARDS_EVENT) { + return parse_transfer_values_from_events(vec![withdraw_rewards_event]); + }; + // Filter out irrelevant events let mut events: Vec<&Event> = tx_events .iter() @@ -617,15 +660,7 @@ where .collect(); if events.len() > DEFAULT_TRANSFER_EVENT_COUNT { - let is_undelegate_tx = events.iter().any(|e| e.kind == UNDELEGATE_EVENT); - events.retain(|event| { - // We only interested `UNDELEGATE_EVENT` events for undelegation transactions, - // so we drop the rest. - if is_undelegate_tx && event.kind != UNDELEGATE_EVENT { - return false; - } - // Fees are included in `TRANSFER_EVENT` events, but since we handle fees // separately, drop them from this list as we use them to extract the user // amounts. @@ -661,9 +696,13 @@ where }, token_id, }, + (TransferEventType::IBCSend, _) | (TransferEventType::IBCReceive, _) => { + TransactionType::TendermintIBCTransfer + }, (TransferEventType::Delegate, _) => TransactionType::StakingDelegation, (TransferEventType::Undelegate, _) => TransactionType::RemoveDelegation, (_, Some(token_id)) => TransactionType::TokenTransfer(token_id), + (TransferEventType::ClaimRewards, _) => TransactionType::ClaimDelegationRewards, _ => TransactionType::StandardTransfer, } } @@ -686,7 +725,8 @@ where TransferEventType::Standard | TransferEventType::IBCSend | TransferEventType::IBCReceive - | TransferEventType::Delegate => { + | TransferEventType::Delegate + | TransferEventType::ClaimRewards => { Some((vec![transfer_details.from.clone()], vec![transfer_details.to.clone()])) }, TransferEventType::Undelegate => Some((vec![my_address], vec![])), From f15f2d5bff29718d7d7e8636da1e1a6018766bc2 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 13 Feb 2025 07:02:44 +0000 Subject: [PATCH 12/18] keep helper functions private Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 465d23ed67..cfc2f7bbad 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2430,7 +2430,7 @@ impl TendermintCoin { }) } - pub(crate) async fn get_delegated_amount( + async fn get_delegated_amount( &self, validator_addr: &AccountId, // keep this as `AccountId` to make it pre-validated ) -> MmResult<(BigDecimal, u64), DelegationError> { @@ -2477,7 +2477,7 @@ impl TendermintCoin { Ok((big_decimal_from_sat_unsigned(uamount, self.decimals()), uamount)) } - pub(crate) async fn get_delegation_reward_amount( + async fn get_delegation_reward_amount( &self, validator_addr: &AccountId, // keep this as `AccountId` to make it pre-validated ) -> MmResult { From 78d96f3307f4fba3b90243ec6f82bacc28e0ca70 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 17 Feb 2025 08:10:45 +0000 Subject: [PATCH 13/18] improve reward calculation Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index cfc2f7bbad..a92ff95823 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2507,17 +2507,14 @@ impl TendermintCoin { .map_err(|e| DelegationError::InternalError(e.to_string()))?; let denom = self.denom.to_string(); - let mut amount = BigDecimal::from(0); - for reward in decoded_response.rewards { - if denom == reward.denom { - let reward_amount = extract_big_decimal_from_dec_coin(&reward, self.decimals as u32) - .map_err(|e| DelegationError::InternalError(e.to_string()))?; - amount += reward_amount; - } + match decoded_response.rewards.iter().find(|t| t.denom == denom) { + Some(dec_coin) => extract_big_decimal_from_dec_coin(dec_coin, self.decimals as u32) + .map_to_mm(|e| DelegationError::InternalError(e.to_string())), + None => MmError::err(DelegationError::NothingToClaim { + coin: self.ticker.clone(), + }), } - - Ok(amount) } pub(crate) async fn claim_staking_rewards( From 585c3b3d437f74680e995fd1af15fa1c0fe85c21 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 17 Feb 2025 08:54:05 +0000 Subject: [PATCH 14/18] fix incorrect tx type in tx history dump Signed-off-by: onur-ozkan --- mm2src/mm2_test_helpers/dummy_files/nucleus-history.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json b/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json index 4400c318e0..2caab09b28 100644 --- a/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json +++ b/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json @@ -22,7 +22,7 @@ }, "coin": "NUCLEUS-TEST", "internal_id": "4335303135373938433145384342333931424145373846420000000000000000", - "transaction_type": "StandardTransfer", + "transaction_type": "TendermintIBCTransfer", "memo": "rly(2.5.2-28-gdf42391)" }, { From 6abb8c5ed04f09b727e815364a71728601e21827 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 17 Feb 2025 16:32:43 +0000 Subject: [PATCH 15/18] fix review notes Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 8 ++++---- mm2src/coins/tendermint/tendermint_tx_history_v2.rs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 2c059bae4f..04a6dbab6f 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -4950,17 +4950,17 @@ pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationRe } pub async fn claim_staking_rewards(ctx: MmArc, req: ClaimStakingRewardsRequest) -> DelegationResult { - let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - match req.claiming_details { - ClaimingDetails::Cosmos(req) => { + ClaimingDetails::Cosmos(r) => { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let MmCoinEnum::Tendermint(tendermint) = coin else { return MmError::err(DelegationError::CoinDoesntSupportDelegation { coin: coin.ticker().to_string(), }); }; - tendermint.claim_staking_rewards(req).await + tendermint.claim_staking_rewards(r).await }, } } diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 389363e53f..9355de60ed 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -637,17 +637,17 @@ where } fn get_transfer_details(tx_events: Vec, fee_amount_with_denom: String) -> Vec { - // We only interested `DELEGATE_EVENT` events for delegation transactions. + // We are only interested `DELEGATE_EVENT` events for delegation transactions. if let Some(delegate_event) = tx_events.iter().find(|e| e.kind == DELEGATE_EVENT) { return parse_transfer_values_from_events(vec![delegate_event]); }; - // We only interested `UNDELEGATE_EVENT` events for undelegation transactions. + // We are only interested `UNDELEGATE_EVENT` events for undelegation transactions. if let Some(undelegate_event) = tx_events.iter().find(|e| e.kind == UNDELEGATE_EVENT) { return parse_transfer_values_from_events(vec![undelegate_event]); }; - // We only interested `WITHDRAW_REWARDS_EVENT` events for withdraw reward transactions. + // We are only interested `WITHDRAW_REWARDS_EVENT` events for withdraw reward transactions. if let Some(withdraw_rewards_event) = tx_events.iter().find(|e| e.kind == WITHDRAW_REWARDS_EVENT) { return parse_transfer_values_from_events(vec![withdraw_rewards_event]); }; From f032a5e6b7f1fe46d9a0573bcd6eae61f631af4b Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 17 Feb 2025 17:53:57 +0000 Subject: [PATCH 16/18] test Signed-off-by: onur-ozkan --- .../tendermint/tendermint_tx_history_v2.rs | 19 +++++++++++++------ .../dummy_files/nucleus-history.json | 4 ++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 9355de60ed..6f9e532085 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -435,50 +435,57 @@ where /// Reads sender and receiver addresses properly from an IBC event. fn read_real_ibc_addresses(transfer_details: &mut TransferDetails, msg_event: &Event) { - transfer_details.transfer_event_type = match msg_event.kind.as_str() { + let event_type = match msg_event.kind.as_str() { IBC_SEND_EVENT => TransferEventType::IBCSend, IBC_RECEIVE_EVENT | IBC_NFT_RECEIVE_EVENT => TransferEventType::IBCReceive, _ => unreachable!("`read_real_ibc_addresses` shouldn't be called for non-IBC events."), }; - transfer_details.from = some_or_return!(get_value_from_event_attributes( + let from = some_or_return!(get_value_from_event_attributes( &msg_event.attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64 )); - transfer_details.to = some_or_return!(get_value_from_event_attributes( + let to = some_or_return!(get_value_from_event_attributes( &msg_event.attributes, RECEIVER_TAG_KEY, RECEIVER_TAG_KEY_BASE64, )); + + transfer_details.from = from; + transfer_details.to = to; + transfer_details.transfer_event_type = event_type; } /// Reads sender and receiver addresses properly from an HTLC event. fn read_real_htlc_addresses(transfer_details: &mut TransferDetails, msg_event: &&Event) { match msg_event.kind.as_str() { CREATE_HTLC_EVENT => { - transfer_details.from = some_or_return!(get_value_from_event_attributes( + let from = some_or_return!(get_value_from_event_attributes( &msg_event.attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64 )); - transfer_details.to = some_or_return!(get_value_from_event_attributes( + let to = some_or_return!(get_value_from_event_attributes( &msg_event.attributes, RECEIVER_TAG_KEY, RECEIVER_TAG_KEY_BASE64, )); + transfer_details.from = from; + transfer_details.to = to; transfer_details.transfer_event_type = TransferEventType::CreateHtlc; }, CLAIM_HTLC_EVENT => { - transfer_details.from = some_or_return!(get_value_from_event_attributes( + let from = some_or_return!(get_value_from_event_attributes( &msg_event.attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64 )); + transfer_details.from = from; transfer_details.transfer_event_type = TransferEventType::ClaimHtlc; }, _ => unreachable!("`read_real_htlc_addresses` shouldn't be called for non-HTLC events."), diff --git a/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json b/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json index 2caab09b28..de1a79b1e8 100644 --- a/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json +++ b/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json @@ -22,7 +22,7 @@ }, "coin": "NUCLEUS-TEST", "internal_id": "4335303135373938433145384342333931424145373846420000000000000000", - "transaction_type": "TendermintIBCTransfer", + "transaction_type": "StandardTransfer", "memo": "rly(2.5.2-28-gdf42391)" }, { @@ -48,7 +48,7 @@ }, "coin": "NUCLEUS-TEST", "internal_id": "3031414542453143353638454230314344383437414235390000000000000000", - "transaction_type": "StandardTransfer", + "transaction_type": "TendermintIBCTransfer", "memo": "" }, { From 8aca55973b1bb84c3c5dd2641b415ed4208fd2bd Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 18 Feb 2025 16:51:31 +0300 Subject: [PATCH 17/18] fix notes Signed-off-by: onur-ozkan --- mm2src/coins/rpc_command/tendermint/staking.rs | 3 +++ mm2src/coins/tendermint/tendermint_coin.rs | 14 +++++++++----- .../src/tendermint_with_assets_activation.rs | 10 ++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index ac61d4e6f4..5bf6b2a427 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -169,6 +169,9 @@ pub struct ClaimRewardsPayload { pub fee: Option, #[serde(default)] pub memo: String, + /// If transaction fee exceeds the reward amount users will be + /// prevented from claiming their rewards as it will not be profitable. + /// Setting `force` to `true` disables this logic. #[serde(default)] pub force: bool, } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index a92ff95823..63a060c6a5 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -75,6 +75,7 @@ use mm2_git::{FileMetadata, GitController, GithubClient, RepositoryOperations, G use mm2_number::bigdecimal::ParseBigDecimalError; use mm2_number::MmNumber; use mm2_p2p::p2p_ctx::P2PContext; +use num_traits::Zero; use parking_lot::Mutex as PaMutex; use primitives::hash::H256; use regex::Regex; @@ -186,7 +187,7 @@ pub struct TendermintFeeDetails { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct TendermintProtocolInfo { - decimals: u8, + pub decimals: u8, denom: String, pub account_prefix: String, chain_id: String, @@ -412,6 +413,7 @@ pub enum TendermintInitErrorKind { RpcClientInitError(String), InvalidChainId(String), InvalidDenom(String), + InvalidProtocolData(String), InvalidPathToAddress(String), #[display(fmt = "'derivation_path' field is not found in config")] DerivationPathIsNotSet, @@ -2506,9 +2508,11 @@ impl TendermintCoin { let decoded_response = QueryDelegationRewardsResponse::decode(raw_response.value.as_slice()) .map_err(|e| DelegationError::InternalError(e.to_string()))?; - let denom = self.denom.to_string(); - - match decoded_response.rewards.iter().find(|t| t.denom == denom) { + match decoded_response + .rewards + .iter() + .find(|t| t.denom == self.denom.to_string()) + { Some(dec_coin) => extract_big_decimal_from_dec_coin(dec_coin, self.decimals as u32) .map_to_mm(|e| DelegationError::InternalError(e.to_string())), None => MmError::err(DelegationError::NothingToClaim { @@ -2537,7 +2541,7 @@ impl TendermintCoin { let reward_amount = self.get_delegation_reward_amount(&validator_address).await?; - if reward_amount == BigDecimal::from(0) { + if reward_amount.is_zero() { return MmError::err(DelegationError::NothingToClaim { coin: self.ticker.clone(), }); diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index d62c9ebd8b..29ce30b76b 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -235,6 +235,16 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { activation_request: Self::ActivationRequest, protocol_conf: Self::PlatformProtocolInfo, ) -> Result> { + if protocol_conf.decimals > 18 { + return MmError::err(TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::InvalidProtocolData(format!( + "'decimals' value is too high; it must be 18 or lower but the current value is {}", + protocol_conf.decimals + )), + }); + } + let conf = TendermintConf::try_from_json(&ticker, coin_conf)?; let is_keplr_from_ledger = activation_request.is_keplr_from_ledger && activation_request.with_pubkey.is_some(); From 2ca8c49d6d17c90cfe3656084ea94df4299a7c31 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 25 Feb 2025 09:55:50 +0300 Subject: [PATCH 18/18] update error Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 589ed869be..ed5635ea45 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -5012,8 +5012,8 @@ pub async fn claim_staking_rewards(ctx: MmArc, req: ClaimStakingRewardsRequest) let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; let MmCoinEnum::Tendermint(tendermint) = coin else { - return MmError::err(DelegationError::CoinDoesntSupportDelegation { - coin: coin.ticker().to_string(), + return MmError::err(DelegationError::InvalidPayload { + reason: format!("{} is not a Cosmos coin", req.coin) }); };