diff --git a/Cargo.lock b/Cargo.lock index fabae55f36..0e94215126 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1077,6 +1077,7 @@ dependencies = [ "mm2_core", "mm2_db", "mm2_err_handle", + "mm2_git", "mm2_io", "mm2_metamask", "mm2_metrics", @@ -4290,6 +4291,19 @@ dependencies = [ "web3", ] +[[package]] +name = "mm2_git" +version = "0.1.0" +dependencies = [ + "async-trait", + "common", + "http 0.2.7", + "mm2_err_handle", + "mm2_net", + "serde", + "serde_json", +] + [[package]] name = "mm2_gui_storage" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9c3c237abc..421bdca2c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "mm2src/mm2_db", "mm2src/mm2_err_handle", "mm2src/mm2_eth", + "mm2src/mm2_git", "mm2src/mm2_io", "mm2src/mm2_libp2p", "mm2src/mm2_metamask", diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 73d955ff45..7da462f746 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -58,6 +58,7 @@ lazy_static = "1.4" libc = "0.2" mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } +mm2_git = { path = "../mm2_git" } mm2_io = { path = "../mm2_io" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 41ede7f020..55d2bd7680 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -1794,6 +1794,8 @@ pub enum WithdrawError { AddressMismatchError { my_address: String, from: String }, #[display(fmt = "Contract type {} doesnt support 'withdraw_nft' yet", _0)] ContractTypeDoesntSupportNftWithdrawing(String), + #[display(fmt = "Action not allowed for coin: {}", _0)] + ActionNotAllowed(String), } impl HttpStatusCode for WithdrawError { @@ -1815,6 +1817,7 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::UnexpectedUserAction { .. } | WithdrawError::CoinDoesntSupportNftWithdraw { .. } | WithdrawError::AddressMismatchError { .. } + | WithdrawError::ActionNotAllowed(_) | WithdrawError::ContractTypeDoesntSupportNftWithdrawing(_) => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs index 945cea77fe..c401853b2d 100644 --- a/mm2src/coins/rpc_command/mod.rs +++ b/mm2src/coins/rpc_command/mod.rs @@ -8,3 +8,4 @@ pub mod init_create_account; pub mod init_scan_for_new_addresses; pub mod init_withdraw; #[cfg(not(target_arch = "wasm32"))] pub mod lightning; +pub mod tendermint; diff --git a/mm2src/coins/rpc_command/tendermint/ibc_chains.rs b/mm2src/coins/rpc_command/tendermint/ibc_chains.rs new file mode 100644 index 0000000000..67ed93e9fa --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/ibc_chains.rs @@ -0,0 +1,35 @@ +use common::HttpStatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; + +use crate::tendermint; + +pub type IBCChainRegistriesResult = Result>; + +#[derive(Clone, Serialize)] +pub struct IBCChainRegistriesResponse { + pub(crate) chain_registry_list: Vec, +} + +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType, PartialEq)] +#[serde(tag = "error_type", content = "error_data")] +pub enum IBCChainsRequestError { + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for IBCChainsRequestError { + fn status_code(&self) -> common::StatusCode { + match self { + IBCChainsRequestError::Transport(_) => common::StatusCode::SERVICE_UNAVAILABLE, + IBCChainsRequestError::InternalError(_) => common::StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +#[inline(always)] +pub async fn ibc_chains(_ctx: MmArc, _req: serde_json::Value) -> IBCChainRegistriesResult { + tendermint::get_ibc_chain_list().await +} diff --git a/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs b/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs new file mode 100644 index 0000000000..fce69042c6 --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs @@ -0,0 +1,76 @@ +use common::HttpStatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; + +use crate::{lp_coinfind_or_err, MmCoinEnum}; + +pub type IBCTransferChannelsResult = Result>; + +#[derive(Clone, Deserialize)] +pub struct IBCTransferChannelsRequest { + pub(crate) coin: String, + pub(crate) destination_chain_registry_name: String, +} + +#[derive(Clone, Serialize)] +pub struct IBCTransferChannelsResponse { + pub(crate) ibc_transfer_channels: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +pub(crate) struct IBCTransferChannel { + pub(crate) channel_id: String, + pub(crate) ordering: String, + pub(crate) version: String, + pub(crate) tags: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct IBCTransferChannelTag { + pub(crate) status: String, + pub(crate) preferred: bool, + pub(crate) dex: Option, +} + +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType, PartialEq)] +#[serde(tag = "error_type", content = "error_data")] +pub enum IBCTransferChannelsRequestError { + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display( + fmt = "Only tendermint based coins are allowed for `ibc_transfer_channels` operation. Current coin: {}", + _0 + )] + UnsupportedCoin(String), + #[display(fmt = "Could not find '{}' registry source.", _0)] + RegistrySourceCouldNotFound(String), + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for IBCTransferChannelsRequestError { + fn status_code(&self) -> common::StatusCode { + match self { + IBCTransferChannelsRequestError::UnsupportedCoin(_) | IBCTransferChannelsRequestError::NoSuchCoin(_) => { + common::StatusCode::BAD_REQUEST + }, + IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(_) => common::StatusCode::NOT_FOUND, + IBCTransferChannelsRequestError::Transport(_) => common::StatusCode::SERVICE_UNAVAILABLE, + IBCTransferChannelsRequestError::InternalError(_) => common::StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn ibc_transfer_channels(ctx: MmArc, req: IBCTransferChannelsRequest) -> IBCTransferChannelsResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin) + .await + .map_err(|_| IBCTransferChannelsRequestError::NoSuchCoin(req.coin.clone()))?; + + match coin { + MmCoinEnum::Tendermint(coin) => coin.get_ibc_transfer_channels(req).await, + MmCoinEnum::TendermintToken(token) => token.platform_coin.get_ibc_transfer_channels(req).await, + _ => MmError::err(IBCTransferChannelsRequestError::UnsupportedCoin(req.coin)), + } +} diff --git a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs b/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs new file mode 100644 index 0000000000..a490ef5cec --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs @@ -0,0 +1,27 @@ +use common::Future01CompatExt; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; +use mm2_number::BigDecimal; + +use crate::{lp_coinfind_or_err, MmCoinEnum, WithdrawError, WithdrawResult}; + +#[derive(Clone, Deserialize)] +pub struct IBCWithdrawRequest { + pub(crate) ibc_source_channel: String, + pub(crate) coin: String, + pub(crate) to: String, + #[serde(default)] + pub(crate) amount: BigDecimal, + #[serde(default)] + pub(crate) max: bool, + pub(crate) memo: Option, +} + +pub async fn ibc_withdraw(ctx: MmArc, req: IBCWithdrawRequest) -> WithdrawResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + match coin { + MmCoinEnum::Tendermint(coin) => coin.ibc_withdraw(req).compat().await, + MmCoinEnum::TendermintToken(token) => token.ibc_withdraw(req).compat().await, + _ => MmError::err(WithdrawError::ActionNotAllowed(req.coin)), + } +} diff --git a/mm2src/coins/rpc_command/tendermint/mod.rs b/mm2src/coins/rpc_command/tendermint/mod.rs new file mode 100644 index 0000000000..d8211abeac --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/mod.rs @@ -0,0 +1,14 @@ +mod ibc_chains; +mod ibc_transfer_channels; +mod ibc_withdraw; + +pub use ibc_chains::*; +pub use ibc_transfer_channels::*; +pub use ibc_withdraw::*; + +// Global constants for interacting with https://github.com/KomodoPlatform/chain-registry repository +// using `mm2_git` crate. +pub(crate) const CHAIN_REGISTRY_REPO_OWNER: &str = "KomodoPlatform"; +pub(crate) const CHAIN_REGISTRY_REPO_NAME: &str = "chain-registry"; +pub(crate) const CHAIN_REGISTRY_BRANCH: &str = "master"; +pub(crate) const CHAIN_REGISTRY_IBC_DIR_NAME: &str = "_IBC"; diff --git a/mm2src/coins/tendermint/ibc/ibc_proto.rs b/mm2src/coins/tendermint/ibc/ibc_proto.rs new file mode 100644 index 0000000000..ceccb128f8 --- /dev/null +++ b/mm2src/coins/tendermint/ibc/ibc_proto.rs @@ -0,0 +1,20 @@ +#[derive(prost::Message)] +pub(crate) struct IBCTransferV1Proto { + #[prost(string, tag = "1")] + pub(crate) source_port: prost::alloc::string::String, + #[prost(string, tag = "2")] + pub(crate) source_channel: prost::alloc::string::String, + #[prost(message, optional, tag = "3")] + pub(crate) token: Option, + #[prost(string, tag = "4")] + pub(crate) sender: prost::alloc::string::String, + #[prost(string, tag = "5")] + pub(crate) receiver: prost::alloc::string::String, + #[prost(message, optional, tag = "6")] + pub(crate) timeout_height: Option, + #[prost(uint64, tag = "7")] + pub(crate) timeout_timestamp: u64, + // Not supported by some of the cosmos chains like IRIS + // #[prost(string, optional, tag = "8")] + // pub(crate) memo: Option, +} diff --git a/mm2src/coins/tendermint/ibc/mod.rs b/mm2src/coins/tendermint/ibc/mod.rs new file mode 100644 index 0000000000..9e1c905398 --- /dev/null +++ b/mm2src/coins/tendermint/ibc/mod.rs @@ -0,0 +1,6 @@ +mod ibc_proto; +pub(crate) mod transfer_v1; + +pub(crate) const IBC_OUT_SOURCE_PORT: &str = "transfer"; +pub(crate) const IBC_OUT_TIMEOUT_IN_NANOS: u64 = 60000000000 * 15; // 15 minutes +pub(crate) const IBC_GAS_LIMIT_DEFAULT: u64 = 150_000; diff --git a/mm2src/coins/tendermint/ibc/transfer_v1.rs b/mm2src/coins/tendermint/ibc/transfer_v1.rs new file mode 100644 index 0000000000..34f693aaaa --- /dev/null +++ b/mm2src/coins/tendermint/ibc/transfer_v1.rs @@ -0,0 +1,108 @@ +use super::{ibc_proto::IBCTransferV1Proto, IBC_OUT_SOURCE_PORT, IBC_OUT_TIMEOUT_IN_NANOS}; +use crate::tendermint::type_urls::IBC_TRANSFER_TYPE_URL; +use common::number_type_casting::SafeTypeCastingNumbers; +use cosmrs::{tx::{Msg, MsgProto}, + AccountId, Coin, ErrorReport}; +use std::convert::TryFrom; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct MsgTransfer { + /// the port on which the packet will be sent + pub(crate) source_port: String, + /// the channel by which the packet will be sent + pub(crate) source_channel: String, + /// the tokens to be transferred + pub(crate) token: Coin, + /// the sender address + pub(crate) sender: AccountId, + /// the recipient address on the destination chain + pub(crate) receiver: AccountId, + /// Timeout height relative to the current block height. + /// The timeout is disabled when set to 0. + pub(crate) timeout_height: Option, + /// Timeout timestamp in absolute nanoseconds since unix epoch. + /// The timeout is disabled when set to 0. + pub(crate) timeout_timestamp: u64, + // Not supported by some of the cosmos chains like IRIS + // pub(crate) memo: Option, +} + +impl MsgTransfer { + pub(crate) fn new_with_default_timeout( + source_channel: String, + sender: AccountId, + receiver: AccountId, + token: Coin, + ) -> Self { + let timestamp_as_nanos: u64 = common::get_local_duration_since_epoch() + .expect("get_local_duration_since_epoch shouldn't fail") + .as_nanos() + .into_or_max(); + + Self { + source_port: IBC_OUT_SOURCE_PORT.to_owned(), + source_channel, + sender, + receiver, + token, + timeout_height: None, + timeout_timestamp: timestamp_as_nanos + IBC_OUT_TIMEOUT_IN_NANOS, + // memo: Some(memo.clone()), + } + } +} + +impl Msg for MsgTransfer { + type Proto = IBCTransferV1Proto; +} + +impl TryFrom for MsgTransfer { + type Error = ErrorReport; + + #[inline(always)] + fn try_from(proto: IBCTransferV1Proto) -> Result { MsgTransfer::try_from(&proto) } +} + +impl TryFrom<&IBCTransferV1Proto> for MsgTransfer { + type Error = ErrorReport; + + fn try_from(proto: &IBCTransferV1Proto) -> Result { + Ok(MsgTransfer { + source_port: proto.source_port.to_owned(), + source_channel: proto.source_channel.to_owned(), + token: proto + .token + .to_owned() + .map(TryFrom::try_from) + .ok_or_else(|| ErrorReport::msg("token can't be empty"))??, + sender: proto.sender.parse()?, + receiver: proto.receiver.parse()?, + timeout_height: None, + timeout_timestamp: proto.timeout_timestamp, + // memo: proto.memo.to_owned(), + }) + } +} + +impl From for IBCTransferV1Proto { + fn from(coin: MsgTransfer) -> IBCTransferV1Proto { IBCTransferV1Proto::from(&coin) } +} + +impl From<&MsgTransfer> for IBCTransferV1Proto { + fn from(msg: &MsgTransfer) -> IBCTransferV1Proto { + IBCTransferV1Proto { + source_port: msg.source_port.to_owned(), + source_channel: msg.source_channel.to_owned(), + token: Some(msg.token.to_owned().into()), + sender: msg.sender.to_string(), + receiver: msg.receiver.to_string(), + timeout_height: None, + timeout_timestamp: msg.timeout_timestamp, + // memo: msg.memo.to_owned(), + } + } +} + +impl MsgProto for IBCTransferV1Proto { + const TYPE_URL: &'static str = IBC_TRANSFER_TYPE_URL; +} diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index 6e904c4567..d480a4964e 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -2,6 +2,7 @@ // Useful resources // https://docs.cosmos.network/ +mod ibc; mod iris; mod rpc; mod tendermint_coin; @@ -25,6 +26,8 @@ pub(crate) const TENDERMINT_COIN_PROTOCOL_TYPE: &str = "TENDERMINT"; pub(crate) const TENDERMINT_ASSET_PROTOCOL_TYPE: &str = "TENDERMINTTOKEN"; pub(crate) mod type_urls { + pub(crate) const IBC_TRANSFER_TYPE_URL: &str = "/ibc.applications.transfer.v1.MsgTransfer"; + pub(crate) const CREATE_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgCreateHTLC"; pub(crate) const CLAIM_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgClaimHTLC"; } diff --git a/mm2src/coins/tendermint/rpc/mod.rs b/mm2src/coins/tendermint/rpc/mod.rs index bd34834ce9..26ccdf6553 100644 --- a/mm2src/coins/tendermint/rpc/mod.rs +++ b/mm2src/coins/tendermint/rpc/mod.rs @@ -1,9 +1,10 @@ #[cfg(not(target_arch = "wasm32"))] mod tendermint_native_rpc; #[cfg(not(target_arch = "wasm32"))] -pub use tendermint_native_rpc::*; +pub(crate) use tendermint_native_rpc::*; #[cfg(target_arch = "wasm32")] mod tendermint_wasm_rpc; -#[cfg(target_arch = "wasm32")] pub use tendermint_wasm_rpc::*; +#[cfg(target_arch = "wasm32")] +pub(crate) use tendermint_wasm_rpc::*; pub(crate) const TX_SUCCESS_CODE: u32 = 0; diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 1d09ee3b18..ac05c98565 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -1,8 +1,16 @@ +use super::ibc::transfer_v1::MsgTransfer; +use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::iris::htlc::{IrisHtlc, MsgClaimHtlc, MsgCreateHtlc, HTLC_STATE_COMPLETED, HTLC_STATE_OPEN, HTLC_STATE_REFUNDED}; use super::iris::htlc_proto::{CreateHtlcProtoRep, QueryHtlcRequestProto, QueryHtlcResponseProto}; use super::rpc::*; use crate::coin_errors::{MyAddressError, ValidatePaymentError}; +use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, + IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequest, + IBCTransferChannelsRequestError, IBCTransferChannelsResponse, + IBCTransferChannelsResult, IBCWithdrawRequest, CHAIN_REGISTRY_BRANCH, + CHAIN_REGISTRY_IBC_DIR_NAME, CHAIN_REGISTRY_REPO_NAME, CHAIN_REGISTRY_REPO_OWNER}; +use crate::tendermint::ibc::IBC_OUT_SOURCE_PORT; use crate::utxo::sat_from_big_decimal; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, @@ -49,6 +57,7 @@ use itertools::Itertools; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_git::{FileMetadata, GitController, GithubClient, RepositoryOperations, GITHUB_API_URI}; use mm2_number::MmNumber; use parking_lot::Mutex as PaMutex; use primitives::hash::H256; @@ -119,6 +128,7 @@ pub struct TendermintProtocolInfo { pub account_prefix: String, chain_id: String, gas_price: Option, + chain_registry_name: Option, } #[derive(Clone)] @@ -219,6 +229,7 @@ pub struct TendermintCoinImpl { pub(super) abortable_system: AbortableQueue, pub(crate) history_sync_state: Mutex, client: TendermintRpcClient, + chain_registry_name: Option, } #[derive(Clone)] @@ -501,9 +512,235 @@ impl TendermintCoin { abortable_system, history_sync_state: Mutex::new(history_sync_state), client: TendermintRpcClient(AsyncMutex::new(client_impl)), + chain_registry_name: protocol_info.chain_registry_name, }))) } + pub fn ibc_withdraw(&self, req: IBCWithdrawRequest) -> WithdrawFut { + let coin = self.clone(); + let fut = async move { + let to_address = + AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + + let (balance_denom, balance_dec) = coin + .get_balance_as_unsigned_and_decimal(&coin.denom, coin.decimals()) + .await?; + + // << BEGIN TX SIMULATION FOR FEE CALCULATION + let (amount_denom, amount_dec) = if req.max { + let amount_denom = balance_denom; + (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) + } else { + (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) + }; + + if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { + return MmError::err(WithdrawError::AmountTooLow { + amount: amount_dec, + threshold: coin.min_tx_amount(), + }); + } + + let received_by_me = if to_address == coin.account_id { + amount_dec + } else { + BigDecimal::default() + }; + + let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); + + let msg_transfer = MsgTransfer::new_with_default_timeout( + req.ibc_source_channel.clone(), + coin.account_id.clone(), + to_address.clone(), + Coin { + denom: coin.denom.clone(), + amount: amount_denom.into(), + }, + ) + .to_any() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let current_block = coin + .current_block() + .compat() + .await + .map_to_mm(WithdrawError::Transport)?; + + let _sequence_lock = coin.sequence_lock.lock().await; + let account_info = coin.my_account_info().await?; + + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + + let simulated_tx = coin + .gen_simulated_tx(account_info.clone(), msg_transfer.clone(), timeout_height, memo.clone()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + // >> END TX SIMULATION FOR FEE CALCULATION + + let (fee_amount_u64, fee_amount_dec) = coin + .calculate_fee_as_unsigned_and_decimal(simulated_tx, coin.decimals()) + .await?; + + let fee_amount = Coin { + denom: coin.denom.clone(), + amount: fee_amount_u64.into(), + }; + + let fee = Fee::from_amount_and_gas(fee_amount, IBC_GAS_LIMIT_DEFAULT); + + let (amount_denom, total_amount) = if req.max { + if balance_denom < fee_amount_u64 { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: balance_dec, + required: fee_amount_dec, + }); + } + let amount_denom = balance_denom - fee_amount_u64; + (amount_denom, balance_dec) + } else { + let total = &req.amount + &fee_amount_dec; + if balance_dec < total { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: balance_dec, + required: total, + }); + } + + (sat_from_big_decimal(&req.amount, coin.decimals)?, total) + }; + + let msg_transfer = MsgTransfer::new_with_default_timeout( + req.ibc_source_channel.clone(), + coin.account_id.clone(), + to_address.clone(), + Coin { + denom: coin.denom.clone(), + amount: amount_denom.into(), + }, + ) + .to_any() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let tx_raw = coin + .any_to_signed_raw_tx(account_info, msg_transfer, fee, timeout_height, memo.clone()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let tx_bytes = tx_raw + .to_bytes() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let hash = sha256(&tx_bytes); + Ok(TransactionDetails { + tx_hash: hex::encode_upper(hash.as_slice()), + tx_hex: tx_bytes.into(), + from: vec![coin.account_id.to_string()], + to: vec![req.to], + my_balance_change: &received_by_me - &total_amount, + spent_by_me: total_amount.clone(), + total_amount, + received_by_me, + block_height: 0, + timestamp: 0, + fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { + coin: coin.ticker.clone(), + amount: fee_amount_dec, + uamount: fee_amount_u64, + gas_limit: IBC_GAS_LIMIT_DEFAULT, + })), + coin: coin.ticker.to_string(), + internal_id: hash.to_vec().into(), + kmd_rewards: None, + transaction_type: TransactionType::default(), + memo: Some(memo), + }) + }; + Box::new(fut.boxed().compat()) + } + + pub async fn get_ibc_transfer_channels(&self, req: IBCTransferChannelsRequest) -> IBCTransferChannelsResult { + #[derive(Deserialize)] + struct ChainRegistry { + channels: Vec, + } + + #[derive(Deserialize)] + struct ChannelInfo { + channel_id: String, + port_id: String, + } + + #[derive(Deserialize)] + struct IbcChannel { + chain_1: ChannelInfo, + #[allow(dead_code)] + chain_2: ChannelInfo, + ordering: String, + version: String, + tags: Option, + } + + let src_chain_registry_name = self.chain_registry_name.as_ref().or_mm_err(|| { + IBCTransferChannelsRequestError::InternalError(format!( + "`chain_registry_name` is not set for '{}'", + self.platform_ticker() + )) + })?; + + let source_filename = format!( + "{}-{}.json", + src_chain_registry_name, req.destination_chain_registry_name + ); + + let git_controller: GitController = GitController::new(GITHUB_API_URI); + + let metadata_list = git_controller + .client + .get_file_metadata_list( + CHAIN_REGISTRY_REPO_OWNER, + CHAIN_REGISTRY_REPO_NAME, + CHAIN_REGISTRY_BRANCH, + CHAIN_REGISTRY_IBC_DIR_NAME, + ) + .await + .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + + let source_channel_file = metadata_list + .iter() + .find(|metadata| metadata.name == source_filename) + .or_mm_err(|| IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(source_filename))?; + + let mut registry_object = git_controller + .client + .deserialize_json_source::(source_channel_file.to_owned()) + .await + .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + + registry_object + .channels + .retain(|ch| ch.chain_1.port_id == *IBC_OUT_SOURCE_PORT); + + let result: Vec = registry_object + .channels + .iter() + .map(|ch| IBCTransferChannel { + channel_id: ch.chain_1.channel_id.clone(), + ordering: ch.ordering.clone(), + version: ch.version.clone(), + tags: ch.tags.clone().map(|t| IBCTransferChannelTag { + status: t.status, + preferred: t.preferred, + dex: t.dex, + }), + }) + .collect(); + + Ok(IBCTransferChannelsResponse { + ibc_transfer_channels: result, + }) + } + #[inline(always)] fn gas_price(&self) -> f64 { self.gas_price.unwrap_or(DEFAULT_GAS_PRICE) } @@ -576,7 +813,7 @@ impl TendermintCoin { amount: Vec, secret_hash: &[u8], ) -> String { - // Needs to be sorted if cointains multiple coins + // Needs to be sorted if contains multiple coins // let mut amount = amount; // amount.sort(); @@ -1250,6 +1487,28 @@ impl TendermintCoin { }) } + pub(super) async fn calculate_fee_as_unsigned_and_decimal( + &self, + tx: Vec, + decimals: u8, + ) -> MmResult<(u64, BigDecimal), TendermintCoinRpcError> { + let fee_amount_u64 = self.calculate_fee_amount_as_u64(tx).await?; + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, decimals); + + Ok((fee_amount_u64, fee_amount_dec)) + } + + pub(super) async fn get_balance_as_unsigned_and_decimal( + &self, + denom: &Denom, + decimals: u8, + ) -> MmResult<(u64, BigDecimal), TendermintCoinRpcError> { + let denom_ubalance = self.balance_for_denom(denom.to_string()).await?; + let denom_balance_dec = big_decimal_from_sat_unsigned(denom_ubalance, decimals); + + Ok((denom_ubalance, denom_balance_dec)) + } + async fn request_tx(&self, hash: String) -> MmResult { let path = AbciPath::from_str(ABCI_GET_TX_PATH).expect("valid path"); let request = GetTxRequest { hash }; @@ -1417,6 +1676,46 @@ fn clients_from_urls(rpc_urls: &[String]) -> MmResult, Tendermin Ok(clients) } +pub async fn get_ibc_chain_list() -> IBCChainRegistriesResult { + fn map_metadata_to_chain_registry_name(metadata: &FileMetadata) -> Result> { + let split_filename_by_dash: Vec<&str> = metadata.name.split('-').collect(); + let chain_registry_name = split_filename_by_dash + .first() + .or_mm_err(|| { + IBCChainsRequestError::InternalError(format!( + "Could not read chain registry name from '{}'", + metadata.name + )) + })? + .to_string(); + + Ok(chain_registry_name) + } + + let git_controller: GitController = GitController::new(GITHUB_API_URI); + + let metadata_list = git_controller + .client + .get_file_metadata_list( + CHAIN_REGISTRY_REPO_OWNER, + CHAIN_REGISTRY_REPO_NAME, + CHAIN_REGISTRY_BRANCH, + CHAIN_REGISTRY_IBC_DIR_NAME, + ) + .await + .map_err(|e| IBCChainsRequestError::Transport(format!("{:?}", e)))?; + + let chain_list: Result, MmError> = + metadata_list.iter().map(map_metadata_to_chain_registry_name).collect(); + + let mut distinct_chain_list = chain_list?; + distinct_chain_list.dedup(); + + Ok(IBCChainRegistriesResponse { + chain_registry_list: distinct_chain_list, + }) +} + #[async_trait] #[allow(unused_variables)] impl MmCoin for TendermintCoin { @@ -1435,25 +1734,19 @@ impl MmCoin for TendermintCoin { coin.account_prefix ))); } - let balance_denom = coin.balance_for_denom(coin.denom.to_string()).await?; - let balance_dec = big_decimal_from_sat_unsigned(balance_denom, coin.decimals); + + let (balance_denom, balance_dec) = coin + .get_balance_as_unsigned_and_decimal(&coin.denom, coin.decimals()) + .await?; // << BEGIN TX SIMULATION FOR FEE CALCULATION - let (amount_denom, amount_dec, total_amount) = if req.max { + let (amount_denom, amount_dec) = if req.max { let amount_denom = balance_denom; - ( - amount_denom, - big_decimal_from_sat_unsigned(amount_denom, coin.decimals), - balance_dec.clone(), - ) + (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) } else { let total = req.amount.clone(); - ( - sat_from_big_decimal(&req.amount, coin.decimals)?, - req.amount.clone(), - total, - ) + (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) }; if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { @@ -1497,8 +1790,9 @@ impl MmCoin for TendermintCoin { .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; // >> END TX SIMULATION FOR FEE CALCULATION - let fee_amount_u64 = coin.calculate_fee_amount_as_u64(simulated_tx).await?; - let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); + let (fee_amount_u64, fee_amount_dec) = coin + .calculate_fee_as_unsigned_and_decimal(simulated_tx, coin.decimals()) + .await?; let fee_amount = Coin { denom: coin.denom.clone(), @@ -1507,7 +1801,7 @@ impl MmCoin for TendermintCoin { let fee = Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT); - let (amount_denom, amount_dec, total_amount) = if req.max { + let (amount_denom, total_amount) = if req.max { if balance_denom < fee_amount_u64 { return MmError::err(WithdrawError::NotSufficientBalance { coin: coin.ticker.clone(), @@ -1516,11 +1810,7 @@ impl MmCoin for TendermintCoin { }); } let amount_denom = balance_denom - fee_amount_u64; - ( - amount_denom, - big_decimal_from_sat_unsigned(amount_denom, coin.decimals), - balance_dec, - ) + (amount_denom, balance_dec) } else { let total = &req.amount + &fee_amount_dec; if balance_dec < total { @@ -1531,11 +1821,7 @@ impl MmCoin for TendermintCoin { }); } - ( - sat_from_big_decimal(&req.amount, coin.decimals)?, - req.amount.clone(), - total, - ) + (sat_from_big_decimal(&req.amount, coin.decimals)?, total) }; let msg_send = MsgSend { @@ -2337,6 +2623,7 @@ pub mod tendermint_coin_tests { account_prefix: String::from("iaa"), chain_id: String::from("nyancat-9"), gas_price: None, + chain_registry_name: None, } } @@ -2347,6 +2634,7 @@ pub mod tendermint_coin_tests { account_prefix: String::from("iaa"), chain_id: String::from("nyancat-9"), gas_price: None, + chain_registry_name: None, } } diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 777df3ac67..60deb075b7 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -1,6 +1,9 @@ +use super::ibc::transfer_v1::MsgTransfer; +use super::ibc::IBC_GAS_LIMIT_DEFAULT; /// Module containing implementation for Tendermint Tokens. They include native assets + IBC use super::{TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATOSHIS, TIMEOUT_HEIGHT_DELTA, TX_DEFAULT_MEMO}; +use crate::rpc_command::tendermint::IBCWithdrawRequest; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, @@ -96,6 +99,140 @@ impl TendermintToken { }; Ok(TendermintToken(Arc::new(token_impl))) } + + pub fn ibc_withdraw(&self, req: IBCWithdrawRequest) -> WithdrawFut { + let platform = self.platform_coin.clone(); + let token = self.clone(); + let fut = async move { + let to_address = + AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + + let (base_denom_balance, base_denom_balance_dec) = platform + .get_balance_as_unsigned_and_decimal(&platform.denom, token.decimals()) + .await?; + + let (balance_denom, balance_dec) = platform + .get_balance_as_unsigned_and_decimal(&token.denom, token.decimals()) + .await?; + + let (amount_denom, amount_dec, total_amount) = if req.max { + ( + balance_denom, + big_decimal_from_sat_unsigned(balance_denom, token.decimals), + balance_dec, + ) + } else { + if balance_dec < req.amount { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: token.ticker.clone(), + available: balance_dec, + required: req.amount, + }); + } + + ( + sat_from_big_decimal(&req.amount, token.decimals())?, + req.amount.clone(), + req.amount, + ) + }; + + if !platform.is_tx_amount_enough(token.decimals, &amount_dec) { + return MmError::err(WithdrawError::AmountTooLow { + amount: amount_dec, + threshold: token.min_tx_amount(), + }); + } + + let received_by_me = if to_address == platform.account_id { + amount_dec + } else { + BigDecimal::default() + }; + + let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); + + let msg_transfer = MsgTransfer::new_with_default_timeout( + req.ibc_source_channel.clone(), + platform.account_id.clone(), + to_address.clone(), + Coin { + denom: token.denom.clone(), + amount: amount_denom.into(), + }, + ) + .to_any() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let current_block = token + .current_block() + .compat() + .await + .map_to_mm(WithdrawError::Transport)?; + + let _sequence_lock = platform.sequence_lock.lock().await; + let account_info = platform.my_account_info().await?; + + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + + let simulated_tx = platform + .gen_simulated_tx(account_info.clone(), msg_transfer.clone(), timeout_height, memo.clone()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let (fee_amount_u64, fee_amount_dec) = platform + .calculate_fee_as_unsigned_and_decimal(simulated_tx, platform.decimals()) + .await?; + + if base_denom_balance < fee_amount_u64 { + return MmError::err(WithdrawError::NotSufficientPlatformBalanceForFee { + coin: platform.ticker().to_string(), + available: base_denom_balance_dec, + required: fee_amount_dec, + }); + } + + let fee_amount = Coin { + denom: platform.denom.clone(), + amount: fee_amount_u64.into(), + }; + + let fee = Fee::from_amount_and_gas(fee_amount, IBC_GAS_LIMIT_DEFAULT); + + let tx_raw = platform + .any_to_signed_raw_tx(account_info, msg_transfer, fee, timeout_height, memo.clone()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let tx_bytes = tx_raw + .to_bytes() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let hash = sha256(&tx_bytes); + Ok(TransactionDetails { + tx_hash: hex::encode_upper(hash.as_slice()), + tx_hex: tx_bytes.into(), + from: vec![platform.account_id.to_string()], + to: vec![req.to], + my_balance_change: &received_by_me - &total_amount, + spent_by_me: total_amount.clone(), + total_amount, + received_by_me, + block_height: 0, + timestamp: 0, + fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { + coin: platform.ticker().to_string(), + amount: fee_amount_dec, + uamount: fee_amount_u64, + gas_limit: IBC_GAS_LIMIT_DEFAULT, + })), + coin: token.ticker.clone(), + internal_id: hash.to_vec().into(), + kmd_rewards: None, + transaction_type: TransactionType::default(), + memo: Some(memo), + }) + }; + Box::new(fut.boxed().compat()) + } } #[async_trait] @@ -453,11 +590,13 @@ impl MmCoin for TendermintToken { ))); } - let base_denom_balance = platform.balance_for_denom(platform.denom.to_string()).await?; - let base_denom_balance_dec = big_decimal_from_sat_unsigned(base_denom_balance, token.decimals()); + let (base_denom_balance, base_denom_balance_dec) = platform + .get_balance_as_unsigned_and_decimal(&platform.denom, token.decimals()) + .await?; - let balance_denom = platform.balance_for_denom(token.denom.to_string()).await?; - let balance_dec = big_decimal_from_sat_unsigned(balance_denom, token.decimals()); + let (balance_denom, balance_dec) = platform + .get_balance_as_unsigned_and_decimal(&token.denom, token.decimals()) + .await?; let (amount_denom, amount_dec, total_amount) = if req.max { ( @@ -521,8 +660,9 @@ impl MmCoin for TendermintToken { .gen_simulated_tx(account_info.clone(), msg_send.clone(), timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let fee_amount_u64 = platform.calculate_fee_amount_as_u64(simulated_tx).await?; - let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); + let (fee_amount_u64, fee_amount_dec) = platform + .calculate_fee_as_unsigned_and_decimal(simulated_tx, platform.decimals()) + .await?; if base_denom_balance < fee_amount_u64 { return MmError::err(WithdrawError::NotSufficientPlatformBalanceForFee { diff --git a/mm2src/mm2_git/Cargo.toml b/mm2src/mm2_git/Cargo.toml new file mode 100644 index 0000000000..ee06101400 --- /dev/null +++ b/mm2src/mm2_git/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "mm2_git" +version = "0.1.0" +edition = "2021" + +[lib] +doctest = false + +[dependencies] +async-trait = "0.1" +common = { path = "../common" } +http = "0.2" +mm2_err_handle = { path = "../mm2_err_handle" } +mm2_net = { path = "../mm2_net" } +serde = "1" +serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } diff --git a/mm2src/mm2_git/src/github_client.rs b/mm2src/mm2_git/src/github_client.rs new file mode 100644 index 0000000000..e13f97ba18 --- /dev/null +++ b/mm2src/mm2_git/src/github_client.rs @@ -0,0 +1,132 @@ +use async_trait::async_trait; +use mm2_err_handle::prelude::MmError; +use mm2_net::transport::slurp_url_with_headers; +use serde::de::DeserializeOwned; + +use crate::{FileMetadata, GitCommons, GitControllerError, RepositoryOperations}; + +const GITHUB_CLIENT_USER_AGENT: &str = "mm2"; + +pub struct GithubClient { + api_address: String, +} + +impl GitCommons for GithubClient { + fn new(api_address: String) -> Self { Self { api_address } } +} + +#[async_trait] +impl RepositoryOperations for GithubClient { + async fn deserialize_json_source( + &self, + file_metadata: FileMetadata, + ) -> Result> { + let (_status_code, _headers, data_buffer) = slurp_url_with_headers(&file_metadata.download_url, vec![( + http::header::USER_AGENT.as_str(), + GITHUB_CLIENT_USER_AGENT, + )]) + .await + .map_err(|e| GitControllerError::HttpError(e.to_string()))?; + + Ok( + serde_json::from_slice(&data_buffer) + .map_err(|e| GitControllerError::DeserializationError(e.to_string()))?, + ) + } + + async fn get_file_metadata_list( + &self, + owner: &str, + repository_name: &str, + branch: &str, + dir: &str, + ) -> Result, MmError> { + let uri = format!( + "{}/repos/{}/{}/contents/{}?ref={}", + &self.api_address, owner, repository_name, dir, branch + ); + + let (_status_code, _headers, data_buffer) = slurp_url_with_headers(&uri, vec![( + http::header::USER_AGENT.as_str(), + GITHUB_CLIENT_USER_AGENT, + )]) + .await + .map_err(|e| GitControllerError::HttpError(e.to_string()))?; + + Ok( + serde_json::from_slice(&data_buffer) + .map_err(|e| GitControllerError::DeserializationError(e.to_string()))?, + ) + } +} + +#[cfg(test)] +#[allow(unused)] +mod tests { + use crate::{GitController, GITHUB_API_URI}; + + use super::*; + use serde::Deserialize; + + #[derive(Debug, Deserialize)] + struct ChainRegistry { + chain_1: ChainInfo, + chain_2: ChainInfo, + channels: Vec, + } + + #[derive(Debug, Deserialize)] + struct IbcChannel { + chain_1: ChannelInfo, + chain_2: ChannelInfo, + ordering: String, + version: String, + tags: Option, + } + + #[derive(Debug, Deserialize)] + struct ChainInfo { + chain_name: String, + client_id: String, + connection_id: String, + } + + #[derive(Debug, Deserialize)] + struct ChannelInfo { + channel_id: String, + port_id: String, + } + + #[derive(Debug, Deserialize)] + struct ChannelTag { + status: String, + preferred: bool, + dex: Option, + } + + #[test] + fn test_metadata_list_and_json_deserialization() { + const REPO_OWNER: &str = "KomodoPlatform"; + const REPO_NAME: &str = "chain-registry"; + const BRANCH: &str = "master"; + const DIR_NAME: &str = "_IBC"; + + let git_controller: GitController = GitController::new(GITHUB_API_URI); + + let metadata_list = common::block_on( + git_controller + .client + .get_file_metadata_list(REPO_OWNER, REPO_NAME, BRANCH, DIR_NAME), + ) + .unwrap(); + + assert!(!metadata_list.is_empty()); + + common::block_on( + git_controller + .client + .deserialize_json_source::(metadata_list.first().unwrap().clone()), + ) + .unwrap(); + } +} diff --git a/mm2src/mm2_git/src/lib.rs b/mm2src/mm2_git/src/lib.rs new file mode 100644 index 0000000000..15bc8408f1 --- /dev/null +++ b/mm2src/mm2_git/src/lib.rs @@ -0,0 +1,59 @@ +//! This crate provides an abstraction layer on Git for doing query/parse +//! operations over the repositories. +//! +//! Implementation of generic `GitController` provides the flexibility of +//! adding any Git clients(like Gitlab, Bitbucket, etc) when needed. + +use async_trait::async_trait; +use mm2_err_handle::prelude::MmError; +use serde::{de::DeserializeOwned, Deserialize}; + +pub mod github_client; +pub use github_client::*; + +pub const GITHUB_API_URI: &str = "https://api.github.com"; + +#[derive(Clone, Debug, Deserialize)] +pub struct FileMetadata { + pub name: String, + pub download_url: String, + pub size: usize, +} + +pub trait GitCommons { + fn new(api_address: String) -> Self; +} + +#[async_trait] +pub trait RepositoryOperations { + async fn deserialize_json_source( + &self, + file_metadata: FileMetadata, + ) -> Result>; + + async fn get_file_metadata_list( + &self, + owner: &str, + repository_name: &str, + branch: &str, + dir: &str, + ) -> Result, MmError>; +} + +pub struct GitController { + pub client: T, +} + +impl GitController { + pub fn new(api_address: &str) -> Self { + Self { + client: T::new(api_address.to_owned()), + } + } +} + +#[derive(Debug)] +pub enum GitControllerError { + DeserializationError(String), + HttpError(String), +} diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index ec51a48f15..6ecefa4138 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -12,6 +12,7 @@ use crate::{mm2::lp_stats::{add_node_to_version_stat, remove_node_from_version_s use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; #[cfg(feature = "enable-nft-integration")] use coins::nft; +use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels, ibc_withdraw}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, get_enabled_coins::get_enabled_coins, @@ -191,6 +192,9 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, update_version_stat_collection).await, "verify_message" => handle_mmrpc(ctx, request, verify_message).await, "withdraw" => handle_mmrpc(ctx, request, withdraw).await, + "ibc_withdraw" => handle_mmrpc(ctx, request, ibc_withdraw).await, + "ibc_chains" => handle_mmrpc(ctx, request, ibc_chains).await, + "ibc_transfer_channels" => handle_mmrpc(ctx, request, ibc_transfer_channels).await, #[cfg(feature = "enable-nft-integration")] "withdraw_nft" => handle_mmrpc(ctx, request, withdraw_nft).await, #[cfg(not(target_arch = "wasm32"))] diff --git a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs index 796162de48..51db8dd236 100644 --- a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs @@ -1,12 +1,13 @@ use common::block_on; use mm2_number::BigDecimal; use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_err, enable_tendermint, - enable_tendermint_token, get_tendermint_my_tx_history, iris_nimda_testnet_conf, - iris_testnet_conf, my_balance, send_raw_transaction, withdraw_v1, MarketMakerIt, - Mm2TestConf}; + enable_tendermint_token, get_tendermint_my_tx_history, ibc_withdraw, + iris_nimda_testnet_conf, iris_testnet_conf, my_balance, send_raw_transaction, + withdraw_v1, MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::structs::{RpcV2Response, TendermintActivationResult, TransactionDetails}; use serde_json::{self as json, json}; +const IRIS_TEST_SEED: &str = "iris test seed"; const ATOM_TEST_BALANCE_SEED: &str = "atom test seed"; const ATOM_TEST_WITHDRAW_SEED: &str = "atom test withdraw seed"; const ATOM_TICKER: &str = "ATOM"; @@ -112,14 +113,45 @@ fn test_tendermint_withdraw() { println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); } +#[test] +fn test_tendermint_ibc_withdraw() { + const IBC_SOURCE_CHANNEL: &str = "channel-81"; + const IBC_TARGET_ADDRESS: &str = "cosmos1r5v5srda7xfth3hn2s26txvrcrntldjumt8mhl"; + const MY_ADDRESS: &str = "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2"; + + let coins = json!([iris_testnet_conf(), iris_nimda_testnet_conf()]); + let platform_coin = coins[0]["coin"].as_str().unwrap(); + let token = coins[1]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode(IRIS_TEST_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_tendermint(&mm, platform_coin, &[], IRIS_TESTNET_RPC_URLS, false)); + println!("Activation with assets {}", json::to_string(&activation_res).unwrap()); + + let activation_res = block_on(enable_tendermint_token(&mm, token)); + println!("Token activation {}", json::to_string(&activation_res).unwrap()); + + let tx_details = block_on(ibc_withdraw(&mm, IBC_SOURCE_CHANNEL, token, IBC_TARGET_ADDRESS, "0.1")); + println!("IBC transfer to atom address {}", json::to_string(&tx_details).unwrap()); + + let expected_spent: BigDecimal = "0.1".parse().unwrap(); + assert_eq!(tx_details.spent_by_me, expected_spent); + + assert_eq!(tx_details.to, vec![IBC_TARGET_ADDRESS.to_owned()]); + assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); + + let send_raw_tx = block_on(send_raw_transaction(&mm, token, &tx_details.tx_hex)); + println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); +} + #[test] fn test_tendermint_token_activation_and_withdraw() { - const TEST_SEED: &str = "iris test seed"; let coins = json!([iris_testnet_conf(), iris_nimda_testnet_conf()]); let platform_coin = coins[0]["coin"].as_str().unwrap(); let token = coins[1]["coin"].as_str().unwrap(); - let conf = Mm2TestConf::seednode(TEST_SEED, &coins); + let conf = Mm2TestConf::seednode(IRIS_TEST_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let activation_res = block_on(enable_tendermint(&mm, platform_coin, &[], IRIS_TESTNET_RPC_URLS, false)); @@ -277,13 +309,7 @@ fn test_disable_tendermint_platform_coin_with_token() { let conf = Mm2TestConf::seednode(TEST_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); // Enable platform coin IRIS-TEST - let activation_res = block_on(enable_tendermint( - &mm, - platform_coin, - &[], - &["http://34.80.202.172:26657"], - false, - )); + let activation_res = block_on(enable_tendermint(&mm, platform_coin, &[], IRIS_TESTNET_RPC_URLS, false)); assert!(&activation_res.get("result").unwrap().get("address").is_some()); // Enable platform coin token IRIS-NIMDA diff --git a/mm2src/mm2_net/src/native_http.rs b/mm2src/mm2_net/src/native_http.rs index 641a0cae24..700bdb1efa 100644 --- a/mm2src/mm2_net/src/native_http.rs +++ b/mm2src/mm2_net/src/native_http.rs @@ -2,7 +2,7 @@ use crate::transport::{SlurpError, SlurpResult, SlurpResultJson}; use common::wio::{drive03, HYPER}; use common::APPLICATION_JSON; use futures::channel::oneshot::Canceled; -use http::{header, Request}; +use http::{header, HeaderValue, Request}; use hyper::Body; use mm2_err_handle::prelude::*; use serde_json::Value as Json; @@ -75,6 +75,22 @@ pub async fn slurp_url(url: &str) -> SlurpResult { slurp_req(req).await } +/// Executes a GET request with additional headers. +/// Returning the response status, headers and body. +pub async fn slurp_url_with_headers(url: &str, headers: Vec<(&'static str, &'static str)>) -> SlurpResult { + let mut req = Request::builder(); + let h = req + .headers_mut() + .or_mm_err(|| SlurpError::Internal("An error occured while accessing to the request headers.".to_string()))?; + + for (key, value) in headers { + h.insert(key, HeaderValue::from_static(value)); + } + + let req = req.uri(url).body(Vec::new())?; + slurp_req(req).await +} + /// Executes a POST request, returning the response status, headers and body. pub async fn slurp_post_json(url: &str, body: String) -> SlurpResult { let request = Request::builder() diff --git a/mm2src/mm2_net/src/transport.rs b/mm2src/mm2_net/src/transport.rs index 18e4f4feb6..2ba04b65e1 100644 --- a/mm2src/mm2_net/src/transport.rs +++ b/mm2src/mm2_net/src/transport.rs @@ -7,10 +7,10 @@ use serde::{Deserialize, Serialize}; use serde_json::{Error, Value as Json}; #[cfg(not(target_arch = "wasm32"))] -pub use crate::native_http::{slurp_post_json, slurp_req, slurp_req_body, slurp_url}; +pub use crate::native_http::{slurp_post_json, slurp_req, slurp_req_body, slurp_url, slurp_url_with_headers}; #[cfg(target_arch = "wasm32")] -pub use crate::wasm_http::{slurp_post_json, slurp_url}; +pub use crate::wasm_http::{slurp_post_json, slurp_url, slurp_url_with_headers}; pub type SlurpResult = Result<(StatusCode, HeaderMap, Vec), MmError>; diff --git a/mm2src/mm2_net/src/wasm_http.rs b/mm2src/mm2_net/src/wasm_http.rs index afa24d1848..3767c2f40c 100644 --- a/mm2src/mm2_net/src/wasm_http.rs +++ b/mm2src/mm2_net/src/wasm_http.rs @@ -24,6 +24,17 @@ pub async fn slurp_url(url: &str) -> SlurpResult { .map(|(status_code, response)| (status_code, HeaderMap::new(), response.into_bytes())) } +/// Executes a GET request with additional headers. +/// Returning the response status, headers and body. +/// Please note the return header map is empty, because `wasm_bindgen` doesn't provide the way to extract all headers. +pub async fn slurp_url_with_headers(url: &str, headers: Vec<(&str, &str)>) -> SlurpResult { + FetchRequest::get(url) + .headers(headers) + .request_str() + .await + .map(|(status_code, response)| (status_code, HeaderMap::new(), response.into_bytes())) +} + /// Executes a POST request, returning the response status, headers and body. /// Please note the return header map is empty, because `wasm_bindgen` doesn't provide the way to extract all headers. pub async fn slurp_post_json(url: &str, body: String) -> SlurpResult { @@ -86,6 +97,13 @@ impl FetchRequest { self } + pub fn headers(mut self, headers: Vec<(&str, &str)>) -> FetchRequest { + for (key, value) in headers { + self.headers.insert(key.to_owned(), value.to_owned()); + } + self + } + pub async fn request_str(self) -> FetchResult { let (tx, rx) = oneshot::channel(); Self::spawn_fetch_str(self, tx); diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 74d0bd1fb8..841b62e755 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -2207,6 +2207,33 @@ pub async fn withdraw_v1(mm: &MarketMakerIt, coin: &str, to: &str, amount: &str) json::from_str(&request.1).unwrap() } +pub async fn ibc_withdraw( + mm: &MarketMakerIt, + source_channel: &str, + coin: &str, + to: &str, + amount: &str, +) -> TransactionDetails { + let request = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "ibc_withdraw", + "mmrpc": "2.0", + "params": { + "ibc_source_channel": source_channel, + "coin": coin, + "to": to, + "amount": amount + } + })) + .await + .unwrap(); + assert_eq!(request.0, StatusCode::OK, "'ibc_withdraw' failed: {}", request.1); + + let json: Json = json::from_str(&request.1).unwrap(); + json::from_value(json["result"].clone()).unwrap() +} + pub async fn withdraw_status(mm: &MarketMakerIt, task_id: u64) -> Json { let request = mm .rpc(&json!({