From 3368dec4a73009202467e6eaf0d3d2ade20d6fc7 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 18 Dec 2024 05:26:16 +0000 Subject: [PATCH 01/14] read new field `ibc_channels` from coins file Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 323637599d..b4068667ef 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -181,6 +181,7 @@ pub struct TendermintProtocolInfo { chain_id: String, gas_price: Option, chain_registry_name: Option, + ibc_channels: HashMap, } #[derive(Clone)] @@ -375,6 +376,9 @@ pub struct TendermintCoinImpl { pub(crate) chain_registry_name: Option, pub(crate) ctx: MmWeak, pub(crate) is_keplr_from_ledger: bool, + /// Key represents the account prefix of the target chain and + /// the value is the channel ID used for sending transactions. + ibc_channels: HashMap, } #[derive(Clone)] @@ -701,6 +705,7 @@ impl TendermintCoin { history_sync_state: Mutex::new(history_sync_state), client: TendermintRpcClient(AsyncMutex::new(client_impl)), chain_registry_name: protocol_info.chain_registry_name, + ibc_channels: protocol_info.ibc_channels, ctx: ctx.weak(), is_keplr_from_ledger, }))) @@ -3371,6 +3376,7 @@ pub mod tendermint_coin_tests { chain_id: String::from("nyancat-9"), gas_price: None, chain_registry_name: None, + ibc_channels: HashMap::new(), } } @@ -3382,6 +3388,7 @@ pub mod tendermint_coin_tests { chain_id: String::from("nyancat-9"), gas_price: None, chain_registry_name: None, + ibc_channels: HashMap::new(), } } From 8696db653e07a38df66eecc10f07a79de2f3ce48 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 18 Dec 2024 05:54:15 +0000 Subject: [PATCH 02/14] remove old IBC logics Signed-off-by: onur-ozkan --- Cargo.lock | 1 - mm2src/coins/Cargo.toml | 1 - mm2src/coins/rpc_command/mod.rs | 1 - .../rpc_command/tendermint/ibc_chains.rs | 35 ---- .../tendermint/ibc_transfer_channels.rs | 105 ---------- mm2src/coins/rpc_command/tendermint/mod.rs | 12 -- mm2src/coins/tendermint/tendermint_coin.rs | 192 +----------------- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 3 - 8 files changed, 2 insertions(+), 348 deletions(-) delete mode 100644 mm2src/coins/rpc_command/tendermint/ibc_chains.rs delete mode 100644 mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs delete mode 100644 mm2src/coins/rpc_command/tendermint/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 7332358679..07718f00d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -876,7 +876,6 @@ dependencies = [ "mm2_db", "mm2_err_handle", "mm2_event_stream", - "mm2_git", "mm2_io", "mm2_metamask", "mm2_metrics", diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 290a0dd7f5..4f357143f2 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -64,7 +64,6 @@ nom = "6.1.2" mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } -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/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs index 0bec5ef493..8ff4ae5c1b 100644 --- a/mm2src/coins/rpc_command/mod.rs +++ b/mm2src/coins/rpc_command/mod.rs @@ -9,4 +9,3 @@ 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 deleted file mode 100644 index 67ed93e9fa..0000000000 --- a/mm2src/coins/rpc_command/tendermint/ibc_chains.rs +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 4edcd0cd55..0000000000 --- a/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs +++ /dev/null @@ -1,105 +0,0 @@ -use common::HttpStatusCode; -use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::MmError; - -use crate::{coin_conf, tendermint::get_ibc_transfer_channels}; - -pub type IBCTransferChannelsResult = Result>; - -#[derive(Clone, Deserialize)] -pub struct IBCTransferChannelsRequest { - pub(crate) source_coin: String, - pub(crate) destination_coin: 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 = "'chain_registry_name' was not found in coins configuration for '{}' prefix. Either update the coins configuration or use 'ibc_source_channel' in the request.", - _0 - )] - RegistryNameIsMissing(String), - #[display(fmt = "Could not find '{}' registry source.", _0)] - RegistrySourceCouldNotFound(String), - #[display(fmt = "Transport error: {}", _0)] - Transport(String), - #[display(fmt = "Could not found channel for '{}'.", _0)] - CouldNotFindChannel(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::CouldNotFindChannel(_) - | IBCTransferChannelsRequestError::RegistryNameIsMissing(_) - | 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 source_coin_conf = coin_conf(&ctx, &req.source_coin); - let source_registry_name = source_coin_conf - .get("protocol") - .unwrap_or(&serde_json::Value::Null) - .get("protocol_data") - .unwrap_or(&serde_json::Value::Null) - .get("chain_registry_name") - .map(|t| t.as_str().unwrap_or_default().to_owned()); - - let Some(source_registry_name) = source_registry_name else { - return MmError::err(IBCTransferChannelsRequestError::RegistryNameIsMissing(req.source_coin)); - }; - - let destination_coin_conf = coin_conf(&ctx, &req.destination_coin); - let destination_registry_name = destination_coin_conf - .get("protocol") - .unwrap_or(&serde_json::Value::Null) - .get("protocol_data") - .unwrap_or(&serde_json::Value::Null) - .get("chain_registry_name") - .map(|t| t.as_str().unwrap_or_default().to_owned()); - - let Some(destination_registry_name) = destination_registry_name else { - return MmError::err(IBCTransferChannelsRequestError::RegistryNameIsMissing( - req.destination_coin, - )); - }; - - get_ibc_transfer_channels(source_registry_name, destination_registry_name).await -} diff --git a/mm2src/coins/rpc_command/tendermint/mod.rs b/mm2src/coins/rpc_command/tendermint/mod.rs deleted file mode 100644 index 3e2b664aec..0000000000 --- a/mm2src/coins/rpc_command/tendermint/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod ibc_chains; -mod ibc_transfer_channels; - -pub use ibc_chains::*; -pub use ibc_transfer_channels::*; - -// 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 = "nucl"; -pub(crate) const CHAIN_REGISTRY_IBC_DIR_NAME: &str = "_IBC"; diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index b4068667ef..80ee6c3d5d 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -6,10 +6,6 @@ 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::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, - IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, - IBCTransferChannelsResponse, IBCTransferChannelsResult, 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; @@ -64,7 +60,6 @@ use itertools::Itertools; use keys::{KeyPair, Public}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; -use mm2_git::{FileMetadata, GitController, GithubClient, RepositoryOperations, GITHUB_API_URI}; use mm2_number::MmNumber; use mm2_p2p::p2p_ctx::P2PContext; use parking_lot::Mutex as PaMutex; @@ -180,7 +175,6 @@ pub struct TendermintProtocolInfo { pub account_prefix: String, chain_id: String, gas_price: Option, - chain_registry_name: Option, ibc_channels: HashMap, } @@ -373,7 +367,6 @@ pub struct TendermintCoinImpl { pub(super) abortable_system: AbortableQueue, pub(crate) history_sync_state: Mutex, client: TendermintRpcClient, - pub(crate) chain_registry_name: Option, pub(crate) ctx: MmWeak, pub(crate) is_keplr_from_ledger: bool, /// Key represents the account prefix of the target chain and @@ -704,38 +697,18 @@ 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, ibc_channels: protocol_info.ibc_channels, ctx: ctx.weak(), is_keplr_from_ledger, }))) } - /// Extracts corresponding IBC channel ID for `AccountId` from https://github.com/KomodoPlatform/chain-registry/tree/nucl. + /// TODO pub(crate) async fn detect_channel_id_for_ibc_transfer( &self, to_address: &AccountId, ) -> Result> { - let ctx = MmArc::from_weak(&self.ctx).ok_or_else(|| WithdrawError::InternalError("No context".to_owned()))?; - - let source_registry_name = self - .chain_registry_name - .clone() - .ok_or_else(|| WithdrawError::RegistryNameIsMissing(to_address.prefix().to_owned()))?; - - let destination_registry_name = chain_registry_name_from_account_prefix(&ctx, to_address.prefix()) - .ok_or_else(|| WithdrawError::RegistryNameIsMissing(to_address.prefix().to_owned()))?; - - let channels = get_ibc_transfer_channels(source_registry_name, destination_registry_name) - .await - .map_err(|_| WithdrawError::IBCChannelCouldNotFound(to_address.to_string()))?; - - Ok(channels - .ibc_transfer_channels - .last() - .ok_or_else(|| WithdrawError::InternalError("channel list can not be empty".to_owned()))? - .channel_id - .clone()) + todo!() } #[inline(always)] @@ -2120,46 +2093,6 @@ fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult 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 { @@ -3162,45 +3095,6 @@ pub fn tendermint_priv_key_policy( } } -pub(crate) fn chain_registry_name_from_account_prefix(ctx: &MmArc, prefix: &str) -> Option { - let Some(coins) = ctx.conf["coins"].as_array() else { - return None; - }; - - for coin in coins { - let protocol = coin - .get("protocol") - .unwrap_or(&serde_json::Value::Null) - .get("type") - .unwrap_or(&serde_json::Value::Null) - .as_str(); - - if protocol != Some(TENDERMINT_COIN_PROTOCOL_TYPE) { - continue; - } - - let coin_account_prefix = coin - .get("protocol") - .unwrap_or(&serde_json::Value::Null) - .get("protocol_data") - .unwrap_or(&serde_json::Value::Null) - .get("account_prefix") - .map(|t| t.as_str().unwrap_or_default()); - - if coin_account_prefix == Some(prefix) { - return coin - .get("protocol") - .unwrap_or(&serde_json::Value::Null) - .get("protocol_data") - .unwrap_or(&serde_json::Value::Null) - .get("chain_registry_name") - .map(|t| t.as_str().unwrap_or_default().to_owned()); - } - } - - None -} - pub(crate) async fn create_withdraw_msg_as_any( sender: AccountId, receiver: AccountId, @@ -3228,86 +3122,6 @@ pub(crate) async fn create_withdraw_msg_as_any( .map_to_mm(|e| WithdrawError::InternalError(e.to_string())) } -pub async fn get_ibc_transfer_channels( - source_registry_name: String, - destination_registry_name: String, -) -> IBCTransferChannelsResult { - #[derive(Deserialize)] - struct ChainRegistry { - channels: Vec, - } - - #[derive(Deserialize)] - struct ChannelInfo { - channel_id: String, - port_id: String, - } - - #[derive(Deserialize)] - struct IbcChannel { - #[allow(dead_code)] - chain_1: ChannelInfo, - chain_2: ChannelInfo, - ordering: String, - version: String, - tags: Option, - } - - let source_filename = format!("{}-{}.json", source_registry_name, destination_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_2.port_id == *IBC_OUT_SOURCE_PORT); - - let result: Vec = registry_object - .channels - .iter() - .map(|ch| IBCTransferChannel { - channel_id: ch.chain_2.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(); - - if result.is_empty() { - return MmError::err(IBCTransferChannelsRequestError::CouldNotFindChannel( - destination_registry_name, - )); - } - - Ok(IBCTransferChannelsResponse { - ibc_transfer_channels: result, - }) -} - fn parse_expected_sequence_number(e: &str) -> MmResult { if let Some(sequence) = SEQUENCE_PARSER_REGEX.captures(e).and_then(|c| c.get(1)) { let account_sequence = @@ -3375,7 +3189,6 @@ pub mod tendermint_coin_tests { account_prefix: String::from("iaa"), chain_id: String::from("nyancat-9"), gas_price: None, - chain_registry_name: None, ibc_channels: HashMap::new(), } } @@ -3387,7 +3200,6 @@ pub mod tendermint_coin_tests { account_prefix: String::from("iaa"), chain_id: String::from("nyancat-9"), gas_price: None, - chain_registry_name: None, ibc_channels: HashMap::new(), } } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 937db9631b..249e2f57cd 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -23,7 +23,6 @@ use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; -use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, get_enabled_coins::get_enabled_coins, @@ -218,8 +217,6 @@ 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_chains" => handle_mmrpc(ctx, request, ibc_chains).await, - "ibc_transfer_channels" => handle_mmrpc(ctx, request, ibc_transfer_channels).await, "peer_connection_healthcheck" => handle_mmrpc(ctx, request, peer_connection_healthcheck_rpc).await, "withdraw_nft" => handle_mmrpc(ctx, request, withdraw_nft).await, "start_eth_fee_estimator" => handle_mmrpc(ctx, request, start_eth_fee_estimator).await, From a8ce1677b9b6079754c22209548cac1f2e7a8657 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 18 Dec 2024 06:17:11 +0000 Subject: [PATCH 03/14] update channel finding logic Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 7 ++----- mm2src/coins/tendermint/tendermint_coin.rs | 17 ++++++++++------- mm2src/coins/tendermint/tendermint_token.rs | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 16ade17a6b..7a4bd0c35c 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -3016,12 +3016,10 @@ pub enum WithdrawError { }, #[display(fmt = "Signing error {}", _0)] SigningError(String), - #[display(fmt = "Eth transaction type not supported")] + #[display(fmt = "Transaction type not supported")] TxTypeNotSupported, - #[display(fmt = "'chain_registry_name' was not found in coins configuration for '{}'", _0)] - RegistryNameIsMissing(String), #[display( - fmt = "IBC channel could not found for '{}' address. Consider providing it manually with 'ibc_source_channel' in the request.", + fmt = "IBC channel could not found for '{}' address. Provide it manually by including `ibc_source_channel` in the request. See https://ibc.iobscan.io/channels for reference.", _0 )] IBCChannelCouldNotFound(String), @@ -3053,7 +3051,6 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::NoChainIdSet { .. } | WithdrawError::TxTypeNotSupported | WithdrawError::SigningError(_) - | WithdrawError::RegistryNameIsMissing(_) | WithdrawError::IBCChannelCouldNotFound(_) | WithdrawError::MyAddressNotNftOwner { .. } => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 80ee6c3d5d..1f82ad9f64 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -3,10 +3,9 @@ use super::htlc::{ClaimHtlcMsg, ClaimHtlcProto, CreateHtlcMsg, CreateHtlcProto, QueryHtlcResponse, TendermintHtlc, HTLC_STATE_COMPLETED, HTLC_STATE_OPEN, HTLC_STATE_REFUNDED}; use super::ibc::transfer_v1::MsgTransfer; use super::ibc::IBC_GAS_LIMIT_DEFAULT; -use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; +use super::rpc::*; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; -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, @@ -703,12 +702,16 @@ impl TendermintCoin { }))) } - /// TODO - pub(crate) async fn detect_channel_id_for_ibc_transfer( + pub(crate) async fn get_ibc_channel_for_target_address( &self, - to_address: &AccountId, + target_address: &AccountId, ) -> Result> { - todo!() + let id = self + .ibc_channels + .get(target_address.prefix()) + .ok_or(WithdrawError::IBCChannelCouldNotFound(target_address.to_string()))?; + + Ok(format!("channel-{id}")) } #[inline(always)] @@ -2148,7 +2151,7 @@ impl MmCoin for TendermintCoin { let channel_id = if is_ibc_transfer { match &req.ibc_source_channel { Some(_) => req.ibc_source_channel, - None => Some(coin.detect_channel_id_for_ibc_transfer(&to_address).await?), + None => Some(coin.get_ibc_channel_for_target_address(&to_address).await?), } } else { None diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index e5cc90f895..3e564cdf49 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -559,7 +559,7 @@ impl MmCoin for TendermintToken { let channel_id = if is_ibc_transfer { match &req.ibc_source_channel { Some(_) => req.ibc_source_channel, - None => Some(platform.detect_channel_id_for_ibc_transfer(&to_address).await?), + None => Some(platform.get_ibc_channel_for_target_address(&to_address).await?), } } else { None From 963ae4e5d88dad9d68a21650ec171ec0d5682dc2 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 7 Apr 2025 15:58:52 +0300 Subject: [PATCH 04/14] use default `HashMap` if `ibc_channels` isn't provided Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index b8f143c54c..026a5c34f4 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -191,6 +191,7 @@ pub struct TendermintProtocolInfo { pub account_prefix: String, chain_id: String, gas_price: Option, + #[serde(default)] ibc_channels: HashMap, } From a412636d65b4490099cce97079b51b63be01a350 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 7 Apr 2025 16:36:24 +0300 Subject: [PATCH 05/14] support backup IBC clients for recovery when first one isn't available Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 026a5c34f4..bdfac95f38 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -77,7 +77,7 @@ use primitives::hash::H256; use regex::Regex; use rpc::v1::types::Bytes as BytesJson; use serde_json::{self as json, Value as Json}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::convert::{TryFrom, TryInto}; use std::io; use std::num::NonZeroU32; @@ -192,7 +192,7 @@ pub struct TendermintProtocolInfo { chain_id: String, gas_price: Option, #[serde(default)] - ibc_channels: HashMap, + ibc_channels: HashMap>, } #[derive(Clone)] @@ -388,7 +388,7 @@ pub struct TendermintCoinImpl { pub(crate) is_keplr_from_ledger: bool, /// Key represents the account prefix of the target chain and /// the value is the channel ID used for sending transactions. - ibc_channels: HashMap, + ibc_channels: HashMap>, } #[derive(Clone)] @@ -739,6 +739,12 @@ impl TendermintCoin { .get(target_address.prefix()) .ok_or(WithdrawError::IBCChannelCouldNotFound(target_address.to_string()))?; + // TODO: validate channels and pick the healthy one. + let id = id + .iter() + .last() + .ok_or(WithdrawError::IBCChannelCouldNotFound(target_address.to_string()))?; + Ok(format!("channel-{id}")) } From effa2d27103ec75da3bcb50188cbf374be919296 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 8 Apr 2025 11:40:00 +0000 Subject: [PATCH 06/14] implement channel querying Signed-off-by: onur-ozkan --- .../coins/rpc_command/tendermint/staking.rs | 3 +- mm2src/coins/tendermint/tendermint_coin.rs | 93 +++++++++++++++++-- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 190477b7bd..8a83e0e6cc 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -47,7 +47,8 @@ impl From for StakingInfoError { match e { TendermintCoinRpcError::InvalidResponse(e) | TendermintCoinRpcError::PerformError(e) - | TendermintCoinRpcError::RpcClientError(e) => StakingInfoError::Transport(e), + | TendermintCoinRpcError::RpcClientError(e) + | TendermintCoinRpcError::NotFound(e) => StakingInfoError::Transport(e), TendermintCoinRpcError::Prost(e) | TendermintCoinRpcError::InternalError(e) => StakingInfoError::Internal(e), TendermintCoinRpcError::UnexpectedAccountType { .. } => StakingInfoError::Internal( "RPC client got an unexpected error 'TendermintCoinRpcError::UnexpectedAccountType', this isn't normal." diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index bdfac95f38..4d2e0a2256 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -49,6 +49,8 @@ use cosmrs::proto::cosmos::staking::v1beta1::{QueryDelegationRequest, QueryDeleg QueryValidatorsResponse as QueryValidatorsResponseProto}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; +use cosmrs::proto::ibc; +use cosmrs::proto::ibc::core::channel::v1::{QueryChannelRequest, QueryChannelResponse}; use cosmrs::proto::prost::{DecodeError, Message}; use cosmrs::staking::{MsgDelegate, MsgUndelegate, QueryValidatorsResponse, Validator}; use cosmrs::tendermint::block::Height; @@ -100,6 +102,7 @@ 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"; +const ABCI_IBC_CHANNEL_QUERY_PATH: &str = "/ibc.core.channel.v1.Query/Channel"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -452,6 +455,7 @@ pub enum TendermintCoinRpcError { UnexpectedAccountType { prefix: String, }, + NotFound(String), } impl From for TendermintCoinRpcError { @@ -478,7 +482,7 @@ impl From for BalanceError { TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { BalanceError::Transport(e) }, - TendermintCoinRpcError::InternalError(e) => BalanceError::Internal(e), + TendermintCoinRpcError::InternalError(e) | TendermintCoinRpcError::NotFound(e) => BalanceError::Internal(e), TendermintCoinRpcError::UnexpectedAccountType { prefix } => { BalanceError::Internal(format!("Account type '{prefix}' is not supported for HTLCs")) }, @@ -494,7 +498,9 @@ impl From for ValidatePaymentError { TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { ValidatePaymentError::Transport(e) }, - TendermintCoinRpcError::InternalError(e) => ValidatePaymentError::InternalError(e), + TendermintCoinRpcError::InternalError(e) | TendermintCoinRpcError::NotFound(e) => { + ValidatePaymentError::InternalError(e) + }, TendermintCoinRpcError::UnexpectedAccountType { prefix } => { ValidatePaymentError::InvalidParameter(format!("Account type '{prefix}' is not supported for HTLCs")) }, @@ -730,22 +736,56 @@ impl TendermintCoin { }))) } + async fn query_ibc_channel( + &self, + channel_id: u16, + port_id: String, + ) -> MmResult { + let payload = QueryChannelRequest { + channel_id: format!("channel-{channel_id}"), + port_id: port_id.clone(), + } + .encode_to_vec(); + + let request = AbciRequest::new( + Some(ABCI_IBC_CHANNEL_QUERY_PATH.to_string()), + payload, + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ); + + let response = self.rpc_client().await?.perform(request).await?; + let response = QueryChannelResponse::decode(response.response.value.as_slice())?; + + response.channel.ok_or_else(|| { + MmError::new(TendermintCoinRpcError::NotFound(format!( + "No result for channel id: {channel_id}, port: {port_id}." + ))) + }) + } + pub(crate) async fn get_ibc_channel_for_target_address( &self, target_address: &AccountId, ) -> Result> { - let id = self + let channel_ids = self .ibc_channels .get(target_address.prefix()) .ok_or(WithdrawError::IBCChannelCouldNotFound(target_address.to_string()))?; - // TODO: validate channels and pick the healthy one. - let id = id - .iter() - .last() - .ok_or(WithdrawError::IBCChannelCouldNotFound(target_address.to_string()))?; + let coin = self.clone(); + for channel_id in channel_ids { + let channel = coin.query_ibc_channel(*channel_id, "transfer".into()).await?; + // ref: https://github.com/cosmos/ibc-go/blob/7f34724b982581435441e0bb70598c3e3a77f061/proto/ibc/core/channel/v1/channel.proto#L51-L68 + let channel_is_open = |state_id| state_id == 3; + + if channel_is_open(channel.state) { + // TODO: should we also check the counter channel? + return Ok(format!("channel-{channel_id}")); + } + } - Ok(format!("channel-{id}")) + MmError::err(WithdrawError::IBCChannelCouldNotFound(target_address.to_string())) } #[inline(always)] @@ -3302,7 +3342,7 @@ impl MarketCoinOps for TendermintCoin { self.send_raw_tx_bytes(&tx_bytes) } - /// Consider using `seq_safe_raw_tx_bytes` instead. + /// Consider using `seq_safe_send_raw_tx_bytes` instead. /// This is considered as unsafe due to sequence mismatches. fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { // as sanity check @@ -4942,4 +4982,37 @@ pub mod tendermint_coin_tests { assert_eq!(expected_list, actual_list); } + + #[test] + fn test_debugging() { + let nodes = vec![RpcNode::for_test(IRIS_TESTNET_RPC_URL)]; + + let protocol_conf = get_iris_protocol(); + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + 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 _ = block_on(coin.query_ibc_channel(0, "transfer".into())); + } } From 0bc8220be1e88675171cb7f503184c1fa7411050 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 9 Apr 2025 11:25:18 +0300 Subject: [PATCH 07/14] finish test coverage Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 8 +++-- mm2src/coins/tendermint/tendermint_coin.rs | 35 +++++++++++++++------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 8937c04e50..a5fd5b40b4 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -3181,9 +3181,11 @@ pub enum WithdrawError { TxTypeNotSupported, #[display( fmt = "IBC channel could not found for '{}' address. Provide it manually by including `ibc_source_channel` in the request. See https://ibc.iobscan.io/channels for reference.", - _0 + target_address )] - IBCChannelCouldNotFound(String), + IBCChannelCouldNotFound { + target_address: String, + }, } impl HttpStatusCode for WithdrawError { @@ -3212,7 +3214,7 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::NoChainIdSet { .. } | WithdrawError::TxTypeNotSupported | WithdrawError::SigningError(_) - | WithdrawError::IBCChannelCouldNotFound(_) + | WithdrawError::IBCChannelCouldNotFound { .. } | WithdrawError::MyAddressNotNftOwner { .. } => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 4d2e0a2256..165ac8493a 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -28,7 +28,7 @@ use bip32::DerivationPath; use bitcrypto::{dhash160, sha256}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; use common::executor::{AbortedError, Timer}; -use common::log::{debug, warn}; +use common::log::{debug, info, warn}; use common::{get_utc_timestamp, now_sec, Future01CompatExt, PagingOptions, DEX_FEE_ADDR_PUBKEY}; use compatible_time::Duration; use cosmrs::bank::{MsgMultiSend, MsgSend, MultiSendIo}; @@ -768,10 +768,12 @@ impl TendermintCoin { &self, target_address: &AccountId, ) -> Result> { - let channel_ids = self - .ibc_channels - .get(target_address.prefix()) - .ok_or(WithdrawError::IBCChannelCouldNotFound(target_address.to_string()))?; + let channel_ids = + self.ibc_channels + .get(target_address.prefix()) + .ok_or_else(|| WithdrawError::IBCChannelCouldNotFound { + target_address: target_address.to_string(), + })?; let coin = self.clone(); for channel_id in channel_ids { @@ -780,12 +782,15 @@ impl TendermintCoin { let channel_is_open = |state_id| state_id == 3; if channel_is_open(channel.state) { - // TODO: should we also check the counter channel? return Ok(format!("channel-{channel_id}")); + } else { + info!("Skipping channel-{channel_id} as it is not healthy"); } } - MmError::err(WithdrawError::IBCChannelCouldNotFound(target_address.to_string())) + MmError::err(WithdrawError::IBCChannelCouldNotFound { + target_address: target_address.to_string(), + }) } #[inline(always)] @@ -3883,7 +3888,7 @@ pub mod tendermint_coin_tests { use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse}; use crypto::privkey::key_pair_from_seed; use mocktopus::mocking::{MockResult, Mockable}; - use std::{mem::discriminant, num::NonZeroUsize}; + use std::{iter::FromIterator, mem::discriminant, num::NonZeroUsize}; pub const IRIS_TESTNET_HTLC_PAIR1_SEED: &str = "iris test seed"; // pub const IRIS_TESTNET_HTLC_PAIR1_PUB_KEY: &[u8] = &[ @@ -3934,13 +3939,16 @@ pub mod tendermint_coin_tests { } fn get_iris_protocol() -> TendermintProtocolInfo { + let mut ibc_channels = HashMap::new(); + ibc_channels.insert("cosmos".into(), HashSet::from_iter([0])); + TendermintProtocolInfo { decimals: 6, denom: String::from("unyan"), account_prefix: String::from("iaa"), chain_id: String::from("nyancat-9"), gas_price: None, - ibc_channels: HashMap::new(), + ibc_channels, } } @@ -4984,7 +4992,7 @@ pub mod tendermint_coin_tests { } #[test] - fn test_debugging() { + fn test_get_ibc_channel_for_target_address() { let nodes = vec![RpcNode::for_test(IRIS_TESTNET_RPC_URL)]; let protocol_conf = get_iris_protocol(); @@ -5013,6 +5021,11 @@ pub mod tendermint_coin_tests { )) .unwrap(); - let _ = block_on(coin.query_ibc_channel(0, "transfer".into())); + let expected_channel = "channel-0"; + + let addr = AccountId::from_str("cosmos1aghdjgt5gzntzqgdxdzhjfry90upmtfsy2wuwp").unwrap(); + let actual_channel = block_on(coin.get_ibc_channel_for_target_address(&addr)).unwrap(); + + assert_eq!(expected_channel, actual_channel); } } From a85b082280477c7ce767aacd11bac71cc748ff38 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 9 Apr 2025 11:27:07 +0300 Subject: [PATCH 08/14] better error message Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index a5fd5b40b4..3a4bd362a1 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -3180,7 +3180,7 @@ pub enum WithdrawError { #[display(fmt = "Transaction type not supported")] TxTypeNotSupported, #[display( - fmt = "IBC channel could not found for '{}' address. Provide it manually by including `ibc_source_channel` in the request. See https://ibc.iobscan.io/channels for reference.", + fmt = "IBC channel could not found from coins file for '{}' address. Provide it manually by including `ibc_source_channel` in the request. See https://ibc.iobscan.io/channels for reference.", target_address )] IBCChannelCouldNotFound { From 9cd189de355dd47aedea4a75ea70afb0237eff29 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 9 Apr 2025 11:50:56 +0300 Subject: [PATCH 09/14] handle IBC channel IDs stricter Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 3 +- mm2src/coins/rpc_command/tendermint/ibc.rs | 12 +++++ mm2src/coins/rpc_command/tendermint/mod.rs | 1 + mm2src/coins/tendermint/tendermint_coin.rs | 47 +++++++++++-------- mm2src/coins/tendermint/tendermint_token.rs | 4 +- .../tests/docker_tests/tendermint_tests.rs | 4 +- mm2src/mm2_test_helpers/src/for_tests.rs | 2 +- 7 files changed, 47 insertions(+), 26 deletions(-) create mode 100644 mm2src/coins/rpc_command/tendermint/ibc.rs diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 3a4bd362a1..2798bb77d0 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -73,6 +73,7 @@ use mm2_rpc::data::legacy::{EnabledCoin, GetEnabledResponse, Mm2RpcResult}; use mocktopus::macros::*; use parking_lot::Mutex as PaMutex; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; +use rpc_command::tendermint::ibc::ChannelId; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::{self as json, Value as Json}; use std::array::TryFromSliceError; @@ -2220,7 +2221,7 @@ pub struct WithdrawRequest { fee: Option, memo: Option, /// Tendermint specific field used for manually providing the IBC channel IDs. - ibc_source_channel: Option, + ibc_source_channel: Option, /// Currently, this flag is used by ETH/ERC20 coins activated with MetaMask **only**. #[cfg(target_arch = "wasm32")] #[serde(default)] diff --git a/mm2src/coins/rpc_command/tendermint/ibc.rs b/mm2src/coins/rpc_command/tendermint/ibc.rs new file mode 100644 index 0000000000..48df82c44a --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/ibc.rs @@ -0,0 +1,12 @@ +use std::fmt; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, Hash)] +pub struct ChannelId(u16); + +impl fmt::Display for ChannelId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "channel-{}", self.0) } +} + +impl ChannelId { + pub fn new(id: u16) -> Self { Self(id) } +} diff --git a/mm2src/coins/rpc_command/tendermint/mod.rs b/mm2src/coins/rpc_command/tendermint/mod.rs index ba07cfb75c..d0f2c82685 100644 --- a/mm2src/coins/rpc_command/tendermint/mod.rs +++ b/mm2src/coins/rpc_command/tendermint/mod.rs @@ -1 +1,2 @@ +pub mod ibc; pub mod staking; diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 165ac8493a..748552913f 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -6,6 +6,7 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::rpc::*; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; +use crate::rpc_command::tendermint::ibc::ChannelId; use crate::rpc_command::tendermint::staking::{ClaimRewardsPayload, Delegation, DelegationPayload, DelegationsQueryResponse, Undelegation, UndelegationEntry, UndelegationsQueryResponse, ValidatorStatus}; @@ -195,7 +196,7 @@ pub struct TendermintProtocolInfo { chain_id: String, gas_price: Option, #[serde(default)] - ibc_channels: HashMap>, + ibc_channels: HashMap>, } #[derive(Clone)] @@ -391,7 +392,7 @@ pub struct TendermintCoinImpl { pub(crate) is_keplr_from_ledger: bool, /// Key represents the account prefix of the target chain and /// the value is the channel ID used for sending transactions. - ibc_channels: HashMap>, + ibc_channels: HashMap>, } #[derive(Clone)] @@ -736,13 +737,15 @@ impl TendermintCoin { }))) } + /// Finds the IBC channel by querying the given channel ID and port ID + /// and returns its information. async fn query_ibc_channel( &self, - channel_id: u16, + channel_id: ChannelId, port_id: String, ) -> MmResult { let payload = QueryChannelRequest { - channel_id: format!("channel-{channel_id}"), + channel_id: channel_id.to_string(), port_id: port_id.clone(), } .encode_to_vec(); @@ -764,10 +767,14 @@ impl TendermintCoin { }) } - pub(crate) async fn get_ibc_channel_for_target_address( + /// Returns a **healthy** IBC channel ID for the given target address. + pub(crate) async fn get_healthy_ibc_channel_for_address( &self, target_address: &AccountId, - ) -> Result> { + ) -> Result> { + // ref: https://github.com/cosmos/ibc-go/blob/7f34724b982581435441e0bb70598c3e3a77f061/proto/ibc/core/channel/v1/channel.proto#L51-L68 + const STATE_OPEN: i32 = 3; + let channel_ids = self.ibc_channels .get(target_address.prefix()) @@ -778,13 +785,12 @@ impl TendermintCoin { let coin = self.clone(); for channel_id in channel_ids { let channel = coin.query_ibc_channel(*channel_id, "transfer".into()).await?; - // ref: https://github.com/cosmos/ibc-go/blob/7f34724b982581435441e0bb70598c3e3a77f061/proto/ibc/core/channel/v1/channel.proto#L51-L68 - let channel_is_open = |state_id| state_id == 3; + let channel_is_open = |state_id| state_id == STATE_OPEN; if channel_is_open(channel.state) { - return Ok(format!("channel-{channel_id}")); + return Ok(*channel_id); } else { - info!("Skipping channel-{channel_id} as it is not healthy"); + info!("Skipping {channel_id} as it is not healthy"); } } @@ -3030,7 +3036,7 @@ impl MmCoin for TendermintCoin { let channel_id = if is_ibc_transfer { match &req.ibc_source_channel { Some(_) => req.ibc_source_channel, - None => Some(coin.get_ibc_channel_for_target_address(&to_address).await?), + None => Some(coin.get_healthy_ibc_channel_for_address(&to_address).await?), } } else { None @@ -3041,7 +3047,7 @@ impl MmCoin for TendermintCoin { to_address.clone(), &coin.denom, amount_denom, - channel_id.clone(), + channel_id, ) .await?; @@ -3836,10 +3842,10 @@ pub(crate) async fn create_withdraw_msg_as_any( receiver: AccountId, denom: &Denom, amount: u64, - ibc_source_channel: Option, + ibc_source_channel: Option, ) -> Result> { if let Some(channel_id) = ibc_source_channel { - MsgTransfer::new_with_default_timeout(channel_id, sender, receiver, Coin { + MsgTransfer::new_with_default_timeout(channel_id.to_string(), sender, receiver, Coin { denom: denom.clone(), amount: amount.into(), }) @@ -3940,7 +3946,7 @@ pub mod tendermint_coin_tests { fn get_iris_protocol() -> TendermintProtocolInfo { let mut ibc_channels = HashMap::new(); - ibc_channels.insert("cosmos".into(), HashSet::from_iter([0])); + ibc_channels.insert("cosmos".into(), HashSet::from_iter([ChannelId::new(0)])); TendermintProtocolInfo { decimals: 6, @@ -4994,11 +5000,8 @@ pub mod tendermint_coin_tests { #[test] fn test_get_ibc_channel_for_target_address() { let nodes = vec![RpcNode::for_test(IRIS_TESTNET_RPC_URL)]; - let protocol_conf = get_iris_protocol(); - let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); - let conf = TendermintConf { avg_blocktime: AVG_BLOCKTIME, derivation_path: None, @@ -5021,11 +5024,15 @@ pub mod tendermint_coin_tests { )) .unwrap(); - let expected_channel = "channel-0"; + let expected_channel = ChannelId::new(0); + let expected_channel_str = "channel-0"; let addr = AccountId::from_str("cosmos1aghdjgt5gzntzqgdxdzhjfry90upmtfsy2wuwp").unwrap(); - let actual_channel = block_on(coin.get_ibc_channel_for_target_address(&addr)).unwrap(); + + let actual_channel = block_on(coin.get_healthy_ibc_channel_for_address(&addr)).unwrap(); + let actual_channel_str = actual_channel.to_string(); assert_eq!(expected_channel, actual_channel); + assert_eq!(expected_channel_str, actual_channel_str); } } diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 24ce49a1ac..f03e18654a 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -424,7 +424,7 @@ impl MmCoin for TendermintToken { let channel_id = if is_ibc_transfer { match &req.ibc_source_channel { Some(_) => req.ibc_source_channel, - None => Some(platform.get_ibc_channel_for_target_address(&to_address).await?), + None => Some(platform.get_healthy_ibc_channel_for_address(&to_address).await?), } } else { None @@ -435,7 +435,7 @@ impl MmCoin for TendermintToken { to_address.clone(), &token.denom, amount_denom, - channel_id.clone(), + channel_id, ) .await?; diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index 45e3b4a03e..f29a7ef34b 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -329,7 +329,7 @@ fn test_custom_gas_limit_on_tendermint_withdraw() { fn test_tendermint_ibc_withdraw() { let _lock = SEQUENCE_LOCK.lock().unwrap(); // visit `{swagger_address}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels - const IBC_SOURCE_CHANNEL: &str = "channel-3"; + const IBC_SOURCE_CHANNEL: u16 = 3; const IBC_TARGET_ADDRESS: &str = "cosmos1r5v5srda7xfth3hn2s26txvrcrntldjumt8mhl"; const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; @@ -376,7 +376,7 @@ fn test_tendermint_ibc_withdraw() { fn test_tendermint_ibc_withdraw_hd() { let _lock = SEQUENCE_LOCK.lock().unwrap(); // visit `{swagger_address}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels - const IBC_SOURCE_CHANNEL: &str = "channel-3"; + const IBC_SOURCE_CHANNEL: u16 = 3; const IBC_TARGET_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; const MY_ADDRESS: &str = "cosmos134h9tv7866jcuw708w5w76lcfx7s3x2ysyalxy"; diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 621e76a597..b6e67525dc 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -2729,7 +2729,7 @@ pub async fn withdraw_v1( pub async fn ibc_withdraw( mm: &MarketMakerIt, - source_channel: &str, + source_channel: u16, coin: &str, to: &str, amount: &str, From cad4e60c98bc67c88a501bcde5fa000a571b9178 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 10 Apr 2025 05:04:51 +0000 Subject: [PATCH 10/14] more fee for IBC withdraws Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 748552913f..40834e577a 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -3085,6 +3085,8 @@ impl MmCoin for TendermintCoin { // calculates a higher fee than us, the withdrawal might fail), we use three times // the actual fee. fee_amount_u64 * 3 + } else if is_ibc_transfer { + fee_amount_u64 * 3 / 2 } else { fee_amount_u64 }; From c9858fda917c81e9d96a83f469ded778c7bd1bc3 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 14 Apr 2025 14:25:27 +0300 Subject: [PATCH 11/14] accept only one channel id on per network Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 8 +++++ mm2src/coins/tendermint/tendermint_coin.rs | 39 +++++++++++----------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 2798bb77d0..2c350ac3ca 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -3187,6 +3187,13 @@ pub enum WithdrawError { IBCChannelCouldNotFound { target_address: String, }, + #[display( + fmt = "IBC channel '{}' is not healthy. Provide a healthy one by including `ibc_source_channel` in the request. See https://ibc.iobscan.io/channels for reference.", + channel_id + )] + IBCChannelNotHealthy { + channel_id: ChannelId, + }, } impl HttpStatusCode for WithdrawError { @@ -3216,6 +3223,7 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::TxTypeNotSupported | WithdrawError::SigningError(_) | WithdrawError::IBCChannelCouldNotFound { .. } + | WithdrawError::IBCChannelNotHealthy { .. } | WithdrawError::MyAddressNotNftOwner { .. } => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 40834e577a..823b78865e 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -29,7 +29,7 @@ use bip32::DerivationPath; use bitcrypto::{dhash160, sha256}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; use common::executor::{AbortedError, Timer}; -use common::log::{debug, info, warn}; +use common::log::{debug, warn}; use common::{get_utc_timestamp, now_sec, Future01CompatExt, PagingOptions, DEX_FEE_ADDR_PUBKEY}; use compatible_time::Duration; use cosmrs::bank::{MsgMultiSend, MsgSend, MultiSendIo}; @@ -80,7 +80,7 @@ use primitives::hash::H256; use regex::Regex; use rpc::v1::types::Bytes as BytesJson; use serde_json::{self as json, Value as Json}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::io; use std::num::NonZeroU32; @@ -196,7 +196,7 @@ pub struct TendermintProtocolInfo { chain_id: String, gas_price: Option, #[serde(default)] - ibc_channels: HashMap>, + ibc_channels: HashMap, } #[derive(Clone)] @@ -392,7 +392,7 @@ pub struct TendermintCoinImpl { pub(crate) is_keplr_from_ledger: bool, /// Key represents the account prefix of the target chain and /// the value is the channel ID used for sending transactions. - ibc_channels: HashMap>, + ibc_channels: HashMap, } #[derive(Clone)] @@ -775,28 +775,27 @@ impl TendermintCoin { // ref: https://github.com/cosmos/ibc-go/blob/7f34724b982581435441e0bb70598c3e3a77f061/proto/ibc/core/channel/v1/channel.proto#L51-L68 const STATE_OPEN: i32 = 3; - let channel_ids = - self.ibc_channels + let channel_id = + *self + .ibc_channels .get(target_address.prefix()) .ok_or_else(|| WithdrawError::IBCChannelCouldNotFound { target_address: target_address.to_string(), })?; - let coin = self.clone(); - for channel_id in channel_ids { - let channel = coin.query_ibc_channel(*channel_id, "transfer".into()).await?; - let channel_is_open = |state_id| state_id == STATE_OPEN; + let channel = self.query_ibc_channel(channel_id, "transfer".into()).await?; + let channel_is_open = |state_id| state_id == STATE_OPEN; - if channel_is_open(channel.state) { - return Ok(*channel_id); - } else { - info!("Skipping {channel_id} as it is not healthy"); - } + // TODO: Extend the validation logic to also include: + // + // - Checking the time of the last update on the channel + // - Verifying the total amount transferred since the channel was created + // - Check the channel creation time + if !channel_is_open(channel.state) { + return MmError::err(WithdrawError::IBCChannelNotHealthy { channel_id }); } - MmError::err(WithdrawError::IBCChannelCouldNotFound { - target_address: target_address.to_string(), - }) + Ok(channel_id) } #[inline(always)] @@ -3896,7 +3895,7 @@ pub mod tendermint_coin_tests { use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse}; use crypto::privkey::key_pair_from_seed; use mocktopus::mocking::{MockResult, Mockable}; - use std::{iter::FromIterator, mem::discriminant, num::NonZeroUsize}; + 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] = &[ @@ -3948,7 +3947,7 @@ pub mod tendermint_coin_tests { fn get_iris_protocol() -> TendermintProtocolInfo { let mut ibc_channels = HashMap::new(); - ibc_channels.insert("cosmos".into(), HashSet::from_iter([ChannelId::new(0)])); + ibc_channels.insert("cosmos".into(), ChannelId::new(0)); TendermintProtocolInfo { decimals: 6, From d4ccb83b76fed2890fecf98ecbf0c2a0682db0db Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 15 Apr 2025 19:32:19 +0300 Subject: [PATCH 12/14] address nits Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/tendermint_coin.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 823b78865e..813de460a1 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -480,10 +480,10 @@ impl From for BalanceError { match err { TendermintCoinRpcError::InvalidResponse(e) => BalanceError::InvalidResponse(e), TendermintCoinRpcError::Prost(e) => BalanceError::InvalidResponse(e), - TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { - BalanceError::Transport(e) - }, - TendermintCoinRpcError::InternalError(e) | TendermintCoinRpcError::NotFound(e) => BalanceError::Internal(e), + TendermintCoinRpcError::PerformError(e) + | TendermintCoinRpcError::RpcClientError(e) + | TendermintCoinRpcError::NotFound(e) => BalanceError::Transport(e), + TendermintCoinRpcError::InternalError(e) => BalanceError::Internal(e), TendermintCoinRpcError::UnexpectedAccountType { prefix } => { BalanceError::Internal(format!("Account type '{prefix}' is not supported for HTLCs")) }, @@ -742,11 +742,11 @@ impl TendermintCoin { async fn query_ibc_channel( &self, channel_id: ChannelId, - port_id: String, + port_id: &str, ) -> MmResult { let payload = QueryChannelRequest { channel_id: channel_id.to_string(), - port_id: port_id.clone(), + port_id: port_id.to_string(), } .encode_to_vec(); @@ -783,7 +783,7 @@ impl TendermintCoin { target_address: target_address.to_string(), })?; - let channel = self.query_ibc_channel(channel_id, "transfer".into()).await?; + let channel = self.query_ibc_channel(channel_id, "transfer").await?; let channel_is_open = |state_id| state_id == STATE_OPEN; // TODO: Extend the validation logic to also include: From 28576910a3a2900492722b18d31e5807c8c4397b Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 16 Apr 2025 09:14:38 +0300 Subject: [PATCH 13/14] nits Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 2 +- mm2src/coins/tendermint/tendermint_coin.rs | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 2c350ac3ca..5549903df8 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -3181,7 +3181,7 @@ pub enum WithdrawError { #[display(fmt = "Transaction type not supported")] TxTypeNotSupported, #[display( - fmt = "IBC channel could not found from coins file for '{}' address. Provide it manually by including `ibc_source_channel` in the request. See https://ibc.iobscan.io/channels for reference.", + fmt = "IBC channel could not be found in coins file for '{}' address. Provide it manually by including `ibc_source_channel` in the request. See https://ibc.iobscan.io/channels for reference.", target_address )] IBCChannelCouldNotFound { diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 813de460a1..d529af2298 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -496,12 +496,10 @@ impl From for ValidatePaymentError { match err { TendermintCoinRpcError::InvalidResponse(e) => ValidatePaymentError::InvalidRpcResponse(e), TendermintCoinRpcError::Prost(e) => ValidatePaymentError::InvalidRpcResponse(e), - TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { - ValidatePaymentError::Transport(e) - }, - TendermintCoinRpcError::InternalError(e) | TendermintCoinRpcError::NotFound(e) => { - ValidatePaymentError::InternalError(e) - }, + TendermintCoinRpcError::PerformError(e) + | TendermintCoinRpcError::RpcClientError(e) + | TendermintCoinRpcError::NotFound(e) => ValidatePaymentError::Transport(e), + TendermintCoinRpcError::InternalError(e) => ValidatePaymentError::InternalError(e), TendermintCoinRpcError::UnexpectedAccountType { prefix } => { ValidatePaymentError::InvalidParameter(format!("Account type '{prefix}' is not supported for HTLCs")) }, @@ -784,14 +782,13 @@ impl TendermintCoin { })?; let channel = self.query_ibc_channel(channel_id, "transfer").await?; - let channel_is_open = |state_id| state_id == STATE_OPEN; // TODO: Extend the validation logic to also include: // // - Checking the time of the last update on the channel // - Verifying the total amount transferred since the channel was created // - Check the channel creation time - if !channel_is_open(channel.state) { + if channel.state != STATE_OPEN { return MmError::err(WithdrawError::IBCChannelNotHealthy { channel_id }); } From b2add2fa567758ff3aa31a3be89b783ceef64beb Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Fri, 18 Apr 2025 18:49:07 +0300 Subject: [PATCH 14/14] remove iobscan ref link Signed-off-by: onur-ozkan --- mm2src/coins/lp_coins.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 5549903df8..05872037a5 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -3181,14 +3181,14 @@ pub enum WithdrawError { #[display(fmt = "Transaction type not supported")] TxTypeNotSupported, #[display( - fmt = "IBC channel could not be found in coins file for '{}' address. Provide it manually by including `ibc_source_channel` in the request. See https://ibc.iobscan.io/channels for reference.", + fmt = "IBC channel could not be found in coins file for '{}' address. Provide it manually by including `ibc_source_channel` in the request.", target_address )] IBCChannelCouldNotFound { target_address: String, }, #[display( - fmt = "IBC channel '{}' is not healthy. Provide a healthy one by including `ibc_source_channel` in the request. See https://ibc.iobscan.io/channels for reference.", + fmt = "IBC channel '{}' is not healthy. Provide a healthy one manually by including `ibc_source_channel` in the request.", channel_id )] IBCChannelNotHealthy {