Skip to content

Commit

Permalink
feat(tendermint): staking/delegation (#2322)
Browse files Browse the repository at this point in the history
Adds tendermint protocol support on add_delegation RPC, and extends tendermint transaction history implementation to support delegation transactions.
  • Loading branch information
onur-ozkan authored Jan 27, 2025
1 parent a873525 commit 73e0650
Show file tree
Hide file tree
Showing 8 changed files with 462 additions and 165 deletions.
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>,
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

0 comments on commit 73e0650

Please sign in to comment.