Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tendermint): staking/delegation #2322

Merged
merged 13 commits into from
Jan 27, 2025
2 changes: 1 addition & 1 deletion mm2src/coins/hd_wallet/withdraw_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type HDCoinPubKey<T> =
<<<<T as HDWalletCoinOps>::HDWallet as HDWalletOps>::HDAccount as HDAccountOps>::HDAddress as HDAddressOps>::Pubkey;

/// Represents the source of the funds for a withdrawal operation.
#[derive(Clone, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum WithdrawFrom {
/// The address id of the sender address which is specified by the account id, chain, and address id.
Expand Down
30 changes: 20 additions & 10 deletions mm2src/coins/lp_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2135,6 +2135,7 @@ pub struct WithdrawRequest {
#[serde(tag = "type")]
pub enum StakingDetails {
Qtum(QtumDelegationRequest),
Cosmos(Box<rpc_command::tendermint::staking::DelegatePayload>),
}

#[allow(dead_code)]
Expand Down Expand Up @@ -4878,17 +4879,26 @@ pub async fn get_staking_infos(ctx: MmArc, req: GetStakingInfosRequest) -> Staki

pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationResult {
let coin = lp_coinfind_or_err(&ctx, &req.coin).await?;
// Need to find a way to do a proper dispatch
let coin_concrete = match coin {
MmCoinEnum::QtumCoin(qtum) => qtum,
_ => {
return MmError::err(DelegationError::CoinDoesntSupportDelegation {
coin: coin.ticker().to_string(),
})
},
};

match req.staking_details {
StakingDetails::Qtum(qtum_staking) => coin_concrete.add_delegation(qtum_staking).compat().await,
StakingDetails::Qtum(req) => {
let MmCoinEnum::QtumCoin(qtum) = coin else {
return MmError::err(DelegationError::CoinDoesntSupportDelegation {
coin: coin.ticker().to_string(),
});
};

qtum.add_delegation(req).compat().await
},
StakingDetails::Cosmos(req) => {
let MmCoinEnum::Tendermint(tendermint) = coin else {
return MmError::err(DelegationError::CoinDoesntSupportDelegation {
coin: coin.ticker().to_string(),
});
};

tendermint.add_delegate(*req).await
},
}
}

Expand Down
16 changes: 15 additions & 1 deletion mm2src/coins/rpc_command/tendermint/staking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ use common::{HttpStatusCode, PagingOptions, StatusCode};
use cosmrs::staking::{Commission, Description, Validator};
use mm2_core::mm_ctx::MmArc;
use mm2_err_handle::prelude::MmError;
use mm2_number::BigDecimal;

use crate::{lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum};
use crate::{hd_wallet::WithdrawFrom, lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum, WithdrawFee};

/// Represents current status of the validator.
#[derive(Default, Deserialize)]
Expand Down Expand Up @@ -148,3 +149,16 @@ pub async fn validators_rpc(
validators: validators.into_iter().map(jsonize_validator).collect(),
})
}

#[derive(Clone, Debug, Deserialize)]
pub struct DelegatePayload {
pub validator_address: String,
pub fee: Option<WithdrawFee>,
shamardy marked this conversation as resolved.
Show resolved Hide resolved
pub withdraw_from: Option<WithdrawFrom>,
#[serde(default)]
pub memo: String,
#[serde(default)]
pub amount: BigDecimal,
#[serde(default)]
pub max: bool,
}
327 changes: 243 additions & 84 deletions mm2src/coins/tendermint/tendermint_coin.rs

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions mm2src/coins/tendermint/tendermint_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,9 @@ impl MmCoin for TendermintToken {

let is_ibc_transfer = to_address.prefix() != platform.account_prefix || req.ibc_source_channel.is_some();

let (account_id, maybe_pk) = platform.account_id_and_pk_for_withdraw(req.from)?;
let (account_id, maybe_priv_key) = platform
.extract_account_id_and_private_key(req.from)
.map_err(|e| WithdrawError::InternalError(e.to_string()))?;

let (base_denom_balance, base_denom_balance_dec) = platform
.get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals())
Expand Down Expand Up @@ -592,10 +594,10 @@ impl MmCoin for TendermintToken {
let fee_amount_u64 = platform
.calculate_account_fee_amount_as_u64(
&account_id,
maybe_pk,
maybe_priv_key,
msg_payload.clone(),
timeout_height,
memo.clone(),
&memo,
req.fee,
)
.await?;
Expand All @@ -620,7 +622,7 @@ impl MmCoin for TendermintToken {
let account_info = platform.account_info(&account_id).await?;

let tx = platform
.any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone())
.any_to_transaction_data(maybe_priv_key, msg_payload, &account_info, fee, timeout_height, &memo)
.map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?;

let internal_id = {
Expand Down
173 changes: 110 additions & 63 deletions mm2src/coins/tendermint/tendermint_tx_history_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const CLAIM_HTLC_EVENT: &str = "claim_htlc";
const IBC_SEND_EVENT: &str = "ibc_transfer";
const IBC_RECEIVE_EVENT: &str = "fungible_token_packet";
const IBC_NFT_RECEIVE_EVENT: &str = "non_fungible_token_packet";
const DELEGATE_EVENT: &str = "delegate";

const ACCEPTED_EVENTS: &[&str] = &[
TRANSFER_EVENT,
Expand All @@ -45,6 +46,7 @@ const ACCEPTED_EVENTS: &[&str] = &[
IBC_SEND_EVENT,
IBC_RECEIVE_EVENT,
IBC_NFT_RECEIVE_EVENT,
DELEGATE_EVENT,
];

const RECEIVER_TAG_KEY: &str = "receiver";
Expand All @@ -56,6 +58,12 @@ const RECIPIENT_TAG_KEY_BASE64: &str = "cmVjaXBpZW50";
const SENDER_TAG_KEY: &str = "sender";
const SENDER_TAG_KEY_BASE64: &str = "c2VuZGVy";

const DELEGATOR_TAG_KEY: &str = "delegator";
const DELEGATOR_TAG_KEY_BASE64: &str = "ZGVsZWdhdG9y";

const VALIDATOR_TAG_KEY: &str = "validator";
const VALIDATOR_TAG_KEY_BASE64: &str = "dmFsaWRhdG9y";

const AMOUNT_TAG_KEY: &str = "amount";
const AMOUNT_TAG_KEY_BASE64: &str = "YW1vdW50";

Expand Down Expand Up @@ -403,6 +411,7 @@ where
ClaimHtlc,
IBCSend,
IBCReceive,
Delegate,
}

#[derive(Clone)]
Expand Down Expand Up @@ -470,77 +479,111 @@ where
let mut transfer_details_list: Vec<TransferDetails> = vec![];

for event in tx_events.iter() {
if event.kind.as_str() == TRANSFER_EVENT {
let amount_with_denoms = some_or_continue!(get_value_from_event_attributes(
&event.attributes,
AMOUNT_TAG_KEY,
AMOUNT_TAG_KEY_BASE64
));

let amount_with_denoms = amount_with_denoms.split(',');

for amount_with_denom in amount_with_denoms {
let extracted_amount: String =
amount_with_denom.chars().take_while(|c| c.is_numeric()).collect();
let denom = &amount_with_denom[extracted_amount.len()..];
let amount = some_or_continue!(extracted_amount.parse().ok());

let from = some_or_continue!(get_value_from_event_attributes(
&event.attributes,
SENDER_TAG_KEY,
SENDER_TAG_KEY_BASE64
));

let to = some_or_continue!(get_value_from_event_attributes(
&event.attributes,
RECIPIENT_TAG_KEY,
RECIPIENT_TAG_KEY_BASE64,
));

let mut tx_details = TransferDetails {
from,
to,
denom: denom.to_owned(),
amount,
// Default is Standard, can be changed later in read_real_htlc_addresses
transfer_event_type: TransferEventType::default(),
};
let amount_with_denoms = some_or_continue!(get_value_from_event_attributes(
&event.attributes,
AMOUNT_TAG_KEY,
AMOUNT_TAG_KEY_BASE64
));

// For HTLC transactions, the sender and receiver addresses in the "transfer" event will be incorrect.
// Use `read_real_htlc_addresses` to handle them properly.
if let Some(htlc_event) = tx_events
.iter()
.find(|e| [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&e.kind.as_str()))
{
read_real_htlc_addresses(&mut tx_details, htlc_event);
}
// For IBC transactions, the sender and receiver addresses in the "transfer" event will be incorrect.
// Use `read_real_ibc_addresses` to handle them properly.
else if let Some(ibc_event) = tx_events.iter().find(|e| {
[IBC_SEND_EVENT, IBC_RECEIVE_EVENT, IBC_NFT_RECEIVE_EVENT].contains(&e.kind.as_str())
}) {
read_real_ibc_addresses(&mut tx_details, ibc_event);
}
let amount_with_denoms = amount_with_denoms.split(',');
for amount_with_denom in amount_with_denoms {
let extracted_amount: String = amount_with_denom.chars().take_while(|c| c.is_numeric()).collect();
let denom = &amount_with_denom[extracted_amount.len()..];
let amount = some_or_continue!(extracted_amount.parse().ok());

match event.kind.as_str() {
TRANSFER_EVENT => {
let from = some_or_continue!(get_value_from_event_attributes(
&event.attributes,
SENDER_TAG_KEY,
SENDER_TAG_KEY_BASE64
));

let to = some_or_continue!(get_value_from_event_attributes(
&event.attributes,
RECIPIENT_TAG_KEY,
RECIPIENT_TAG_KEY_BASE64,
));

let mut tx_details = TransferDetails {
from,
to,
denom: denom.to_owned(),
amount,
// Default is Standard, can be changed later in read_real_htlc_addresses
transfer_event_type: TransferEventType::default(),
};

// For HTLC transactions, the sender and receiver addresses in the "transfer" event will be incorrect.
// Use `read_real_htlc_addresses` to handle them properly.
if let Some(htlc_event) = tx_events
.iter()
.find(|e| [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&e.kind.as_str()))
{
read_real_htlc_addresses(&mut tx_details, htlc_event);
}
// For IBC transactions, the sender and receiver addresses in the "transfer" event will be incorrect.
// Use `read_real_ibc_addresses` to handle them properly.
else if let Some(ibc_event) = tx_events.iter().find(|e| {
[IBC_SEND_EVENT, IBC_RECEIVE_EVENT, IBC_NFT_RECEIVE_EVENT].contains(&e.kind.as_str())
}) {
read_real_ibc_addresses(&mut tx_details, ibc_event);
}

handle_new_transfer_event(&mut transfer_details_list, tx_details);
},

// sum the amounts coins and pairs are same
let mut duplicated_details = transfer_details_list.iter_mut().find(|details| {
details.from == tx_details.from
&& details.to == tx_details.to
&& details.denom == tx_details.denom
});
DELEGATE_EVENT => {
let from = some_or_continue!(get_value_from_event_attributes(
&event.attributes,
DELEGATOR_TAG_KEY,
DELEGATOR_TAG_KEY_BASE64,
));

let to = some_or_continue!(get_value_from_event_attributes(
&event.attributes,
VALIDATOR_TAG_KEY,
VALIDATOR_TAG_KEY_BASE64,
));

let tx_details = TransferDetails {
from,
to,
denom: denom.to_owned(),
amount,
transfer_event_type: TransferEventType::Delegate,
};

handle_new_transfer_event(&mut transfer_details_list, tx_details);
},

if let Some(duplicated_details) = &mut duplicated_details {
duplicated_details.amount += tx_details.amount;
} else {
transfer_details_list.push(tx_details);
}
}
unrecognized => {
log::warn!(
"Found an unrecognized event '{unrecognized}' in transaction history processing."
);
},
};
}
}

transfer_details_list
}

fn handle_new_transfer_event(transfer_details_list: &mut Vec<TransferDetails>, new_transfer: TransferDetails) {
let mut existing_transfer = transfer_details_list.iter_mut().find(|details| {
details.from == new_transfer.from
&& details.to == new_transfer.to
&& details.denom == new_transfer.denom
});

if let Some(existing_transfer) = &mut existing_transfer {
// Handle multi-amount transfer events
existing_transfer.amount += new_transfer.amount;
} else {
transfer_details_list.push(new_transfer);
}
}

fn get_transfer_details(tx_events: Vec<Event>, fee_amount_with_denom: String) -> Vec<TransferDetails> {
// Filter out irrelevant events
let mut events: Vec<&Event> = tx_events
Expand Down Expand Up @@ -584,6 +627,7 @@ where
},
token_id,
},
(TransferEventType::Delegate, _) => TransactionType::StakingDelegation,
(_, Some(token_id)) => TransactionType::TokenTransfer(token_id),
_ => TransactionType::StandardTransfer,
}
Expand All @@ -604,7 +648,10 @@ where
}
},
TransferEventType::ClaimHtlc => Some((vec![my_address], vec![])),
TransferEventType::Standard | TransferEventType::IBCSend | TransferEventType::IBCReceive => {
TransferEventType::Standard
| TransferEventType::IBCSend
| TransferEventType::IBCReceive
| TransferEventType::Delegate => {
Some((vec![transfer_details.from.clone()], vec![transfer_details.to.clone()]))
},
}
Expand Down
39 changes: 37 additions & 2 deletions mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_
enable_tendermint_token, enable_tendermint_without_balance,
get_tendermint_my_tx_history, ibc_withdraw, iris_ibc_nucleus_testnet_conf,
my_balance, nucleus_testnet_conf, orderbook, orderbook_v2, send_raw_transaction,
set_price, tendermint_validators, withdraw_v1, MarketMakerIt, Mm2TestConf};
set_price, tendermint_add_delegation, tendermint_validators, withdraw_v1,
MarketMakerIt, Mm2TestConf};
use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, OrderbookAddress, OrderbookV2Response, RpcV2Response,
TendermintActivationResult, TransactionDetails};
TendermintActivationResult, TransactionDetails, TransactionType};
use serde_json::json;
use std::collections::HashSet;
use std::iter::FromIterator;
Expand Down Expand Up @@ -677,6 +678,40 @@ fn test_tendermint_validators_rpc() {
assert_eq!(validators_raw_response["result"]["validators"][0]["jailed"], false);
}

#[test]
fn test_tendermint_add_delegation() {
const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr";
const VALIDATOR_ADDRESS: &str = "nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu";

let coins = json!([nucleus_testnet_conf()]);
let coin_ticker = coins[0]["coin"].as_str().unwrap();

let conf = Mm2TestConf::seednode(TENDERMINT_TEST_SEED, &coins);
let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap();

let activation_res = block_on(enable_tendermint(
&mm,
coin_ticker,
&[],
NUCLEUS_TESTNET_RPC_URLS,
false,
));

log!(
"Activation with assets {}",
serde_json::to_string(&activation_res).unwrap()
);

let tx_details = block_on(tendermint_add_delegation(&mm, coin_ticker, VALIDATOR_ADDRESS, "0.5"));

assert_eq!(tx_details.to, vec![VALIDATOR_ADDRESS.to_owned()]);
assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]);
assert_eq!(tx_details.transaction_type, TransactionType::StakingDelegation);

let send_raw_tx = block_on(send_raw_transaction(&mm, coin_ticker, &tx_details.tx_hex));
log!("Send raw tx {}", serde_json::to_string(&send_raw_tx).unwrap());
}

mod swap {
use super::*;

Expand Down
Loading
Loading