diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index ce23c3bfbe..3f2c58069e 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)] @@ -2149,6 +2149,7 @@ pub struct AddDelegateRequest { #[derive(Deserialize)] pub struct RemoveDelegateRequest { pub coin: String, + pub staking_details: Option, } #[derive(Deserialize)] @@ -2764,6 +2765,24 @@ pub enum DelegationError { CoinDoesntSupportDelegation { coin: String }, #[display(fmt = "No such coin {}", coin)] NoSuchCoin { coin: String }, + #[display( + fmt = "Delegator '{}' does not have any delegation on validator '{}'.", + delegator_addr, + validator_addr + )] + CanNotUndelegate { + 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")] @@ -2775,6 +2794,8 @@ 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 = "Internal error: {}", _0)] InternalError(String), @@ -4855,12 +4876,35 @@ 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)) => { + 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(), + }); + }; + + tendermint.undelegate(*req).await + }, + + 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, + _ => { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }) + }, }, } } @@ -4897,7 +4941,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..5733fdd229 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, @@ -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, 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; @@ -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; @@ -2123,20 +2124,19 @@ 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, 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. @@ -2203,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() @@ -2253,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?; @@ -2296,6 +2296,183 @@ impl TendermintCoin { memo: Some(req.memo), }) } + + pub(crate) async fn undelegate(&self, req: DelegationPayload) -> MmResult { + 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() + } + + 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 (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(|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, + 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; + + 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().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), + }) + } + + pub(crate) async fn get_delegated_amount( + &self, + validator_addr: &AccountId, // keep this as `AccountId` to make it pre-validated + ) -> MmResult<(BigDecimal, u64), DelegationError> { + 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, + validator_addr, + }; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_DELEGATION_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .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 uamount = u64::from_str(&balance.amount).map_err(|e| DelegationError::InternalError(e.to_string()))?; + + Ok((big_decimal_from_sat_unsigned(uamount, self.decimals()), uamount)) + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index b170f08b81..ec89444831 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." @@ -589,20 +610,30 @@ 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 { - // 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.as_deref() != Some(&fee_amount_with_denom); } + + true }); } @@ -628,6 +659,7 @@ where token_id, }, (TransferEventType::Delegate, _) => TransactionType::StakingDelegation, + (TransferEventType::Undelegate, _) => TransactionType::RemoveDelegation, (_, Some(token_id)) => TransactionType::TokenTransfer(token_id), _ => TransactionType::StandardTransfer, } @@ -654,6 +686,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 79d1247401..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_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,42 @@ 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"; + + 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()); +} + +#[test] +fn test_tendermint_remove_delegation() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; const VALIDATOR_ADDRESS: &str = "nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu"; @@ -710,6 +759,35 @@ fn test_tendermint_add_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, + "3.4", + )); + assert_eq!(raw_response.0, http::StatusCode::BAD_REQUEST); + + // 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 + + 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::RemoveDelegation); + + // TODO: check currently delegated stakes and assert them + // This requires delegation listing feature } mod swap { @@ -735,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); @@ -813,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"; @@ -919,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); diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index d1db770ea2..f67fb4252f 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -3148,6 +3148,46 @@ pub async fn tendermint_add_delegation( json::from_value(json["result"].clone()).unwrap() } +pub async fn tendermint_remove_delegation_raw( + mm: &MarketMakerIt, + coin: &str, + validator_address: &str, + amount: &str, +) -> (StatusCode, String, HeaderMap) { + 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()); + + 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); + + 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,