From 6a52a2d3cf03a05d7aaa7c714bce1215f9622ec3 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 27 Jan 2025 10:57:48 +0000 Subject: [PATCH 01/12] save initial state Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 4 +- .../coins/rpc_command/tendermint/staking.rs | 2 +- mm2src/coins/tendermint/tendermint_coin.rs | 51 +++++++++++++++++-- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index ce23c3bfbe..b904c88bfe 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2135,7 +2135,7 @@ pub struct WithdrawRequest { #[serde(tag = "type")] pub enum StakingDetails { Qtum(QtumDelegationRequest), - Cosmos(Box), + Cosmos(Box), } #[allow(dead_code)] @@ -4897,7 +4897,7 @@ pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationRe }); }; - tendermint.add_delegate(*req).await + tendermint.delegate(*req).await }, } } diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index e2d50ad9c0..3ca77b0295 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -151,7 +151,7 @@ pub async fn validators_rpc( } #[derive(Clone, Debug, Deserialize)] -pub struct DelegatePayload { +pub struct DelegationPayload { pub validator_address: String, pub fee: Option, pub withdraw_from: Option, diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 9a1339b965..72066a3cad 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::{DelegatePayload, ValidatorStatus}; +use crate::rpc_command::tendermint::staking::{DelegationPayload, ValidatorStatus}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -51,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::{MsgDelegate, QueryValidatorsResponse, Validator}; +use cosmrs::staking::{MsgDelegate, MsgUndelegate, QueryValidatorRequest, QueryValidatorsResponse, Validator}; use cosmrs::tendermint::block::Height; use cosmrs::tendermint::chain::Id as ChainId; use cosmrs::tendermint::PublicKey; @@ -2123,7 +2123,7 @@ impl TendermintCoin { Ok(typed_response.validators) } - pub(crate) async fn add_delegate(&self, req: DelegatePayload) -> MmResult { + pub(crate) async fn delegate(&self, req: DelegationPayload) -> MmResult { fn generate_message( delegator_address: AccountId, validator_address: AccountId, @@ -2296,6 +2296,51 @@ impl TendermintCoin { memo: Some(req.memo), }) } + + pub(crate) async fn undelegate(&self, req: DelegationPayload) -> MmResult { + // TODO: + // 1. Should we have prevalidation? (e.g., check if user delegated the undelegate amount) + // a) or, leave the validation to the network (this will cost users to pay the transaction fee, which isn't too bad if we don't want them to spam invalid values). + // + // 2. How can we utilize `max` ? If we want to utilize `max` parameter, we need to + // query existing delegation and parse the amount from there. This is basically implementing 1. above. + // a) e.g., query http://35.234.10.84:1317/cosmos/staking/v1beta1/delegations/iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2 + // + // 3. Add test coverage for: + // a) delegate some funds + // b) query and assert the delegated value + // c) undelegate + + QueryValidatorRequest { + validator_addr: todo!(), + }; + + fn generate_message( + delegator_address: AccountId, + validator_address: AccountId, + denom: Denom, + amount: u128, + ) -> Result { + MsgUndelegate { + delegator_address, + validator_address, + amount: Coin { denom, amount }, + } + .to_any() + .map_err(|e| e.to_string()) + } + + todo!() + } + + pub(crate) async fn get_delegated_amount( + &self, + validator_addr: AccountId, + ) -> MmResult { + let request = QueryValidatorRequest { validator_addr }; + + todo!() + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { From 6061ec5ee24a0595dc2510cbc7e19bc2a9ca8e2b Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 27 Jan 2025 13:05:31 +0000 Subject: [PATCH 02/12] save p.o.c state Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 2 ++ mm2src/coins/tendermint/tendermint_coin.rs | 40 +++++++++++++++++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index b904c88bfe..dc8971fa05 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -4863,6 +4863,8 @@ pub async fn remove_delegation(ctx: MmArc, req: RemoveDelegateRequest) -> Delega }) }, } + + // TODO: support tendermint::undelegate } pub async fn get_staking_infos(ctx: MmArc, req: GetStakingInfosRequest) -> StakingInfosResult { diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 72066a3cad..68c4a90a3e 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -46,12 +46,12 @@ 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::staking::v1beta1::{QueryValidatorsRequest, +use cosmrs::proto::cosmos::staking::v1beta1::{QueryDelegationRequest, QueryDelegationResponse, QueryValidatorsRequest, QueryValidatorsResponse as QueryValidatorsResponseProto}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; use cosmrs::proto::prost::{DecodeError, Message}; -use cosmrs::staking::{MsgDelegate, MsgUndelegate, QueryValidatorRequest, QueryValidatorsResponse, Validator}; +use cosmrs::staking::{MsgDelegate, MsgUndelegate, QueryValidatorsResponse, Validator}; use cosmrs::tendermint::block::Height; use cosmrs::tendermint::chain::Id as ChainId; use cosmrs::tendermint::PublicKey; @@ -2311,10 +2311,6 @@ impl TendermintCoin { // b) query and assert the delegated value // c) undelegate - QueryValidatorRequest { - validator_addr: todo!(), - }; - fn generate_message( delegator_address: AccountId, validator_address: AccountId, @@ -2330,16 +2326,42 @@ impl TendermintCoin { .map_err(|e| e.to_string()) } + let validator_address = + AccountId::from_str(&req.validator_address).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; + + let delegated_amount = self.get_delegated_amount(&validator_address).await; + println!("===== {:?}", delegated_amount); + todo!() } pub(crate) async fn get_delegated_amount( &self, - validator_addr: AccountId, + validator_addr: &AccountId, // keep this as `AccountId` to have it pre-validated ) -> MmResult { - let request = QueryValidatorRequest { validator_addr }; + let request = QueryDelegationRequest { + delegator_addr: self.my_address().expect("TODO"), + validator_addr: validator_addr.to_string(), + }; - todo!() + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_VALIDATORS_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await + .expect("TODO"); + + let decoded_proto = QueryDelegationResponse::decode(raw_response.value.as_slice()).expect("TODO"); + let uamount = decoded_proto.delegation_response.unwrap().balance.unwrap().amount; + let uamount = u64::from_str(&uamount).expect("TODO"); + let amount_dec = big_decimal_from_sat_unsigned(uamount, self.decimals()); + + Ok(amount_dec) } } From d43a50020c6f0d034e3b9442086150bacc91e122 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 28 Jan 2025 09:31:20 +0000 Subject: [PATCH 03/12] wire cosmos `undelegate` to KDF RPC Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 34 +++++++++++++++++----- mm2src/coins/tendermint/tendermint_coin.rs | 5 +++- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index dc8971fa05..8aa394d689 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2149,6 +2149,7 @@ pub struct AddDelegateRequest { #[derive(Deserialize)] pub struct RemoveDelegateRequest { pub coin: String, + pub staking_details: Option, } #[derive(Deserialize)] @@ -2776,6 +2777,8 @@ pub enum DelegationError { #[display(fmt = "Transport error: {}", _0)] Transport(String), #[from_stringify("MyAddressError")] + #[display(fmt = "Invalid payload: {}", _0)] + InvalidPayload(String), #[display(fmt = "Internal error: {}", _0)] InternalError(String), } @@ -4855,16 +4858,31 @@ pub async fn sign_raw_transaction(ctx: MmArc, req: SignRawTransactionRequest) -> pub async fn remove_delegation(ctx: MmArc, req: RemoveDelegateRequest) -> DelegationResult { let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - match coin { - MmCoinEnum::QtumCoin(qtum) => qtum.remove_delegation().compat().await, - _ => { - return MmError::err(DelegationError::CoinDoesntSupportDelegation { - coin: coin.ticker().to_string(), - }) + + match req.staking_details { + Some(StakingDetails::Cosmos(req)) => { + let MmCoinEnum::Tendermint(tendermint) = coin else { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }); + }; + + tendermint.undelegate(*req).await }, - } - // TODO: support tendermint::undelegate + Some(StakingDetails::Qtum(_)) => MmError::err(DelegationError::InvalidPayload( + "staking_details isn't supported for Qtum".into(), + )), + + None => match coin { + MmCoinEnum::QtumCoin(qtum) => qtum.remove_delegation().compat().await, + _ => { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }) + }, + }, + } } pub async fn get_staking_infos(ctx: MmArc, req: GetStakingInfosRequest) -> StakingInfosResult { diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 68c4a90a3e..327aa76359 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -96,6 +96,7 @@ const ABCI_QUERY_BALANCE_PATH: &str = "/cosmos.bank.v1beta1.Query/Balance"; 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"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -2343,12 +2344,13 @@ impl TendermintCoin { delegator_addr: self.my_address().expect("TODO"), validator_addr: validator_addr.to_string(), }; + println!("!! {:?}", request); let raw_response = self .rpc_client() .await? .abci_query( - Some(ABCI_VALIDATORS_PATH.to_owned()), + Some(ABCI_DELEGATION_PATH.to_owned()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -2357,6 +2359,7 @@ impl TendermintCoin { .expect("TODO"); let decoded_proto = QueryDelegationResponse::decode(raw_response.value.as_slice()).expect("TODO"); + println!("000 {:?}", decoded_proto); let uamount = decoded_proto.delegation_response.unwrap().balance.unwrap().amount; let uamount = u64::from_str(&uamount).expect("TODO"); let amount_dec = big_decimal_from_sat_unsigned(uamount, self.decimals()); From d931c0dbaf2bc8dd82b99eedac8a34f5777f9090 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 28 Jan 2025 11:40:24 +0000 Subject: [PATCH 04/12] stabilize `get_delegated_amount` Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 19 +++++++--- mm2src/coins/tendermint/tendermint_coin.rs | 40 +++++++++++++++------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 8aa394d689..326e4b1a92 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2765,6 +2765,15 @@ pub enum DelegationError { CoinDoesntSupportDelegation { coin: String }, #[display(fmt = "No such coin {}", coin)] NoSuchCoin { coin: String }, + #[display( + fmt = "Delegator '{}' does not have any delegation to validator '{}'.", + delegator_addr, + validator_addr + )] + CanNotUndelegate { + delegator_addr: String, + validator_addr: String, + }, #[display(fmt = "{}", _0)] CannotInteractWithSmartContract(String), #[from_stringify("ScriptHashTypeNotSupported")] @@ -2776,9 +2785,9 @@ pub enum DelegationError { DelegationOpsNotSupported { reason: String }, #[display(fmt = "Transport error: {}", _0)] Transport(String), + #[display(fmt = "Invalid payload: {}", reason)] + InvalidPayload { reason: String }, #[from_stringify("MyAddressError")] - #[display(fmt = "Invalid payload: {}", _0)] - InvalidPayload(String), #[display(fmt = "Internal error: {}", _0)] InternalError(String), } @@ -4870,9 +4879,9 @@ pub async fn remove_delegation(ctx: MmArc, req: RemoveDelegateRequest) -> Delega tendermint.undelegate(*req).await }, - Some(StakingDetails::Qtum(_)) => MmError::err(DelegationError::InvalidPayload( - "staking_details isn't supported for Qtum".into(), - )), + Some(StakingDetails::Qtum(_)) => MmError::err(DelegationError::InvalidPayload { + reason: "staking_details isn't supported for Qtum".into(), + }), None => match coin { MmCoinEnum::QtumCoin(qtum) => qtum.remove_delegation().compat().await, diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 327aa76359..0bcb8afa28 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2338,13 +2338,17 @@ impl TendermintCoin { pub(crate) async fn get_delegated_amount( &self, - validator_addr: &AccountId, // keep this as `AccountId` to have it pre-validated + validator_addr: &AccountId, // keep this as `AccountId` to make it pre-validated ) -> MmResult { + let delegator_addr = self + .my_address() + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + let validator_addr = validator_addr.to_string(); + let request = QueryDelegationRequest { - delegator_addr: self.my_address().expect("TODO"), - validator_addr: validator_addr.to_string(), + delegator_addr, + validator_addr, }; - println!("!! {:?}", request); let raw_response = self .rpc_client() @@ -2355,16 +2359,28 @@ impl TendermintCoin { ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, ) - .await - .expect("TODO"); + .map_err(|e| DelegationError::Transport(e.to_string())) + .await?; + + let decoded_response = QueryDelegationResponse::decode(raw_response.value.as_slice()) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + let Some(delegation_response) = decoded_response.delegation_response else { + return MmError::err(DelegationError::CanNotUndelegate { + delegator_addr: request.delegator_addr, + validator_addr: request.validator_addr, + }); + }; + + let Some(balance) = delegation_response.balance else { + return MmError::err(DelegationError::Transport( + format!("Unexpected response from '{ABCI_DELEGATION_PATH}' with {request:?} request; balance field should not be empty.") + )); + }; - let decoded_proto = QueryDelegationResponse::decode(raw_response.value.as_slice()).expect("TODO"); - println!("000 {:?}", decoded_proto); - let uamount = decoded_proto.delegation_response.unwrap().balance.unwrap().amount; - let uamount = u64::from_str(&uamount).expect("TODO"); - let amount_dec = big_decimal_from_sat_unsigned(uamount, self.decimals()); + let uamount = u64::from_str(&balance.amount).map_err(|e| DelegationError::InternalError(e.to_string()))?; - Ok(amount_dec) + Ok(big_decimal_from_sat_unsigned(uamount, self.decimals())) } } From 0fef1c5cd05eb37d8781a5028357c70456affe8c Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 28 Jan 2025 12:51:18 +0000 Subject: [PATCH 05/12] implement tendermint undelegate tx handling Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 15 +++ mm2src/coins/tendermint/tendermint_coin.rs | 117 ++++++++++++++++++++- 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 326e4b1a92..05f3eef945 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2774,6 +2774,15 @@ pub enum DelegationError { delegator_addr: String, validator_addr: String, }, + #[display( + fmt = "Max available amount to undelegate is '{}' but '{}' was requested.", + available, + requested + )] + TooMuchToUndelegate { + available: BigDecimal, + requested: BigDecimal, + }, #[display(fmt = "{}", _0)] CannotInteractWithSmartContract(String), #[from_stringify("ScriptHashTypeNotSupported")] @@ -4870,6 +4879,12 @@ pub async fn remove_delegation(ctx: MmArc, req: RemoveDelegateRequest) -> Delega match req.staking_details { Some(StakingDetails::Cosmos(req)) => { + if req.withdraw_from.is_some() { + return MmError::err(DelegationError::InvalidPayload { + reason: "Can't use `withdraw_from` field on 'remove_delegation' RPC for Cosmos.".to_owned(), + }); + } + let MmCoinEnum::Tendermint(tendermint) = coin else { return MmError::err(DelegationError::CoinDoesntSupportDelegation { coin: coin.ticker().to_string(), diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 0bcb8afa28..3c1a7b9561 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2327,19 +2327,126 @@ impl TendermintCoin { .map_err(|e| e.to_string()) } + 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 delegated_amount = self.get_delegated_amount(&validator_address).await; - println!("===== {:?}", delegated_amount); + let (total_delegated_amount, total_delegated_uamount) = self.get_delegated_amount(&validator_address).await?; + + let uamount_to_undelegate = if req.max { + total_delegated_uamount + } else { + if req.amount > total_delegated_amount { + return MmError::err(DelegationError::TooMuchToUndelegate { + available: total_delegated_amount, + requested: req.amount, + }); + }; + + sat_from_big_decimal(&req.amount, self.decimals) + .map_err(|e| DelegationError::InternalError(e.to_string()))? + }; + + let undelegate_msg = generate_message( + delegator_address.clone(), + validator_address.clone(), + self.denom.clone(), + uamount_to_undelegate.into(), + ) + .map_err(DelegationError::InternalError)?; + + let timeout_height = self + .current_block() + .compat() + .await + .map_to_mm(DelegationError::Transport)? + + TIMEOUT_HEIGHT_DELTA; + + // This uses more gas than the regular transactions + // TODO: this is common logic with `delegate` function + 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, + undelegate_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; - todo!() + 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, + undelegate_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().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() - &fee_amount_dec, + spent_by_me: fee_amount_dec.clone(), + total_amount: &BigDecimal::default() - &fee_amount_dec, + 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), + }) } pub(crate) async fn get_delegated_amount( &self, validator_addr: &AccountId, // keep this as `AccountId` to make it pre-validated - ) -> MmResult { + ) -> MmResult<(BigDecimal, u64), DelegationError> { let delegator_addr = self .my_address() .map_err(|e| DelegationError::InternalError(e.to_string()))?; @@ -2380,7 +2487,7 @@ impl TendermintCoin { let uamount = u64::from_str(&balance.amount).map_err(|e| DelegationError::InternalError(e.to_string()))?; - Ok(big_decimal_from_sat_unsigned(uamount, self.decimals())) + Ok((big_decimal_from_sat_unsigned(uamount, self.decimals()), uamount)) } } From 46ffac4ad4be65962648f30e90aa4d578d629012 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 29 Jan 2025 06:53:15 +0000 Subject: [PATCH 06/12] update transaction details for undelegation Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 3c1a7b9561..bfb9eac2a8 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2299,19 +2299,6 @@ impl TendermintCoin { } pub(crate) async fn undelegate(&self, req: DelegationPayload) -> MmResult { - // TODO: - // 1. Should we have prevalidation? (e.g., check if user delegated the undelegate amount) - // a) or, leave the validation to the network (this will cost users to pay the transaction fee, which isn't too bad if we don't want them to spam invalid values). - // - // 2. How can we utilize `max` ? If we want to utilize `max` parameter, we need to - // query existing delegation and parse the amount from there. This is basically implementing 1. above. - // a) e.g., query http://35.234.10.84:1317/cosmos/staking/v1beta1/delegations/iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2 - // - // 3. Add test coverage for: - // a) delegate some funds - // b) query and assert the delegated value - // c) undelegate - fn generate_message( delegator_address: AccountId, validator_address: AccountId, @@ -2365,8 +2352,7 @@ impl TendermintCoin { .map_to_mm(DelegationError::Transport)? + TIMEOUT_HEIGHT_DELTA; - // This uses more gas than the regular transactions - // TODO: this is common logic with `delegate` function + // 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); @@ -2422,10 +2408,10 @@ impl TendermintCoin { Ok(TransactionDetails { tx, from: vec![delegator_address.to_string()], - to: vec![req.validator_address], + 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: &BigDecimal::default() - &fee_amount_dec, + total_amount: fee_amount_dec.clone(), received_by_me: BigDecimal::default(), block_height: 0, timestamp: 0, From 2fd997d85ebf9427f9dbed7d0c059f5e2f947c42 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 29 Jan 2025 07:13:05 +0000 Subject: [PATCH 07/12] add test coverage Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 2 +- .../tests/docker_tests/tendermint_tests.rs | 50 ++++++++++++++++++- mm2src/mm2_test_helpers/src/for_tests.rs | 30 +++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 05f3eef945..3f2c58069e 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2766,7 +2766,7 @@ pub enum DelegationError { #[display(fmt = "No such coin {}", coin)] NoSuchCoin { coin: String }, #[display( - fmt = "Delegator '{}' does not have any delegation to validator '{}'.", + fmt = "Delegator '{}' does not have any delegation on validator '{}'.", delegator_addr, validator_addr )] diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index 79d1247401..e4b68b8b99 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -5,8 +5,8 @@ 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_add_delegation, tendermint_validators, withdraw_v1, - MarketMakerIt, Mm2TestConf}; + set_price, tendermint_add_delegation, tendermint_remove_delegation, + tendermint_validators, withdraw_v1, MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, OrderbookAddress, OrderbookV2Response, RpcV2Response, TendermintActivationResult, TransactionDetails, TransactionType}; use serde_json::json; @@ -712,6 +712,52 @@ fn test_tendermint_add_delegation() { log!("Send raw tx {}", serde_json::to_string(&send_raw_tx).unwrap()); } +#[test] +fn test_tendermint_remove_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()); + + // TODO: check currently delegated stakes and assert them + // This requires delegation listing feature + + let tx_details = block_on(tendermint_remove_delegation(&mm, coin_ticker, VALIDATOR_ADDRESS, "0.5")); + + assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); + assert!(tx_details.to.is_empty()); + assert_eq!(tx_details.transaction_type, TransactionType::StakingDelegation); + + // TODO: check currently delegated stakes and assert them + // This requires delegation listing feature +} + 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 d1db770ea2..2c988b1a69 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -3148,6 +3148,36 @@ pub async fn tendermint_add_delegation( json::from_value(json["result"].clone()).unwrap() } +pub async fn tendermint_remove_delegation( + mm: &MarketMakerIt, + coin: &str, + validator_address: &str, + amount: &str, +) -> TransactionDetails { + let rpc_endpoint = "remove_delegation"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "coin": coin, + "staking_details": { + "type": "Cosmos", + "validator_address": validator_address, + "amount": amount, + } + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let response = mm.rpc(&request).await.unwrap(); + assert_eq!(response.0, StatusCode::OK, "{rpc_endpoint} failed: {}", response.1); + log!("{rpc_endpoint} response {}", response.1); + + let json: Json = json::from_str(&response.1).unwrap(); + json::from_value(json["result"].clone()).unwrap() +} + pub async fn init_utxo_electrum( mm: &MarketMakerIt, coin: &str, From 7e361b5226393de50a7318ff86856f7d707e4417 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 29 Jan 2025 10:22:01 +0000 Subject: [PATCH 08/12] add tx history support for cosmos undelegate txs Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 2 +- .../tendermint/tendermint_tx_history_v2.rs | 42 +++++++++++++++++-- .../tests/docker_tests/tendermint_tests.rs | 2 +- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index bfb9eac2a8..b467af7ae0 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2424,7 +2424,7 @@ impl TendermintCoin { coin: self.ticker.to_string(), internal_id, kmd_rewards: None, - transaction_type: TransactionType::StakingDelegation, + transaction_type: TransactionType::RemoveDelegation, memo: Some(req.memo), }) } diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index b170f08b81..00c3e19682 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -38,6 +38,7 @@ 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 ACCEPTED_EVENTS: &[&str] = &[ TRANSFER_EVENT, @@ -47,6 +48,7 @@ const ACCEPTED_EVENTS: &[&str] = &[ IBC_RECEIVE_EVENT, IBC_NFT_RECEIVE_EVENT, DELEGATE_EVENT, + UNDELEGATE_EVENT, ]; const RECEIVER_TAG_KEY: &str = "receiver"; @@ -412,6 +414,7 @@ where IBCSend, IBCReceive, Delegate, + Undelegate, } #[derive(Clone)] @@ -557,6 +560,24 @@ where handle_new_transfer_event(&mut transfer_details_list, tx_details); }, + UNDELEGATE_EVENT => { + let from = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + DELEGATOR_TAG_KEY, + DELEGATOR_TAG_KEY_BASE64, + )); + + let tx_details = TransferDetails { + from, + to: String::default(), + denom: denom.to_owned(), + amount: 0, + transfer_event_type: TransferEventType::Undelegate, + }; + + handle_new_transfer_event(&mut transfer_details_list, tx_details); + }, + unrecognized => { log::warn!( "Found an unrecognized event '{unrecognized}' in transaction history processing." @@ -594,15 +615,26 @@ where events.reverse(); if events.len() > DEFAULT_TRANSFER_EVENT_COUNT { - // Retain fee related events + 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. if event.kind == TRANSFER_EVENT { let amount_with_denom = get_value_from_event_attributes(&event.attributes, AMOUNT_TAG_KEY, AMOUNT_TAG_KEY_BASE64); - amount_with_denom != Some(fee_amount_with_denom.clone()) - } else { - true + + return amount_with_denom != Some(fee_amount_with_denom.clone()); } + + true }); } @@ -628,6 +660,7 @@ where token_id, }, (TransferEventType::Delegate, _) => TransactionType::StakingDelegation, + (TransferEventType::Undelegate, _) => TransactionType::RemoveDelegation, (_, Some(token_id)) => TransactionType::TokenTransfer(token_id), _ => TransactionType::StandardTransfer, } @@ -654,6 +687,7 @@ where | TransferEventType::Delegate => { Some((vec![transfer_details.from.clone()], vec![transfer_details.to.clone()])) }, + TransferEventType::Undelegate => Some((vec![my_address], vec![])), } } diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index e4b68b8b99..3e7954ae07 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -752,7 +752,7 @@ fn test_tendermint_remove_delegation() { assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); assert!(tx_details.to.is_empty()); - assert_eq!(tx_details.transaction_type, TransactionType::StakingDelegation); + assert_eq!(tx_details.transaction_type, TransactionType::RemoveDelegation); // TODO: check currently delegated stakes and assert them // This requires delegation listing feature From a82d4c2d1f92892cc2bb7ad3910fc9b12742ef41 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Sun, 2 Feb 2025 07:20:00 +0000 Subject: [PATCH 09/12] add coverage for `DelegationError::TooMuchToUndelegate` error Signed-off-by: onur-ozkan --- .../tests/docker_tests/tendermint_tests.rs | 7 ++++++- mm2src/mm2_test_helpers/src/for_tests.rs | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index 3e7954ae07..f15b10a4d6 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -5,7 +5,7 @@ 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_add_delegation, tendermint_remove_delegation, + set_price, tendermint_add_delegation, tendermint_remove_delegation_raw, tendermint_remove_delegation, tendermint_validators, withdraw_v1, MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, OrderbookAddress, OrderbookV2Response, RpcV2Response, TendermintActivationResult, TransactionDetails, TransactionType}; @@ -745,6 +745,11 @@ fn test_tendermint_remove_delegation() { 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()); + // Try to undelegate more than the total delegated amount + let raw_response = block_on(tendermint_remove_delegation_raw(&mm, coin_ticker, VALIDATOR_ADDRESS, "0.77")); + assert_eq!(raw_response.0, http::StatusCode::BAD_REQUEST); + assert!(raw_response.1.contains("DelegationError::TooMuchToUndelegate")); + // TODO: check currently delegated stakes and assert them // This requires delegation listing feature diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 2c988b1a69..f67fb4252f 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -3148,12 +3148,12 @@ pub async fn tendermint_add_delegation( json::from_value(json["result"].clone()).unwrap() } -pub async fn tendermint_remove_delegation( +pub async fn tendermint_remove_delegation_raw( mm: &MarketMakerIt, coin: &str, validator_address: &str, amount: &str, -) -> TransactionDetails { +) -> (StatusCode, String, HeaderMap) { let rpc_endpoint = "remove_delegation"; let request = json!({ "userpass": mm.userpass, @@ -3170,7 +3170,17 @@ pub async fn tendermint_remove_delegation( }); log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); - let response = mm.rpc(&request).await.unwrap(); + mm.rpc(&request).await.unwrap() +} + +pub async fn tendermint_remove_delegation( + mm: &MarketMakerIt, + coin: &str, + validator_address: &str, + amount: &str, +) -> TransactionDetails { + let rpc_endpoint = "remove_delegation"; + let response = tendermint_remove_delegation_raw(mm, coin, validator_address, amount).await; assert_eq!(response.0, StatusCode::OK, "{rpc_endpoint} failed: {}", response.1); log!("{rpc_endpoint} response {}", response.1); From 7f5f68a20565268f8a4d5eda1137acd6664f303e Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Sun, 2 Feb 2025 07:49:37 +0000 Subject: [PATCH 10/12] lock certain tests Signed-off-by: onur-ozkan --- .../tests/docker_tests/tendermint_tests.rs | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index f15b10a4d6..52788ab1df 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -5,13 +5,15 @@ 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_add_delegation, tendermint_remove_delegation_raw, tendermint_remove_delegation, - tendermint_validators, withdraw_v1, MarketMakerIt, Mm2TestConf}; + set_price, tendermint_add_delegation, tendermint_remove_delegation, + tendermint_remove_delegation_raw, tendermint_validators, withdraw_v1, MarketMakerIt, + Mm2TestConf}; use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, OrderbookAddress, OrderbookV2Response, RpcV2Response, TendermintActivationResult, TransactionDetails, TransactionType}; use serde_json::json; use std::collections::HashSet; use std::iter::FromIterator; +use std::sync::Mutex; const TENDERMINT_TEST_SEED: &str = "tendermint test seed"; const TENDERMINT_CONSTANT_BALANCE_SEED: &str = "tendermint constant balance seed"; @@ -22,6 +24,12 @@ const NUCLEUS_TESTNET_RPC_URLS: &[&str] = &["http://localhost:26657"]; const TENDERMINT_TEST_BIP39_SEED: &str = "emerge canoe salmon dolphin glow priority random become gasp sell blade argue"; +lazy_static! { + /// Makes sure that tests sending transactions run sequentially to prevent account sequence + /// mismatches as some addresses are used in multiple tests. + static ref SEQUENCE_LOCK: Mutex<()> = Mutex::new(()); +} + #[test] fn test_tendermint_balance() { let coins = json!([atom_testnet_conf()]); @@ -160,6 +168,7 @@ fn test_tendermint_hd_address() { #[test] fn test_tendermint_withdraw() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); const MY_ADDRESS: &str = "cosmos150evuj4j7k9kgu38e453jdv9m3u0ft2n53flg6"; let coins = json!([atom_testnet_conf()]); @@ -217,6 +226,7 @@ fn test_tendermint_withdraw() { #[test] fn test_tendermint_withdraw_hd() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); const MY_ADDRESS: &str = "cosmos134h9tv7866jcuw708w5w76lcfx7s3x2ysyalxy"; let coins = json!([atom_testnet_conf()]); @@ -314,6 +324,7 @@ fn test_custom_gas_limit_on_tendermint_withdraw() { #[test] fn test_tendermint_ibc_withdraw() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); // visit `{swagger_address}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels const IBC_SOURCE_CHANNEL: &str = "channel-3"; @@ -360,6 +371,7 @@ fn test_tendermint_ibc_withdraw() { #[test] fn test_tendermint_ibc_withdraw_hd() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); // visit `{swagger_address}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels const IBC_SOURCE_CHANNEL: &str = "channel-3"; @@ -407,6 +419,7 @@ fn test_tendermint_ibc_withdraw_hd() { #[test] fn test_tendermint_token_withdraw() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; let coins = json!([nucleus_testnet_conf(), iris_ibc_nucleus_testnet_conf()]); @@ -680,6 +693,7 @@ fn test_tendermint_validators_rpc() { #[test] fn test_tendermint_add_delegation() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; const VALIDATOR_ADDRESS: &str = "nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu"; @@ -714,6 +728,7 @@ fn test_tendermint_add_delegation() { #[test] fn test_tendermint_remove_delegation() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; const VALIDATOR_ADDRESS: &str = "nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu"; @@ -746,9 +761,21 @@ fn test_tendermint_remove_delegation() { log!("Send raw tx {}", serde_json::to_string(&send_raw_tx).unwrap()); // Try to undelegate more than the total delegated amount - let raw_response = block_on(tendermint_remove_delegation_raw(&mm, coin_ticker, VALIDATOR_ADDRESS, "0.77")); + let raw_response = block_on(tendermint_remove_delegation_raw( + &mm, + coin_ticker, + VALIDATOR_ADDRESS, + "3.4", + )); assert_eq!(raw_response.0, http::StatusCode::BAD_REQUEST); - assert!(raw_response.1.contains("DelegationError::TooMuchToUndelegate")); + + // Track this type here to enforce compiler to help us to update this test coverage + // whenever this type is removed/renamed. + let _ = coins::DelegationError::TooMuchToUndelegate { + available: BigDecimal::default(), + requested: BigDecimal::default(), + }; + assert!(raw_response.1.contains("TooMuchToUndelegate")); // TODO: check currently delegated stakes and assert them // This requires delegation listing feature @@ -786,6 +813,7 @@ mod swap { #[test] fn swap_nucleus_with_doc() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); let bob_passphrase = String::from(BOB_PASSPHRASE); let alice_passphrase = String::from(ALICE_PASSPHRASE); @@ -864,6 +892,7 @@ mod swap { #[test] fn swap_nucleus_with_eth() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); let bob_passphrase = String::from(BOB_PASSPHRASE); let alice_passphrase = String::from(ALICE_PASSPHRASE); const BOB_ETH_ADDRESS: &str = "0x7b338250f990954E3Ab034ccD32a917c2F607C2d"; @@ -970,6 +999,7 @@ mod swap { #[test] fn swap_doc_with_iris_ibc_nucleus() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); let bob_passphrase = String::from(BOB_PASSPHRASE); let alice_passphrase = String::from(ALICE_PASSPHRASE); From 209c51ec282225632d5c0e38dfe6afccfe1892e1 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 4 Feb 2025 06:44:35 +0000 Subject: [PATCH 11/12] apply nits Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 2 +- mm2src/coins/tendermint/tendermint_tx_history_v2.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index b467af7ae0..fe2268c559 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2401,7 +2401,7 @@ impl TendermintCoin { .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; let internal_id = { - let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); + let hex_vec = tx.tx_hex().map_or_else(Vec::new, |h| h.to_vec()); sha256(&hex_vec).to_vec().into() }; diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 00c3e19682..ec89444831 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -610,10 +610,9 @@ where let mut events: Vec<&Event> = tx_events .iter() .filter(|event| ACCEPTED_EVENTS.contains(&event.kind.as_str())) + .rev() .collect(); - events.reverse(); - if events.len() > DEFAULT_TRANSFER_EVENT_COUNT { let is_undelegate_tx = events.iter().any(|e| e.kind == UNDELEGATE_EVENT); @@ -631,7 +630,7 @@ where let amount_with_denom = get_value_from_event_attributes(&event.attributes, AMOUNT_TAG_KEY, AMOUNT_TAG_KEY_BASE64); - return amount_with_denom != Some(fee_amount_with_denom.clone()); + return amount_with_denom.as_deref() != Some(&fee_amount_with_denom); } true From c54ba1eff23ffc4508bf03754bb80f3d4629748f Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 6 Feb 2025 19:12:29 +0000 Subject: [PATCH 12/12] apply nit Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index fe2268c559..5733fdd229 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2130,14 +2130,13 @@ impl TendermintCoin { validator_address: AccountId, denom: Denom, amount: u128, - ) -> Result { + ) -> Result { MsgDelegate { delegator_address, validator_address, amount: Coin { denom, amount }, } .to_any() - .map_err(|e| e.to_string()) } /// Calculates the send and total amounts. @@ -2204,7 +2203,7 @@ impl TendermintCoin { self.denom.clone(), amount_u64.into(), ) - .map_err(DelegationError::InternalError)?; + .map_err(|e| DelegationError::InternalError(e.to_string()))?; let timeout_height = self .current_block() @@ -2254,7 +2253,7 @@ impl TendermintCoin { self.denom.clone(), amount_u64.into(), ) - .map_err(DelegationError::InternalError)?; + .map_err(|e| DelegationError::InternalError(e.to_string()))?; let account_info = self.account_info(&delegator_address).await?; @@ -2304,14 +2303,13 @@ impl TendermintCoin { validator_address: AccountId, denom: Denom, amount: u128, - ) -> Result { + ) -> Result { MsgUndelegate { delegator_address, validator_address, amount: Coin { denom, amount }, } .to_any() - .map_err(|e| e.to_string()) } let (delegator_address, maybe_priv_key) = self @@ -2343,7 +2341,7 @@ impl TendermintCoin { self.denom.clone(), uamount_to_undelegate.into(), ) - .map_err(DelegationError::InternalError)?; + .map_err(|e| DelegationError::InternalError(e.to_string()))?; let timeout_height = self .current_block()