diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index cb3402aec0..9d630744f2 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2238,6 +2238,19 @@ pub struct DelegationsInfo { #[serde(tag = "type")] pub enum DelegationsInfoDetails { Qtum, + Cosmos(rpc_command::tendermint::staking::SimpleListQuery), +} + +#[derive(Deserialize)] +pub struct UndelegationsInfo { + pub coin: String, + info_details: UndelegationsInfoDetails, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum UndelegationsInfoDetails { + Cosmos(rpc_command::tendermint::staking::SimpleListQuery), } #[derive(Deserialize)] @@ -2249,7 +2262,7 @@ pub struct ValidatorsInfo { #[derive(Debug, Deserialize)] #[serde(tag = "type")] pub enum ValidatorsInfoDetails { - Cosmos(rpc_command::tendermint::staking::ValidatorsRPC), + Cosmos(rpc_command::tendermint::staking::ValidatorsQuery), } #[derive(Serialize, Deserialize)] @@ -4994,6 +5007,32 @@ pub async fn delegations_info(ctx: MmArc, req: DelegationsInfo) -> Result match coin { + MmCoinEnum::Tendermint(t) => Ok(t.delegations_list(r.paging).await.map(|v| json!(v))?), + MmCoinEnum::TendermintToken(_) => MmError::err(StakingInfoError::InvalidPayload { + reason: "Tokens are not supported for delegation".into(), + }), + _ => MmError::err(StakingInfoError::InvalidPayload { + reason: format!("{} is not a Cosmos coin", req.coin), + }), + }, + } +} + +pub async fn ongoing_undelegations_info(ctx: MmArc, req: UndelegationsInfo) -> Result> { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + + match req.info_details { + UndelegationsInfoDetails::Cosmos(r) => match coin { + MmCoinEnum::Tendermint(t) => Ok(t.ongoing_undelegations_list(r.paging).await.map(|v| json!(v))?), + MmCoinEnum::TendermintToken(_) => MmError::err(StakingInfoError::InvalidPayload { + reason: "Tokens are not supported for delegation".into(), + }), + _ => MmError::err(StakingInfoError::InvalidPayload { + reason: format!("{} is not a Cosmos coin", req.coin), + }), + }, } } diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index a9c81f9696..190477b7bd 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -30,7 +30,7 @@ impl ToString for ValidatorStatus { } #[derive(Debug, Deserialize)] -pub struct ValidatorsRPC { +pub struct ValidatorsQuery { #[serde(flatten)] paging: PagingOptions, #[serde(default)] @@ -38,7 +38,7 @@ pub struct ValidatorsRPC { } #[derive(Clone, Serialize)] -pub struct ValidatorsRPCResponse { +pub struct ValidatorsQueryResponse { validators: Vec, } @@ -59,8 +59,8 @@ impl From for StakingInfoError { pub async fn validators_rpc( coin: MmCoinEnum, - req: ValidatorsRPC, -) -> Result> { + req: ValidatorsQuery, +) -> Result> { fn maybe_jsonize_description(description: Option) -> Option { description.map(|d| { json!({ @@ -121,7 +121,7 @@ pub async fn validators_rpc( }, }; - Ok(ValidatorsRPCResponse { + Ok(ValidatorsQueryResponse { validators: validators.into_iter().map(jsonize_validator).collect(), }) } @@ -151,3 +151,39 @@ pub struct ClaimRewardsPayload { #[serde(default)] pub force: bool, } + +#[derive(Debug, Deserialize)] +pub struct SimpleListQuery { + #[serde(flatten)] + pub(crate) paging: PagingOptions, +} + +#[derive(Debug, PartialEq, Serialize)] +pub struct DelegationsQueryResponse { + pub(crate) delegations: Vec, +} + +#[derive(Debug, PartialEq, Serialize)] +pub(crate) struct Delegation { + pub(crate) validator_address: String, + pub(crate) delegated_amount: BigDecimal, + pub(crate) reward_amount: BigDecimal, +} + +#[derive(Serialize)] +pub struct UndelegationsQueryResponse { + pub(crate) ongoing_undelegations: Vec, +} + +#[derive(Serialize)] +pub(crate) struct Undelegation { + pub(crate) validator_address: String, + pub(crate) entries: Vec, +} + +#[derive(Serialize)] +pub(crate) struct UndelegationEntry { + pub(crate) creation_height: i64, + pub(crate) completion_datetime: String, + pub(crate) balance: BigDecimal, +} diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 6f3df3775d..e7e8c4d71c 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -6,7 +6,9 @@ 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::{ClaimRewardsPayload, DelegationPayload, ValidatorStatus}; +use crate::rpc_command::tendermint::staking::{ClaimRewardsPayload, Delegation, DelegationPayload, + DelegationsQueryResponse, Undelegation, UndelegationEntry, + UndelegationsQueryResponse, ValidatorStatus}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -43,7 +45,10 @@ use cosmrs::proto::cosmos::base::tendermint::v1beta1::{GetBlockByHeightRequest, GetLatestBlockRequest, GetLatestBlockResponse}; use cosmrs::proto::cosmos::base::v1beta1::{Coin as CoinProto, DecCoin}; use cosmrs::proto::cosmos::distribution::v1beta1::{QueryDelegationRewardsRequest, QueryDelegationRewardsResponse}; -use cosmrs::proto::cosmos::staking::v1beta1::{QueryDelegationRequest, QueryDelegationResponse, QueryValidatorsRequest, +use cosmrs::proto::cosmos::staking::v1beta1::{QueryDelegationRequest, QueryDelegationResponse, + QueryDelegatorDelegationsRequest, QueryDelegatorDelegationsResponse, + QueryDelegatorUnbondingDelegationsRequest, + QueryDelegatorUnbondingDelegationsResponse, QueryValidatorsRequest, QueryValidatorsResponse as QueryValidatorsResponseProto}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; @@ -95,6 +100,8 @@ const ABCI_QUERY_BALANCE_PATH: &str = "/cosmos.bank.v1beta1.Query/Balance"; const ABCI_GET_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTx"; const ABCI_VALIDATORS_PATH: &str = "/cosmos.staking.v1beta1.Query/Validators"; const ABCI_DELEGATION_PATH: &str = "/cosmos.staking.v1beta1.Query/Delegation"; +const ABCI_DELEGATOR_DELEGATIONS_PATH: &str = "/cosmos.staking.v1beta1.Query/DelegatorDelegations"; +const ABCI_DELEGATOR_UNDELEGATIONS_PATH: &str = "/cosmos.staking.v1beta1.Query/DelegatorUnbondingDelegations"; const ABCI_DELEGATION_REWARDS_PATH: &str = "/cosmos.distribution.v1beta1.Query/DelegationRewards"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -2617,6 +2624,120 @@ impl TendermintCoin { memo: Some(req.memo), }) } + + pub(crate) async fn delegations_list( + &self, + paging: PagingOptions, + ) -> MmResult { + let request = QueryDelegatorDelegationsRequest { + delegator_addr: self.account_id.to_string(), + pagination: Some(PageRequest { + key: vec![], + offset: ((paging.page_number.get() - 1usize) * paging.limit) as u64, + limit: paging.limit as u64, + count_total: false, + reverse: false, + }), + }; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_DELEGATOR_DELEGATIONS_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + let decoded_proto = QueryDelegatorDelegationsResponse::decode(raw_response.value.as_slice())?; + + let mut delegations = Vec::new(); + for response in decoded_proto.delegation_responses { + let Some(delegation) = response.delegation else { continue }; + let Some(balance) = response.balance else { continue }; + + let account_id = AccountId::from_str(&delegation.validator_address) + .map_err(|e| TendermintCoinRpcError::InternalError(e.to_string()))?; + + let reward_amount = match self.get_delegation_reward_amount(&account_id).await { + Ok(reward) => reward, + Err(e) => match e.get_inner() { + DelegationError::NothingToClaim { .. } => BigDecimal::zero(), + _ => return MmError::err(TendermintCoinRpcError::InvalidResponse(e.to_string())), + }, + }; + + let amount = balance + .amount + .parse::() + .map_err(|e| TendermintCoinRpcError::InternalError(e.to_string()))?; + + delegations.push(Delegation { + validator_address: delegation.validator_address, + delegated_amount: big_decimal_from_sat_unsigned(amount, self.decimals()), + reward_amount, + }); + } + + Ok(DelegationsQueryResponse { delegations }) + } + + pub(crate) async fn ongoing_undelegations_list( + &self, + paging: PagingOptions, + ) -> MmResult { + let request = QueryDelegatorUnbondingDelegationsRequest { + delegator_addr: self.account_id.to_string(), + pagination: Some(PageRequest { + key: vec![], + offset: ((paging.page_number.get() - 1usize) * paging.limit) as u64, + limit: paging.limit as u64, + count_total: false, + reverse: false, + }), + }; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_DELEGATOR_UNDELEGATIONS_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + let decoded_proto = QueryDelegatorUnbondingDelegationsResponse::decode(raw_response.value.as_slice())?; + let ongoing_undelegations = decoded_proto + .unbonding_responses + .into_iter() + .map(|r| { + let entries = r + .entries + .into_iter() + .filter_map(|e| { + let balance: u64 = e.balance.parse().ok()?; + + Some(UndelegationEntry { + creation_height: e.creation_height, + completion_datetime: e.completion_time?.to_string(), + balance: big_decimal_from_sat_unsigned(balance, self.decimals()), + }) + }) + .collect(); + + Undelegation { + validator_address: r.validator_address, + entries, + } + }) + .collect(); + + Ok(UndelegationsQueryResponse { ongoing_undelegations }) + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { @@ -3729,7 +3850,7 @@ pub mod tendermint_coin_tests { use common::{block_on, wait_until_ms, DEX_FEE_ADDR_RAW_PUBKEY}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse}; use crypto::privkey::key_pair_from_seed; - use std::mem::discriminant; + use std::{mem::discriminant, num::NonZeroUsize}; pub const IRIS_TESTNET_HTLC_PAIR1_SEED: &str = "iris test seed"; // pub const IRIS_TESTNET_HTLC_PAIR1_PUB_KEY: &[u8] = &[ @@ -4687,4 +4808,56 @@ pub mod tendermint_coin_tests { // tx fee must be taken into account assert!(reward_amount > res.my_balance_change); } + + #[test] + fn test_delegations_list() { + let nodes = vec![RpcNode::for_test(IRIS_TESTNET_RPC_URL)]; + let protocol_conf = get_iris_protocol(); + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); + + let coin = block_on(TendermintCoin::init( + &ctx, + "IRIS-TEST".to_string(), + conf, + protocol_conf, + nodes, + false, + activation_policy, + false, + )) + .unwrap(); + + let validator_address = "iva1svannhv2zaxefq83m7treg078udfk37lpjufkw"; + let reward_amount = + block_on(coin.get_delegation_reward_amount(&AccountId::from_str(validator_address).unwrap())).unwrap(); + + let expected_list = DelegationsQueryResponse { + delegations: vec![Delegation { + validator_address: validator_address.to_owned(), + delegated_amount: BigDecimal::from_str("1.98").unwrap(), + reward_amount: reward_amount.round(4), + }], + }; + + let mut actual_list = block_on(coin.delegations_list(PagingOptions { + limit: 0, + page_number: NonZeroUsize::new(1).unwrap(), + from_uuid: None, + })) + .unwrap(); + for delegation in &mut actual_list.delegations { + delegation.reward_amount = delegation.reward_amount.round(4); + } + + assert_eq!(expected_list, actual_list); + } } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index a399d04c35..f3286e5ab5 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -46,8 +46,9 @@ use coins::utxo::slp::SlpToken; use coins::utxo::utxo_standard::UtxoStandardCoin; use coins::z_coin::ZCoin; use coins::{add_delegation, claim_staking_rewards, delegations_info, get_my_address, get_raw_transaction, - get_swap_transaction_fee_policy, nft, remove_delegation, set_swap_transaction_fee_policy, sign_message, - sign_raw_transaction, validators_info, verify_message, withdraw}; + get_swap_transaction_fee_policy, nft, ongoing_undelegations_info, remove_delegation, + set_swap_transaction_fee_policy, sign_message, sign_raw_transaction, validators_info, verify_message, + withdraw}; use coins_activation::{cancel_init_l2, cancel_init_platform_coin_with_tokens, cancel_init_standalone_coin, cancel_init_token, enable_platform_coin_with_tokens, enable_token, init_l2, init_l2_status, init_l2_user_action, init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, @@ -468,8 +469,9 @@ async fn staking_dispatcher( staking_query_method: &str, ) -> DispatcherResult>> { match staking_query_method { - "validators" => handle_mmrpc(ctx, request, validators_info).await, "delegations" => handle_mmrpc(ctx, request, delegations_info).await, + "ongoing_undelegations" => handle_mmrpc(ctx, request, ongoing_undelegations_info).await, + "validators" => handle_mmrpc(ctx, request, validators_info).await, _ => MmError::err(DispatcherError::NoSuchMethod), } } diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index 52788ab1df..00e2399556 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -1,11 +1,13 @@ use common::{block_on, log}; +use instant::Duration; use mm2_number::BigDecimal; use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_err, enable_tendermint, 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_delegations, + tendermint_ongoing_undelegations, tendermint_remove_delegation, tendermint_remove_delegation_raw, tendermint_validators, withdraw_v1, MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, OrderbookAddress, OrderbookV2Response, RpcV2Response, @@ -14,6 +16,7 @@ use serde_json::json; use std::collections::HashSet; use std::iter::FromIterator; use std::sync::Mutex; +use std::thread; const TENDERMINT_TEST_SEED: &str = "tendermint test seed"; const TENDERMINT_CONSTANT_BALANCE_SEED: &str = "tendermint constant balance seed"; @@ -760,6 +763,12 @@ 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()); + thread::sleep(Duration::from_secs(1)); + + let r = block_on(tendermint_delegations(&mm, coin_ticker)); + let delegation_info = r["result"]["delegations"].as_array().unwrap().last().unwrap(); + assert_eq!(delegation_info["validator_address"], VALIDATOR_ADDRESS); + // Try to undelegate more than the total delegated amount let raw_response = block_on(tendermint_remove_delegation_raw( &mm, @@ -777,17 +786,27 @@ fn test_tendermint_remove_delegation() { }; 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")); + let tx_details = block_on(tendermint_remove_delegation( + &mm, + coin_ticker, + VALIDATOR_ADDRESS, + "0.15", + )); 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 + 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()); + + thread::sleep(Duration::from_secs(1)); + + let r = block_on(tendermint_ongoing_undelegations(&mm, coin_ticker)); + let undelegation_info = r["result"]["ongoing_undelegations"].as_array().unwrap().last().unwrap(); + assert_eq!(undelegation_info["validator_address"], VALIDATOR_ADDRESS); + let undelegation_entry = undelegation_info["entries"].as_array().unwrap().last().unwrap(); + assert_eq!(undelegation_entry["balance"], "0.15"); } mod swap { @@ -799,14 +818,13 @@ mod swap { use common::executor::Timer; use common::log; use ethereum_types::{Address, U256}; - use instant::Duration; use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::for_tests::{check_my_swap_status, check_recent_swaps, doc_conf, enable_eth_coin, iris_ibc_nucleus_testnet_conf, nucleus_testnet_conf, wait_check_stats_swap_status, DOC_ELECTRUM_ADDRS}; use std::convert::TryFrom; + use std::env; use std::str::FromStr; - use std::{env, thread}; const BOB_PASSPHRASE: &str = "iris test seed"; const ALICE_PASSPHRASE: &str = "iris test2 seed"; diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 9d51dbdfeb..fae56dc835 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -237,10 +237,7 @@ pub const QRC20_ELECTRUMS: &[&str] = &[ "electrum3.cipig.net:10071", ]; pub const T_BCH_ELECTRUMS: &[&str] = &["tbch.loping.net:60001", "bch0.kister.net:51001"]; -pub const TBTC_ELECTRUMS: &[&str] = &[ - "electrum3.cipig.net:10068", - "testnet.aranguren.org:51001", -]; +pub const TBTC_ELECTRUMS: &[&str] = &["electrum3.cipig.net:10068", "testnet.aranguren.org:51001"]; pub const ETH_MAINNET_NODES: &[&str] = &[ "https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b", @@ -831,13 +828,9 @@ pub fn eth_testnet_conf_trezor() -> Json { } /// ETH configuration used for dockerized Geth dev node -pub fn eth_dev_conf() -> Json { - eth_conf("ETH") -} +pub fn eth_dev_conf() -> Json { eth_conf("ETH") } -pub fn eth1_dev_conf() -> Json { - eth_conf("ETH1") -} +pub fn eth1_dev_conf() -> Json { eth_conf("ETH1") } fn eth_conf(coin: &str) -> Json { json!({ @@ -2102,7 +2095,12 @@ pub async fn enable_eth_coin_v2( })) .await .unwrap(); - assert_eq!(enable.0, StatusCode::OK, "'enable_eth_with_tokens' failed: {}", enable.1); + assert_eq!( + enable.0, + StatusCode::OK, + "'enable_eth_with_tokens' failed: {}", + enable.1 + ); json::from_str(&enable.1).unwrap() } @@ -3133,6 +3131,52 @@ pub async fn get_tendermint_my_tx_history(mm: &MarketMakerIt, coin: &str, limit: json::from_str(&request.1).unwrap() } +pub async fn tendermint_delegations(mm: &MarketMakerIt, coin: &str) -> Json { + let rpc_endpoint = "experimental::staking::query::delegations"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "coin": coin, + "info_details": { + "type": "Cosmos", + "limit": 0, + "page_number": 1 + } + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!(request.0, StatusCode::OK, "'{rpc_endpoint}' failed: {}", request.1); + log!("{rpc_endpoint} response {}", request.1); + json::from_str(&request.1).unwrap() +} + +pub async fn tendermint_ongoing_undelegations(mm: &MarketMakerIt, coin: &str) -> Json { + let rpc_endpoint = "experimental::staking::query::ongoing_undelegations"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "coin": coin, + "info_details": { + "type": "Cosmos", + "limit": 0, + "page_number": 1 + } + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!(request.0, StatusCode::OK, "'{rpc_endpoint}' failed: {}", request.1); + log!("{rpc_endpoint} response {}", request.1); + json::from_str(&request.1).unwrap() +} + pub async fn enable_tendermint_token(mm: &MarketMakerIt, coin: &str) -> Json { let request = json!({ "userpass": mm.userpass,