diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index fd6b20c80b..6ba9b7ac69 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -242,8 +242,9 @@ use rpc_command::{get_new_address::{GetNewAddressTaskManager, GetNewAddressTaskM init_withdraw::{WithdrawTaskManager, WithdrawTaskManagerShared}}; pub mod tendermint; -use tendermint::{CosmosTransaction, CustomTendermintMsgType, TendermintCoin, TendermintFeeDetails, - TendermintProtocolInfo, TendermintToken, TendermintTokenProtocolInfo}; +use tendermint::htlc::CustomTendermintMsgType; +use tendermint::{CosmosTransaction, TendermintCoin, TendermintFeeDetails, TendermintProtocolInfo, TendermintToken, + TendermintTokenProtocolInfo}; #[doc(hidden)] #[allow(unused_variables)] diff --git a/mm2src/coins/tendermint/iris/ethermint_account.rs b/mm2src/coins/tendermint/ethermint_account.rs similarity index 100% rename from mm2src/coins/tendermint/iris/ethermint_account.rs rename to mm2src/coins/tendermint/ethermint_account.rs diff --git a/mm2src/coins/tendermint/htlc/iris/htlc.rs b/mm2src/coins/tendermint/htlc/iris/htlc.rs new file mode 100644 index 0000000000..09561a4048 --- /dev/null +++ b/mm2src/coins/tendermint/htlc/iris/htlc.rs @@ -0,0 +1,146 @@ +use super::htlc_proto::{IrisClaimHtlcProto, IrisCreateHtlcProto}; + +use cosmrs::proto::traits::TypeUrl; +use cosmrs::{tx::Msg, AccountId, Coin, ErrorReport}; +use std::convert::TryFrom; + +pub(crate) const IRIS_CREATE_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgCreateHTLC"; +pub(crate) const IRIS_CLAIM_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgClaimHTLC"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct IrisCreateHtlcMsg { + /// Sender's address. + pub(crate) to: AccountId, + + /// Recipient's address. + pub(crate) sender: AccountId, + + /// The claim receiving address on the other chain. + pub(crate) receiver_on_other_chain: String, + + /// The counterparty creator address on the other chain. + pub(crate) sender_on_other_chain: String, + + /// Amount to send. + pub(crate) amount: Vec, + + /// The sha256 hash generated from secret and timestamp. + pub(crate) hash_lock: String, + + /// The number of blocks to wait before the asset may be returned to. + pub(crate) time_lock: u64, + + /// The timestamp in seconds for generating hash lock if provided. + pub(crate) timestamp: u64, + + /// Whether it is an HTLT transaction. + pub(crate) transfer: bool, +} + +impl Msg for IrisCreateHtlcMsg { + type Proto = IrisCreateHtlcProto; +} + +impl TryFrom for IrisCreateHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: IrisCreateHtlcProto) -> Result { + IrisCreateHtlcMsg::try_from(&proto) + } +} + +impl TryFrom<&IrisCreateHtlcProto> for IrisCreateHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: &IrisCreateHtlcProto) -> Result { + Ok(IrisCreateHtlcMsg { + sender: proto.sender.parse()?, + to: proto.to.parse()?, + amount: proto.amount.iter().map(TryFrom::try_from).collect::>()?, + receiver_on_other_chain: proto.receiver_on_other_chain.clone(), + sender_on_other_chain: proto.sender_on_other_chain.clone(), + hash_lock: proto.hash_lock.clone(), + timestamp: proto.timestamp, + time_lock: proto.time_lock, + transfer: proto.transfer, + }) + } +} + +impl From for IrisCreateHtlcProto { + fn from(t: IrisCreateHtlcMsg) -> IrisCreateHtlcProto { IrisCreateHtlcProto::from(&t) } +} + +impl From<&IrisCreateHtlcMsg> for IrisCreateHtlcProto { + fn from(msg: &IrisCreateHtlcMsg) -> IrisCreateHtlcProto { + IrisCreateHtlcProto { + sender: msg.sender.to_string(), + to: msg.to.to_string(), + amount: msg.amount.iter().map(Into::into).collect(), + receiver_on_other_chain: msg.receiver_on_other_chain.clone(), + sender_on_other_chain: msg.sender_on_other_chain.clone(), + hash_lock: msg.hash_lock.clone(), + timestamp: msg.timestamp, + time_lock: msg.time_lock, + transfer: msg.transfer, + } + } +} + +impl TypeUrl for IrisCreateHtlcProto { + const TYPE_URL: &'static str = IRIS_CREATE_HTLC_TYPE_URL; +} + +#[derive(Clone)] +pub(crate) struct IrisClaimHtlcMsg { + /// Sender's address. + pub(crate) sender: AccountId, + + /// Generated HTLC ID + pub(crate) id: String, + + /// Secret that has been used for generating hash_lock + pub(crate) secret: String, +} + +impl Msg for IrisClaimHtlcMsg { + type Proto = IrisClaimHtlcProto; +} + +impl TryFrom for IrisClaimHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: IrisClaimHtlcProto) -> Result { + IrisClaimHtlcMsg::try_from(&proto) + } +} + +impl TryFrom<&IrisClaimHtlcProto> for IrisClaimHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: &IrisClaimHtlcProto) -> Result { + Ok(IrisClaimHtlcMsg { + sender: proto.sender.parse()?, + id: proto.id.clone(), + secret: proto.secret.clone(), + }) + } +} + +impl From for IrisClaimHtlcProto { + fn from(coin: IrisClaimHtlcMsg) -> IrisClaimHtlcProto { IrisClaimHtlcProto::from(&coin) } +} + +impl From<&IrisClaimHtlcMsg> for IrisClaimHtlcProto { + fn from(msg: &IrisClaimHtlcMsg) -> IrisClaimHtlcProto { + IrisClaimHtlcProto { + sender: msg.sender.to_string(), + id: msg.id.clone(), + secret: msg.secret.clone(), + } + } +} + +impl TypeUrl for IrisClaimHtlcProto { + const TYPE_URL: &'static str = IRIS_CLAIM_HTLC_TYPE_URL; +} diff --git a/mm2src/coins/tendermint/iris/htlc_proto.rs b/mm2src/coins/tendermint/htlc/iris/htlc_proto.rs similarity index 83% rename from mm2src/coins/tendermint/iris/htlc_proto.rs rename to mm2src/coins/tendermint/htlc/iris/htlc_proto.rs index 7bf9b5281b..922da3eebc 100644 --- a/mm2src/coins/tendermint/iris/htlc_proto.rs +++ b/mm2src/coins/tendermint/htlc/iris/htlc_proto.rs @@ -1,5 +1,7 @@ +use crate::tendermint::htlc::HtlcState; + #[derive(prost::Message)] -pub(crate) struct CreateHtlcProtoRep { +pub(crate) struct IrisCreateHtlcProto { #[prost(string, tag = "1")] pub(crate) sender: prost::alloc::string::String, #[prost(string, tag = "2")] @@ -21,7 +23,7 @@ pub(crate) struct CreateHtlcProtoRep { } #[derive(prost::Message)] -pub(crate) struct ClaimHtlcProtoRep { +pub(crate) struct IrisClaimHtlcProto { #[prost(string, tag = "1")] pub(crate) sender: prost::alloc::string::String, #[prost(string, tag = "2")] @@ -31,21 +33,7 @@ pub(crate) struct ClaimHtlcProtoRep { } #[derive(prost::Message)] -pub(crate) struct QueryHtlcRequestProto { - #[prost(string, tag = "1")] - pub(crate) id: prost::alloc::string::String, -} - -#[derive(prost::Enumeration, Debug)] -#[repr(i32)] -pub enum HtlcState { - Open = 0, - Completed = 1, - Refunded = 2, -} - -#[derive(prost::Message)] -pub struct HtlcProto { +pub struct IrisHtlcProto { #[prost(string, tag = "1")] pub(crate) id: prost::alloc::string::String, #[prost(string, tag = "2")] @@ -75,7 +63,7 @@ pub struct HtlcProto { } #[derive(prost::Message)] -pub(crate) struct QueryHtlcResponseProto { +pub(crate) struct IrisQueryHtlcResponseProto { #[prost(message, tag = "1")] - pub(crate) htlc: Option, + pub(crate) htlc: Option, } diff --git a/mm2src/coins/tendermint/htlc/iris/mod.rs b/mm2src/coins/tendermint/htlc/iris/mod.rs new file mode 100644 index 0000000000..7e3fe6e382 --- /dev/null +++ b/mm2src/coins/tendermint/htlc/iris/mod.rs @@ -0,0 +1,24 @@ +//! IRIS HTLC implementation in Rust on top of Cosmos SDK(cosmrs) for komodo-defi-framework. +//! +//! This module includes HTLC creating & claiming representation structstures +//! and their trait implementations. +//! +//! ** Acquiring testnet assets ** +//! +//! Since there is no sdk exists for Rust on Iris Network, we should +//! either implement some of the Iris Network funcionality on Rust or +//! simply use their unit tests. +//! +//! Because we had limited time for the HTLC implementation, for now +//! we can use their unit tests in order to acquire IBC assets. +//! For that, clone https://github.com/onur-ozkan/irishub-sdk-js repository and check +//! dummy.test.ts file(change the asset, amount, target address if needed) +//! and then run the following commands: +//! - yarn +//! - npm run test +//! +//! If the sender address doesn't have enough nyan tokens to complete unit tests, +//! check this page https://www.irisnet.org/docs/get-started/testnet.html#faucet + +pub(crate) mod htlc; +pub(crate) mod htlc_proto; diff --git a/mm2src/coins/tendermint/htlc/mod.rs b/mm2src/coins/tendermint/htlc/mod.rs new file mode 100644 index 0000000000..35a093b1d3 --- /dev/null +++ b/mm2src/coins/tendermint/htlc/mod.rs @@ -0,0 +1,303 @@ +mod iris; +mod nucleus; + +use std::{convert::TryFrom, str::FromStr}; + +use cosmrs::{tx::Msg, AccountId, Any, Coin, ErrorReport}; +use iris::htlc::{IrisClaimHtlcMsg, IrisCreateHtlcMsg}; +use nucleus::htlc::{NucleusClaimHtlcMsg, NucleusCreateHtlcMsg}; + +use iris::htlc_proto::{IrisClaimHtlcProto, IrisCreateHtlcProto, IrisQueryHtlcResponseProto}; +use nucleus::htlc_proto::{NucleusClaimHtlcProto, NucleusCreateHtlcProto, NucleusQueryHtlcResponseProto}; +use prost::{DecodeError, Message}; +use std::io; + +/// Defines an open state. +pub(crate) const HTLC_STATE_OPEN: i32 = 0; + +/// Defines a completed state. +pub(crate) const HTLC_STATE_COMPLETED: i32 = 1; + +/// Defines a refunded state. +pub(crate) const HTLC_STATE_REFUNDED: i32 = 2; + +/// Indicates whether this is an IRIS or Nucleus HTLC. +#[derive(Copy, Clone)] +pub(crate) enum HtlcType { + Nucleus, + Iris, +} + +impl FromStr for HtlcType { + type Err = io::Error; + + fn from_str(s: &str) -> Result { + match s { + "iaa" => Ok(HtlcType::Iris), + "nuc" => Ok(HtlcType::Nucleus), + unsupported => Err(io::Error::new( + io::ErrorKind::Unsupported, + format!("Account type '{unsupported}' is not supported for HTLCs"), + )), + } + } +} + +impl HtlcType { + /// Returns the ABCI endpoint path for querying HTLCs. + pub(crate) fn get_htlc_abci_query_path(&self) -> String { + const NUCLEUS_PATH: &str = "/nucleus.htlc.Query/HTLC"; + const IRIS_PATH: &str = "/irismod.htlc.Query/HTLC"; + + match self { + Self::Nucleus => NUCLEUS_PATH.to_owned(), + Self::Iris => IRIS_PATH.to_owned(), + } + } +} + +/// Custom Tendermint message types specific to certain Cosmos chains and may not be available on all chains. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum CustomTendermintMsgType { + /// Create HTLC as sender. + SendHtlcAmount, + /// Claim HTLC as reciever. + ClaimHtlcAmount, + /// Claim HTLC for reciever. + SignClaimHtlc, +} + +/// Defines the state of an HTLC. +#[derive(prost::Enumeration, Debug)] +#[repr(i32)] +pub enum HtlcState { + /// Open state. + Open = HTLC_STATE_OPEN, + /// Completed state. + Completed = HTLC_STATE_COMPLETED, + /// Refunded state. + Refunded = HTLC_STATE_REFUNDED, +} + +#[allow(dead_code)] +pub(crate) struct TendermintHtlc { + /// Generated HTLC's ID. + pub(crate) id: String, + + /// Message payload to be sent. + pub(crate) msg_payload: cosmrs::Any, +} + +#[derive(prost::Message)] +pub(crate) struct QueryHtlcRequestProto { + /// HTLC ID to query. + #[prost(string, tag = "1")] + pub(crate) id: prost::alloc::string::String, +} + +/// Generic enum for abstracting multiple types of create HTLC messages. +#[derive(Debug, PartialEq)] +pub(crate) enum CreateHtlcMsg { + Nucleus(NucleusCreateHtlcMsg), + Iris(IrisCreateHtlcMsg), +} + +impl TryFrom for CreateHtlcMsg { + type Error = ErrorReport; + + fn try_from(value: CreateHtlcProto) -> Result { + match value { + CreateHtlcProto::Nucleus(inner) => Ok(CreateHtlcMsg::Nucleus(NucleusCreateHtlcMsg::try_from(inner)?)), + CreateHtlcProto::Iris(inner) => Ok(CreateHtlcMsg::Iris(IrisCreateHtlcMsg::try_from(inner)?)), + } + } +} + +impl CreateHtlcMsg { + pub(crate) fn new( + htlc_type: HtlcType, + sender: AccountId, + to: AccountId, + amount: Vec, + hash_lock: String, + timestamp: u64, + time_lock: u64, + ) -> Self { + match htlc_type { + HtlcType::Iris => CreateHtlcMsg::Iris(IrisCreateHtlcMsg { + to, + sender, + receiver_on_other_chain: String::default(), + sender_on_other_chain: String::default(), + amount, + hash_lock, + time_lock, + timestamp, + transfer: false, + }), + HtlcType::Nucleus => CreateHtlcMsg::Nucleus(NucleusCreateHtlcMsg { + to, + sender, + amount, + hash_lock, + time_lock, + timestamp, + }), + } + } + + /// Returns the inner field `sender`. + pub(crate) fn sender(&self) -> &AccountId { + match self { + Self::Iris(inner) => &inner.sender, + Self::Nucleus(inner) => &inner.sender, + } + } + + /// Returns the inner field `to`. + pub(crate) fn to(&self) -> &AccountId { + match self { + Self::Iris(inner) => &inner.to, + Self::Nucleus(inner) => &inner.to, + } + } + + /// Returns the inner field `amount`. + pub(crate) fn amount(&self) -> &[Coin] { + match self { + Self::Iris(inner) => &inner.amount, + Self::Nucleus(inner) => &inner.amount, + } + } + + /// Generates `Any` from the inner CreateHTLC message. + pub(crate) fn to_any(&self) -> Result { + match self { + Self::Iris(inner) => inner.to_any(), + Self::Nucleus(inner) => inner.to_any(), + } + } +} + +/// Generic enum for abstracting multiple types of claim HTLC messages. +pub(crate) enum ClaimHtlcMsg { + Nucleus(NucleusClaimHtlcMsg), + Iris(IrisClaimHtlcMsg), +} + +impl ClaimHtlcMsg { + pub(crate) fn new(htlc_type: HtlcType, id: String, sender: AccountId, secret: String) -> Self { + match htlc_type { + HtlcType::Iris => ClaimHtlcMsg::Iris(IrisClaimHtlcMsg { sender, id, secret }), + HtlcType::Nucleus => ClaimHtlcMsg::Nucleus(NucleusClaimHtlcMsg { sender, id, secret }), + } + } + + /// Returns the inner field `secret`. + pub(crate) fn secret(&self) -> &str { + match self { + Self::Iris(inner) => &inner.secret, + Self::Nucleus(inner) => &inner.secret, + } + } + + /// Generates `Any` from the inner ClaimHTLC message. + pub(crate) fn to_any(&self) -> Result { + match self { + Self::Iris(inner) => inner.to_any(), + Self::Nucleus(inner) => inner.to_any(), + } + } +} + +impl TryFrom for ClaimHtlcMsg { + type Error = ErrorReport; + + fn try_from(value: ClaimHtlcProto) -> Result { + match value { + ClaimHtlcProto::Nucleus(inner) => Ok(ClaimHtlcMsg::Nucleus(NucleusClaimHtlcMsg::try_from(inner)?)), + ClaimHtlcProto::Iris(inner) => Ok(ClaimHtlcMsg::Iris(IrisClaimHtlcMsg::try_from(inner)?)), + } + } +} + +/// Generic enum for abstracting multiple types of create HTLC protos. +pub(crate) enum CreateHtlcProto { + Nucleus(NucleusCreateHtlcProto), + Iris(IrisCreateHtlcProto), +} + +impl CreateHtlcProto { + /// Decodes an instance (depending on the given `htlc_type`) of `CreateHtlcProto` from a buffer. + pub(crate) fn decode(htlc_type: HtlcType, buffer: &[u8]) -> Result { + match htlc_type { + HtlcType::Nucleus => Ok(Self::Nucleus(NucleusCreateHtlcProto::decode(buffer)?)), + HtlcType::Iris => Ok(Self::Iris(IrisCreateHtlcProto::decode(buffer)?)), + } + } + + /// Returns the inner field `hash_lock`. + pub(crate) fn hash_lock(&self) -> &str { + match self { + Self::Iris(inner) => &inner.hash_lock, + Self::Nucleus(inner) => &inner.hash_lock, + } + } +} + +/// Generic enum for abstracting multiple types of claim HTLC protos. +pub(crate) enum ClaimHtlcProto { + Nucleus(NucleusClaimHtlcProto), + Iris(IrisClaimHtlcProto), +} + +impl ClaimHtlcProto { + /// Decodes an instance (depending on the given `htlc_type`) of `ClaimHtlcProto` from a buffer. + pub(crate) fn decode(htlc_type: HtlcType, buffer: &[u8]) -> Result { + match htlc_type { + HtlcType::Nucleus => Ok(Self::Nucleus(NucleusClaimHtlcProto::decode(buffer)?)), + HtlcType::Iris => Ok(Self::Iris(IrisClaimHtlcProto::decode(buffer)?)), + } + } + + /// Returns the inner field `secret`. + #[cfg(test)] + pub(crate) fn secret(&self) -> &str { + match self { + Self::Iris(inner) => &inner.secret, + Self::Nucleus(inner) => &inner.secret, + } + } +} + +/// Generic enum for abstracting multiple types of HTLC responses. +pub(crate) enum QueryHtlcResponse { + Nucleus(NucleusQueryHtlcResponseProto), + Iris(IrisQueryHtlcResponseProto), +} + +impl QueryHtlcResponse { + /// Decodes an instance (depending on the given `htlc_type`) of `QueryHtlcResponse` from a buffer. + pub(crate) fn decode(htlc_type: HtlcType, buffer: &[u8]) -> Result { + match htlc_type { + HtlcType::Nucleus => Ok(Self::Nucleus(NucleusQueryHtlcResponseProto::decode(buffer)?)), + HtlcType::Iris => Ok(Self::Iris(IrisQueryHtlcResponseProto::decode(buffer)?)), + } + } + + /// Returns the inner field `htlc_state`. + pub(crate) fn htlc_state(&self) -> Option { + match self { + Self::Iris(inner) => Some(inner.htlc.as_ref()?.state), + Self::Nucleus(inner) => Some(inner.htlc.as_ref()?.state), + } + } + + /// Returns the inner field `hash_lock`. + pub(crate) fn hash_lock(&self) -> Option<&str> { + match self { + Self::Iris(inner) => Some(&inner.htlc.as_ref()?.hash_lock), + Self::Nucleus(inner) => Some(&inner.htlc.as_ref()?.hash_lock), + } + } +} diff --git a/mm2src/coins/tendermint/htlc/nucleus/htlc.rs b/mm2src/coins/tendermint/htlc/nucleus/htlc.rs new file mode 100644 index 0000000000..a768f7fd4a --- /dev/null +++ b/mm2src/coins/tendermint/htlc/nucleus/htlc.rs @@ -0,0 +1,131 @@ +use super::htlc_proto::{NucleusClaimHtlcProto, NucleusCreateHtlcProto}; + +use cosmrs::proto::traits::TypeUrl; +use cosmrs::{tx::Msg, AccountId, Coin, ErrorReport}; +use std::convert::TryFrom; + +pub(crate) const NUCLEUS_CREATE_HTLC_TYPE_URL: &str = "/nucleus.htlc.MsgCreateHTLC"; +pub(crate) const NUCLEUS_CLAIM_HTLC_TYPE_URL: &str = "/nucleus.htlc.MsgClaimHTLC"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct NucleusCreateHtlcMsg { + /// Sender's address. + pub(crate) to: AccountId, + + /// Recipient's address. + pub(crate) sender: AccountId, + + /// Amount to send. + pub(crate) amount: Vec, + + /// The sha256 hash generated from secret and timestamp. + pub(crate) hash_lock: String, + + /// The number of blocks to wait before the asset may be returned to. + pub(crate) time_lock: u64, + + /// The timestamp in seconds for generating hash lock if provided. + pub(crate) timestamp: u64, +} + +impl Msg for NucleusCreateHtlcMsg { + type Proto = NucleusCreateHtlcProto; +} + +impl TryFrom for NucleusCreateHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: NucleusCreateHtlcProto) -> Result { + NucleusCreateHtlcMsg::try_from(&proto) + } +} + +impl TryFrom<&NucleusCreateHtlcProto> for NucleusCreateHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: &NucleusCreateHtlcProto) -> Result { + Ok(NucleusCreateHtlcMsg { + sender: proto.sender.parse()?, + to: proto.to.parse()?, + amount: proto.amount.iter().map(TryFrom::try_from).collect::>()?, + hash_lock: proto.hash_lock.clone(), + timestamp: proto.timestamp, + time_lock: proto.time_lock, + }) + } +} + +impl From for NucleusCreateHtlcProto { + fn from(coin: NucleusCreateHtlcMsg) -> NucleusCreateHtlcProto { NucleusCreateHtlcProto::from(&coin) } +} + +impl From<&NucleusCreateHtlcMsg> for NucleusCreateHtlcProto { + fn from(msg: &NucleusCreateHtlcMsg) -> NucleusCreateHtlcProto { + NucleusCreateHtlcProto { + sender: msg.sender.to_string(), + to: msg.to.to_string(), + amount: msg.amount.iter().map(Into::into).collect(), + hash_lock: msg.hash_lock.clone(), + timestamp: msg.timestamp, + time_lock: msg.time_lock, + } + } +} + +impl TypeUrl for NucleusCreateHtlcProto { + const TYPE_URL: &'static str = NUCLEUS_CREATE_HTLC_TYPE_URL; +} + +#[derive(Clone)] +pub(crate) struct NucleusClaimHtlcMsg { + /// Sender's address. + pub(crate) sender: AccountId, + + /// Generated HTLC ID + pub(crate) id: String, + + /// Secret that has been used for generating hash_lock + pub(crate) secret: String, +} + +impl Msg for NucleusClaimHtlcMsg { + type Proto = NucleusClaimHtlcProto; +} + +impl TryFrom for NucleusClaimHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: NucleusClaimHtlcProto) -> Result { + NucleusClaimHtlcMsg::try_from(&proto) + } +} + +impl TryFrom<&NucleusClaimHtlcProto> for NucleusClaimHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: &NucleusClaimHtlcProto) -> Result { + Ok(NucleusClaimHtlcMsg { + sender: proto.sender.parse()?, + id: proto.id.clone(), + secret: proto.secret.clone(), + }) + } +} + +impl From for NucleusClaimHtlcProto { + fn from(coin: NucleusClaimHtlcMsg) -> NucleusClaimHtlcProto { NucleusClaimHtlcProto::from(&coin) } +} + +impl From<&NucleusClaimHtlcMsg> for NucleusClaimHtlcProto { + fn from(msg: &NucleusClaimHtlcMsg) -> NucleusClaimHtlcProto { + NucleusClaimHtlcProto { + sender: msg.sender.to_string(), + id: msg.id.clone(), + secret: msg.secret.clone(), + } + } +} + +impl TypeUrl for NucleusClaimHtlcProto { + const TYPE_URL: &'static str = NUCLEUS_CLAIM_HTLC_TYPE_URL; +} diff --git a/mm2src/coins/tendermint/htlc/nucleus/htlc_proto.rs b/mm2src/coins/tendermint/htlc/nucleus/htlc_proto.rs new file mode 100644 index 0000000000..3a4c200eb6 --- /dev/null +++ b/mm2src/coins/tendermint/htlc/nucleus/htlc_proto.rs @@ -0,0 +1,57 @@ +use crate::tendermint::htlc::HtlcState; + +#[derive(prost::Message)] +pub(crate) struct NucleusCreateHtlcProto { + #[prost(string, tag = "1")] + pub(crate) sender: prost::alloc::string::String, + #[prost(string, tag = "2")] + pub(crate) to: prost::alloc::string::String, + #[prost(message, repeated, tag = "3")] + pub(crate) amount: prost::alloc::vec::Vec, + #[prost(string, tag = "4")] + pub(crate) hash_lock: prost::alloc::string::String, + #[prost(uint64, tag = "5")] + pub(crate) timestamp: u64, + #[prost(uint64, tag = "6")] + pub(crate) time_lock: u64, +} + +#[derive(prost::Message)] +pub(crate) struct NucleusClaimHtlcProto { + #[prost(string, tag = "1")] + pub(crate) sender: prost::alloc::string::String, + #[prost(string, tag = "2")] + pub(crate) id: prost::alloc::string::String, + #[prost(string, tag = "3")] + pub(crate) secret: prost::alloc::string::String, +} + +#[derive(prost::Message)] +pub struct NucleusHtlcProto { + #[prost(string, tag = "1")] + pub(crate) id: prost::alloc::string::String, + #[prost(string, tag = "2")] + pub(crate) sender: prost::alloc::string::String, + #[prost(string, tag = "3")] + pub(crate) to: prost::alloc::string::String, + #[prost(message, repeated, tag = "4")] + pub(crate) amount: prost::alloc::vec::Vec, + #[prost(string, tag = "5")] + pub(crate) hash_lock: prost::alloc::string::String, + #[prost(string, tag = "6")] + pub(crate) secret: prost::alloc::string::String, + #[prost(uint64, tag = "7")] + pub(crate) timestamp: u64, + #[prost(uint64, tag = "8")] + pub(crate) expiration_height: u64, + #[prost(enumeration = "HtlcState", tag = "9")] + pub(crate) state: i32, + #[prost(uint64, tag = "10")] + pub(crate) closed_block: u64, +} + +#[derive(prost::Message)] +pub(crate) struct NucleusQueryHtlcResponseProto { + #[prost(message, tag = "1")] + pub(crate) htlc: Option, +} diff --git a/mm2src/coins/tendermint/htlc/nucleus/mod.rs b/mm2src/coins/tendermint/htlc/nucleus/mod.rs new file mode 100644 index 0000000000..3b96898679 --- /dev/null +++ b/mm2src/coins/tendermint/htlc/nucleus/mod.rs @@ -0,0 +1,7 @@ +//! Nucleus HTLC implementation in Rust on top of Cosmos SDK(cosmrs) for komodo-defi-framework. +//! +//! This module includes HTLC creating & claiming representation structstures +//! and their trait implementations. + +pub(crate) mod htlc; +pub(crate) mod htlc_proto; diff --git a/mm2src/coins/tendermint/ibc/mod.rs b/mm2src/coins/tendermint/ibc/mod.rs index 9e1c905398..51df375ab1 100644 --- a/mm2src/coins/tendermint/ibc/mod.rs +++ b/mm2src/coins/tendermint/ibc/mod.rs @@ -1,6 +1,7 @@ mod ibc_proto; pub(crate) mod transfer_v1; +pub(crate) const IBC_TRANSFER_TYPE_URL: &str = "/ibc.applications.transfer.v1.MsgTransfer"; 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 index f1caf4f3f3..e7bf37697f 100644 --- a/mm2src/coins/tendermint/ibc/transfer_v1.rs +++ b/mm2src/coins/tendermint/ibc/transfer_v1.rs @@ -1,5 +1,5 @@ use super::{ibc_proto::IBCTransferV1Proto, IBC_OUT_SOURCE_PORT, IBC_OUT_TIMEOUT_IN_NANOS}; -use crate::tendermint::type_urls::IBC_TRANSFER_TYPE_URL; +use crate::tendermint::ibc::IBC_TRANSFER_TYPE_URL; use common::number_type_casting::SafeTypeCastingNumbers; use cosmrs::proto::traits::TypeUrl; use cosmrs::{tx::Msg, AccountId, Coin, ErrorReport}; diff --git a/mm2src/coins/tendermint/iris/htlc.rs b/mm2src/coins/tendermint/iris/htlc.rs deleted file mode 100644 index beac68c418..0000000000 --- a/mm2src/coins/tendermint/iris/htlc.rs +++ /dev/null @@ -1,176 +0,0 @@ -// IRIS HTLC implementation in Rust on top of Cosmos SDK(cosmrs) for AtomicDEX. -// -// This module includes HTLC creating & claiming representation structstures -// and their trait implementations. -// -// ** Acquiring testnet assets ** -// -// Since there is no sdk exists for Rust on Iris Network, we should -// either implement some of the Iris Network funcionality on Rust or -// simply use their unit tests. -// -// Because we had limited time for the HTLC implementation, for now -// we can use their unit tests in order to acquire IBC assets. -// For that, clone https://github.com/onur-ozkan/irishub-sdk-js repository and check -// dummy.test.ts file(change the asset, amount, target address if needed) -// and then run the following commands: -// - yarn -// - npm run test -// -// If the sender address doesn't have enough nyan tokens to complete unit tests, -// check this page https://www.irisnet.org/docs/get-started/testnet.html#faucet - -use super::htlc_proto::{ClaimHtlcProtoRep, CreateHtlcProtoRep}; - -use crate::tendermint::type_urls::{CLAIM_HTLC_TYPE_URL, CREATE_HTLC_TYPE_URL}; -use cosmrs::proto::traits::TypeUrl; -use cosmrs::{tx::Msg, AccountId, Coin, ErrorReport}; -use std::convert::TryFrom; - -// https://github.com/irisnet/irismod/blob/043e058cd6e17f4f96d32f17bfd20b67debfab0b/proto/htlc/htlc.proto#L36 -pub const HTLC_STATE_OPEN: i32 = 0; -pub const HTLC_STATE_COMPLETED: i32 = 1; -pub const HTLC_STATE_REFUNDED: i32 = 2; - -#[allow(dead_code)] -pub(crate) struct IrisHtlc { - /// Generated HTLC's ID. - pub(crate) id: String, - - /// Message payload to be sent - pub(crate) msg_payload: cosmrs::Any, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct MsgCreateHtlc { - /// Sender's address. - pub(crate) to: AccountId, - - /// Recipient's address. - pub(crate) sender: AccountId, - - /// The claim receiving address on the other chain. - pub(crate) receiver_on_other_chain: String, - - /// The counterparty creator address on the other chain. - pub(crate) sender_on_other_chain: String, - - /// Amount to send. - pub(crate) amount: Vec, - - /// The sha256 hash generated from secret and timestamp. - pub(crate) hash_lock: String, - - /// The number of blocks to wait before the asset may be returned to. - pub(crate) time_lock: u64, - - /// The timestamp in seconds for generating hash lock if provided. - pub(crate) timestamp: u64, - - /// Whether it is an HTLT transaction. - pub(crate) transfer: bool, -} - -impl Msg for MsgCreateHtlc { - type Proto = CreateHtlcProtoRep; -} - -impl TryFrom for MsgCreateHtlc { - type Error = ErrorReport; - - fn try_from(proto: CreateHtlcProtoRep) -> Result { MsgCreateHtlc::try_from(&proto) } -} - -impl TryFrom<&CreateHtlcProtoRep> for MsgCreateHtlc { - type Error = ErrorReport; - - fn try_from(proto: &CreateHtlcProtoRep) -> Result { - Ok(MsgCreateHtlc { - sender: proto.sender.parse()?, - to: proto.to.parse()?, - amount: proto.amount.iter().map(TryFrom::try_from).collect::>()?, - receiver_on_other_chain: proto.receiver_on_other_chain.clone(), - sender_on_other_chain: proto.sender_on_other_chain.clone(), - hash_lock: proto.hash_lock.clone(), - timestamp: proto.timestamp, - time_lock: proto.time_lock, - transfer: proto.transfer, - }) - } -} - -impl From for CreateHtlcProtoRep { - fn from(coin: MsgCreateHtlc) -> CreateHtlcProtoRep { CreateHtlcProtoRep::from(&coin) } -} - -impl From<&MsgCreateHtlc> for CreateHtlcProtoRep { - fn from(msg: &MsgCreateHtlc) -> CreateHtlcProtoRep { - CreateHtlcProtoRep { - sender: msg.sender.to_string(), - to: msg.to.to_string(), - amount: msg.amount.iter().map(Into::into).collect(), - receiver_on_other_chain: msg.receiver_on_other_chain.clone(), - sender_on_other_chain: msg.sender_on_other_chain.clone(), - hash_lock: msg.hash_lock.clone(), - timestamp: msg.timestamp, - time_lock: msg.time_lock, - transfer: msg.transfer, - } - } -} - -impl TypeUrl for CreateHtlcProtoRep { - const TYPE_URL: &'static str = CREATE_HTLC_TYPE_URL; -} - -#[derive(Clone)] -pub(crate) struct MsgClaimHtlc { - /// Sender's address. - pub(crate) sender: AccountId, - - /// Generated HTLC ID - pub(crate) id: String, - - /// Secret that has been used for generating hash_lock - pub(crate) secret: String, -} - -impl Msg for MsgClaimHtlc { - type Proto = ClaimHtlcProtoRep; -} - -impl TryFrom for MsgClaimHtlc { - type Error = ErrorReport; - - fn try_from(proto: ClaimHtlcProtoRep) -> Result { MsgClaimHtlc::try_from(&proto) } -} - -impl TryFrom<&ClaimHtlcProtoRep> for MsgClaimHtlc { - type Error = ErrorReport; - - fn try_from(proto: &ClaimHtlcProtoRep) -> Result { - Ok(MsgClaimHtlc { - sender: proto.sender.parse()?, - id: proto.id.clone(), - secret: proto.secret.clone(), - }) - } -} - -impl From for ClaimHtlcProtoRep { - fn from(coin: MsgClaimHtlc) -> ClaimHtlcProtoRep { ClaimHtlcProtoRep::from(&coin) } -} - -impl From<&MsgClaimHtlc> for ClaimHtlcProtoRep { - fn from(msg: &MsgClaimHtlc) -> ClaimHtlcProtoRep { - ClaimHtlcProtoRep { - sender: msg.sender.to_string(), - id: msg.id.clone(), - secret: msg.secret.clone(), - } - } -} - -impl TypeUrl for ClaimHtlcProtoRep { - const TYPE_URL: &'static str = CLAIM_HTLC_TYPE_URL; -} diff --git a/mm2src/coins/tendermint/iris/mod.rs b/mm2src/coins/tendermint/iris/mod.rs deleted file mode 100644 index 03331ac181..0000000000 --- a/mm2src/coins/tendermint/iris/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod ethermint_account; -pub(crate) mod htlc; -pub(crate) mod htlc_proto; diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index 60a4c61ec1..a1fd9beb57 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -2,8 +2,9 @@ // Useful resources // https://docs.cosmos.network/ +pub(crate) mod ethermint_account; +pub mod htlc; mod ibc; -mod iris; mod rpc; mod tendermint_balance_events; mod tendermint_coin; @@ -13,22 +14,5 @@ pub mod tendermint_tx_history_v2; pub use tendermint_coin::*; pub use tendermint_token::*; -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub enum CustomTendermintMsgType { - /// Create HTLC as sender - SendHtlcAmount, - /// Claim HTLC as reciever - ClaimHtlcAmount, - /// Claim HTLC for reciever - SignClaimHtlc, -} - 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/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index fe4e1f5e80..bee0848a13 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -1,9 +1,8 @@ +use super::ethermint_account::EthermintAccount; +use super::htlc::{ClaimHtlcMsg, ClaimHtlcProto, CreateHtlcMsg, CreateHtlcProto, HtlcType, QueryHtlcRequestProto, + 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::iris::ethermint_account::EthermintAccount; -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, ValidatePaymentResult}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, @@ -85,7 +84,6 @@ const ABCI_SIMULATE_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/Simulate"; const ABCI_QUERY_ACCOUNT_PATH: &str = "/cosmos.auth.v1beta1.Query/Account"; const ABCI_QUERY_BALANCE_PATH: &str = "/cosmos.bank.v1beta1.Query/Balance"; const ABCI_GET_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTx"; -const ABCI_QUERY_HTLC_PATH: &str = "/irismod.htlc.Query/HTLC"; const ABCI_GET_TXS_EVENT_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTxsEvent"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -292,6 +290,10 @@ pub enum TendermintCoinRpcError { PerformError(String), RpcClientError(String), InternalError(String), + #[display(fmt = "Account type '{}' is not supported for HTLCs", prefix)] + UnexpectedAccountType { + prefix: String, + }, } impl From for TendermintCoinRpcError { @@ -314,6 +316,9 @@ impl From for BalanceError { TendermintCoinRpcError::PerformError(e) => BalanceError::Transport(e), TendermintCoinRpcError::RpcClientError(e) => BalanceError::Transport(e), TendermintCoinRpcError::InternalError(e) => BalanceError::Internal(e), + TendermintCoinRpcError::UnexpectedAccountType { prefix } => { + BalanceError::Internal(format!("Account type '{prefix}' is not supported for HTLCs")) + }, } } } @@ -326,6 +331,9 @@ impl From for ValidatePaymentError { TendermintCoinRpcError::PerformError(e) => ValidatePaymentError::Transport(e), TendermintCoinRpcError::RpcClientError(e) => ValidatePaymentError::Transport(e), TendermintCoinRpcError::InternalError(e) => ValidatePaymentError::InternalError(e), + TendermintCoinRpcError::UnexpectedAccountType { prefix } => { + ValidatePaymentError::InvalidParameter(format!("Account type '{prefix}' is not supported for HTLCs")) + }, } } } @@ -406,6 +414,10 @@ enum SearchForSwapTxSpendErr { TxMessagesEmpty, ClaimHtlcTxNotFound, UnexpectedHtlcState(i32), + #[display(fmt = "Account type '{}' is not supported for HTLCs", prefix)] + UnexpectedAccountType { + prefix: String, + }, Proto(DecodeError), } @@ -865,7 +877,7 @@ impl TendermintCoin { &self, from_address: &AccountId, to_address: &AccountId, - amount: Vec, + amount: &[Coin], secret_hash: &[u8], ) -> String { // Needs to be sorted if contains multiple coins @@ -1114,24 +1126,30 @@ impl TendermintCoin { amount: cosmrs::Amount, secret_hash: &[u8], time_lock: u64, - ) -> MmResult { + ) -> MmResult { let amount = vec![Coin { denom, amount }]; let timestamp = 0_u64; - let msg_payload = MsgCreateHtlc { - sender: self.account_id.clone(), - to: to.clone(), - receiver_on_other_chain: "".to_string(), - sender_on_other_chain: "".to_string(), - amount: amount.clone(), - hash_lock: hex::encode(secret_hash), + + let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + TxMarshalingErr::NotSupported(format!( + "Account type '{}' is not supported for HTLCs", + self.account_prefix + )) + })?; + + let msg_payload = CreateHtlcMsg::new( + htlc_type, + self.account_id.clone(), + to.clone(), + amount.clone(), + hex::encode(secret_hash), timestamp, time_lock, - transfer: false, - }; + ); - let htlc_id = self.calculate_htlc_id(&self.account_id, to, amount, secret_hash); + let htlc_id = self.calculate_htlc_id(&self.account_id, to, &amount, secret_hash); - Ok(IrisHtlc { + Ok(TendermintHtlc { id: htlc_id, msg_payload: msg_payload .to_any() @@ -1139,14 +1157,17 @@ impl TendermintCoin { }) } - fn gen_claim_htlc_tx(&self, htlc_id: String, secret: &[u8]) -> MmResult { - let msg_payload = MsgClaimHtlc { - id: htlc_id.clone(), - sender: self.account_id.clone(), - secret: hex::encode(secret), - }; + fn gen_claim_htlc_tx(&self, htlc_id: String, secret: &[u8]) -> MmResult { + let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + TxMarshalingErr::NotSupported(format!( + "Account type '{}' is not supported for HTLCs", + self.account_prefix + )) + })?; - Ok(IrisHtlc { + let msg_payload = ClaimHtlcMsg::new(htlc_type, htlc_id.clone(), self.account_id.clone(), hex::encode(secret)); + + Ok(TendermintHtlc { id: htlc_id, msg_payload: msg_payload .to_any() @@ -1199,17 +1220,17 @@ impl TendermintCoin { let pubkey_hash = dhash160(other_pub); let to_address = try_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); - let htlc_id = self.calculate_htlc_id(&self.account_id, &to_address, amount, secret_hash); + let htlc_id = self.calculate_htlc_id(&self.account_id, &to_address, &amount, secret_hash); let coin = self.clone(); let fut = async move { let htlc_response = try_s!(coin.query_htlc(htlc_id.clone()).await); - let htlc_data = match htlc_response.htlc { - Some(htlc) => htlc, - None => return Ok(None), + + let Some(htlc_state) = htlc_response.htlc_state() else { + return Ok(None); }; - match htlc_data.state { + match htlc_state { HTLC_STATE_OPEN | HTLC_STATE_COMPLETED | HTLC_STATE_REFUNDED => {}, unexpected_state => return Err(format!("Unexpected state for HTLC {}", unexpected_state)), }; @@ -1240,9 +1261,16 @@ impl TendermintCoin { let deserialized_tx = try_s!(cosmrs::Tx::from_bytes(&tx.tx)); let msg = try_s!(deserialized_tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc = try_s!(CreateHtlcProtoRep::decode(msg.value.as_slice())); + let htlc = try_s!(CreateHtlcProto::decode( + try_s!(HtlcType::from_str(&coin.account_prefix)), + msg.value.as_slice() + )); + + let Some(hash_lock) = htlc_response.hash_lock() else { + return Ok(None); + }; - if htlc.hash_lock.to_uppercase() == htlc_data.hash_lock.to_uppercase() { + if htlc.hash_lock().to_uppercase() == hash_lock.to_uppercase() { let htlc = TransactionEnum::CosmosTransaction(CosmosTransaction { data: try_s!(TxRaw::decode(tx.tx.as_slice())), }); @@ -1476,10 +1504,16 @@ impl TendermintCoin { "Payment tx must have exactly one message".into(), )); } + let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + ValidatePaymentError::InvalidParameter(format!( + "Account type '{}' is not supported for HTLCs", + self.account_prefix + )) + })?; - let create_htlc_msg_proto = CreateHtlcProtoRep::decode(tx.body.messages[0].value.as_slice()) + let create_htlc_msg_proto = CreateHtlcProto::decode(htlc_type, tx.body.messages[0].value.as_slice()) .map_to_mm(|e| ValidatePaymentError::WrongPaymentTx(e.to_string()))?; - let create_htlc_msg = MsgCreateHtlc::try_from(create_htlc_msg_proto) + let create_htlc_msg = CreateHtlcMsg::try_from(create_htlc_msg_proto) .map_to_mm(|e| ValidatePaymentError::WrongPaymentTx(e.to_string()))?; let sender_pubkey_hash = dhash160(&input.other_pub); @@ -1494,17 +1528,15 @@ impl TendermintCoin { let time_lock = self.estimate_blocks_from_duration(input.time_lock_duration); - let expected_msg = MsgCreateHtlc { - sender: sender.clone(), - to: self.account_id.clone(), - receiver_on_other_chain: "".into(), - sender_on_other_chain: "".into(), - amount: amount.clone(), - hash_lock: hex::encode(&input.secret_hash), - timestamp: 0, - time_lock: time_lock as u64, - transfer: false, - }; + let expected_msg = CreateHtlcMsg::new( + htlc_type, + sender.clone(), + self.account_id.clone(), + amount.clone(), + hex::encode(&input.secret_hash), + 0, + time_lock as u64, + ); if create_htlc_msg != expected_msg { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( @@ -1521,14 +1553,14 @@ impl TendermintCoin { )); } - let htlc_id = self.calculate_htlc_id(&sender, &self.account_id, amount, &input.secret_hash); + let htlc_id = self.calculate_htlc_id(&sender, &self.account_id, &amount, &input.secret_hash); let htlc_response = self.query_htlc(htlc_id.clone()).await?; - let htlc_data = htlc_response - .htlc + let htlc_state = htlc_response + .htlc_state() .or_mm_err(|| ValidatePaymentError::InvalidRpcResponse(format!("No HTLC data for {}", htlc_id)))?; - match htlc_data.state { + match htlc_state { HTLC_STATE_OPEN => Ok(()), unexpected_state => MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( "{}", @@ -1712,20 +1744,25 @@ impl TendermintCoin { } } - pub(crate) async fn query_htlc(&self, id: String) -> MmResult { + pub(crate) async fn query_htlc(&self, id: String) -> MmResult { + let htlc_type = + HtlcType::from_str(&self.account_prefix).map_err(|_| TendermintCoinRpcError::UnexpectedAccountType { + prefix: self.account_prefix.clone(), + })?; + let request = QueryHtlcRequestProto { id }; let response = self .rpc_client() .await? .abci_query( - Some(ABCI_QUERY_HTLC_PATH.to_string()), + Some(htlc_type.get_htlc_abci_query_path()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, ) .await?; - Ok(QueryHtlcResponseProto::decode(response.value.as_slice())?) + Ok(QueryHtlcResponse::decode(htlc_type, response.value.as_slice())?) } #[inline] @@ -1744,17 +1781,24 @@ impl TendermintCoin { .messages .first() .or_mm_err(|| SearchForSwapTxSpendErr::TxMessagesEmpty)?; - let htlc_proto = CreateHtlcProtoRep::decode(first_message.value.as_slice())?; - let htlc = MsgCreateHtlc::try_from(htlc_proto)?; - let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, htlc.amount, input.secret_hash); + + let htlc_type = + HtlcType::from_str(&self.account_prefix).map_err(|_| SearchForSwapTxSpendErr::UnexpectedAccountType { + prefix: self.account_prefix.clone(), + })?; + + let htlc_proto = CreateHtlcProto::decode(htlc_type, first_message.value.as_slice())?; + let htlc = CreateHtlcMsg::try_from(htlc_proto)?; + let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), htlc.amount(), input.secret_hash); let htlc_response = self.query_htlc(htlc_id.clone()).await?; - let htlc_data = match htlc_response.htlc { - Some(htlc) => htlc, + + let htlc_state = match htlc_response.htlc_state() { + Some(htlc_state) => htlc_state, None => return Ok(None), }; - match htlc_data.state { + match htlc_state { HTLC_STATE_OPEN => Ok(None), HTLC_STATE_COMPLETED => { let events_string = format!("claim_htlc.id='{}'", htlc_id); @@ -2337,9 +2381,12 @@ impl MarketCoinOps for TendermintCoin { fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { let tx = try_tx_fus!(cosmrs::Tx::from_bytes(args.tx_bytes)); let first_message = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto = try_tx_fus!(CreateHtlcProtoRep::decode(first_message.value.as_slice())); - let htlc = try_tx_fus!(MsgCreateHtlc::try_from(htlc_proto)); - let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, htlc.amount, args.secret_hash); + let htlc_proto = try_tx_fus!(CreateHtlcProto::decode( + try_tx_fus!(HtlcType::from_str(&self.account_prefix)), + first_message.value.as_slice() + )); + let htlc = try_tx_fus!(CreateHtlcMsg::try_from(htlc_proto)); + let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), htlc.amount(), args.secret_hash); let events_string = format!("claim_htlc.id='{}'", htlc_id); // TODO: Remove deprecated attribute when new version of tendermint-rs is released @@ -2454,10 +2501,14 @@ impl SwapOps for TendermintCoin { fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { let tx = try_tx_fus!(cosmrs::Tx::from_bytes(maker_spends_payment_args.other_payment_tx)); let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto: CreateHtlcProtoRep = try_tx_fus!(Message::decode(msg.value.as_slice())); - let htlc = try_tx_fus!(MsgCreateHtlc::try_from(htlc_proto)); - let mut amount = htlc.amount.clone(); + let htlc_proto = try_tx_fus!(CreateHtlcProto::decode( + try_tx_fus!(HtlcType::from_str(&self.account_prefix)), + msg.value.as_slice() + )); + let htlc = try_tx_fus!(CreateHtlcMsg::try_from(htlc_proto)); + + let mut amount = htlc.amount().to_vec(); amount.sort(); drop_mutability!(amount); @@ -2467,7 +2518,7 @@ impl SwapOps for TendermintCoin { .collect::>() .join(","); - let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, amount, maker_spends_payment_args.secret_hash); + let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), &amount, maker_spends_payment_args.secret_hash); let claim_htlc_tx = try_tx_fus!(self.gen_claim_htlc_tx(htlc_id, maker_spends_payment_args.secret)); let coin = self.clone(); @@ -2507,10 +2558,14 @@ impl SwapOps for TendermintCoin { fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { let tx = try_tx_fus!(cosmrs::Tx::from_bytes(taker_spends_payment_args.other_payment_tx)); let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto: CreateHtlcProtoRep = try_tx_fus!(Message::decode(msg.value.as_slice())); - let htlc = try_tx_fus!(MsgCreateHtlc::try_from(htlc_proto)); - let mut amount = htlc.amount.clone(); + let htlc_proto = try_tx_fus!(CreateHtlcProto::decode( + try_tx_fus!(HtlcType::from_str(&self.account_prefix)), + msg.value.as_slice() + )); + let htlc = try_tx_fus!(CreateHtlcMsg::try_from(htlc_proto)); + + let mut amount = htlc.amount().to_vec(); amount.sort(); drop_mutability!(amount); @@ -2520,7 +2575,7 @@ impl SwapOps for TendermintCoin { .collect::>() .join(","); - let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, amount, taker_spends_payment_args.secret_hash); + let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), &amount, taker_spends_payment_args.secret_hash); let claim_htlc_tx = try_tx_fus!(self.gen_claim_htlc_tx(htlc_id, taker_spends_payment_args.secret)); let coin = self.clone(); @@ -2626,10 +2681,14 @@ impl SwapOps for TendermintCoin { ) -> Result, String> { let tx = try_s!(cosmrs::Tx::from_bytes(spend_tx)); let msg = try_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto: super::iris::htlc_proto::ClaimHtlcProtoRep = try_s!(Message::decode(msg.value.as_slice())); - let htlc = try_s!(MsgClaimHtlc::try_from(htlc_proto)); - Ok(try_s!(hex::decode(htlc.secret))) + let htlc_proto = try_s!(ClaimHtlcProto::decode( + try_s!(HtlcType::from_str(&self.account_prefix)), + msg.value.as_slice() + )); + let htlc = try_s!(ClaimHtlcMsg::try_from(htlc_proto)); + + Ok(try_s!(hex::decode(htlc.secret()))) } fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { @@ -2843,7 +2902,7 @@ pub mod tendermint_coin_tests { use std::mem::discriminant; pub const IRIS_TESTNET_HTLC_PAIR1_SEED: &str = "iris test seed"; - // pub const IRIS_TESTNET_HTLC_PAIR1_PUB_KEY: &str = &[ + // pub const IRIS_TESTNET_HTLC_PAIR1_PUB_KEY: &[u8] = &[ // 2, 35, 133, 39, 114, 92, 150, 175, 252, 203, 124, 85, 243, 144, 11, 52, 91, 128, 236, 82, 104, 212, 131, 40, // 79, 22, 40, 7, 119, 93, 50, 179, 43, // ]; @@ -3081,10 +3140,9 @@ pub mod tendermint_coin_tests { let first_msg = tx.body.as_ref().unwrap().messages.first().unwrap(); println!("{:?}", first_msg); - let claim_htlc = - crate::tendermint::iris::htlc_proto::ClaimHtlcProtoRep::decode(first_msg.value.as_slice()).unwrap(); + let claim_htlc = ClaimHtlcProto::decode(HtlcType::Iris, first_msg.value.as_slice()).unwrap(); let expected_secret = [1; 32]; - let actual_secret = hex::decode(claim_htlc.secret).unwrap(); + let actual_secret = hex::decode(claim_htlc.secret()).unwrap(); assert_eq!(actual_secret, expected_secret); } diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 5c976bfad2..7ae6f92228 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -1,7 +1,8 @@ use super::{rpc::*, AllBalancesResult, TendermintCoin, TendermintCommons, TendermintToken}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; -use crate::tendermint::{CustomTendermintMsgType, TendermintFeeDetails}; +use crate::tendermint::htlc::CustomTendermintMsgType; +use crate::tendermint::TendermintFeeDetails; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; use crate::{HistorySyncState, MarketCoinOps, MmCoin, TransactionDetails, TransactionType, TxFeeDetails}; diff --git a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs index 048a7c7abd..eb507d1a1c 100644 --- a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs @@ -17,6 +17,8 @@ const ATOM_TENDERMINT_RPC_URLS: &[&str] = &["https://rpc.sentry-02.theta-testnet const IRIS_TEST_SEED: &str = "iris test seed"; const IRIS_TESTNET_RPC_URLS: &[&str] = &["http://34.80.202.172:26657"]; +const NUCLEUS_TESTNET_RPC_URLS: &[&str] = &["http://5.161.55.53:26657"]; + const TENDERMINT_TEST_BIP39_SEED: &str = "emerge canoe salmon dolphin glow priority random become gasp sell blade argue"; @@ -585,7 +587,8 @@ mod swap { use common::log; use instant::Duration; use mm2_rpc::data::legacy::OrderbookResponse; - use mm2_test_helpers::for_tests::{check_my_swap_status, check_recent_swaps, enable_eth_coin, rick_conf, tbnb_conf, + use mm2_test_helpers::for_tests::{check_my_swap_status, check_recent_swaps, doc_conf, enable_eth_coin, + iris_ibc_nucleus_testnet_conf, nucleus_testnet_conf, tbnb_conf, usdc_ibc_iris_testnet_conf, wait_check_stats_swap_status, DOC_ELECTRUM_ADDRS}; use std::convert::TryFrom; use std::{env, thread}; @@ -677,11 +680,11 @@ mod swap { } #[test] - fn swap_iris_with_rick() { + fn swap_nucleus_with_doc() { let bob_passphrase = String::from(BOB_PASSPHRASE); let alice_passphrase = String::from(ALICE_PASSPHRASE); - let coins = json!([iris_testnet_conf(), rick_conf()]); + let coins = json!([nucleus_testnet_conf(), doc_conf()]); let mm_bob = MarketMakerIt::start( json!({ @@ -725,23 +728,23 @@ mod swap { dbg!(block_on(enable_tendermint( &mm_bob, - "IRIS-TEST", + "NUCLEUS-TEST", &[], - IRIS_TESTNET_RPC_URLS, + NUCLEUS_TESTNET_RPC_URLS, false ))); dbg!(block_on(enable_tendermint( &mm_alice, - "IRIS-TEST", + "NUCLEUS-TEST", &[], - IRIS_TESTNET_RPC_URLS, + NUCLEUS_TESTNET_RPC_URLS, false ))); dbg!(block_on(enable_electrum( &mm_bob, - "RICK", + "DOC", false, DOC_ELECTRUM_ADDRS, None @@ -749,7 +752,7 @@ mod swap { dbg!(block_on(enable_electrum( &mm_alice, - "RICK", + "DOC", false, DOC_ELECTRUM_ADDRS, None @@ -758,8 +761,278 @@ mod swap { block_on(trade_base_rel_tendermint( mm_bob, mm_alice, - "IRIS-TEST", - "RICK", + "NUCLEUS-TEST", + "DOC", + 1, + 2, + 0.008, + )); + } + + #[test] + fn swap_doc_with_nucleus() { + let bob_passphrase = String::from(BOB_PASSPHRASE); + let alice_passphrase = String::from(ALICE_PASSPHRASE); + + let coins = json!([nucleus_testnet_conf(), doc_conf()]); + + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("BOB_TRADE_IP") .ok(), + "rpcip": env::var("BOB_TRADE_IP") .ok(), + "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": bob_passphrase, + "coins": coins, + "rpc_password": "password", + "i_am_seed": true, + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("ALICE_TRADE_IP") .ok(), + "rpcip": env::var("ALICE_TRADE_IP") .ok(), + "passphrase": alice_passphrase, + "coins": coins, + "seednodes": [mm_bob.my_seed_addr()], + "rpc_password": "password", + "skip_startup_checks": true, + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + dbg!(block_on(enable_tendermint( + &mm_bob, + "NUCLEUS-TEST", + &[], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + dbg!(block_on(enable_tendermint( + &mm_alice, + "NUCLEUS-TEST", + &[], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + dbg!(block_on(enable_electrum( + &mm_bob, + "DOC", + false, + DOC_ELECTRUM_ADDRS, + None + ))); + + dbg!(block_on(enable_electrum( + &mm_alice, + "DOC", + false, + DOC_ELECTRUM_ADDRS, + None + ))); + + block_on(trade_base_rel_tendermint( + mm_bob, + mm_alice, + "DOC", + "NUCLEUS-TEST", + 1, + 2, + 0.008, + )); + } + + #[test] + fn swap_iris_ibc_nucleus_with_doc() { + let bob_passphrase = String::from(BOB_PASSPHRASE); + let alice_passphrase = String::from(ALICE_PASSPHRASE); + + let coins = json!([nucleus_testnet_conf(), iris_ibc_nucleus_testnet_conf(), doc_conf()]); + + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("BOB_TRADE_IP") .ok(), + "rpcip": env::var("BOB_TRADE_IP") .ok(), + "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": bob_passphrase, + "coins": coins, + "rpc_password": "password", + "i_am_seed": true, + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("ALICE_TRADE_IP") .ok(), + "rpcip": env::var("ALICE_TRADE_IP") .ok(), + "passphrase": alice_passphrase, + "coins": coins, + "seednodes": [mm_bob.my_seed_addr()], + "rpc_password": "password", + "skip_startup_checks": true, + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + dbg!(block_on(enable_tendermint( + &mm_bob, + "NUCLEUS-TEST", + &["IRIS-IBC-NUCLEUS-TEST"], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + dbg!(block_on(enable_tendermint( + &mm_alice, + "NUCLEUS-TEST", + &["IRIS-IBC-NUCLEUS-TEST"], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + dbg!(block_on(enable_electrum( + &mm_bob, + "DOC", + false, + DOC_ELECTRUM_ADDRS, + None + ))); + + dbg!(block_on(enable_electrum( + &mm_alice, + "DOC", + false, + DOC_ELECTRUM_ADDRS, + None + ))); + + block_on(trade_base_rel_tendermint( + mm_bob, + mm_alice, + "IRIS-IBC-NUCLEUS-TEST", + "DOC", + 1, + 2, + 0.008, + )); + } + + #[test] + fn swap_doc_with_iris_ibc_nucleus() { + let bob_passphrase = String::from(BOB_PASSPHRASE); + let alice_passphrase = String::from(ALICE_PASSPHRASE); + + let coins = json!([nucleus_testnet_conf(), iris_ibc_nucleus_testnet_conf(), doc_conf()]); + + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("BOB_TRADE_IP") .ok(), + "rpcip": env::var("BOB_TRADE_IP") .ok(), + "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": bob_passphrase, + "coins": coins, + "rpc_password": "password", + "i_am_seed": true, + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("ALICE_TRADE_IP") .ok(), + "rpcip": env::var("ALICE_TRADE_IP") .ok(), + "passphrase": alice_passphrase, + "coins": coins, + "seednodes": [mm_bob.my_seed_addr()], + "rpc_password": "password", + "skip_startup_checks": true, + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + dbg!(block_on(enable_tendermint( + &mm_bob, + "NUCLEUS-TEST", + &["IRIS-IBC-NUCLEUS-TEST"], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + dbg!(block_on(enable_tendermint( + &mm_alice, + "NUCLEUS-TEST", + &["IRIS-IBC-NUCLEUS-TEST"], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + dbg!(block_on(enable_electrum( + &mm_bob, + "DOC", + false, + DOC_ELECTRUM_ADDRS, + None + ))); + + dbg!(block_on(enable_electrum( + &mm_alice, + "DOC", + false, + DOC_ELECTRUM_ADDRS, + None + ))); + + block_on(trade_base_rel_tendermint( + mm_bob, + mm_alice, + "DOC", + "IRIS-IBC-NUCLEUS-TEST", 1, 2, 0.008, @@ -943,7 +1216,7 @@ mod swap { for uuid in uuids.iter() { match mm_bob - .wait_for_log(900., |log| log.contains(&format!("[swap uuid={}] Finished", uuid))) + .wait_for_log(180., |log| log.contains(&format!("[swap uuid={}] Finished", uuid))) .await { Ok(_) => (), @@ -953,7 +1226,7 @@ mod swap { } match mm_alice - .wait_for_log(900., |log| log.contains(&format!("[swap uuid={}] Finished", uuid))) + .wait_for_log(180., |log| log.contains(&format!("[swap uuid={}] Finished", uuid))) .await { Ok(_) => (), diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index b70e2bd718..f10388fb52 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -519,6 +519,20 @@ pub fn rick_conf() -> Json { }) } +pub fn doc_conf() -> Json { + json!({ + "coin":"DOC", + "asset":"DOC", + "required_confirmations":0, + "txversion":4, + "overwintered":1, + "derivation_path": "m/44'/141'", + "protocol":{ + "type":"UTXO" + } + }) +} + pub fn morty_conf() -> Json { json!({ "coin":"MORTY", @@ -889,6 +903,23 @@ pub fn iris_testnet_conf() -> Json { }) } +pub fn nucleus_testnet_conf() -> Json { + json!({ + "coin": "NUCLEUS-TEST", + "avg_blocktime": 5, + "derivation_path": "m/44'/566'", + "protocol":{ + "type":"TENDERMINT", + "protocol_data": { + "decimals": 6, + "denom": "unucl", + "account_prefix": "nuc", + "chain_id": "nucleus-3", + }, + } + }) +} + pub fn iris_nimda_testnet_conf() -> Json { json!({ "coin": "IRIS-NIMDA", @@ -904,6 +935,20 @@ pub fn iris_nimda_testnet_conf() -> Json { }) } +pub fn iris_ibc_nucleus_testnet_conf() -> Json { + json!({ + "coin":"IRIS-IBC-NUCLEUS-TEST", + "protocol":{ + "type":"TENDERMINTTOKEN", + "protocol_data": { + "platform": "NUCLEUS-TEST", + "decimals": 6, + "denom": "ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C", + }, + } + }) +} + pub fn usdc_ibc_iris_testnet_conf() -> Json { json!({ "coin":"USDC-IBC-IRIS",