diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 902d7f3fb9..ed5635ea45 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2209,20 +2209,30 @@ 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, pub staking_details: Option, } +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum ClaimingDetails { + Cosmos(rpc_command::tendermint::staking::ClaimRewardsPayload), +} + +#[derive(Deserialize)] +pub struct ClaimStakingRewardsRequest { + pub coin: String, + pub claiming_details: ClaimingDetails, +} + #[derive(Deserialize)] pub struct GetStakingInfosRequest { pub coin: String, @@ -2349,6 +2359,7 @@ impl KmdRewardsDetails { pub enum TransactionType { StakingDelegation, RemoveDelegation, + ClaimDelegationRewards, #[default] StandardTransfer, TokenTransfer(BytesJson), @@ -2848,6 +2859,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")] @@ -4987,6 +5006,22 @@ pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationRe } } +pub async fn claim_staking_rewards(ctx: MmArc, req: ClaimStakingRewardsRequest) -> DelegationResult { + match req.claiming_details { + ClaimingDetails::Cosmos(r) => { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + + let MmCoinEnum::Tendermint(tendermint) = coin else { + return MmError::err(DelegationError::InvalidPayload { + reason: format!("{} is not a Cosmos coin", req.coin) + }); + }; + + tendermint.claim_staking_rewards(r).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/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/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 3ca77b0295..5bf6b2a427 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -162,3 +162,16 @@ 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, + /// 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 2b5439c6d4..d93d7be0d4 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, @@ -35,12 +35,14 @@ 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; 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}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, @@ -66,8 +68,10 @@ 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 num_traits::Zero; use parking_lot::Mutex as PaMutex; use primitives::hash::H256; use regex::Regex; @@ -92,6 +96,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; @@ -178,7 +183,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, @@ -404,6 +409,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, @@ -2422,7 +2428,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> { @@ -2468,6 +2474,158 @@ impl TendermintCoin { Ok((big_decimal_from_sat_unsigned(uamount, self.decimals()), uamount)) } + + 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 raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_DELEGATION_REWARDS_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()))?; + + 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 { + coin: self.ticker.clone(), + }), + } + } + + 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 reward_amount = self.get_delegation_reward_amount(&validator_address).await?; + + if reward_amount.is_zero() { + return MmError::err(DelegationError::NothingToClaim { + coin: self.ticker.clone(), + }); + } + + let timeout_height = self + .current_block() + .compat() + .await + .map_to_mm(DelegationError::Transport)? + + TIMEOUT_HEIGHT_DELTA; + + // 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 + .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, + }); + } + + 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(), + 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![validator_address.to_string()], + to: vec![delegator_address.to_string()], + my_balance_change: &reward_amount - &fee_amount_dec, + spent_by_me: fee_amount_dec.clone(), + total_amount: reward_amount.clone(), + received_by_me: reward_amount, + 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::ClaimDelegationRewards, + memo: Some(req.memo), + }) + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { @@ -3562,6 +3720,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 = @@ -4479,4 +4644,74 @@ 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); + } + + #[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); + } } diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 22caa0554b..6f9e532085 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)] @@ -431,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."), @@ -581,6 +592,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 +644,21 @@ where } fn get_transfer_details(tx_events: Vec, fee_amount_with_denom: String) -> Vec { + // 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 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 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]); + }; + // Filter out irrelevant events let mut events: Vec<&Event> = tx_events .iter() @@ -617,15 +667,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 +703,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 +732,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![])), 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(); diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 4dc4f9dae4..ea0e3258f0 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}; +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, @@ -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, diff --git a/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json b/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json index 4400c318e0..de1a79b1e8 100644 --- a/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json +++ b/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json @@ -48,7 +48,7 @@ }, "coin": "NUCLEUS-TEST", "internal_id": "3031414542453143353638454230314344383437414235390000000000000000", - "transaction_type": "StandardTransfer", + "transaction_type": "TendermintIBCTransfer", "memo": "" }, {