From bccdb93c03745e8225e3cf81121a8b3d70d0ee07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 5 May 2025 19:15:05 +0300 Subject: [PATCH 01/36] improvement(tendermint): safer IBC channel handler (#2298) Replaces chain-registry parsing with embedded IBC channels in coins file --- Cargo.lock | 1 - mm2src/coins/Cargo.toml | 1 - mm2src/coins/lp_coins.rs | 26 +- mm2src/coins/rpc_command/mod.rs | 3 +- mm2src/coins/rpc_command/tendermint/ibc.rs | 12 + .../rpc_command/tendermint/ibc_chains.rs | 35 -- .../tendermint/ibc_transfer_channels.rs | 105 ------ mm2src/coins/rpc_command/tendermint/mod.rs | 13 +- .../coins/rpc_command/tendermint/staking.rs | 3 +- mm2src/coins/tendermint/tendermint_coin.rs | 328 +++++++----------- mm2src/coins/tendermint/tendermint_token.rs | 4 +- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 3 - .../tests/docker_tests/tendermint_tests.rs | 4 +- mm2src/mm2_test_helpers/src/for_tests.rs | 2 +- 14 files changed, 163 insertions(+), 377 deletions(-) create mode 100644 mm2src/coins/rpc_command/tendermint/ibc.rs delete mode 100644 mm2src/coins/rpc_command/tendermint/ibc_chains.rs delete mode 100644 mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs diff --git a/Cargo.lock b/Cargo.lock index e2ebd94a86..321396ac58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -881,7 +881,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 13aa9c2b72..33c8457dce 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -66,7 +66,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/lp_coins.rs b/mm2src/coins/lp_coins.rs index 4de2ab31fc..785711e3f2 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)] @@ -3179,15 +3180,22 @@ 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.", - _0 + 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(String), + IBCChannelCouldNotFound { + target_address: String, + }, + #[display( + fmt = "IBC channel '{}' is not healthy. Provide a healthy one manually by including `ibc_source_channel` in the request.", + channel_id + )] + IBCChannelNotHealthy { + channel_id: ChannelId, + }, } impl HttpStatusCode for WithdrawError { @@ -3216,8 +3224,8 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::NoChainIdSet { .. } | WithdrawError::TxTypeNotSupported | WithdrawError::SigningError(_) - | WithdrawError::RegistryNameIsMissing(_) - | WithdrawError::IBCChannelCouldNotFound(_) + | WithdrawError::IBCChannelCouldNotFound { .. } + | WithdrawError::IBCChannelNotHealthy { .. } | WithdrawError::MyAddressNotNftOwner { .. } => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs index c401853b2d..a47a9bf25a 100644 --- a/mm2src/coins/rpc_command/mod.rs +++ b/mm2src/coins/rpc_command/mod.rs @@ -7,5 +7,6 @@ pub mod init_account_balance; 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; + +#[cfg(not(target_arch = "wasm32"))] pub mod lightning; 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/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 index 9a3d714bd3..d0f2c82685 100644 --- a/mm2src/coins/rpc_command/tendermint/mod.rs +++ b/mm2src/coins/rpc_command/tendermint/mod.rs @@ -1,13 +1,2 @@ -mod ibc_chains; -mod ibc_transfer_channels; +pub mod ibc; pub mod staking; - -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/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 3f5f8a33de..201e215c19 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -3,17 +3,13 @@ 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::rpc_command::tendermint::ibc::ChannelId; use crate::rpc_command::tendermint::staking::{ClaimRewardsPayload, Delegation, DelegationPayload, DelegationsQueryResponse, Undelegation, UndelegationEntry, UndelegationsQueryResponse, ValidatorStatus}; -use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, - IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, - IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, - CHAIN_REGISTRY_IBC_DIR_NAME, CHAIN_REGISTRY_REPO_NAME, CHAIN_REGISTRY_REPO_OWNER}; -use crate::tendermint::ibc::IBC_OUT_SOURCE_PORT; use crate::utxo::sat_from_big_decimal; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, @@ -54,6 +50,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; @@ -73,7 +71,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::bigdecimal::ParseBigDecimalError; use mm2_number::MmNumber; use mm2_p2p::p2p_ctx::P2PContext; @@ -106,6 +103,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; @@ -199,7 +197,8 @@ pub struct TendermintProtocolInfo { pub account_prefix: String, chain_id: String, gas_price: Option, - chain_registry_name: Option, + #[serde(default)] + ibc_channels: HashMap, } #[derive(Clone)] @@ -391,9 +390,11 @@ pub struct TendermintCoinImpl { pub(super) abortable_system: AbortableQueue, pub(crate) history_sync_state: Mutex, client: TendermintRpcClient, - pub(crate) chain_registry_name: Option, - pub ctx: MmWeak, + 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)] @@ -455,6 +456,7 @@ pub enum TendermintCoinRpcError { UnexpectedAccountType { prefix: String, }, + NotFound(String), } impl From for TendermintCoinRpcError { @@ -478,9 +480,9 @@ 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::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")) @@ -494,9 +496,9 @@ 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::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")) @@ -742,37 +744,70 @@ 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. - pub(crate) async fn detect_channel_id_for_ibc_transfer( + /// Finds the IBC channel by querying the given channel ID and port ID + /// and returns its information. + async fn query_ibc_channel( &self, - to_address: &AccountId, - ) -> Result> { - let ctx = MmArc::from_weak(&self.ctx).ok_or_else(|| WithdrawError::InternalError("No context".to_owned()))?; + channel_id: ChannelId, + port_id: &str, + ) -> MmResult { + let payload = QueryChannelRequest { + channel_id: channel_id.to_string(), + port_id: port_id.to_string(), + } + .encode_to_vec(); - let source_registry_name = self - .chain_registry_name - .clone() - .ok_or_else(|| WithdrawError::RegistryNameIsMissing(to_address.prefix().to_owned()))?; + let request = AbciRequest::new( + Some(ABCI_IBC_CHANNEL_QUERY_PATH.to_string()), + payload, + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ); - 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 response = self.rpc_client().await?.perform(request).await?; + let response = QueryChannelResponse::decode(response.response.value.as_slice())?; - let channels = get_ibc_transfer_channels(source_registry_name, destination_registry_name) - .await - .map_err(|_| WithdrawError::IBCChannelCouldNotFound(to_address.to_string()))?; + response.channel.ok_or_else(|| { + MmError::new(TendermintCoinRpcError::NotFound(format!( + "No result for channel id: {channel_id}, port: {port_id}." + ))) + }) + } + + /// 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> { + // 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_id = + *self + .ibc_channels + .get(target_address.prefix()) + .ok_or_else(|| WithdrawError::IBCChannelCouldNotFound { + target_address: target_address.to_string(), + })?; + + let channel = self.query_ibc_channel(channel_id, "transfer").await?; - Ok(channels - .ibc_transfer_channels - .last() - .ok_or_else(|| WithdrawError::InternalError("channel list can not be empty".to_owned()))? - .channel_id - .clone()) + // 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.state != STATE_OPEN { + return MmError::err(WithdrawError::IBCChannelNotHealthy { channel_id }); + } + + Ok(channel_id) } #[inline(always)] @@ -2955,46 +2990,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 { @@ -3052,7 +3047,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_healthy_ibc_channel_for_address(&to_address).await?), } } else { None @@ -3063,7 +3058,7 @@ impl MmCoin for TendermintCoin { to_address.clone(), &coin.denom, amount_denom, - channel_id.clone(), + channel_id, ) .await?; @@ -3101,6 +3096,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 }; @@ -3369,7 +3366,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 @@ -3858,54 +3855,15 @@ 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, 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(), }) @@ -3924,86 +3882,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 extract_big_decimal_from_dec_coin(dec_coin: &DecCoin, decimals: u32) -> Result { let raw = BigDecimal::from_str(&dec_coin.amount)?; // `DecCoin` represents decimal numbers as integer-like strings where the last 18 digits are the decimal part. @@ -4080,18 +3958,21 @@ 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(), } } fn get_iris_protocol() -> TendermintProtocolInfo { + let mut ibc_channels = HashMap::new(); + ibc_channels.insert("cosmos".into(), ChannelId::new(0)); + TendermintProtocolInfo { decimals: 6, denom: String::from("unyan"), account_prefix: String::from("iaa"), chain_id: String::from("nyancat-9"), gas_price: None, - chain_registry_name: None, + ibc_channels, } } @@ -4102,7 +3983,7 @@ pub mod tendermint_coin_tests { account_prefix: String::from("nuc"), chain_id: String::from("nucleus-testnet"), gas_price: None, - chain_registry_name: None, + ibc_channels: HashMap::new(), } } @@ -5133,4 +5014,43 @@ pub mod tendermint_coin_tests { assert_eq!(expected_list, actual_list); } + + #[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, + }; + + 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 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_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 589220b555..b30d07c1a5 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -430,7 +430,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_healthy_ibc_channel_for_address(&to_address).await?), } } else { None @@ -441,7 +441,7 @@ impl MmCoin for TendermintToken { to_address.clone(), &token.denom, amount_denom, - channel_id.clone(), + channel_id, ) .await?; diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index f3286e5ab5..bbbeca9050 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -25,7 +25,6 @@ use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; use coins::eth::fee_estimation::rpc::get_eth_estimated_fee_per_gas; 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, @@ -244,8 +243,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, "get_eth_estimated_fee_per_gas" => handle_mmrpc(ctx, request, get_eth_estimated_fee_per_gas).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 9285768bc1..772fd63449 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -2730,7 +2730,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 bfb7664f455f99f515fd5f79e3e201b89344454b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 5 May 2025 19:20:09 +0300 Subject: [PATCH 02/36] deps(timed-map): bump to 1.3.1 (#2413) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 321396ac58..51a278251f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6975,9 +6975,9 @@ dependencies = [ [[package]] name = "timed-map" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30565aee368a9b233f397f46cd803c59285b61d54c5b3ae378611bd467beecbe" +checksum = "07be2341cfbd1b8b9a84eb9212476ea383ef5cddeb85fa3ef89dc66666196619" dependencies = [ "rustc-hash", "web-time", From b4b7caf816b5687fa24e91f8fef50348d3d09521 Mon Sep 17 00:00:00 2001 From: dimxy Date: Tue, 6 May 2025 19:16:44 +0500 Subject: [PATCH 03/36] fix(UTXO): improve tx fee calculation and min relay fee handling (#2316) This commit fundamentally restructures the UTXO transaction building algorithm to fix issues with fee calculation and to address minimum relay fee requirements. The core change replaces the previous single-pass transaction assembly with an iterative approach that properly accounts for fee-output dependencies. --- mm2src/coins/lightning/ln_platform.rs | 10 +- mm2src/coins/lp_coins.rs | 6 +- mm2src/coins/qrc20.rs | 30 +- mm2src/coins/qrc20/qrc20_tests.rs | 14 +- .../rpc_command/lightning/open_channel.rs | 2 +- mm2src/coins/test_coin.rs | 2 +- mm2src/coins/utxo.rs | 58 +- mm2src/coins/utxo/bch.rs | 14 +- mm2src/coins/utxo/qtum.rs | 14 +- mm2src/coins/utxo/qtum_delegation.rs | 3 +- mm2src/coins/utxo/slp.rs | 26 +- .../utxo/utxo_builder/utxo_coin_builder.rs | 10 +- mm2src/coins/utxo/utxo_common.rs | 542 +++++++++-------- mm2src/coins/utxo/utxo_common_tests.rs | 2 +- mm2src/coins/utxo/utxo_standard.rs | 16 +- mm2src/coins/utxo/utxo_tests.rs | 547 +++++++++++++++++- mm2src/coins/utxo/utxo_withdraw.rs | 13 +- mm2src/coins/z_coin.rs | 22 +- 18 files changed, 911 insertions(+), 420 deletions(-) diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs index e7d57cb217..0d289aa8d0 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -568,7 +568,7 @@ impl FeeEstimator for Platform { ConfirmationTarget::Normal => self.confirmations_targets.normal, ConfirmationTarget::HighPriority => self.confirmations_targets.high_priority, }; - let fee_per_kb = tokio::task::block_in_place(move || { + let fee_rate = tokio::task::block_in_place(move || { block_on_f01(self.rpc_client().estimate_fee_sat( platform_coin.decimals(), // Todo: when implementing Native client detect_fee_method should be used for Native and @@ -582,16 +582,16 @@ impl FeeEstimator for Platform { // Set default fee to last known fee for the corresponding confirmation target match confirmation_target { - ConfirmationTarget::Background => self.latest_fees.set_background_fees(fee_per_kb), - ConfirmationTarget::Normal => self.latest_fees.set_normal_fees(fee_per_kb), - ConfirmationTarget::HighPriority => self.latest_fees.set_high_priority_fees(fee_per_kb), + ConfirmationTarget::Background => self.latest_fees.set_background_fees(fee_rate), + ConfirmationTarget::Normal => self.latest_fees.set_normal_fees(fee_rate), + ConfirmationTarget::HighPriority => self.latest_fees.set_high_priority_fees(fee_rate), }; // Must be no smaller than 253 (ie 1 satoshi-per-byte rounded up to ensure later round-downs don’t put us below 1 satoshi-per-byte). // https://docs.rs/lightning/0.0.101/lightning/chain/chaininterface/trait.FeeEstimator.html#tymethod.get_est_sat_per_1000_weight // This has changed in rust-lightning v0.0.110 as LDK currently wraps get_est_sat_per_1000_weight to ensure that the value returned is // no smaller than 253. https://github.com/lightningdevkit/rust-lightning/pull/1552 - (fee_per_kb as f64 / 4.0).ceil() as u32 + (fee_rate as f64 / 4.0).ceil() as u32 } } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 785711e3f2..e1adf1cdad 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2144,8 +2144,8 @@ pub trait MarketCoinOps { /// Is privacy coin like zcash or pirate fn is_privacy(&self) -> bool { false } - /// Is KMD coin - fn is_kmd(&self) -> bool { false } + /// Returns `true` for coins (like KMD) that should use direct DEX fee burning via OP_RETURN. + fn should_burn_directly(&self) -> bool { false } /// Should burn part of dex fee coin fn should_burn_dex_fee(&self) -> bool; @@ -3834,7 +3834,7 @@ impl DexFee { let dex_fee = trade_amount * &rate; let min_tx_amount = MmNumber::from(taker_coin.min_tx_amount()); - if taker_coin.is_kmd() { + if taker_coin.should_burn_directly() { // use a special dex fee option for kmd return Self::calc_dex_fee_for_op_return(dex_fee, min_tx_amount); } diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 3049a56ec2..ac10ba3151 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -11,11 +11,10 @@ use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoi UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_utxo_inputs_signed_by_pub, UtxoTxBuilder}; -use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, AddrFromStrError, BroadcastTxErr, FeePolicy, GenerateTxError, - GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, - UnsupportedAddr, UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, - UtxoFromLegacyReqErr, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, - UTXO_LOCK}; +use crate::utxo::{qtum, ActualFeeRate, AddrFromStrError, BroadcastTxErr, FeePolicy, GenerateTxError, GetUtxoListOps, + HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, UnsupportedAddr, + UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFromLegacyReqErr, + UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, Eip1559Ops, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, @@ -489,8 +488,8 @@ impl Qrc20Coin { /// `gas_fee` should be calculated by: gas_limit * gas_price * (count of contract calls), /// or should be sum of gas fee of all contract calls. pub async fn get_qrc20_tx_fee(&self, gas_fee: u64) -> Result { - match try_s!(self.get_tx_fee().await) { - ActualTxFee::Dynamic(amount) | ActualTxFee::FixedPerKb(amount) => Ok(amount + gas_fee), + match try_s!(self.get_fee_rate().await) { + ActualFeeRate::Dynamic(amount) | ActualFeeRate::FixedPerKb(amount) => Ok(amount + gas_fee), } } @@ -545,10 +544,9 @@ impl Qrc20Coin { self.utxo.conf.fork_id, )?; - let miner_fee = data.fee_amount + data.unused_change; Ok(GenerateQrc20TxResult { signed, - miner_fee, + miner_fee: data.fee_amount, gas_fee, }) } @@ -609,17 +607,13 @@ impl UtxoTxBroadcastOps for Qrc20Coin { #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for Qrc20Coin { /// Get only QTUM transaction fee. - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo).await } + async fn get_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: ScriptBytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index caf5546de2..01cf4adcad 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -59,8 +59,8 @@ pub fn qrc20_coin_for_test(priv_key: [u8; 32], fallback_swap: Option<&str>) -> ( (ctx, coin) } -fn check_tx_fee(coin: &Qrc20Coin, expected_tx_fee: ActualTxFee) { - let actual_tx_fee = block_on(coin.get_tx_fee()).unwrap(); +fn check_tx_fee(coin: &Qrc20Coin, expected_tx_fee: ActualFeeRate) { + let actual_tx_fee = block_on(coin.get_fee_rate()).unwrap(); assert_eq!(actual_tx_fee, expected_tx_fee); } @@ -712,7 +712,7 @@ fn test_get_trade_fee() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let actual_trade_fee = block_on_f01(coin.get_trade_fee()).unwrap(); let expected_trade_fee_amount = big_decimal_from_sat( @@ -739,7 +739,7 @@ fn test_sender_trade_preimage_zero_allowance() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let allowance = block_on(coin.allowance(coin.swap_contract_address)).expect("!allowance"); assert_eq!(allowance, 0.into()); @@ -775,7 +775,7 @@ fn test_sender_trade_preimage_with_allowance() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let allowance = block_on(coin.allowance(coin.swap_contract_address)).expect("!allowance"); assert_eq!(allowance, 300_000_000.into()); @@ -886,7 +886,7 @@ fn test_receiver_trade_preimage() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let actual = block_on_f01(coin.get_receiver_trade_fee(FeeApproxStage::WithoutApprox)).expect("!get_receiver_trade_fee"); @@ -911,7 +911,7 @@ fn test_taker_fee_tx_fee() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let expected_balance = CoinBalance { spendable: BigDecimal::from(5u32), unspendable: BigDecimal::from(0u32), diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs index fdb5b9caa9..095ee1c4ee 100644 --- a/mm2src/coins/rpc_command/lightning/open_channel.rs +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -174,7 +174,7 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes .with_fee_policy(fee_policy); let fee = platform_coin - .get_tx_fee() + .get_fee_rate() .await .map_err(|e| OpenChannelError::RpcError(e.to_string()))?; tx_builder = tx_builder.with_fee(fee); diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index ce2511b5b0..278492638c 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -109,7 +109,7 @@ impl MarketCoinOps for TestCoin { fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } - fn is_kmd(&self) -> bool { &self.ticker == "KMD" } + fn should_burn_directly(&self) -> bool { &self.ticker == "KMD" } fn should_burn_dex_fee(&self) -> bool { false } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 35e7281d4e..6fd2f28fe4 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -260,29 +260,61 @@ pub struct AdditionalTxData { pub received_by_me: u64, pub spent_by_me: u64, pub fee_amount: u64, - pub unused_change: u64, pub kmd_rewards: Option, } /// The fee set from coins config #[derive(Debug)] -pub enum TxFee { +pub enum FeeRate { /// Tell the coin that it should request the fee from daemon RPC and calculate it relying on tx size Dynamic(EstimateFeeMethod), /// Tell the coin that it has fixed tx fee per kb. FixedPerKb(u64), } -/// The actual "runtime" fee that is received from RPC in case of dynamic calculation +/// The actual "runtime" tx fee rate (per kb) that is received from RPC in case of dynamic calculation +/// or fixed tx fee rate #[derive(Copy, Clone, Debug, PartialEq)] -pub enum ActualTxFee { +pub enum ActualFeeRate { /// fee amount per Kbyte received from coin RPC Dynamic(u64), - /// Use specified amount per each 1 kb of transaction and also per each output less than amount. + /// Use specified fee amount per each 1 kb of transaction and also per each output less than the fee amount. /// Used by DOGE, but more coins might support it too. FixedPerKb(u64), } +impl ActualFeeRate { + fn get_tx_fee(&self, tx_size: u64) -> u64 { + match self { + ActualFeeRate::Dynamic(fee_rate) => (fee_rate * tx_size) / KILO_BYTE, + // return fee_rate here as swap spend transaction size is always less than 1 kb + ActualFeeRate::FixedPerKb(fee_rate) => { + let tx_size_kb = if tx_size % KILO_BYTE == 0 { + tx_size / KILO_BYTE + } else { + tx_size / KILO_BYTE + 1 + }; + fee_rate * tx_size_kb + }, + } + } + + /// Return extra tx fee for the change output as p2pkh + fn get_tx_fee_for_change(&self, tx_size: u64) -> u64 { + match self { + ActualFeeRate::Dynamic(fee_rate) => (*fee_rate * P2PKH_OUTPUT_LEN) / KILO_BYTE, + ActualFeeRate::FixedPerKb(fee_rate) => { + // take into account the change output if tx_size_kb(tx with change) > tx_size_kb(tx without change) + if tx_size % KILO_BYTE + P2PKH_OUTPUT_LEN > KILO_BYTE { + *fee_rate + } else { + 0 + } + }, + } + } +} + /// Fee policy applied on transaction creation pub enum FeePolicy { /// Send the exact amount specified in output(s), fee is added to spent input amount @@ -577,7 +609,7 @@ pub struct UtxoCoinFields { /// Emercoin has 6 /// Bitcoin Diamond has 7 pub decimals: u8, - pub tx_fee: TxFee, + pub tx_fee: FeeRate, /// Minimum transaction value at which the value is not less than fee pub dust_amount: u64, /// RPC client @@ -839,18 +871,15 @@ pub trait UtxoTxBroadcastOps { #[async_trait] #[cfg_attr(test, mockable)] pub trait UtxoTxGenerationOps { - async fn get_tx_fee(&self) -> UtxoRpcResult; + async fn get_fee_rate(&self) -> UtxoRpcResult; /// Calculates interest if the coin is KMD /// Adds the value to existing output to my_script_pub or creates additional interest output /// returns transaction and data as is if the coin is not KMD - async fn calc_interest_if_required( - &self, - mut unsigned: TransactionInputSigner, - mut data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)>; + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult; + + /// Returns `true` if this coin supports Komodo-style interest accrual; otherwise, returns `false`. + fn supports_interest(&self) -> bool; } /// The UTXO address balance scanner. @@ -1747,7 +1776,6 @@ where { let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let key_pair = try_tx_s!(coin.as_ref().priv_key_policy.activated_key_or_err()); - let mut builder = UtxoTxBuilder::new(coin) .await .add_available_inputs(unspents) diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index e38a59ce27..76a2f5d708 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -700,17 +700,13 @@ impl UtxoTxBroadcastOps for BchCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for BchCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 82da94209b..e3214552e6 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -313,17 +313,13 @@ impl UtxoTxBroadcastOps for QtumCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for QtumCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index b83289d97c..3723109720 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -294,10 +294,9 @@ impl QtumCoin { let signed = sign_tx(unsigned, key_pair, utxo.conf.signature_version, utxo.conf.fork_id)?; - let miner_fee = data.fee_amount + data.unused_change; let generated_tx = GenerateQrc20TxResult { signed, - miner_fee, + miner_fee: data.fee_amount, gas_fee, }; diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 1ca37e3eba..5229f07180 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -10,9 +10,9 @@ use crate::utxo::bch::BchCoin; use crate::utxo::bchd_grpc::{check_slp_transaction, validate_slp_utxos, ValidateSlpUtxosErr}; use crate::utxo::rpc_clients::{UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcResult}; use crate::utxo::utxo_common::{self, big_decimal_from_sat_unsigned, payment_script, UtxoTxBuilder}; -use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualTxFee, AdditionalTxData, BroadcastTxErr, - FeePolicy, GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, - UtxoCommonOps, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps}; +use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualFeeRate, BroadcastTxErr, FeePolicy, + GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, UtxoCommonOps, UtxoTx, + UtxoTxBroadcastOps, UtxoTxGenerationOps}; use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DerivationMethod, DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RawTransactionResult, @@ -1073,19 +1073,13 @@ impl UtxoTxBroadcastOps for SlpToken { #[async_trait] impl UtxoTxGenerationOps for SlpToken { - async fn get_tx_fee(&self) -> UtxoRpcResult { self.platform_coin.get_tx_fee().await } + async fn get_fee_rate(&self) -> UtxoRpcResult { self.platform_coin.get_fee_rate().await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - self.platform_coin - .calc_interest_if_required(unsigned, data, my_script_pub, dust) - .await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + self.platform_coin.calc_interest_if_required(unsigned).await } + + fn supports_interest(&self) -> bool { self.platform_coin.supports_interest() } } #[async_trait] @@ -1541,11 +1535,11 @@ impl MmCoin for SlpToken { match req.fee { Some(WithdrawFee::UtxoFixed { amount }) => { let fixed = sat_from_big_decimal(&amount, platform_decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::FixedPerKb(fixed)) + tx_builder = tx_builder.with_fee(ActualFeeRate::FixedPerKb(fixed)) }, Some(WithdrawFee::UtxoPerKbyte { amount }) => { let dynamic = sat_from_big_decimal(&amount, platform_decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::Dynamic(dynamic)); + tx_builder = tx_builder.with_fee(ActualFeeRate::Dynamic(dynamic)); }, Some(fee_policy) => { let error = format!( diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index b916afc232..80fd41c3ee 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -5,7 +5,7 @@ use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientSettings, ElectrumC use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::utxo_conf_builder::{UtxoConfBuilder, UtxoConfError}; -use crate::utxo::{output_script, ElectrumBuilderArgs, RecentlySpentOutPoints, TxFee, UtxoCoinConf, UtxoCoinFields, +use crate::utxo::{output_script, ElectrumBuilderArgs, FeeRate, RecentlySpentOutPoints, UtxoCoinConf, UtxoCoinFields, UtxoHDWallet, UtxoRpcMode, UtxoSyncStatus, UtxoSyncStatusLoopHandle, UTXO_DUST_AMOUNT}; use crate::{BlockchainNetwork, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, @@ -467,9 +467,9 @@ pub trait UtxoCoinBuilderCommonOps { Ok(self.conf()["decimals"].as_u64().unwrap_or(8) as u8) } - async fn tx_fee(&self, rpc_client: &UtxoRpcClientEnum) -> UtxoCoinBuildResult { + async fn tx_fee(&self, rpc_client: &UtxoRpcClientEnum) -> UtxoCoinBuildResult { let tx_fee = match self.conf()["txfee"].as_u64() { - None => TxFee::FixedPerKb(1000), + None => FeeRate::FixedPerKb(1000), Some(0) => { let fee_method = match &rpc_client { UtxoRpcClientEnum::Electrum(_) => EstimateFeeMethod::Standard, @@ -479,9 +479,9 @@ pub trait UtxoCoinBuilderCommonOps { .await .map_to_mm(UtxoCoinBuildError::ErrorDetectingFeeMethod)?, }; - TxFee::Dynamic(fee_method) + FeeRate::Dynamic(fee_method) }, - Some(fee) => TxFee::FixedPerKb(fee), + Some(fee) => FeeRate::FixedPerKb(fee), }; Ok(tx_fee) } diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index db7ad1f65a..2e792f05ac 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -78,9 +78,9 @@ pub const DEFAULT_SWAP_VOUT: usize = 0; pub const DEFAULT_SWAP_VIN: usize = 0; const MIN_BTC_TRADING_VOL: &str = "0.00777"; -macro_rules! true_or { +macro_rules! return_err_if { ($cond: expr, $etype: expr) => { - if !$cond { + if $cond { return Err(MmError::new($etype)); } }; @@ -95,18 +95,18 @@ lazy_static! { pub const HISTORY_TOO_LARGE_ERR_CODE: i64 = -1; -pub async fn get_tx_fee(coin: &UtxoCoinFields) -> UtxoRpcResult { +pub async fn get_fee_rate(coin: &UtxoCoinFields) -> UtxoRpcResult { let conf = &coin.conf; match &coin.tx_fee { - TxFee::Dynamic(method) => { - let fee = coin + FeeRate::Dynamic(method) => { + let fee_rate = coin .rpc_client .estimate_fee_sat(coin.decimals, method, &conf.estimate_fee_mode, conf.estimate_fee_blocks) .compat() .await?; - Ok(ActualTxFee::Dynamic(fee)) + Ok(ActualFeeRate::Dynamic(fee_rate)) }, - TxFee::FixedPerKb(satoshis) => Ok(ActualTxFee::FixedPerKb(*satoshis)), + FeeRate::FixedPerKb(satoshis) => Ok(ActualFeeRate::FixedPerKb(*satoshis)), } } @@ -270,37 +270,22 @@ where pub fn derivation_method(coin: &UtxoCoinFields) -> &DerivationMethod { &coin.derivation_method } -/// returns the fee required to be paid for HTLC spend transaction +/// returns the tx fee required to be paid for HTLC spend transaction pub async fn get_htlc_spend_fee( coin: &T, tx_size: u64, stage: &FeeApproxStage, ) -> UtxoRpcResult { - let coin_fee = coin.get_tx_fee().await?; - let mut fee = match coin_fee { - // atomic swap payment spend transaction is slightly more than 300 bytes in average as of now - ActualTxFee::Dynamic(fee_per_kb) => { - let fee_per_kb = increase_dynamic_fee_by_stage(&coin, fee_per_kb, stage); - (fee_per_kb * tx_size) / KILO_BYTE - }, - // return satoshis here as swap spend transaction size is always less than 1 kb - ActualTxFee::FixedPerKb(satoshis) => { - let tx_size_kb = if tx_size % KILO_BYTE == 0 { - tx_size / KILO_BYTE - } else { - tx_size / KILO_BYTE + 1 - }; - satoshis * tx_size_kb + let fee_rate = coin.get_fee_rate().await?; + let fee_rate = match fee_rate { + ActualFeeRate::Dynamic(dynamic_fee_rate) => { + // increase dynamic fee for a chance if it grows in the swap + ActualFeeRate::Dynamic(increase_dynamic_fee_by_stage(coin, dynamic_fee_rate, stage)) }, + ActualFeeRate::FixedPerKb(_) => fee_rate, }; - if coin.as_ref().conf.force_min_relay_fee { - let relay_fee = coin.as_ref().rpc_client.get_relay_fee().compat().await?; - let relay_fee_sat = sat_from_big_decimal(&relay_fee, coin.as_ref().decimals)?; - if fee < relay_fee_sat { - fee = relay_fee_sat; - } - } - Ok(fee) + let min_relay_fee_rate = get_min_relay_rate(coin).await?; + Ok(get_tx_fee_with_relay_fee(&fee_rate, tx_size, min_relay_fee_rate)) } pub fn addresses_from_script(coin: &T, script: &Script) -> Result, String> { @@ -469,18 +454,21 @@ pub fn output_script_checked(coin: &UtxoCoinFields, addr: &Address) -> MmResult< pub struct UtxoTxBuilder<'a, T: AsRef + UtxoTxGenerationOps> { coin: &'a T, from: Option
, + /// The required inputs that *must* be added in the resulting tx + required_inputs: Vec, /// The available inputs that *can* be included in the resulting tx available_inputs: Vec, + outputs: Vec, fee_policy: FeePolicy, - fee: Option, + fee: Option, gas_fee: Option, tx: TransactionInputSigner, - change: u64, sum_inputs: u64, - sum_outputs_value: u64, - tx_fee: u64, - min_relay_fee: Option, + sum_outputs: u64, + tx_fee_needed: u64, + min_relay_fee_rate: Option, dust: Option, + interest: u64, } impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { @@ -489,16 +477,18 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { tx: coin.as_ref().transaction_preimage(), coin, from: coin.as_ref().derivation_method.single_addr().await, + required_inputs: vec![], available_inputs: vec![], + outputs: vec![], fee_policy: FeePolicy::SendExact, fee: None, gas_fee: None, - change: 0, sum_inputs: 0, - sum_outputs_value: 0, - tx_fee: 0, - min_relay_fee: None, + sum_outputs: 0, + tx_fee_needed: 0, + min_relay_fee_rate: None, dust: None, + interest: 0, } } @@ -513,14 +503,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } pub fn add_required_inputs(mut self, inputs: impl IntoIterator) -> Self { - self.tx - .inputs - .extend(inputs.into_iter().map(|input| UnsignedTransactionInput { - previous_output: input.outpoint, - prev_script: input.script, - sequence: SEQUENCE_FINAL, - amount: input.value, - })); + self.required_inputs.extend(inputs); self } @@ -532,7 +515,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } pub fn add_outputs(mut self, outputs: impl IntoIterator) -> Self { - self.tx.outputs.extend(outputs); + self.outputs.extend(outputs); self } @@ -541,7 +524,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { self } - pub fn with_fee(mut self, fee: ActualTxFee) -> Self { + pub fn with_fee(mut self, fee: ActualFeeRate) -> Self { self.fee = Some(fee); self } @@ -554,75 +537,136 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { self } - /// Recalculates fee and checks whether transaction is complete (inputs collected cover the outputs) - fn update_fee_and_check_completeness( - &mut self, - from_addr_format: &UtxoAddressFormat, - actual_tx_fee: &ActualTxFee, - ) -> bool { - self.tx_fee = match &actual_tx_fee { - ActualTxFee::Dynamic(f) => { - let transaction = UtxoTx::from(self.tx.clone()); - let v_size = tx_size_in_v_bytes(from_addr_format, &transaction); - (f * v_size as u64) / KILO_BYTE - }, - ActualTxFee::FixedPerKb(f) => { - let transaction = UtxoTx::from(self.tx.clone()); - let v_size = tx_size_in_v_bytes(from_addr_format, &transaction) as u64; - let v_size_kb = if v_size % KILO_BYTE == 0 { - v_size / KILO_BYTE - } else { - v_size / KILO_BYTE + 1 - }; - f * v_size_kb + fn required_amount(&self) -> u64 { + let mut sum_output = self + .outputs + .iter() + .fold(0u64, |required, output| required + output.value); + match self.fee_policy { + FeePolicy::SendExact => { + sum_output += self.total_tx_fee_needed(); }, + FeePolicy::DeductFromOutput(_) => {}, }; + sum_output + } + fn add_tx_inputs(&mut self, amount: u64) -> u64 { + self.tx.inputs.clear(); + let mut total = 0u64; + for utxo in &self.required_inputs { + self.tx.inputs.push(UnsignedTransactionInput { + previous_output: utxo.outpoint, + prev_script: utxo.script.clone(), + sequence: SEQUENCE_FINAL, + amount: utxo.value, + }); + total += utxo.value; + } + for utxo in &self.available_inputs { + if total >= amount { + break; + } + self.tx.inputs.push(UnsignedTransactionInput { + previous_output: utxo.outpoint, + prev_script: utxo.script.clone(), + sequence: SEQUENCE_FINAL, + amount: utxo.value, + }); + total += utxo.value; + } + total + } + + fn add_tx_outputs(&mut self) -> u64 { + self.tx.outputs.clear(); + let mut total = 0u64; + for output in self.outputs.clone() { + total += output.value; + self.tx.outputs.push(output); + } + total + } + + fn make_kmd_rewards_data(coin: &T, interest: u64) -> Option { + let rewards_amount = big_decimal_from_sat_unsigned(interest, coin.as_ref().decimals); + if coin.supports_interest() { + Some(KmdRewardsDetails::claimed_by_me(rewards_amount)) + } else { + None + } + } + + /// Adds change output. + /// Returns change value and dust change + fn add_change(&mut self, change_script_pubkey: &Bytes) -> u64 { + let sum_output_with_fee = self.sum_outputs + self.total_tx_fee_needed(); + if self.sum_inputs < sum_output_with_fee { + return 0u64; + } + let change = self.sum_inputs + self.interest - sum_output_with_fee; + if change < self.dust() { + return 0u64; + }; + self.tx.outputs.push({ + TransactionOutput { + value: change, + script_pubkey: change_script_pubkey.clone(), + } + }); + change + } + + /// Recalculates tx fee for tx size. + /// If needed, checks if tx fee is not less than min relay tx fee + fn update_tx_fee(&mut self, from_addr_format: &UtxoAddressFormat, fee_rate: &ActualFeeRate) { + let transaction = UtxoTx::from(self.tx.clone()); + let v_size = tx_size_in_v_bytes(from_addr_format, &transaction) as u64; + self.tx_fee_needed = get_tx_fee_with_relay_fee(fee_rate, v_size, self.min_relay_fee_rate); + } + + /// Deduct tx fee from output if requested by fee_policy + fn deduct_txfee_from_output(&mut self) -> MmResult { match self.fee_policy { - FeePolicy::SendExact => { - let mut outputs_plus_fee = self.sum_outputs_value + self.tx_fee; - if self.sum_inputs >= outputs_plus_fee { - self.change = self.sum_inputs - outputs_plus_fee; - if self.change > self.dust() { - // there will be change output - if let ActualTxFee::Dynamic(ref f) = actual_tx_fee { - self.tx_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; - outputs_plus_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; - } - } - if let Some(min_relay) = self.min_relay_fee { - if self.tx_fee < min_relay { - let fee_diff = min_relay - self.tx_fee; - outputs_plus_fee += fee_diff; - self.tx_fee += fee_diff; - } - } - self.sum_inputs >= outputs_plus_fee - } else { - false - } - }, - FeePolicy::DeductFromOutput(_) => { - if self.sum_inputs >= self.sum_outputs_value { - self.change = self.sum_inputs - self.sum_outputs_value; - if self.change > self.dust() { - if let ActualTxFee::Dynamic(ref f) = actual_tx_fee { - self.tx_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; - } - } - if let Some(min_relay) = self.min_relay_fee { - if self.tx_fee < min_relay { - self.tx_fee = min_relay; - } - } - true - } else { - false - } + FeePolicy::SendExact => Ok(0), + FeePolicy::DeductFromOutput(i) => { + let tx_fee = self.total_tx_fee_needed(); + let min_output = tx_fee + self.dust(); + let val = self.tx.outputs[i].value; + return_err_if!(val < min_output, GenerateTxError::DeductFeeFromOutputFailed { + output_idx: i, + output_value: val, + required: min_output, + }); + self.tx.outputs[i].value -= tx_fee; + Ok(tx_fee) }, } } + fn validate_not_dust(&self) -> MmResult<(), GenerateTxError> { + for output in self.outputs.iter() { + let script: Script = output.script_pubkey.clone().into(); + if script.opcodes().next() != Some(Ok(Opcode::OP_RETURN)) { + return_err_if!(output.value < self.dust(), GenerateTxError::OutputValueLessThanDust { + value: output.value, + dust: self.dust() + }); + } + } + Ok(()) + } + + fn sum_received_by_me(&self, change_script_pubkey: &Bytes) -> u64 { + self.tx.outputs.iter().fold(0u64, |received_by_me, output| { + if &output.script_pubkey == change_script_pubkey { + received_by_me + output.value + } else { + received_by_me + } + }) + } + fn dust(&self) -> u64 { match self.dust { Some(dust) => dust, @@ -630,143 +674,98 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } } + fn total_tx_fee_needed(&self) -> u64 { self.tx_fee_needed + self.gas_fee.unwrap_or(0u64) } + + fn tx_fee_fact(&self) -> MmResult { + (self.sum_inputs + self.interest) + .checked_sub(self.gas_fee.unwrap_or_default()) + .or_mm_err(|| GenerateTxError::Internal("gas_fee underflow".to_owned()))? + .checked_sub(self.sum_outputs) + .or_mm_err(|| GenerateTxError::Internal("sum_outputs underflow".to_owned())) + } + /// Generates unsigned transaction (TransactionInputSigner) from specified utxos and outputs. /// sends the change (inputs amount - outputs amount) to the [`UtxoTxBuilder::from`] address. /// Also returns additional transaction data pub async fn build(mut self) -> GenerateTxResult { let coin = self.coin; - let dust: u64 = self.dust(); let from = self .from .clone() .or_mm_err(|| GenerateTxError::Internal("'from' address is not specified".to_owned()))?; let change_script_pubkey = output_script(&from).map(|script| script.to_bytes())?; - let actual_tx_fee = match self.fee { + let actual_fee_rate = match self.fee { Some(fee) => fee, - None => coin.get_tx_fee().await?, + None => coin.get_fee_rate().await?, }; - true_or!(!self.tx.outputs.is_empty(), GenerateTxError::EmptyOutputs); + return_err_if!(self.outputs.is_empty(), GenerateTxError::EmptyOutputs); - let mut received_by_me = 0; - for output in self.tx.outputs.iter() { - let script: Script = output.script_pubkey.clone().into(); - if script.opcodes().next() != Some(Ok(Opcode::OP_RETURN)) { - true_or!(output.value >= dust, GenerateTxError::OutputValueLessThanDust { - value: output.value, - dust - }); - } - self.sum_outputs_value += output.value; - if output.script_pubkey == change_script_pubkey { - received_by_me += output.value; - } - } - - if let Some(gas_fee) = self.gas_fee { - self.sum_outputs_value += gas_fee; - } + self.validate_not_dust()?; - true_or!( - !self.available_inputs.is_empty() || !self.tx.inputs.is_empty(), + return_err_if!( + self.available_inputs.is_empty() && self.tx.inputs.is_empty(), GenerateTxError::EmptyUtxoSet { - required: self.sum_outputs_value + required: self.required_amount() } ); - self.min_relay_fee = if coin.as_ref().conf.force_min_relay_fee { - let fee_dec = coin.as_ref().rpc_client.get_relay_fee().compat().await?; - let min_relay_fee = sat_from_big_decimal(&fee_dec, coin.as_ref().decimals)?; - Some(min_relay_fee) - } else { - None - }; - - // The function `update_fee_and_check_completeness` checks if the total value of the current inputs - // (added using add_required_inputs or directly) is enough to cover the transaction outputs and fees. - // If it returns `true`, it indicates that no additional inputs are needed from the available inputs, - // and we can skip the loop that adds these additional inputs. - if !self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { - for utxo in self.available_inputs.clone() { - self.tx.inputs.push(UnsignedTransactionInput { - previous_output: utxo.outpoint, - prev_script: utxo.script, - sequence: SEQUENCE_FINAL, - amount: utxo.value, - }); - self.sum_inputs += utxo.value; + self.min_relay_fee_rate = get_min_relay_rate(coin).await?; - if self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { - break; - } + let mut one_time_fee_update = false; + loop { + let required_amount_0 = self.required_amount(); + self.sum_inputs = self.add_tx_inputs(required_amount_0); + self.sum_outputs = self.add_tx_outputs(); + self.interest = coin.calc_interest_if_required(&mut self.tx).await?; + + // try once tx_fee without the change output (if maybe txfee fits between total inputs and outputs) + if !one_time_fee_update { + self.update_tx_fee(from.addr_format(), &actual_fee_rate); + one_time_fee_update = true; } - } - - match self.fee_policy { - FeePolicy::SendExact => self.sum_outputs_value += self.tx_fee, - FeePolicy::DeductFromOutput(i) => { - let min_output = self.tx_fee + dust; - let val = self.tx.outputs[i].value; - true_or!(val >= min_output, GenerateTxError::DeductFeeFromOutputFailed { - output_idx: i, - output_value: val, - required: min_output, - }); - self.tx.outputs[i].value -= self.tx_fee; - if self.tx.outputs[i].script_pubkey == change_script_pubkey { - received_by_me -= self.tx_fee; - } - }, - }; - true_or!( - self.sum_inputs >= self.sum_outputs_value, - GenerateTxError::NotEnoughUtxos { + return_err_if!(self.sum_inputs < required_amount_0, GenerateTxError::NotEnoughUtxos { sum_utxos: self.sum_inputs, - required: self.sum_outputs_value - } - ); - - let change = self.sum_inputs - self.sum_outputs_value; - let unused_change = if change > dust { - self.tx.outputs.push({ - TransactionOutput { - value: change, - script_pubkey: change_script_pubkey.clone(), - } + required: self.required_amount(), // send updated required amount, with txfee }); - received_by_me += change; - 0 - } else { - change - }; + + self.sum_outputs = self + .sum_outputs + .checked_sub(self.deduct_txfee_from_output()?) + .or_mm_err(|| GenerateTxError::Internal("sum_outputs underflow".to_owned()))?; + let change = self.add_change(&change_script_pubkey); + self.sum_outputs += change; + self.update_tx_fee(from.addr_format(), &actual_fee_rate); // recalculate txfee with the change output, if added + if self.sum_inputs + self.interest >= self.sum_outputs + self.total_tx_fee_needed() { + break; + } + } let data = AdditionalTxData { - fee_amount: self.tx_fee, - received_by_me, + fee_amount: self.tx_fee_fact()?, // we return only txfee here (w/o gas_fee) + received_by_me: self.sum_received_by_me(&change_script_pubkey), spent_by_me: self.sum_inputs, - unused_change, // will be changed if the ticker is KMD - kmd_rewards: None, + kmd_rewards: Self::make_kmd_rewards_data(coin, self.interest), }; - Ok(coin - .calc_interest_if_required(self.tx, data, change_script_pubkey, dust) - .await?) + Ok((self.tx, data)) } /// Generates unsigned transaction (TransactionInputSigner) from specified utxos and outputs. /// Adds or updates inputs with UnspentInfo /// Does not do any checks or add any outputs pub async fn build_unchecked(mut self) -> Result> { + self.sum_outputs = 0u64; for output in self.tx.outputs.iter() { - self.sum_outputs_value += output.value; + self.sum_outputs += output.value; } - true_or!( - !self.available_inputs.is_empty() || !self.tx.inputs.is_empty(), + return_err_if!( + self.available_inputs.is_empty() && self.tx.inputs.is_empty(), GenerateTxError::EmptyUtxoSet { - required: self.sum_outputs_value + required: self.sum_outputs } ); @@ -803,61 +802,62 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { /// returns transaction and data as is if the coin is not KMD pub async fn calc_interest_if_required( coin: &T, - mut unsigned: TransactionInputSigner, - mut data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, -) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - if coin.as_ref().conf.ticker != "KMD" { - return Ok((unsigned, data)); + unsigned: &mut TransactionInputSigner, +) -> UtxoRpcResult { + if !coin.supports_interest() { + return Ok(0); } unsigned.lock_time = coin.get_current_mtp().await?; let mut interest = 0; + let prev_hashes = unsigned + .inputs + .iter() + .map(|input| input.previous_output.hash.reversed().into()) + .collect::>(); + let prev_txns = get_verbose_transactions_from_cache_or_rpc(coin.as_ref(), prev_hashes).await?; for input in unsigned.inputs.iter() { let prev_hash = input.previous_output.hash.reversed().into(); - let tx = coin - .as_ref() - .rpc_client - .get_verbose_transaction(&prev_hash) - .compat() - .await?; + let tx = prev_txns + .get(&prev_hash) + .ok_or(MmError::new(UtxoRpcError::Internal("previous tx not found".to_owned())))? + .to_inner(); if let Ok(output_interest) = kmd_interest(tx.height, input.amount, tx.locktime as u64, unsigned.lock_time as u64) { interest += output_interest; }; } - if interest > 0 { - data.received_by_me += interest; - let mut output_to_me = unsigned - .outputs - .iter_mut() - .find(|out| out.script_pubkey == my_script_pub); - // add calculated interest to existing output to my address - // or create the new one if it's not found - match output_to_me { - Some(ref mut output) => output.value += interest, - None => { - let maybe_change_output_value = interest + data.unused_change; - if maybe_change_output_value > dust { - let change_output = TransactionOutput { - script_pubkey: my_script_pub, - value: maybe_change_output_value, - }; - unsigned.outputs.push(change_output); - data.unused_change = 0; - } else { - data.unused_change += interest; - } - }, - }; - } else { + if interest == 0 { // if interest is zero attempt to set the lowest possible lock_time to claim it later unsigned.lock_time = now_sec_u32() - 3600 + 777 * 2; } - let rewards_amount = big_decimal_from_sat_unsigned(interest, coin.as_ref().decimals); - data.kmd_rewards = Some(KmdRewardsDetails::claimed_by_me(rewards_amount)); - Ok((unsigned, data)) + Ok(interest) +} + +pub fn is_kmd(coin: &T) -> bool { &coin.as_ref().conf.ticker == "KMD" } + +/// Helper to get min relay fee rate and convert to sat +async fn get_min_relay_rate + UtxoTxGenerationOps>(coin: &T) -> UtxoRpcResult> { + if coin.as_ref().conf.force_min_relay_fee { + let fee_dec = coin.as_ref().rpc_client.get_relay_fee().compat().await?; + let min_relay_fee_rate = sat_from_big_decimal(&fee_dec, coin.as_ref().decimals)?; + Ok(Some(min_relay_fee_rate)) + } else { + Ok(None) + } +} + +/// Helper to get tx fee if min relay rate is known +fn get_tx_fee_with_relay_fee(fee_rate: &ActualFeeRate, tx_size: u64, min_relay_fee_rate: Option) -> u64 { + let tx_fee = fee_rate.get_tx_fee(tx_size); + if let Some(min_relay_fee_rate) = min_relay_fee_rate { + let min_relay_dynamic_fee_rate = ActualFeeRate::Dynamic(min_relay_fee_rate); + let min_relay_tx_fee = min_relay_dynamic_fee_rate.get_tx_fee(tx_size); + if tx_fee < min_relay_tx_fee { + return min_relay_tx_fee; + } + } + tx_fee } pub struct P2SHSpendingTxInput<'a> { @@ -3900,7 +3900,7 @@ pub async fn calc_interest_of_tx( tx: &UtxoTx, input_transactions: &mut HistoryUtxoTxMap, ) -> UtxoRpcResult { - if coin.as_ref().conf.ticker != "KMD" { + if !coin.supports_interest() { let error = format!("Expected KMD ticker, found {}", coin.as_ref().conf.ticker); return MmError::err(UtxoRpcError::Internal(error)); } @@ -3935,10 +3935,10 @@ pub fn get_trade_fee(coin: T) -> Box f, - ActualTxFee::FixedPerKb(f) => f, + ActualFeeRate::Dynamic(f) => f, + ActualFeeRate::FixedPerKb(f) => f, }; Ok(TradeFee { coin: ticker, @@ -3973,28 +3973,27 @@ where T: UtxoCommonOps + GetUtxoListOps, { let decimals = coin.as_ref().decimals; - let tx_fee = coin.get_tx_fee().await?; + let fee_rate = coin.get_fee_rate().await?; // [`FeePolicy::DeductFromOutput`] is used if the value is [`TradePreimageValue::UpperBound`] only let is_amount_upper_bound = matches!(fee_policy, FeePolicy::DeductFromOutput(_)); let my_address = coin.as_ref().derivation_method.single_addr_or_err().await?; - match tx_fee { + match fee_rate { // if it's a dynamic fee, we should generate a swap transaction to get an actual trade fee - ActualTxFee::Dynamic(fee) => { - // take into account that the dynamic tx fee may increase during the swap - let dynamic_fee = coin.increase_dynamic_fee_by_stage(fee, stage); + ActualFeeRate::Dynamic(fee_rate) => { + // take into account that the dynamic tx fee rate may increase during the swap + let dynamic_fee_rate = coin.increase_dynamic_fee_by_stage(fee_rate, stage); let outputs_count = outputs.len(); let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; - let actual_tx_fee = ActualTxFee::Dynamic(dynamic_fee); - + let actual_fee_rate = ActualFeeRate::Dynamic(dynamic_fee_rate); let mut tx_builder = UtxoTxBuilder::new(coin) .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy) - .with_fee(actual_tx_fee); + .with_fee(actual_fee_rate); if let Some(gas) = gas_fee { tx_builder = tx_builder.with_gas_fee(gas); } @@ -4002,26 +4001,26 @@ where TradePreimageError::from_generate_tx_error(e, ticker.to_owned(), decimals, is_amount_upper_bound) })?; + // We need to add extra tx fee for the absent change output for e.g. to ensure max_taker_vol is calculated correctly + // (If we do not do this then in a swap the change output may appear and we may not have sufficient balance to pay taker fee) let total_fee = if tx.outputs.len() == outputs_count { // take into account the change output - data.fee_amount + (dynamic_fee * P2PKH_OUTPUT_LEN) / KILO_BYTE + data.fee_amount + actual_fee_rate.get_tx_fee_for_change(0) } else { // the change output is included already data.fee_amount }; - Ok(big_decimal_from_sat(total_fee as i64, decimals)) }, - ActualTxFee::FixedPerKb(fee) => { + ActualFeeRate::FixedPerKb(_fee) => { let outputs_count = outputs.len(); let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; - let mut tx_builder = UtxoTxBuilder::new(coin) .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy) - .with_fee(tx_fee); + .with_fee(fee_rate); if let Some(gas) = gas_fee { tx_builder = tx_builder.with_gas_fee(gas); } @@ -4029,20 +4028,17 @@ where TradePreimageError::from_generate_tx_error(e, ticker.to_string(), decimals, is_amount_upper_bound) })?; + // We need to add extra tx fee for the absent change output for e.g. to ensure max_taker_vol is calculated correctly + // (If we do not do this then in a swap the change output may appear and we may not have sufficient balance to pay taker fee) let total_fee = if tx.outputs.len() == outputs_count { - // take into account the change output if tx_size_kb(tx with change) > tx_size_kb(tx without change) let tx = UtxoTx::from(tx); let tx_bytes = serialize(&tx); - if tx_bytes.len() as u64 % KILO_BYTE + P2PKH_OUTPUT_LEN > KILO_BYTE { - data.fee_amount + fee - } else { - data.fee_amount - } + // take into account the change output + data.fee_amount + fee_rate.get_tx_fee_for_change(tx_bytes.len() as u64) } else { // the change output is included already data.fee_amount }; - Ok(big_decimal_from_sat(total_fee as i64, decimals)) }, } diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index d432207d8e..4203f4d8ba 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -136,7 +136,7 @@ pub(super) fn utxo_coin_fields_for_test( }, decimals: TEST_COIN_DECIMALS, dust_amount: UTXO_DUST_AMOUNT, - tx_fee: TxFee::FixedPerKb(1000), + tx_fee: FeeRate::FixedPerKb(1000), rpc_client, priv_key_policy, derivation_method, diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 99a97d846f..d8191dfef6 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -110,17 +110,13 @@ impl UtxoTxBroadcastOps for UtxoStandardCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for UtxoStandardCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] @@ -915,7 +911,7 @@ impl MarketCoinOps for UtxoStandardCoin { fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } - fn is_kmd(&self) -> bool { &self.utxo_arc.conf.ticker == "KMD" } + fn should_burn_directly(&self) -> bool { &self.utxo_arc.conf.ticker == "KMD" } fn should_burn_dex_fee(&self) -> bool { utxo_common::should_burn_dex_fee() } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 7b9d39d38b..16831a0579 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -50,6 +50,7 @@ use mm2_test_helpers::electrums::doc_electrums; use mm2_test_helpers::for_tests::{electrum_servers_rpc, mm_ctx_with_custom_db, DOC_ELECTRUM_ADDRS, MARTY_ELECTRUM_ADDRS, T_BCH_ELECTRUMS}; use mocktopus::mocking::*; +use rand::{rngs::ThreadRng, Rng}; use rpc::v1::types::H256 as H256Json; use serialization::{deserialize, CoinVariant, CompactInteger, Reader}; use spv_validation::conf::{BlockHeaderValidationParams, SPVBlockHeader}; @@ -227,8 +228,7 @@ fn test_generate_transaction() { // so no extra outputs should appear in generated transaction assert_eq!(generated.0.outputs.len(), 1); - assert_eq!(generated.1.fee_amount, 1000); - assert_eq!(generated.1.unused_change, 999); + assert_eq!(generated.1.fee_amount, 1000 + 999); assert_eq!(generated.1.received_by_me, 0); assert_eq!(generated.1.spent_by_me, 100000); @@ -255,7 +255,6 @@ fn test_generate_transaction() { assert_eq!(generated.0.outputs.len(), 1); assert_eq!(generated.1.fee_amount, 1000); - assert_eq!(generated.1.unused_change, 0); assert_eq!(generated.1.received_by_me, 99000); assert_eq!(generated.1.spent_by_me, 100000); assert_eq!(generated.0.outputs[0].value, 99000); @@ -710,9 +709,9 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); - // The resulting transaction size might be 210 or 211 bytes depending on signature size - // MM2 always expects the worst case during fee calculation - // 0.1 * 211 / 1000 = 0.0211 + // The resulting transaction size might be 210 or 211 bytes (no change output) depending on signature size + // MM2 always expects the worst case during fee calculation: + // tx_fee = 0.1 * 211 / 1000 = 0.0211 let expected_fee = Some( UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -895,10 +894,10 @@ fn test_withdraw_kmd_rewards_impl( }); UtxoStandardCoin::get_current_mtp .mock_safe(move |_fields| MockResult::Return(Box::pin(futures::future::ok(current_mtp)))); - NativeClient::get_verbose_transaction.mock_safe(move |_coin, txid| { - let expected: H256Json = <[u8; 32]>::from_hex(tx_hash).unwrap().into(); - assert_eq!(*txid, expected); - MockResult::Return(Box::new(futures01::future::ok(verbose.clone()))) + NativeClient::get_verbose_transactions.mock_safe(move |_coin, txids| { + let expected = <[u8; 32]>::from_hex(tx_hash).unwrap().into(); + assert_eq!(txids, &[expected]); + MockResult::Return(Box::new(futures01::future::ok([verbose.clone()].into()))) }); let client = NativeClient(Arc::new(NativeClientImpl::default())); @@ -1197,19 +1196,162 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower() { let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) - .with_fee(ActualTxFee::Dynamic(100)); + .with_fee(ActualFeeRate::Dynamic(100)); let generated = block_on(builder.build()).unwrap(); - assert_eq!(generated.0.outputs.len(), 1); + assert_eq!(generated.0.outputs.len(), 2); // generated transaction fee must be equal to relay fee if calculated dynamic fee is lower than relay - assert_eq!(generated.1.fee_amount, 100000000); - assert_eq!(generated.1.unused_change, 0); - assert_eq!(generated.1.received_by_me, 0); + assert_eq!(generated.1.fee_amount, 22000000); + assert_eq!(generated.1.received_by_me, 78000000); assert_eq!(generated.1.spent_by_me, 1000000000); assert!(unsafe { GET_RELAY_FEE_CALLED }); } +/// Test the transaction builder calculations (with random generated values) +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_generate_transaction_random_values() { + let client = NativeClientImpl::default(); + let mut rng = rand::thread_rng(); + + // tx_size for zcash, no shielded + let est_tx_size = |n_inputs: usize, n_outputs: usize| { + 4 + 4 + + 1 + + (n_inputs as u64) * (1 + 1 + 72 + 1 + 33 + 32 + 4 + 4) + + 1 + + (n_outputs as u64) * (1 + 25 + 8) + + 4 + + 4 + + 8 + + 1 + + 1 + + 1 + }; + + let make_random_vec_u64 = |rng: &mut ThreadRng, max_size: usize, max_value: u64| { + let vsize = rng.gen_range(1, max_size); + let mut v = vec![]; + for _i in 0..vsize { + v.push(rng.gen_range(0, max_value)) + } + v + }; + + NativeClient::get_relay_fee + .mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok("0.0".parse().unwrap())))); + let client = UtxoRpcClientEnum::Native(NativeClient(Arc::new(client))); + let mut coin = utxo_coin_fields_for_test(client, None, false); + coin.conf.force_min_relay_fee = false; + let coin = utxo_coin_from_fields(coin); + + for _i in 0..9999 { + let input_vals = make_random_vec_u64(&mut rng, 100, 100_000); + let output_vals = make_random_vec_u64(&mut rng, 100, 100_000); + let dust = rng.gen_range(0, 1000); + let fee_rate = rng.gen_range(0, 1000); + + let mut total_inputs = 0_u64; + let mut unspents = vec![]; + for val in &input_vals { + unspents.push(UnspentInfo { + value: *val, + outpoint: OutPoint::default(), + height: Default::default(), + script: Vec::new().into(), + }); + total_inputs += *val; + } + + let mut has_dust_output = false; + let mut outputs = vec![]; + let mut total_outputs = 0_u64; + for val in &output_vals { + outputs.push(TransactionOutput { + script_pubkey: "76a914124b0846223ef78130b8e544b9afc3b09988238688ac".into(), + value: *val, + }); + if *val < dust { + has_dust_output = true; + } + total_outputs += *val; + } + + let builder = block_on(UtxoTxBuilder::new(&coin)) + .add_available_inputs(unspents) + .add_outputs(outputs.clone()) + .with_dust(dust) + .with_fee(ActualFeeRate::Dynamic(fee_rate)); + + let result = block_on(builder.build()); + if has_dust_output { + let is_err_dust = matches!( + result.unwrap_err().get_inner(), + GenerateTxError::OutputValueLessThanDust { value: _, dust: _ } + ); + assert!(is_err_dust); + continue; + } + if let Err(ref err) = result { + let tx_size_max = est_tx_size(input_vals.len(), output_vals.len() + 1); + let tx_fee_max = fee_rate * tx_size_max / 1000; + if matches!(err.get_inner(), GenerateTxError::NotEnoughUtxos { + sum_utxos: _, + required: _ + }) { + assert!(total_inputs < total_outputs + tx_fee_max); + continue; + } + panic!("unexpected utxo builder err"); + } + + let generated = result.unwrap(); + + // generated transaction has no change output but dust + assert!(generated.0.outputs.len() >= output_vals.len() && generated.0.outputs.len() <= output_vals.len() + 1); + let fact_inputs = generated.0.inputs.iter().fold(0u64, |acc, input| acc + input.amount); + // total w/o change: + let fact_outputs_no_change = generated + .0 + .outputs + .iter() + .take(output_vals.len()) + .fold(0u64, |acc, output| acc + output.value); + + assert_eq!(generated.1.spent_by_me, fact_inputs); + + assert_eq!(total_outputs, fact_outputs_no_change); + + assert_eq!( + generated.1.spent_by_me, + generated.1.fee_amount + generated.1.received_by_me + total_outputs + ); + + let tx_size = est_tx_size(generated.0.inputs.len(), generated.0.outputs.len()); + let estimated_txfee = fee_rate * tx_size / 1000; + //println!("generated.1.fee_amount={} estimated_txfee={} received_by_me={} output_vals.len={} generated.0.outputs.len={} dust={}, fee_rate={}", + // generated.1.fee_amount, estimated_txfee, generated.1.received_by_me, output_vals.len(), generated.0.outputs.len(), dust, fee_rate); + + const CHANGE_OUTPUT_SIZE: u64 = 1 + 25 + 8; + let max_overpay = dust + fee_rate * CHANGE_OUTPUT_SIZE / 1000; // could be slight overpay due to dust change removed from tx + if generated.1.fee_amount > estimated_txfee { + println!( + "overpay detected: generated.1.fee_amount={} estimated_txfee={}", + generated.1.fee_amount, estimated_txfee + ); + } + assert!(generated.1.fee_amount >= estimated_txfee && generated.1.fee_amount <= estimated_txfee + max_overpay); + + let received_by_me = if generated.0.outputs.len() > output_vals.len() { + generated.0.outputs.last().unwrap().value + } else { + 0u64 + }; + assert_eq!(generated.1.received_by_me, received_by_me); + } +} + #[test] #[cfg(not(target_arch = "wasm32"))] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1037 @@ -1241,16 +1383,15 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower_and_ded .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(FeePolicy::DeductFromOutput(0)) - .with_fee(ActualTxFee::Dynamic(100)); + .with_fee(ActualFeeRate::Dynamic(100)); let generated = block_on(tx_builder.build()).unwrap(); assert_eq!(generated.0.outputs.len(), 1); - // `output (= 10.0) - fee_amount (= 1.0)` - assert_eq!(generated.0.outputs[0].value, 900000000); + // `output (= 10.0) - tx_fee (= 186 byte * 100000000 / 1000)` + assert_eq!(generated.0.outputs[0].value, 981400000); - // generated transaction fee must be equal to relay fee if calculated dynamic fee is lower than relay - assert_eq!(generated.1.fee_amount, 100000000); - assert_eq!(generated.1.unused_change, 0); + // generated transaction fee must be equal to relay fee if calculated dynamic fee is lower than relay fee + assert_eq!(generated.1.fee_amount, 18600000); assert_eq!(generated.1.received_by_me, 0); assert_eq!(generated.1.spent_by_me, 1000000000); assert!(unsafe { GET_RELAY_FEE_CALLED }); @@ -1289,7 +1430,7 @@ fn test_generate_tx_fee_is_correct_when_dynamic_fee_is_larger_than_relay() { let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) - .with_fee(ActualTxFee::Dynamic(1000)); + .with_fee(ActualFeeRate::Dynamic(1000)); let generated = block_on(builder.build()).unwrap(); @@ -1298,7 +1439,6 @@ fn test_generate_tx_fee_is_correct_when_dynamic_fee_is_larger_than_relay() { // resulting signed transaction size would be 3032 bytes so fee is 3032 sat assert_eq!(generated.1.fee_amount, 3032); - assert_eq!(generated.1.unused_change, 0); assert_eq!(generated.1.received_by_me, 999996968); assert_eq!(generated.1.spent_by_me, 20000000000); assert!(unsafe { GET_RELAY_FEE_CALLED }); @@ -2633,7 +2773,7 @@ fn test_get_sender_trade_fee_dynamic_tx_fee() { Some("bob passphrase max taker vol with dynamic trade fee"), false, ); - coin_fields.tx_fee = TxFee::Dynamic(EstimateFeeMethod::Standard); + coin_fields.tx_fee = FeeRate::Dynamic(EstimateFeeMethod::Standard); let coin = utxo_coin_from_fields(coin_fields); let my_balance = block_on_f01(coin.my_spendable_balance()).expect("!my_balance"); let expected_balance = BigDecimal::from_str("2.22222").expect("!BigDecimal::from_str"); @@ -3673,6 +3813,365 @@ fn test_split_qtum() { log!("Res = {:?}", res); } +#[test] +fn test_raven_low_tx_fee_okay() { + let config = json!({ + "coin": "RVN", + "name": "raven", + "fname": "RavenCoin", + "sign_message_prefix": "Raven Signed Message:\n", + "rpcport": 8766, + "pubtype": 60, + "p2shtype": 122, + "wiftype": 128, + "segwit": true, + "txfee": 1000000, + "mm2": 1, + "required_confirmations": 3, + "avg_blocktime": 60, + "protocol": { + "type": "UTXO" + }, + "derivation_path": "m/44'/175'", + "trezor_coin": "Ravencoin", + "links": { + "github": "https://github.com/RavenProject/Ravencoin", + "homepage": "https://ravencoin.org" + } + }); + let request = json!({ + "method": "electrum", + "coin": "RVN", + "servers": [{"url": "electrum1.cipig.net:10060"},{"url": "electrum2.cipig.net:10060"},{"url": "electrum3.cipig.net:10060"}], + }); + let ctx = MmCtxBuilder::default().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&request).unwrap(); + + let priv_key = Secp256k1Secret::from([1; 32]); + let raven = block_on(utxo_standard_coin_with_priv_key( + &ctx, "RVN", &config, ¶ms, priv_key, + )) + .unwrap(); + + let unspents = vec![ + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("be3f13e94d4c58293c2fbee40dd70714c3f833a10ab05b6a328b558bb72c38a7").unwrap(), + index: 2, + }, + value: 10618039482, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("2f2eb00dad863079fc20f0c5356bb72e18f3346c126cc3f2e3654360af930f85").unwrap(), + index: 0, + }, + value: 15105673480, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("4a806e97f1fa33439d58ce5fad32c5be1e1f1a59d742050a42f237b33f2196ab").unwrap(), + index: 0, + }, + value: 15376032861, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c0f855886343247051bb42b39f75ff35690ad0fb67a08dba5e9f8b680f6fecf3").unwrap(), + index: 0, + }, + value: 29999000000, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("0e75a62d6bb49c6312a5a1f3635d4bfc39c3d1549a35dc07b253ec1b1dd3b835").unwrap(), + index: 0, + }, + value: 31916552049, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("921554ccd2e50729b521422d3ad22ae00b5721f888e35fca8d2c8ee7a7506490").unwrap(), + index: 0, + }, + value: 33542311009, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9df4256f2e3d0a65745402e7233f309767a2a629755cb3841ff0f47ce90553be").unwrap(), + index: 0, + }, + value: 35133858231, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("bf3e69728fa9a41ab06da0e595da63bc0fbe055c04f0e7125c320b3255067a3b").unwrap(), + index: 0, + }, + value: 46177879500, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c62efa3598fec9332746d0657b7bd2a1974efe637da549ddeb84c952535e214b").unwrap(), + index: 2, + }, + value: 155455117689, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9b676bc6a81e4e801a37b48f11f3834c0b1fd49ff420e104563e0895f0517946").unwrap(), + index: 2, + }, + value: 251289432230, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("210525a94adc033a745bfae158d931464a720b60bd708d00415fa38d7aa1bed1").unwrap(), + index: 0, + }, + value: 260317094896, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("d78d731e8dfc9fc1591da45da7622b13a3e395a73fd3178e6b832cd30436ed14").unwrap(), + index: 0, + }, + value: 460964136766, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("02143bce641ef1f70354085dfdff6f1031db019df561aa09b06835fbcf41b8a4").unwrap(), + index: 0, + }, + value: 515274184960, + height: None, + script: Vec::new().into(), + }, + ]; + let outputs = vec![ + TransactionOutput { + value: 1742160278745, + script_pubkey: "a9147484c59a11d053535314d5a1047005952f7fdf1e87".into(), + }, + TransactionOutput { + value: 0, + script_pubkey: "6a140e7d2af72dc4363283f4b50e1cfe6775a1ad81c1".into(), + }, + TransactionOutput { + value: 119006034408, + script_pubkey: "76a914124b0846223ef78130b8e544b9afc3b09988238688ac".into(), + }, + ]; + let builder = block_on(UtxoTxBuilder::new(&raven)) + .add_available_inputs(unspents) + .add_outputs(outputs); + let (_, data) = block_on(builder.build()).unwrap(); + let expected_fee = 3000000; + assert_eq!(expected_fee, data.fee_amount); +} + +/// Test to validate fix for https://github.com/KomodoPlatform/komodo-defi-framework/issues/2313 +#[test] +fn test_raven_low_tx_fee_error() { + let config = json!({ + "coin": "RVN", + "name": "raven", + "fname": "RavenCoin", + "sign_message_prefix": "Raven Signed Message:\n", + "rpcport": 8766, + "pubtype": 60, + "p2shtype": 122, + "wiftype": 128, + "segwit": true, + "txfee": 1000000, + "mm2": 1, + "required_confirmations": 3, + "avg_blocktime": 60, + "protocol": { + "type": "UTXO" + }, + "derivation_path": "m/44'/175'", + "trezor_coin": "Ravencoin", + "links": { + "github": "https://github.com/RavenProject/Ravencoin", + "homepage": "https://ravencoin.org" + } + }); + let request = json!({ + "method": "electrum", + "coin": "RVN", + "servers": [{"url": "electrum1.cipig.net:10060"},{"url": "electrum2.cipig.net:10060"},{"url": "electrum3.cipig.net:10060"}], + }); + let ctx = MmCtxBuilder::default().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&request).unwrap(); + + let priv_key = Secp256k1Secret::from([1; 32]); + let raven = block_on(utxo_standard_coin_with_priv_key( + &ctx, "RVN", &config, ¶ms, priv_key, + )) + .unwrap(); + + let unspents = vec![ + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("fde4ef4f23edc53085460559702783f7128d4b9bacd6898ffae2234576e7feb9").unwrap(), + index: 2, + }, + value: 11014394719, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("2f2eb00dad863079fc20f0c5356bb72e18f3346c126cc3f2e3654360af930f85").unwrap(), + index: 0, + }, + value: 15105673480, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("4a806e97f1fa33439d58ce5fad32c5be1e1f1a59d742050a42f237b33f2196ab").unwrap(), + index: 0, + }, + value: 15376032861, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c0f855886343247051bb42b39f75ff35690ad0fb67a08dba5e9f8b680f6fecf3").unwrap(), + index: 0, + }, + value: 29999000000, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("0e75a62d6bb49c6312a5a1f3635d4bfc39c3d1549a35dc07b253ec1b1dd3b835").unwrap(), + index: 0, + }, + value: 31916552049, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("921554ccd2e50729b521422d3ad22ae00b5721f888e35fca8d2c8ee7a7506490").unwrap(), + index: 0, + }, + value: 33542311009, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9df4256f2e3d0a65745402e7233f309767a2a629755cb3841ff0f47ce90553be").unwrap(), + index: 0, + }, + value: 35133858231, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("bf3e69728fa9a41ab06da0e595da63bc0fbe055c04f0e7125c320b3255067a3b").unwrap(), + index: 0, + }, + value: 46177879500, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c62efa3598fec9332746d0657b7bd2a1974efe637da549ddeb84c952535e214b").unwrap(), + index: 2, + }, + value: 155455117689, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9b676bc6a81e4e801a37b48f11f3834c0b1fd49ff420e104563e0895f0517946").unwrap(), + index: 2, + }, + value: 251289432230, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("210525a94adc033a745bfae158d931464a720b60bd708d00415fa38d7aa1bed1").unwrap(), + index: 0, + }, + value: 260317094896, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("d78d731e8dfc9fc1591da45da7622b13a3e395a73fd3178e6b832cd30436ed14").unwrap(), + index: 0, + }, + value: 460964136766, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("02143bce641ef1f70354085dfdff6f1031db019df561aa09b06835fbcf41b8a4").unwrap(), + index: 0, + }, + value: 515274184960, + height: None, + script: Vec::new().into(), + }, + ]; + let outputs = vec![ + TransactionOutput { + value: 1752628943415, + script_pubkey: "a91417ad3c3cd6e32aede379ac0efa42e310ba30b81d87".into(), + }, + TransactionOutput { + value: 0, + script_pubkey: "6a145786f27ae947255c21e47a3d3fe0d4e132f34e6c".into(), + }, + ]; + let builder = block_on(UtxoTxBuilder::new(&raven)) + .add_available_inputs(unspents) + .add_outputs(outputs); + let (_, data) = block_on(builder.build()).unwrap(); + let expected_fee = 3000000; + assert_eq!(expected_fee, data.fee_amount); +} + /// `QtumCoin` hasn't to check UTXO maturity if `check_utxo_maturity` is `false`. /// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 #[test] diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 29ef2f5963..353ac230d7 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -1,7 +1,7 @@ use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::utxo::utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; -use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, FeePolicy, GetUtxoListOps, PrivKeyPolicy, - UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; +use crate::utxo::{output_script, sat_from_big_decimal, ActualFeeRate, Address, FeePolicy, GetUtxoListOps, + PrivKeyPolicy, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionData, TransactionDetails, UnexpectedDerivationMethod, WithdrawError, WithdrawFee, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; @@ -180,11 +180,11 @@ where match req.fee { Some(WithdrawFee::UtxoFixed { ref amount }) => { let fixed = sat_from_big_decimal(amount, decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::FixedPerKb(fixed)); + tx_builder = tx_builder.with_fee(ActualFeeRate::FixedPerKb(fixed)); }, Some(WithdrawFee::UtxoPerKbyte { ref amount }) => { - let dynamic = sat_from_big_decimal(amount, decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::Dynamic(dynamic)); + let dynamic_fee_rate = sat_from_big_decimal(amount, decimals)?; + tx_builder = tx_builder.with_fee(ActualFeeRate::Dynamic(dynamic_fee_rate)); }, Some(ref fee_policy) => { let error = format!( @@ -206,10 +206,9 @@ where // Finish by generating `TransactionDetails` from the signed transaction. self.on_finishing()?; - let fee_amount = data.fee_amount + data.unused_change; let fee_details = UtxoFeeDetails { coin: Some(ticker.clone()), - amount: big_decimal_from_sat(fee_amount as i64, decimals), + amount: big_decimal_from_sat(data.fee_amount as i64, decimals), }; let tx_hex = match coin.addr_format() { UtxoAddressFormat::Segwit => serialize_with_flags(&signed, SERIALIZE_TRANSACTION_WITNESS).into(), diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 6a44bf2ddb..ffad4d09ea 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -19,7 +19,7 @@ use crate::utxo::utxo_builder::{UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoF UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat}; use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, payment_script}; -use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxData, AddrFromStrError, Address, +use crate::utxo::{sat_from_big_decimal, utxo_common, ActualFeeRate, AdditionalTxData, AddrFromStrError, Address, BroadcastTxErr, FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, UtxoActivationParams, UtxoAddressFormat, UtxoArc, UtxoCoinFields, UtxoCommonOps, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; @@ -57,7 +57,6 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; -use primitives::bytes::Bytes; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; @@ -371,9 +370,9 @@ impl ZCoin { } async fn get_one_kbyte_tx_fee(&self) -> UtxoRpcResult { - let fee = self.get_tx_fee().await?; + let fee = self.get_fee_rate().await?; match fee { - ActualTxFee::Dynamic(fee) | ActualTxFee::FixedPerKb(fee) => { + ActualFeeRate::Dynamic(fee) | ActualFeeRate::FixedPerKb(fee) => { Ok(big_decimal_from_sat_unsigned(fee, self.decimals())) }, } @@ -483,7 +482,6 @@ impl ZCoin { received_by_me, spent_by_me: sat_from_big_decimal(&total_input_amount, self.decimals())?, fee_amount: sat_from_big_decimal(&tx_fee, self.decimals())?, - unused_change: 0, kmd_rewards: None, }; Ok((tx, additional_data, sync_guard)) @@ -1726,17 +1724,13 @@ impl MmCoin for ZCoin { #[async_trait] impl UtxoTxGenerationOps for ZCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] From ee6bdd7b83b91733ade9b0afa262493935759ffb Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Sat, 10 May 2025 04:58:37 +0300 Subject: [PATCH 04/36] feat(tron): initial groundwork for full TRON integration (#2425) This commit establishes the foundation for native TRON (TRX) support alongside EVM chains using EthCoin implementation. It adds `EthCoin` `ChainSpec` and tron address handling. --- .github/workflows/test.yml | 2 +- Cargo.lock | 1 + mm2src/coins/Cargo.toml | 2 + mm2src/coins/coins_tests.rs | 164 ----------------- mm2src/coins/eth.rs | 119 +++++++++++-- mm2src/coins/eth/eth_tests.rs | 28 ++- mm2src/coins/eth/eth_wasm_tests.rs | 9 +- mm2src/coins/eth/eth_withdraw.rs | 24 ++- mm2src/coins/eth/for_tests.rs | 2 +- mm2src/coins/eth/tron.rs | 24 +++ mm2src/coins/eth/tron/address.rs | 167 ++++++++++++++++++ mm2src/coins/eth/v2_activation.rs | 18 +- mm2src/coins/hd_wallet/pubkey.rs | 2 +- mm2src/coins/lp_coins.rs | 72 +++----- .../coins/rpc_command/init_create_account.rs | 7 +- .../src/eth_with_token_activation.rs | 24 ++- mm2src/mm2_main/src/lp_ordermatch.rs | 5 +- mm2src/mm2_main/src/mm2.rs | 39 ---- .../src/rpc/lp_commands/one_inch/rpcs.rs | 12 +- .../tests/docker_tests/docker_tests_common.rs | 6 +- .../tests/docker_tests/docker_tests_inner.rs | 6 +- .../tests/docker_tests/eth_docker_tests.rs | 25 ++- .../tests/docker_tests/tendermint_tests.rs | 4 +- .../tests/mm2_tests/mm2_tests_inner.rs | 26 +-- mm2src/mm2_test_helpers/src/for_tests.rs | 37 ++-- 25 files changed, 476 insertions(+), 349 deletions(-) delete mode 100644 mm2src/coins/coins_tests.rs create mode 100644 mm2src/coins/eth/tron.rs create mode 100644 mm2src/coins/eth/tron/address.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0da60616b..bc242f6547 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -268,4 +268,4 @@ jobs: uses: ./.github/actions/build-cache - name: Test - run: WASM_BINDGEN_TEST_TIMEOUT=600 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main + run: WASM_BINDGEN_TEST_TIMEOUT=1200 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main diff --git a/Cargo.lock b/Cargo.lock index 51a278251f..3be8929059 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -837,6 +837,7 @@ dependencies = [ "bitcoin_hashes", "bitcrypto", "blake2b_simd", + "bs58 0.4.0", "byteorder", "bytes 0.4.12", "cfg-if 1.0.0", diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 33c8457dce..670688e07d 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -23,11 +23,13 @@ doctest = false async-std = { version = "1.5", features = ["unstable"] } async-trait = "0.1.52" base64 = "0.21.2" +# Todo: remove this and rely on bs58 throught the whole codebase base58 = "0.2.0" bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } bitcoin_hashes = "0.11" bitcrypto = { path = "../mm2_bitcoin/crypto" } blake2b_simd = { version = "0.5.10", optional = true } +bs58 = "0.4.0" byteorder = "1.3" bytes = "0.4" cfg-if = "1.0" diff --git a/mm2src/coins/coins_tests.rs b/mm2src/coins/coins_tests.rs deleted file mode 100644 index bc7ab27897..0000000000 --- a/mm2src/coins/coins_tests.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::update_coins_config; - -#[test] -fn test_update_coin_config_success() { - let conf = json!([ - { - "coin": "RICK", - "asset": "RICK", - "fname": "RICK (TESTCOIN)", - "rpcport": 25435, - "txversion": 4, - "overwintered": 1, - "mm2": 1, - }, - { - "coin": "MORTY", - "asset": "MORTY", - "fname": "MORTY (TESTCOIN)", - "rpcport": 16348, - "txversion": 4, - "overwintered": 1, - "mm2": 1, - }, - { - "coin": "ETH", - "name": "ethereum", - "fname": "Ethereum", - "etomic": "0x0000000000000000000000000000000000000000", - "rpcport": 80, - "mm2": 1, - "required_confirmations": 3, - }, - { - "coin": "ARPA", - "name": "arpa-chain", - "fname": "ARPA Chain", - // ARPA coin contains the protocol already. This coin should be skipped. - "protocol": { - "type":"ERC20", - "protocol_data": { - "platform": "ETH", - "contract_address": "0xBA50933C268F567BDC86E1aC131BE072C6B0b71a" - } - }, - "rpcport": 80, - "mm2": 1, - "required_confirmations": 3, - }, - { - "coin": "JST", - "name": "JST", - "fname": "JST (TESTCOIN)", - "etomic": "0x996a8ae0304680f6a69b8a9d7c6e37d65ab5ab56", - "rpcport": 80, - "mm2": 1, - }, - ]); - let actual = update_coins_config(conf).unwrap(); - let expected = json!([ - { - "coin": "RICK", - "asset": "RICK", - "fname": "RICK (TESTCOIN)", - "rpcport": 25435, - "txversion": 4, - "overwintered": 1, - "mm2": 1, - "protocol": { - "type": "UTXO" - }, - }, - { - "coin": "MORTY", - "asset": "MORTY", - "fname": "MORTY (TESTCOIN)", - "rpcport": 16348, - "txversion": 4, - "overwintered": 1, - "mm2": 1, - "protocol": { - "type": "UTXO" - }, - }, - { - "coin": "ETH", - "name": "ethereum", - "fname": "Ethereum", - "rpcport": 80, - "mm2": 1, - "required_confirmations": 3, - "protocol": { - "type": "ETH" - }, - }, - { - "coin": "ARPA", - "name": "arpa-chain", - "fname": "ARPA Chain", - "protocol": { - "type": "ERC20", - "protocol_data": { - "platform": "ETH", - "contract_address": "0xBA50933C268F567BDC86E1aC131BE072C6B0b71a" - } - }, - "rpcport": 80, - "mm2": 1, - "required_confirmations": 3, - }, - { - "coin": "JST", - "name": "JST", - "fname": "JST (TESTCOIN)", - "rpcport": 80, - "mm2": 1, - "protocol": { - "type": "ERC20", - "protocol_data": { - "platform": "ETH", - "contract_address": "0x996a8ae0304680f6a69b8a9d7c6e37d65ab5ab56" - } - }, - }, - ]); - assert_eq!(actual, expected); -} - -#[test] -fn test_update_coin_config_error_not_array() { - let conf = json!({ - "coin": "RICK", - "asset": "RICK", - "fname": "RICK (TESTCOIN)", - "rpcport": 25435, - "txversion": 4, - "overwintered": 1, - "mm2": 1, - }); - let error = update_coins_config(conf).err().unwrap(); - assert!(error.contains("Coins config must be an array")); -} - -#[test] -fn test_update_coin_config_error_not_object() { - let conf = json!([["Ford", "BMW", "Fiat"]]); - let error = update_coins_config(conf).err().unwrap(); - assert!(error.contains("Expected object, found")); -} - -#[test] -fn test_update_coin_config_invalid_etomic() { - let conf = json!([ - { - "coin": "JST", - "name": "JST", - "fname": "JST (TESTCOIN)", - "etomic": 12345678, - "rpcport": 80, - "mm2": 1, - }, - ]); - let error = update_coins_config(conf).err().unwrap(); - assert!(error.contains("Expected etomic as string, found")); -} diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 7a82841687..6fe52c0fa5 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -161,6 +161,8 @@ use erc20::get_token_decimals; pub(crate) mod eth_swap_v2; use eth_swap_v2::{extract_id_from_tx_data, EthPaymentType, PaymentMethod, SpendTxSearchParams}; +pub mod tron; + /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol /// Dev chain (195.201.137.5:8565) contract address: 0x83965C539899cC0F918552e5A26915de40ee8852 /// Ropsten: https://ropsten.etherscan.io/address/0x7bc1bbdd6a0a722fc9bffc49c921b685ecb84b94 @@ -762,6 +764,30 @@ struct SavedErc20Events { latest_block: U64, } +/// Specifies which blockchain the EthCoin operates on: EVM-compatible or TRON. +/// This distinction allows unified logic for EVM & TRON coins. +#[derive(Clone, Debug)] +pub enum ChainSpec { + Evm { chain_id: u64 }, + Tron { network: tron::Network }, +} + +impl ChainSpec { + pub fn chain_id(&self) -> Option { + match self { + ChainSpec::Evm { chain_id } => Some(*chain_id), + ChainSpec::Tron { .. } => None, + } + } + + pub fn kind(&self) -> &'static str { + match self { + ChainSpec::Evm { .. } => "EVM", + ChainSpec::Tron { .. } => "TRON", + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub enum EthCoinType { /// Ethereum itself or it's forks: ETC/others @@ -816,6 +842,8 @@ impl From for EthPrivKeyBuildPolicy { pub struct EthCoinImpl { ticker: String, pub coin_type: EthCoinType, + /// Specifies the underlying blockchain (EVM or TRON). + pub chain_spec: ChainSpec, pub(crate) priv_key_policy: EthPrivKeyPolicy, /// Either an Iguana address or a 'EthHDWallet' instance. /// Arc is used to use the same hd wallet from platform coin if we need to. @@ -836,7 +864,6 @@ pub struct EthCoinImpl { /// Coin needs access to the context in order to reuse the logging and shutdown facilities. /// Using a weak reference by default in order to avoid circular references and leaks. pub ctx: MmWeak, - chain_id: u64, /// The name of the coin with which Trezor wallet associates this asset. trezor_coin: Option, /// the block range used for eth_getLogs @@ -1043,7 +1070,7 @@ impl EthCoinImpl { } #[inline(always)] - pub fn chain_id(&self) -> u64 { self.chain_id } + pub fn chain_id(&self) -> Option { self.chain_spec.chain_id() } } async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> RawTransactionResult { @@ -1161,7 +1188,16 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit .build() .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); - let signed = tx.sign(secret, Some(eth_coin.chain_id))?; + let chain_id = match eth_coin.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Add support for Tron NFTs + ChainSpec::Tron { .. } => { + return MmError::err(WithdrawError::InternalError( + "Tron is not supported for withdraw_erc1155 yet".to_owned(), + )) + }, + }; + let signed = tx.sign(secret, Some(chain_id))?; let signed_bytes = rlp::encode(&signed); let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; @@ -1252,7 +1288,16 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd .build() .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); - let signed = tx.sign(secret, Some(eth_coin.chain_id))?; + let chain_id = match eth_coin.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Add support for Tron NFTs + ChainSpec::Tron { .. } => { + return MmError::err(WithdrawError::InternalError( + "Tron is not supported for withdraw_erc721 yet".to_owned(), + )) + }, + }; + let signed = tx.sign(secret, Some(chain_id))?; let signed_bytes = rlp::encode(&signed); let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; @@ -2639,9 +2684,18 @@ async fn sign_transaction_with_keypair<'a>( let tx_builder = tx_builder_with_pay_for_gas_option(coin, tx_builder, pay_for_gas_option) .map_err(|e| TransactionErr::Plain(e.get_inner().to_string()))?; let tx = tx_builder.build()?; + let chain_id = match coin.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Add Tron signing logic + ChainSpec::Tron { .. } => { + return Err(TransactionErr::Plain( + "Tron is not supported for sign_transaction_with_keypair yet".into(), + )) + }, + }; Ok(( - tx.sign(key_pair.secret(), Some(coin.chain_id))?, + tx.sign(key_pair.secret(), Some(chain_id))?, web3_instances_with_latest_nonce, )) } @@ -6314,6 +6368,27 @@ pub async fn eth_coin_from_conf_and_request( protocol: CoinProtocol, priv_key_policy: PrivKeyBuildPolicy, ) -> Result { + fn get_chain_id_from_platform(ctx: &MmArc, ticker: &str, platform: &str) -> Result { + let platform_conf = coin_conf(ctx, platform); + if platform_conf.is_null() { + return ERR!( + "Failed to activate ERC20 token '{}': the platform '{}' is not defined in the coins config.", + ticker, + platform + ); + } + let platform_protocol: CoinProtocol = json::from_value(platform_conf["protocol"].clone()) + .map_err(|e| ERRL!("Error parsing platform protocol for '{}': {}", platform, e))?; + match platform_protocol { + CoinProtocol::ETH { chain_id } => Ok(chain_id), + protocol => ERR!( + "Failed to activate ERC20 token '{}': the platform protocol '{:?}' must be ETH", + ticker, + protocol + ), + } + } + // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. let priv_key_policy = try_s!(EthPrivKeyBuildPolicy::try_from(priv_key_policy)); @@ -6402,8 +6477,8 @@ pub async fn eth_coin_from_conf_and_request( return ERR!("Failed to get client version for all urls"); } - let (coin_type, decimals) = match protocol { - CoinProtocol::ETH => (EthCoinType::Eth, ETH_DECIMALS), + let (coin_type, decimals, chain_id) = match protocol { + CoinProtocol::ETH { chain_id } => (EthCoinType::Eth, ETH_DECIMALS, chain_id), CoinProtocol::ERC20 { platform, contract_address, @@ -6422,9 +6497,13 @@ pub async fn eth_coin_from_conf_and_request( ), Some(d) => d as u8, }; - (EthCoinType::Erc20 { platform, token_addr }, decimals) + let chain_id = get_chain_id_from_platform(ctx, ticker, &platform)?; + (EthCoinType::Erc20 { platform, token_addr }, decimals, chain_id) + }, + CoinProtocol::NFT { platform } => { + let chain_id = get_chain_id_from_platform(ctx, ticker, &platform)?; + (EthCoinType::Nft { platform }, ETH_DECIMALS, chain_id) }, - CoinProtocol::NFT { platform } => (EthCoinType::Nft { platform }, ETH_DECIMALS), _ => return ERR!("Expect ETH, ERC20 or NFT protocol"), }; @@ -6444,10 +6523,6 @@ pub async fn eth_coin_from_conf_and_request( let sign_message_prefix: Option = json::from_value(conf["sign_message_prefix"].clone()).unwrap_or(None); - let chain_id = try_s!(conf["chain_id"] - .as_u64() - .ok_or_else(|| format!("chain_id is not set for {}", ticker))); - let trezor_coin: Option = json::from_value(conf["trezor_coin"].clone()).unwrap_or(None); let initial_history_state = if req["tx_history"].as_bool().unwrap_or(false) { @@ -6480,6 +6555,8 @@ pub async fn eth_coin_from_conf_and_request( priv_key_policy: key_pair, derivation_method: Arc::new(derivation_method), coin_type, + // Tron is not supported for v1 activation + chain_spec: ChainSpec::Evm { chain_id }, sign_message_prefix, swap_contract_address, swap_v2_contracts: None, @@ -6493,7 +6570,6 @@ pub async fn eth_coin_from_conf_and_request( max_eth_tx_type, ctx: ctx.weak(), required_confirmations, - chain_id, trezor_coin, logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), address_nonce_locks, @@ -6782,6 +6858,8 @@ fn calc_total_fee(gas: U256, pay_for_gas_option: &PayForGasOption) -> NumConvers } } +// Todo: Tron have a different concept from gas (Energy, Bandwidth and Free Transaction), it should be added as a different function +// and this should be part of a trait abstracted over both types #[allow(clippy::result_large_err)] fn tx_builder_with_pay_for_gas_option( eth_coin: &EthCoin, @@ -6793,9 +6871,14 @@ fn tx_builder_with_pay_for_gas_option( PayForGasOption::Eip1559(Eip1559FeePerGas { max_priority_fee_per_gas, max_fee_per_gas, - }) => tx_builder - .with_priority_fee_per_gas(max_fee_per_gas, max_priority_fee_per_gas) - .with_chain_id(eth_coin.chain_id), + }) => { + let chain_id = eth_coin + .chain_id() + .ok_or_else(|| WithdrawError::InternalError("chain_id should be set for an EVM coin".to_string()))?; + tx_builder + .with_priority_fee_per_gas(max_fee_per_gas, max_priority_fee_per_gas) + .with_chain_id(chain_id) + }, }; Ok(tx_builder) } @@ -7299,6 +7382,7 @@ impl EthCoin { let coin = EthCoinImpl { ticker: self.ticker.clone(), coin_type: new_coin_type, + chain_spec: self.chain_spec.clone(), priv_key_policy: self.priv_key_policy.clone(), derivation_method: Arc::clone(&self.derivation_method), sign_message_prefix: self.sign_message_prefix.clone(), @@ -7315,7 +7399,6 @@ impl EthCoin { swap_txfee_policy: Mutex::new(self.swap_txfee_policy.lock().unwrap().clone()), max_eth_tx_type: self.max_eth_tx_type, ctx: self.ctx.clone(), - chain_id: self.chain_id, trezor_coin: self.trezor_coin.clone(), logs_block_range: self.logs_block_range, address_nonce_locks: Arc::clone(&self.address_nonce_locks), diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 2d2c43d08f..64cd3aecb0 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -26,6 +26,7 @@ cfg_native!( // old way to add some extra gas to the returned value from gas station (non-existent now), still used in tests const GAS_PRICE_PERCENT: u64 = 10; +const MATIC_CHAIN_ID: u64 = 137; fn check_sum(addr: &str, expected: &str) { let actual = checksum_address(addr); @@ -781,11 +782,13 @@ fn polygon_check_if_my_payment_sent() { "fname": "Polygon", "rpcport": 80, "mm2": 1, - "chain_id": 137, "avg_blocktime": 0.03, "required_confirmations": 3, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": MATIC_CHAIN_ID + } } }); @@ -802,7 +805,9 @@ fn polygon_check_if_my_payment_sent() { "MATIC", &conf, &request, - CoinProtocol::ETH, + CoinProtocol::ETH { + chain_id: MATIC_CHAIN_ID, + }, priv_key_policy, )) .unwrap(); @@ -934,11 +939,13 @@ fn test_eth_validate_valid_and_invalid_pubkey() { "fname": "Polygon", "rpcport": 80, "mm2": 1, - "chain_id": 137, "avg_blocktime": 0.03, "required_confirmations": 3, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": MATIC_CHAIN_ID + } } }); @@ -959,7 +966,9 @@ fn test_eth_validate_valid_and_invalid_pubkey() { "MATIC", &conf, &request, - CoinProtocol::ETH, + CoinProtocol::ETH { + chain_id: MATIC_CHAIN_ID, + }, priv_key_policy, )) .unwrap(); @@ -996,11 +1005,12 @@ fn test_gas_limit_conf() { "coin": "ETH", "name": "ethereum", "fname": "Ethereum", - "chain_id": 1337, "protocol":{ - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": ETH_SEPOLIA_CHAIN_ID + } }, - "chain_id": 1, "rpcport": 80, "mm2": 1, "gas_limit": { diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 287b3fb79a..367e88321d 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::lp_coininit; use crypto::CryptoCtx; use mm2_core::mm_ctx::MmCtxBuilder; -use mm2_test_helpers::for_tests::{ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT}; +use mm2_test_helpers::for_tests::{ETH_SEPOLIA_CHAIN_ID, ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT}; use wasm_bindgen_test::*; use web_sys::console; @@ -20,11 +20,12 @@ async fn init_eth_coin_helper() -> Result<(MmArc, MmCoinEnum), String> { "coin": "ETH", "name": "ethereum", "fname": "Ethereum", - "chain_id": 1337, "protocol":{ - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": ETH_SEPOLIA_CHAIN_ID, + } }, - "chain_id": 1, "rpcport": 80, "mm2": 1, "max_eth_tx_type": 2 diff --git a/mm2src/coins/eth/eth_withdraw.rs b/mm2src/coins/eth/eth_withdraw.rs index b3de177a89..05efef9804 100644 --- a/mm2src/coins/eth/eth_withdraw.rs +++ b/mm2src/coins/eth/eth_withdraw.rs @@ -1,4 +1,4 @@ -use super::{checksum_address, u256_to_big_decimal, wei_from_big_decimal, EthCoinType, EthDerivationMethod, +use super::{checksum_address, u256_to_big_decimal, wei_from_big_decimal, ChainSpec, EthCoinType, EthDerivationMethod, EthPrivKeyPolicy, Public, WithdrawError, WithdrawRequest, WithdrawResult, ERC20_CONTRACT, H160, H256}; use crate::eth::{calc_total_fee, get_eth_gas_details_from_withdraw_fee, tx_builder_with_pay_for_gas_option, tx_type_from_pay_for_gas_option, Action, Address, EthTxFeeDetails, KeyPair, PayForGasOption, @@ -128,7 +128,16 @@ where match coin.priv_key_policy { EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { let key_pair = self.get_key_pair(req)?; - let signed = unsigned_tx.sign(key_pair.secret(), Some(coin.chain_id))?; + let chain_id = match coin.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Tron have different transaction signing algorithm, we should probably have a trait abstracting both + ChainSpec::Tron { .. } => { + return MmError::err(WithdrawError::InternalError( + "Tron is not supported for withdraw yet".to_owned(), + )) + }, + }; + let signed = unsigned_tx.sign(key_pair.secret(), Some(chain_id))?; let bytes = rlp::encode(&signed); Ok((signed.tx_hash(), BytesJson::from(bytes.to_vec()))) @@ -374,8 +383,17 @@ impl EthWithdraw for InitEthWithdraw { let sign_processor = TrezorRpcTaskProcessor::new(self.task_handle.clone(), trezor_statuses); let sign_processor = Arc::new(sign_processor); let mut trezor_session = hw_ctx.trezor(sign_processor).await?; + let chain_id = match coin.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Add support for Tron signing with Trezor + ChainSpec::Tron { .. } => { + return MmError::err(WithdrawError::InternalError( + "Tron is not supported for withdraw yet".to_owned(), + )) + }, + }; let unverified_tx = trezor_session - .sign_eth_tx(derivation_path, unsigned_tx, coin.chain_id) + .sign_eth_tx(derivation_path, unsigned_tx, chain_id) .await?; Ok(SignedEthTx::new(unverified_tx).map_to_mm(|err| WithdrawError::InternalError(err.to_string()))?) } diff --git a/mm2src/coins/eth/for_tests.rs b/mm2src/coins/eth/for_tests.rs index 781748832c..9652ba0a80 100644 --- a/mm2src/coins/eth/for_tests.rs +++ b/mm2src/coins/eth/for_tests.rs @@ -55,6 +55,7 @@ pub(crate) fn eth_coin_from_keypair( let eth_coin = EthCoin(Arc::new(EthCoinImpl { coin_type, + chain_spec: ChainSpec::Evm { chain_id }, decimals: 18, history_sync_state: Mutex::new(HistorySyncState::NotEnabled), sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), @@ -69,7 +70,6 @@ pub(crate) fn eth_coin_from_keypair( ctx: ctx.weak(), required_confirmations: 1.into(), swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), - chain_id, trezor_coin: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, address_nonce_locks: Arc::new(AsyncMutex::new(new_nonce_lock())), diff --git a/mm2src/coins/eth/tron.rs b/mm2src/coins/eth/tron.rs new file mode 100644 index 0000000000..c07307c13d --- /dev/null +++ b/mm2src/coins/eth/tron.rs @@ -0,0 +1,24 @@ +//! Minimal Tron placeholders for EthCoin integration. +//! These types will be expanded with full TRON logic in later steps. + +mod address; +pub use address::Address as TronAddress; + +/// Represents TRON chain/network. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum Network { + Mainnet, + Shasta, + Nile, + // TODO: Add more networks as needed. +} + +/// Placeholder for a TRON client. +#[derive(Clone, Debug)] +pub struct TronClient; + +/// Placeholder for TRON fee params. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TronFeeParams { + // TODO: Add TRON-specific fields in future steps. +} diff --git a/mm2src/coins/eth/tron/address.rs b/mm2src/coins/eth/tron/address.rs new file mode 100644 index 0000000000..0d4a45e07c --- /dev/null +++ b/mm2src/coins/eth/tron/address.rs @@ -0,0 +1,167 @@ +//! TRON address handling (base58, hex, validation, serde). + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::convert::{TryFrom, TryInto}; +use std::fmt; +use std::str::FromStr; + +pub const ADDRESS_PREFIX: u8 = 0x41; +pub const ADDRESS_BASE58_PREFIX: char = 'T'; +pub const ADDRESS_HEX_LEN: usize = 42; +pub const ADDRESS_BYTES_LEN: usize = 21; +pub const ADDRESS_BASE58_LEN: usize = 34; + +/// TRON mainnet or testnet address (21 bytes, 0x41 prefix + 20-bytes). +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Address { + pub inner: [u8; ADDRESS_BYTES_LEN], +} + +impl Address { + /// Construct from raw 21 bytes (must be 0x41-prefixed). + pub fn from_bytes(bytes: [u8; ADDRESS_BYTES_LEN]) -> Result { + if bytes[0] != ADDRESS_PREFIX { + return Err("TRON address must start with 0x41".into()); + } + Ok(Self { inner: bytes }) + } + + /// Construct from base58 string (with checksum). + pub fn from_base58(s: &str) -> Result { + let data = bs58::decode(s) + .with_check(None) + .into_vec() + .map_err(|e| format!("Invalid base58check address: {}", e))?; + + // SAFETY: Accessing `data[0]` is safe here because we first check that + // `data.len() == ADDRESS_BYTES_LEN`, guaranteeing the slice is not empty + // and has at least one element. + if data.len() != ADDRESS_BYTES_LEN || data[0] != ADDRESS_PREFIX { + return Err(format!( + "Invalid address: expected {} bytes with prefix 0x{:x}", + ADDRESS_BYTES_LEN, ADDRESS_PREFIX + )); + } + + let inner = data + .try_into() + .map_err(|_| "Failed to convert address bytes to array".to_string())?; + + Ok(Self { inner }) + } + + /// Construct from hex string, with or without `0x` prefix. + pub fn from_hex(s: &str) -> Result { + let s = s.strip_prefix("0x").unwrap_or(s); + let data = hex::decode(s).map_err(|e| format!("Invalid hex address: {}", e))?; + + // SAFETY: Accessing `data[0]` is safe here because we first check that + // `data.len() == ADDRESS_BYTES_LEN`, guaranteeing the slice is not empty + // and has at least one element. + if data.len() != ADDRESS_BYTES_LEN || data[0] != ADDRESS_PREFIX { + return Err(format!( + "Invalid address: expected {} bytes with prefix 0x{:x}", + ADDRESS_BYTES_LEN, ADDRESS_PREFIX + )); + } + + let inner = data + .try_into() + .map_err(|_| "Failed to convert address bytes to array".to_string())?; + + Ok(Self { inner }) + } + + /// Show as base58 string (canonical user format). + pub fn to_base58(&self) -> String { bs58::encode(self.inner).with_check().into_string() } + + /// Show as hex string, lowercase (canonical hex format). + pub fn to_hex(&self) -> String { hex::encode(self.inner) } + + /// Return the 21 bytes (0x41 + 20). + pub fn as_bytes(&self) -> &[u8] { &self.inner } +} + +impl TryFrom<[u8; ADDRESS_BYTES_LEN]> for Address { + type Error = String; + + fn try_from(bytes: [u8; ADDRESS_BYTES_LEN]) -> Result { Self::from_bytes(bytes) } +} + +impl AsRef<[u8]> for Address { + fn as_ref(&self) -> &[u8] { &self.inner } +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_base58()) } +} + +impl fmt::Debug for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Address({} / 0x{})", self.to_base58(), self.to_hex()) + } +} + +impl Serialize for Address { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_base58()) + } +} + +impl<'de> Deserialize<'de> for Address { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = <&str>::deserialize(deserializer)?; + Address::from_str(s).map_err(serde::de::Error::custom) + } +} + +impl FromStr for Address { + type Err = String; + + fn from_str(s: &str) -> Result { + // Check for Base58 format + if s.len() == ADDRESS_BASE58_LEN && s.starts_with(ADDRESS_BASE58_PREFIX) { + return Self::from_base58(s); + } + + // Check for hex format (with or without 0x prefix) + if (s.len() == ADDRESS_HEX_LEN && s.starts_with("41")) + || (s.len() == ADDRESS_HEX_LEN + 2 && s.starts_with("0x41")) + { + return Self::from_hex(s); + } + + Err(format!( + "Invalid TRON address '{}': must be Base58 (34 chars starting with 'T') or hex (42 chars without 0x, 44 chars with 0x prefix)", + s + )) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_tron_address_base58_and_hex() { + let base58 = "TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL"; + let hex = "418840e6c55b9ada326d211d818c34a994aeced808"; + let addr1 = Address::from_str(base58).unwrap(); + let addr2 = Address::from_str(hex).unwrap(); + assert_eq!(addr1, addr2); + assert_eq!(addr1.to_hex(), hex); + assert_eq!(addr2.to_base58(), base58); + } + + #[test] + fn test_invalid_tron_address() { + assert!(Address::from_str("foo").is_err()); + assert!(Address::from_str("0xdeadbeef").is_err()); + } +} diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 4e04bfebed..ecdb4a5c37 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -30,6 +30,11 @@ pub enum EthActivationV2Error { InvalidPathToAddress(String), #[display(fmt = "`chain_id` should be set for evm coins or tokens")] ChainIdNotSet, + #[display(fmt = "{} chains don't support {}", chain, feature)] + UnsupportedChain { + chain: String, + feature: String, + }, #[display(fmt = "Platform coin {} activation failed. {}", ticker, error)] ActivationFailed { ticker: String, @@ -454,6 +459,7 @@ impl EthCoin { // storage ticker will be the platform coin ticker derivation_method: self.derivation_method.clone(), coin_type, + chain_spec: self.chain_spec.clone(), sign_message_prefix: self.sign_message_prefix.clone(), swap_contract_address: self.swap_contract_address, swap_v2_contracts: self.swap_v2_contracts, @@ -467,7 +473,6 @@ impl EthCoin { max_eth_tx_type, ctx: self.ctx.clone(), required_confirmations, - chain_id: self.chain_id, trezor_coin: self.trezor_coin.clone(), logs_block_range: self.logs_block_range, address_nonce_locks: self.address_nonce_locks.clone(), @@ -540,6 +545,7 @@ impl EthCoin { let global_nft = EthCoinImpl { ticker, coin_type, + chain_spec: self.chain_spec.clone(), priv_key_policy: self.priv_key_policy.clone(), derivation_method: self.derivation_method.clone(), sign_message_prefix: self.sign_message_prefix.clone(), @@ -554,7 +560,6 @@ impl EthCoin { max_eth_tx_type, required_confirmations, ctx: self.ctx.clone(), - chain_id: self.chain_id, trezor_coin: self.trezor_coin.clone(), logs_block_range: self.logs_block_range, address_nonce_locks: self.address_nonce_locks.clone(), @@ -576,6 +581,7 @@ pub async fn eth_coin_from_conf_and_request_v2( conf: &Json, req: EthActivationV2Request, priv_key_build_policy: EthPrivKeyBuildPolicy, + chain_spec: ChainSpec, ) -> MmResult { if req.swap_contract_address == Address::default() { return Err(EthActivationV2Error::InvalidSwapContractAddr( @@ -620,7 +626,6 @@ pub async fn eth_coin_from_conf_and_request_v2( ) .await?; - let chain_id = conf["chain_id"].as_u64().ok_or(EthActivationV2Error::ChainIdNotSet)?; let web3_instances = match (req.rpc_mode, &priv_key_policy) { (EthRpcMode::Default, EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. }) | (EthRpcMode::Default, EthPrivKeyPolicy::Trezor) => { @@ -628,6 +633,11 @@ pub async fn eth_coin_from_conf_and_request_v2( }, #[cfg(target_arch = "wasm32")] (EthRpcMode::Metamask, EthPrivKeyPolicy::Metamask(_)) => { + // Metamask doesn't support native Tron + let chain_id = chain_spec.chain_id().ok_or(EthActivationV2Error::UnsupportedChain { + chain: chain_spec.kind().to_string(), + feature: "Metamask".to_string(), + })?; build_metamask_transport(ctx, ticker.to_string(), chain_id).await? }, #[cfg(target_arch = "wasm32")] @@ -675,6 +685,7 @@ pub async fn eth_coin_from_conf_and_request_v2( priv_key_policy, derivation_method: Arc::new(derivation_method), coin_type, + chain_spec, sign_message_prefix, swap_contract_address: req.swap_contract_address, swap_v2_contracts: req.swap_v2_contracts, @@ -688,7 +699,6 @@ pub async fn eth_coin_from_conf_and_request_v2( max_eth_tx_type, ctx: ctx.weak(), required_confirmations, - chain_id, trezor_coin, logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), address_nonce_locks, diff --git a/mm2src/coins/hd_wallet/pubkey.rs b/mm2src/coins/hd_wallet/pubkey.rs index 7babb12bd5..64b24e2abd 100644 --- a/mm2src/coins/hd_wallet/pubkey.rs +++ b/mm2src/coins/hd_wallet/pubkey.rs @@ -130,7 +130,7 @@ where let trezor_message_type = match coin_protocol { CoinProtocol::UTXO => TrezorMessageType::Bitcoin, CoinProtocol::QTUM => TrezorMessageType::Bitcoin, - CoinProtocol::ETH | CoinProtocol::ERC20 { .. } => TrezorMessageType::Ethereum, + CoinProtocol::ETH { .. } | CoinProtocol::ERC20 { .. } => TrezorMessageType::Ethereum, _ => return Err(MmError::new(HDExtractPubkeyError::CoinDoesntSupportTrezor)), }; Ok(RpcTaskXPubExtractor::Trezor { diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index e1adf1cdad..a211761968 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -218,10 +218,6 @@ pub mod coin_errors; use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentResult}; use crypto::secret_hash_algo::SecretHashAlgo; -#[doc(hidden)] -#[cfg(test)] -pub mod coins_tests; - pub mod eth; use eth::erc20::get_erc20_ticker_by_contract_address; use eth::eth_swap_v2::{PrepareTxDataError, ValidatePaymentV2Err}; @@ -4523,11 +4519,19 @@ pub enum CoinProtocol { platform: String, contract_address: String, }, - ETH, + // Todo: Document this + /// # Breaking Changes + ETH { + chain_id: u64, + }, ERC20 { platform: String, contract_address: String, }, + TRX { + network: eth::tron::Network, + }, + // Todo: Add TRC20, Do we need to support TRC10? SLPTOKEN { platform: String, token_id: H256Json, @@ -4586,7 +4590,8 @@ impl CoinProtocol { CoinProtocol::LIGHTNING { platform, .. } => Some(platform), CoinProtocol::UTXO | CoinProtocol::QTUM - | CoinProtocol::ETH + | CoinProtocol::ETH { .. } + | CoinProtocol::TRX { .. } | CoinProtocol::BCH { .. } | CoinProtocol::TENDERMINT(_) | CoinProtocol::ZHTLC(_) => None, @@ -4604,7 +4609,8 @@ impl CoinProtocol { CoinProtocol::SLPTOKEN { .. } | CoinProtocol::UTXO | CoinProtocol::QTUM - | CoinProtocol::ETH + | CoinProtocol::ETH { .. } + | CoinProtocol::TRX { .. } | CoinProtocol::BCH { .. } | CoinProtocol::TENDERMINT(_) | CoinProtocol::TENDERMINTTOKEN(_) @@ -4867,7 +4873,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result { + CoinProtocol::ETH { .. } | CoinProtocol::ERC20 { .. } => { try_s!(eth_coin_from_conf_and_request(ctx, ticker, &coins_en, req, protocol, priv_key_policy).await).into() }, CoinProtocol::QRC20 { @@ -4925,6 +4931,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result return ERR!("TENDERMINTTOKEN protocol is not supported by lp_coininit"), CoinProtocol::ZHTLC { .. } => return ERR!("ZHTLC protocol is not supported by lp_coininit"), CoinProtocol::NFT { .. } => return ERR!("NFT protocol is not supported by lp_coininit"), + CoinProtocol::TRX { .. } => return ERR!("TRX protocol is not supported by lp_coininit"), #[cfg(not(target_arch = "wasm32"))] CoinProtocol::LIGHTNING { .. } => return ERR!("Lightning protocol is not supported by lp_coininit"), #[cfg(feature = "enable-sia")] @@ -5482,47 +5489,6 @@ pub async fn register_balance_update_handler( coins_ctx.balance_update_handlers.lock().await.push(handler); } -pub fn update_coins_config(mut config: Json) -> Result { - let coins = match config.as_array_mut() { - Some(c) => c, - _ => return ERR!("Coins config must be an array"), - }; - - for coin in coins { - // the coin_as_str is used only to be formatted - let coin_as_str = format!("{}", coin); - let coin = try_s!(coin - .as_object_mut() - .ok_or(ERRL!("Expected object, found {:?}", coin_as_str))); - if coin.contains_key("protocol") { - // the coin is up-to-date - continue; - } - let protocol = match coin.remove("etomic") { - Some(etomic) => { - let etomic = etomic - .as_str() - .ok_or(ERRL!("Expected etomic as string, found {:?}", etomic))?; - if etomic == "0x0000000000000000000000000000000000000000" { - CoinProtocol::ETH - } else { - let contract_address = etomic.to_owned(); - CoinProtocol::ERC20 { - platform: "ETH".into(), - contract_address, - } - } - }, - _ => CoinProtocol::UTXO, - }; - - let protocol = json::to_value(protocol).map_err(|e| ERRL!("Error {:?} on process {:?}", e, coin_as_str))?; - coin.insert("protocol".into(), protocol); - } - - Ok(config) -} - #[derive(Deserialize)] struct ConvertUtxoAddressReq { address: String, @@ -5558,7 +5524,11 @@ pub fn address_by_coin_conf_and_pubkey_str( ) -> Result { let protocol: CoinProtocol = try_s!(json::from_value(conf["protocol"].clone())); match protocol { - CoinProtocol::ERC20 { .. } | CoinProtocol::ETH | CoinProtocol::NFT { .. } => eth::addr_from_pubkey_str(pubkey), + CoinProtocol::ERC20 { .. } | CoinProtocol::ETH { .. } | CoinProtocol::NFT { .. } => { + eth::addr_from_pubkey_str(pubkey) + }, + // Todo: implement TRX address generation + CoinProtocol::TRX { .. } => ERR!("TRX address generation is not implemented yet"), CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format) }, @@ -5836,7 +5806,7 @@ pub async fn get_my_address(ctx: MmArc, req: MyAddressReq) -> MmResult get_eth_address(&ctx, &conf, ticker, &req.path_to_address).await?, + CoinProtocol::ETH { .. } => get_eth_address(&ctx, &conf, ticker, &req.path_to_address).await?, _ => { return MmError::err(GetMyAddressError::CoinIsNotSupported(format!( "{} doesn't support get_my_address", diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index c323ffe78a..774ab3bc77 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -318,7 +318,12 @@ impl RpcTask for InitCreateAccountTask { self.task_state.clone(), task_handle, eth.is_trezor(), - CoinProtocol::ETH, + // Todo: add support for Tron by checking eth.chain_spec + CoinProtocol::ETH { + chain_id: eth.chain_id().ok_or_else(|| { + CreateAccountRpcError::Internal("chain_id should be available for an EVM coin".to_string()) + })?, + }, ) .await?, )), diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index d8c4a0f49e..6f8dcccb65 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -12,7 +12,7 @@ use coins::coin_balance::{CoinBalanceReport, EnableCoinBalanceOps}; use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationRequest, EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy}; use coins::eth::v2_activation::{EthTokenActivationError, NftActivationRequest, NftProviderEnum}; -use coins::eth::{Erc20TokenDetails, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; +use coins::eth::{ChainSpec, Erc20TokenDetails, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; use coins::hd_wallet::{DisplayAddress, RpcTaskXPubExtractor}; use coins::my_tx_history_v2::TxHistoryStorage; use coins::nft::nft_structs::NftInfo; @@ -46,6 +46,9 @@ impl From for EnablePlatformCoinWithTokensError { EthActivationV2Error::ChainIdNotSet => { EnablePlatformCoinWithTokensError::Internal("`chain_id` is not set in coin config".to_string()) }, + EthActivationV2Error::UnsupportedChain { .. } => { + EnablePlatformCoinWithTokensError::Internal("Unsupported chain".to_string()) + }, EthActivationV2Error::ActivationFailed { ticker, error } => { EnablePlatformCoinWithTokensError::PlatformCoinCreationError { ticker, error } }, @@ -89,13 +92,14 @@ impl From for EnablePlatformCoinWithTokensError { } } -impl TryFromCoinProtocol for EthCoinType { +impl TryFromCoinProtocol for ChainSpec { fn try_from_coin_protocol(proto: CoinProtocol) -> Result> where Self: Sized, { match proto { - CoinProtocol::ETH => Ok(EthCoinType::Eth), + CoinProtocol::ETH { chain_id } => Ok(ChainSpec::Evm { chain_id }), + CoinProtocol::TRX { network } => Ok(ChainSpec::Tron { network }), protocol => MmError::err(protocol), } } @@ -263,7 +267,7 @@ impl CurrentBlock for EthWithTokensActivationResult { #[async_trait] impl PlatformCoinWithTokensActivationOps for EthCoin { type ActivationRequest = EthWithTokensActivationRequest; - type PlatformProtocolInfo = EthCoinType; + type PlatformProtocolInfo = ChainSpec; type ActivationResult = EthWithTokensActivationResult; type ActivationError = EthActivationV2Error; @@ -276,7 +280,7 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { ticker: String, platform_conf: &Json, activation_request: Self::ActivationRequest, - _protocol: Self::PlatformProtocolInfo, + protocol: Self::PlatformProtocolInfo, ) -> Result> { let priv_key_policy = eth_priv_key_build_policy(&ctx, &activation_request.platform_request.priv_key_policy)?; @@ -286,6 +290,7 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { platform_conf, activation_request.platform_request, priv_key_policy, + protocol, ) .await?; @@ -411,7 +416,14 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { &ctx, task_handle, platform_coin_xpub_extractor_rpc_statuses(), - CoinProtocol::ETH, + // Todo: add support for Tron by checking self.chain_spec + CoinProtocol::ETH { + chain_id: self.chain_id().ok_or_else(|| { + EthActivationV2Error::InternalError( + "chain_id should be available for an EVM coin".to_string(), + ) + })?, + }, ) .map_err(|_| MmError::new(EthActivationV2Error::HwError(HwRpcError::NotInitialized)))?, ) diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 18475ce191..9141dc6284 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -6081,7 +6081,6 @@ pub enum OrderbookAddress { #[derive(Debug, Display)] enum OrderbookAddrErr { AddrFromPubkeyError(String), - #[cfg(feature = "enable-sia")] CoinIsNotSupported(String), DeserializationError(json::Error), InvalidPlatformCoinProtocol(String), @@ -6107,11 +6106,13 @@ fn orderbook_address( ) -> Result> { let protocol: CoinProtocol = json::from_value(conf["protocol"].clone())?; match protocol { - CoinProtocol::ERC20 { .. } | CoinProtocol::ETH | CoinProtocol::NFT { .. } => { + CoinProtocol::ERC20 { .. } | CoinProtocol::ETH { .. } | CoinProtocol::NFT { .. } => { coins::eth::addr_from_pubkey_str(pubkey) .map(OrderbookAddress::Transparent) .map_to_mm(OrderbookAddrErr::AddrFromPubkeyError) }, + // Todo: implement TRX address generation + CoinProtocol::TRX { .. } => MmError::err(OrderbookAddrErr::CoinIsNotSupported(coin.to_owned())), CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { coins::utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format) .map(OrderbookAddress::Transparent) diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index 9e7e465a78..a7c9063725 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -56,7 +56,6 @@ use lp_swap::PAYMENT_LOCKTIME; use std::sync::atomic::Ordering; use gstuff::slurp; -use serde::ser::Serialize; use serde_json::{self as json, Value as Json}; use std::env; @@ -67,7 +66,6 @@ use std::str; pub use self::lp_native_dex::init_hw; pub use self::lp_native_dex::lp_init; -use coins::update_coins_config; use mm2_err_handle::prelude::*; #[cfg(not(target_arch = "wasm32"))] pub mod database; @@ -284,14 +282,6 @@ pub fn mm2_main(version: String, datetime: String) { // we're not checking them for the mode switches in order not to risk [untrusted] data being mistaken for a mode switch. let first_arg = args_os.get(1).and_then(|arg| arg.to_str()); - if first_arg == Some("update_config") { - match on_update_config(&args_os) { - Ok(_) => println!("Success"), - Err(e) => eprintln!("{}", e), - } - return; - } - if first_arg == Some("--version") || first_arg == Some("-v") || first_arg == Some("version") { println!("Komodo DeFi Framework: {version}"); return; @@ -388,35 +378,6 @@ pub fn run_lp_main( Ok(()) } -#[cfg(not(target_arch = "wasm32"))] -fn on_update_config(args: &[OsString]) -> Result<(), String> { - use mm2_io::fs::safe_slurp; - - let src_path = args.get(2).ok_or(ERRL!("Expect path to the source coins config."))?; - let dst_path = args.get(3).ok_or(ERRL!("Expect destination path."))?; - - let config = try_s!(safe_slurp(src_path)); - let mut config: Json = try_s!(json::from_slice(&config)); - - let result = if config.is_array() { - try_s!(update_coins_config(config)) - } else { - // try to get config["coins"] as array - let conf_obj = config.as_object_mut().ok_or(ERRL!("Expected coin list"))?; - let coins = conf_obj.remove("coins").ok_or(ERRL!("Expected coin list"))?; - let updated_coins = try_s!(update_coins_config(coins)); - conf_obj.insert("coins".into(), updated_coins); - config - }; - - let buf = Vec::new(); - let formatter = json::ser::PrettyFormatter::with_indent(b"\t"); - let mut ser = json::Serializer::with_formatter(buf, formatter); - try_s!(result.serialize(&mut ser)); - try_s!(std::fs::write(dst_path, ser.into_inner())); - Ok(()) -} - #[cfg(not(target_arch = "wasm32"))] fn init_logger(_level: LogLevel, silent_console: bool) -> Result<(), String> { common::log::UnifiedLoggerBuilder::default() diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs index aab020e64e..3a2773c8c1 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs @@ -47,7 +47,7 @@ pub async fn one_inch_v6_0_classic_swap_quote_rpc( let quote = ApiClient::new(ctx) .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? .call_swap_api( - base.chain_id(), + base.chain_id().ok_or(ApiIntegrationRpcError::ChainNotSupported)?, ApiClient::get_quote_method().to_owned(), Some(query_params), ) @@ -102,7 +102,7 @@ pub async fn one_inch_v6_0_classic_swap_create_rpc( let swap_with_tx = ApiClient::new(ctx) .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? .call_swap_api( - base.chain_id(), + base.chain_id().ok_or(ApiIntegrationRpcError::ChainNotSupported)?, ApiClient::get_swap_method().to_owned(), Some(query_params), ) @@ -159,7 +159,7 @@ async fn get_coin_for_one_inch(ctx: &MmArc, ticker: &str) -> MmResult<(EthCoin, #[allow(clippy::result_large_err)] fn api_supports_pair(base: &EthCoin, rel: &EthCoin) -> MmResult<(), ApiIntegrationRpcError> { - if !ApiClient::is_chain_supported(base.chain_id()) { + if !ApiClient::is_chain_supported(base.chain_id().ok_or(ApiIntegrationRpcError::ChainNotSupported)?) { return MmError::err(ApiIntegrationRpcError::ChainNotSupported); } if base.chain_id() != rel.chain_id() { @@ -191,9 +191,11 @@ mod tests { "coin": ticker_coin, "name": "ethereum", "derivation_path": "m/44'/1'", - "chain_id": 1, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": 1, + } }, "trezor_coin": "Ethereum" }); diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs index 253c8adb74..2425bf8fbb 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -65,7 +65,7 @@ lazy_static! { // Due to the SLP protocol limitations only 19 outputs (18 + change) can be sent in one transaction, which is sufficient for now though. // Supply more privkeys when 18 will be not enough. pub static ref SLP_TOKEN_OWNERS: Mutex> = Mutex::new(Vec::with_capacity(18)); - pub static ref MM_CTX: MmArc = MmCtxBuilder::new().with_conf(json!({"use_trading_proto_v2": true})).into_mm_arc(); + pub static ref MM_CTX: MmArc = MmCtxBuilder::new().with_conf(json!({"coins":[eth_dev_conf()],"use_trading_proto_v2": true})).into_mm_arc(); /// We need a second `MmCtx` instance when we use the same private keys for Maker and Taker across various tests. /// When enabling coins for both Maker and Taker, two distinct coin instances are created. /// This means that different instances of the same coin should have separate global nonce locks. @@ -1124,10 +1124,10 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { )); log!("Checking alice status.."); - block_on(wait_check_stats_swap_status(&mm_alice, &uuid, 30)); + block_on(wait_check_stats_swap_status(&mm_alice, &uuid, 240)); log!("Checking bob status.."); - block_on(wait_check_stats_swap_status(&mm_bob, &uuid, 30)); + block_on(wait_check_stats_swap_status(&mm_bob, &uuid, 240)); log!("Checking alice recent swaps.."); block_on(check_recent_swaps(&mm_alice, 1)); diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs index ff7e6415fb..936272b063 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -4545,7 +4545,7 @@ fn test_set_price_conf_settings() { .display_priv_key() .unwrap(); - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2,"chain_id": 1337},]); + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); let conf = Mm2TestConf::seednode(&private_key_str, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); @@ -4618,7 +4618,7 @@ fn test_buy_conf_settings() { .display_priv_key() .unwrap(); - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2,"chain_id": 1337},]); + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); let conf = Mm2TestConf::seednode(&private_key_str, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); @@ -4691,7 +4691,7 @@ fn test_sell_conf_settings() { .display_priv_key() .unwrap(); - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2,"chain_id": 1337},]); + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); let conf = Mm2TestConf::seednode(&private_key_str, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index 840e30279a..3b126c39c4 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -10,8 +10,8 @@ use crate::common::Future01CompatExt; use bitcrypto::{dhash160, sha256}; use coins::eth::gas_limit::ETH_MAX_TRADE_GAS; use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, EthActivationV2Request, EthNode}; -use coins::eth::{checksum_address, eth_coin_from_conf_and_request, EthCoin, EthCoinType, EthPrivKeyBuildPolicy, - SignedEthTx, SwapV2Contracts, ERC20_ABI}; +use coins::eth::{checksum_address, eth_coin_from_conf_and_request, ChainSpec, EthCoin, EthCoinType, + EthPrivKeyBuildPolicy, SignedEthTx, SwapV2Contracts, ERC20_ABI}; use coins::hd_wallet::AddrToString; use coins::nft::nft_structs::{Chain, ContractType, NftInfo}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] @@ -35,7 +35,7 @@ use mm2_test_helpers::for_tests::{account_balance, active_swaps, coins_needed_fo enable_erc20_token_v2, enable_eth_coin_v2, enable_eth_with_tokens_v2, erc20_dev_conf, eth1_dev_conf, eth_dev_conf, get_locked_amount, get_new_address, get_token_info, mm_dump, my_swap_status, nft_dev_conf, start_swaps, MarketMakerIt, - Mm2TestConf, SwapV2TestContracts, TestNode}; + Mm2TestConf, SwapV2TestContracts, TestNode, ETH_SEPOLIA_CHAIN_ID}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use mm2_test_helpers::for_tests::{eth_sepolia_conf, sepolia_erc20_dev_conf}; use mm2_test_helpers::structs::{Bip44Chain, EnableCoinBalanceMap, EthWithTokensActivationResult, HDAccountAddressId, @@ -59,6 +59,7 @@ const SEPOLIA_TAKER_PRIV: &str = "e0be82dca60ff7e4c6d6db339ac9e1ae249af081dba211 const NFT_ETH: &str = "NFT_ETH"; const ETH: &str = "ETH"; const ETH1: &str = "ETH1"; +const GETH_DEV_CHAIN_ID: u64 = 1337; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] const ERC20: &str = "ERC20DEV"; @@ -303,7 +304,9 @@ pub fn eth_coin_with_random_privkey_using_urls(swap_contract_address: Address, u "ETH", ð_conf, &req, - CoinProtocol::ETH, + CoinProtocol::ETH { + chain_id: GETH_DEV_CHAIN_ID, + }, PrivKeyBuildPolicy::IguanaPrivKey(secret), )) .unwrap(); @@ -402,6 +405,9 @@ fn global_nft_with_random_privkey( &nft_dev_conf(), platform_request, build_policy, + ChainSpec::Evm { + chain_id: GETH_DEV_CHAIN_ID, + }, )) .unwrap(); @@ -432,6 +438,7 @@ fn global_nft_with_random_privkey( global_nft } +// Todo: This shouldn't be part of docker tests, move it to a separate module or stop relying on it #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] /// Can be used to generate coin from Sepolia Maker/Taker priv keys. fn sepolia_coin_from_privkey(ctx: &MmArc, secret: &'static str, ticker: &str, conf: &Json, erc20: bool) -> EthCoin { @@ -472,6 +479,9 @@ fn sepolia_coin_from_privkey(ctx: &MmArc, secret: &'static str, ticker: &str, co conf, platform_request, build_policy, + ChainSpec::Evm { + chain_id: ETH_SEPOLIA_CHAIN_ID, + }, )) .unwrap(); let coin = if erc20 { @@ -518,7 +528,9 @@ pub fn fill_eth_erc20_with_private_key(priv_key: Secp256k1Secret) { "ETH", ð_conf, &req, - CoinProtocol::ETH, + CoinProtocol::ETH { + chain_id: GETH_DEV_CHAIN_ID, + }, PrivKeyBuildPolicy::IguanaPrivKey(priv_key), )) .unwrap(); @@ -1465,6 +1477,9 @@ fn eth_coin_v2_activation_with_random_privkey( conf, platform_request, build_policy, + ChainSpec::Evm { + chain_id: GETH_DEV_CHAIN_ID, + }, )) .unwrap(); let my_address = block_on(coin.my_addr()); diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index f29a7ef34b..d51bc50d34 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -1227,10 +1227,10 @@ mod swap { for uuid in uuids.iter() { log!("Checking alice status.."); - wait_check_stats_swap_status(&mm_alice, uuid, 30).await; + wait_check_stats_swap_status(&mm_alice, uuid, 240).await; log!("Checking bob status.."); - wait_check_stats_swap_status(&mm_bob, uuid, 30).await; + wait_check_stats_swap_status(&mm_bob, uuid, 240).await; } log!("Checking alice recent swaps.."); diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index a2451ac362..7ad99c3b8c 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -347,8 +347,8 @@ fn test_check_balance_on_order_post() { let coins = json!([ {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, {"coin":"MORTY","asset":"MORTY","rpcport":11608,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"ETH","name":"ethereum","chain_id":1,"protocol":{"type":"ETH"},"rpcport":80}, - {"coin":"JST","name":"jst","chain_id":1,"protocol":{"type":"ERC20", "protocol_data":{"platform":"ETH","contract_address":"0x996a8aE0304680F6A69b8A9d7C6E37D65AB5AB56"}}} + {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH","protocol_data":{"chain_id":1}},"rpcport":80}, + {"coin":"JST","name":"jst","protocol":{"type":"ERC20", "protocol_data":{"platform":"ETH","contract_address":"0x996a8aE0304680F6A69b8A9d7C6E37D65AB5AB56"}}} ]); // start bob and immediately place the order @@ -794,10 +794,10 @@ async fn trade_base_rel_electrum( #[cfg(all(not(target_arch = "wasm32"), not(feature = "zhtlc-native-tests")))] for uuid in uuids.iter() { log!("Checking alice status.."); - wait_check_stats_swap_status(&mm_alice, uuid, 30).await; + wait_check_stats_swap_status(&mm_alice, uuid, 240).await; log!("Checking bob status.."); - wait_check_stats_swap_status(&mm_bob, uuid, 30).await; + wait_check_stats_swap_status(&mm_bob, uuid, 240).await; } log!("Checking alice recent swaps.."); @@ -3541,7 +3541,7 @@ fn test_qrc20_withdraw_error() { fn test_get_raw_transaction() { let coins = json! ([ {"coin":"RICK","asset":"RICK","required_confirmations":0,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"ETH","name":"ethereum","chain_id":1,"protocol":{"type":"ETH"}}, + {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH","protocol_data":{"chain_id":1}}}, ]); let mm = MarketMakerIt::start( json! ({ @@ -5098,10 +5098,14 @@ fn test_sign_verify_message_eth() { "sign_message_prefix": "Ethereum Signed Message:\n", "rpcport": 80, "mm2": 1, - "chain_id": 1, "required_confirmations": 3, "avg_blocktime": 0.25, - "protocol": {"type": "ETH"} + "protocol": { + "type": "ETH", + "protocol_data": { + "chain_id": 1 + } + } } ]); @@ -6102,8 +6106,8 @@ mod trezor_tests { eth_testnet_conf_trezor, init_trezor_rpc, init_trezor_status_rpc, init_trezor_user_action_rpc, init_withdraw, jst_sepolia_trezor_conf, mm_ctx_with_custom_db_with_conf, tbtc_legacy_conf, tbtc_segwit_conf, - withdraw_status, MarketMakerIt, Mm2TestConf, ETH_SEPOLIA_NODES, - ETH_SEPOLIA_SWAP_CONTRACT}; + withdraw_status, MarketMakerIt, Mm2TestConf, ETH_SEPOLIA_CHAIN_ID, + ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT}; use mm2_test_helpers::structs::{InitTaskResult, RpcV2Response, TransactionDetails, WithdrawStatus}; use rpc_task::{rpc_common::RpcTaskStatusRequest, RpcInitReq, RpcTaskStatus}; use serde_json::{self as json, json, Value as Json}; @@ -6207,7 +6211,9 @@ mod trezor_tests { "ETH", ð_conf, &req, - CoinProtocol::ETH, + CoinProtocol::ETH { + chain_id: ETH_SEPOLIA_CHAIN_ID, + }, priv_key_policy, )) .unwrap(); diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 772fd63449..5f5fa8edfc 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -822,11 +822,13 @@ pub fn eth_testnet_conf_trezor() -> Json { "coin": "ETH", "name": "ethereum", "mm2": 1, - "chain_id": 1337, "max_eth_tx_type": 2, "derivation_path": "m/44'/1'", // Trezor uses coin type 1 for testnet "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": 1337, + } }, "trezor_coin": "Ethereum" }) @@ -842,10 +844,12 @@ fn eth_conf(coin: &str) -> Json { "coin": coin, "name": "ethereum", "mm2": 1, - "chain_id": 1337, "derivation_path": "m/44'/60'", "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": 1337, + } }, "max_eth_tx_type": 2 }) @@ -856,7 +860,6 @@ pub fn erc20_dev_conf(contract_address: &str) -> Json { json!({ "coin": "ERC20DEV", "name": "erc20dev", - "chain_id": 1337, "mm2": 1, "derivation_path": "m/44'/60'", "protocol": { @@ -882,7 +885,6 @@ pub fn nft_dev_conf() -> Json { json!({ "coin": "NFT_ETH", "name": "nftdev", - "chain_id": 1337, "mm2": 1, "derivation_path": "m/44'/60'", "protocol": { @@ -902,9 +904,11 @@ pub fn eth_sepolia_conf() -> Json { "coin": "ETH", "name": "ethereum", "derivation_path": "m/44'/60'", - "chain_id": ETH_SEPOLIA_CHAIN_ID, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": ETH_SEPOLIA_CHAIN_ID, + } }, "max_eth_tx_type": 2, "trezor_coin": "Ethereum" @@ -916,9 +920,11 @@ pub fn eth_sepolia_trezor_firmware_compat_conf() -> Json { "coin": "tETH", "name": "ethereum", "derivation_path": "m/44'/1'", // Note: trezor uses coin type 1' for eth for testnet (SLIP44_TESTNET) - "chain_id": ETH_SEPOLIA_CHAIN_ID, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": ETH_SEPOLIA_CHAIN_ID, + } }, "max_eth_tx_type": 2, "trezor_coin": "tETH" @@ -929,7 +935,6 @@ pub fn eth_jst_testnet_conf() -> Json { json!({ "coin": "JST", "name": "jst", - "chain_id": 1337, "derivation_path": "m/44'/60'", "protocol": { "type": "ERC20", @@ -946,12 +951,10 @@ pub fn jst_sepolia_conf() -> Json { json!({ "coin": "JST", "name": "jst", - "chain_id": ETH_SEPOLIA_CHAIN_ID, "protocol": { "type": "ERC20", "protocol_data": { "platform": "ETH", - "chain_id": ETH_SEPOLIA_CHAIN_ID, "contract_address": ETH_SEPOLIA_TOKEN_CONTRACT } }, @@ -963,14 +966,12 @@ pub fn jst_sepolia_trezor_conf() -> Json { json!({ "coin": "tJST", "name": "tjst", - "chain_id": ETH_SEPOLIA_CHAIN_ID, "derivation_path": "m/44'/1'", // Note: Trezor uses 1' coin type for all testnets "trezor_coin": "tETH", "protocol": { "type": "ERC20", "protocol_data": { "platform": "ETH", - "chain_id": ETH_SEPOLIA_CHAIN_ID, "contract_address": ETH_SEPOLIA_TOKEN_CONTRACT } } @@ -1095,11 +1096,13 @@ pub fn tbnb_conf() -> Json { "coin": "tBNB", "name": "binancesmartchaintest", "avg_blocktime": 0.25, - "chain_id": 97, "mm2": 1, "required_confirmations": 0, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": 97 + } } }) } From c1140d0e08f9b0421f4669fbf0b8a296cb8422a6 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Tue, 13 May 2025 00:54:31 +0300 Subject: [PATCH 05/36] chore(deps): remove base58 and replace it completely with bs58 (#2427) --- Cargo.lock | 9 +-------- mm2src/adex_cli/Cargo.lock | 7 ------- mm2src/coins/Cargo.toml | 2 -- mm2src/coins/lp_coins.rs | 14 -------------- mm2src/mm2_bitcoin/keys/Cargo.toml | 2 +- mm2src/mm2_bitcoin/keys/src/legacyaddress.rs | 7 ++++--- mm2src/mm2_bitcoin/keys/src/lib.rs | 2 +- mm2src/mm2_bitcoin/keys/src/private.rs | 5 ++--- 8 files changed, 9 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3be8929059..92649835e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,12 +370,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base58" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" - [[package]] name = "base64" version = "0.11.0" @@ -830,7 +824,6 @@ version = "0.1.0" dependencies = [ "async-std", "async-trait", - "base58", "base64 0.21.7", "bip32", "bitcoin", @@ -3049,9 +3042,9 @@ dependencies = [ name = "keys" version = "0.1.0" dependencies = [ - "base58", "bech32", "bitcrypto", + "bs58 0.4.0", "derive_more", "lazy_static", "primitives", diff --git a/mm2src/adex_cli/Cargo.lock b/mm2src/adex_cli/Cargo.lock index 5d5eb5abeb..099a669565 100644 --- a/mm2src/adex_cli/Cargo.lock +++ b/mm2src/adex_cli/Cargo.lock @@ -336,12 +336,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base58" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" - [[package]] name = "base64" version = "0.21.7" @@ -1714,7 +1708,6 @@ dependencies = [ name = "keys" version = "0.1.0" dependencies = [ - "base58", "bech32", "bitcrypto", "derive_more", diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 670688e07d..47a5baace0 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -23,8 +23,6 @@ doctest = false async-std = { version = "1.5", features = ["unstable"] } async-trait = "0.1.52" base64 = "0.21.2" -# Todo: remove this and rely on bs58 throught the whole codebase -base58 = "0.2.0" bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } bitcoin_hashes = "0.11" bitcrypto = { path = "../mm2_bitcoin/crypto" } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index a211761968..83f1aa0026 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -43,7 +43,6 @@ #[macro_use] extern crate ser_error_derive; use async_trait::async_trait; -use base58::FromBase58Error; use bip32::ExtendedPrivateKey; use common::custom_futures::timeout::TimeoutError; use common::executor::{abortable_queue::WeakSpawner, AbortedError, SpawnFuture}; @@ -3418,19 +3417,6 @@ impl HttpStatusCode for VerificationError { } } -impl From for VerificationError { - fn from(e: FromBase58Error) -> Self { - match e { - FromBase58Error::InvalidBase58Character(c, _) => { - VerificationError::AddressDecodingError(format!("Invalid Base58 Character: {}", c)) - }, - FromBase58Error::InvalidBase58Length => { - VerificationError::AddressDecodingError(String::from("Invalid Base58 Length")) - }, - } - } -} - /// NB: Implementations are expected to follow the pImpl idiom, providing cheap reference-counted cloning and garbage collection. #[async_trait] pub trait MmCoin: SwapOps + WatcherOps + MarketCoinOps + Send + Sync + 'static { diff --git a/mm2src/mm2_bitcoin/keys/Cargo.toml b/mm2src/mm2_bitcoin/keys/Cargo.toml index 990bd3dff1..7002e711a8 100644 --- a/mm2src/mm2_bitcoin/keys/Cargo.toml +++ b/mm2src/mm2_bitcoin/keys/Cargo.toml @@ -7,8 +7,8 @@ authors = ["debris "] doctest = false [dependencies] +bs58 = "0.4.0" rustc-hex = "2" -base58 = "0.2" bech32 = "0.9.1" bitcrypto = { path = "../crypto" } derive_more = "0.99" diff --git a/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs b/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs index a9e93127af..e6a4bf4cd9 100644 --- a/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs +++ b/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs @@ -1,7 +1,6 @@ use std::str::FromStr; use std::{convert::TryInto, fmt}; -use base58::{FromBase58, ToBase58}; use crypto::{checksum, ChecksumType}; use std::ops::Deref; use {AddressHashEnum, AddressPrefix, DisplayLayout}; @@ -82,7 +81,7 @@ impl FromStr for LegacyAddress { where Self: Sized, { - let hex = s.from_base58().map_err(|_| Error::InvalidAddress)?; + let hex = bs58::decode(s).into_vec().map_err(|_| Error::InvalidAddress)?; LegacyAddress::from_layout(&hex) } } @@ -92,7 +91,9 @@ impl From<&'static str> for LegacyAddress { } impl fmt::Display for LegacyAddress { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { self.layout().to_base58().fmt(fmt) } + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + bs58::encode(self.layout().as_ref()).into_string().fmt(fmt) + } } impl LegacyAddress { diff --git a/mm2src/mm2_bitcoin/keys/src/lib.rs b/mm2src/mm2_bitcoin/keys/src/lib.rs index d3a01d854d..88eea9408f 100644 --- a/mm2src/mm2_bitcoin/keys/src/lib.rs +++ b/mm2src/mm2_bitcoin/keys/src/lib.rs @@ -1,8 +1,8 @@ //! Bitcoin keys. -extern crate base58; extern crate bech32; extern crate bitcrypto as crypto; +extern crate bs58; extern crate derive_more; extern crate lazy_static; extern crate primitives; diff --git a/mm2src/mm2_bitcoin/keys/src/private.rs b/mm2src/mm2_bitcoin/keys/src/private.rs index ce4e64a908..435a3b7a46 100644 --- a/mm2src/mm2_bitcoin/keys/src/private.rs +++ b/mm2src/mm2_bitcoin/keys/src/private.rs @@ -2,7 +2,6 @@ use crate::SECP_SIGN; use address::detect_checksum; -use base58::{FromBase58, ToBase58}; use crypto::{checksum, ChecksumType}; use hex::ToHex; use secp256k1::{Message as SecpMessage, SecretKey}; @@ -110,7 +109,7 @@ impl fmt::Debug for Private { } impl fmt::Display for Private { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.layout().to_base58().fmt(f) } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { bs58::encode(self.layout()).into_string().fmt(f) } } impl FromStr for Private { @@ -120,7 +119,7 @@ impl FromStr for Private { where Self: Sized, { - let hex = s.from_base58().map_err(|_| Error::InvalidPrivate)?; + let hex = bs58::decode(s).into_vec().map_err(|_| Error::InvalidPrivate)?; Private::from_layout(&hex) } } From 56dce19819852c0e760595efb0c5e09d4baca568 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 14 May 2025 20:55:51 +0300 Subject: [PATCH 06/36] fix(eth-balance-events): serialize eth address using AddrToString (#2440) --- mm2src/coins/eth/eth_balance_events.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mm2src/coins/eth/eth_balance_events.rs b/mm2src/coins/eth/eth_balance_events.rs index 0cb7afe134..dd3d125832 100644 --- a/mm2src/coins/eth/eth_balance_events.rs +++ b/mm2src/coins/eth/eth_balance_events.rs @@ -1,5 +1,6 @@ use super::EthCoin; use crate::{eth::{u256_to_big_decimal, Erc20TokenDetails}, + hd_wallet::AddrToString, BalanceError, CoinWithDerivationMethod}; use common::{executor::Timer, log, Future01CompatExt}; use mm2_err_handle::prelude::MmError; @@ -111,7 +112,7 @@ async fn fetch_balance( .await .map_err(|error| BalanceFetchError { ticker: token_ticker.clone(), - address: address.to_string(), + address: address.addr_to_string(), error, })?, coin.decimals, @@ -122,7 +123,7 @@ async fn fetch_balance( .await .map_err(|error| BalanceFetchError { ticker: token_ticker.clone(), - address: address.to_string(), + address: address.addr_to_string(), error, })?, info.decimals, @@ -131,13 +132,13 @@ async fn fetch_balance( let balance_as_big_decimal = u256_to_big_decimal(balance_as_u256, decimals).map_err(|e| BalanceFetchError { ticker: token_ticker.clone(), - address: address.to_string(), + address: address.addr_to_string(), error: e.into(), })?; Ok(BalanceData { ticker: token_ticker, - address: address.to_string(), + address: address.addr_to_string(), balance: balance_as_big_decimal, }) } From 66d3b2a47b835461a469328f36507b7b908ba1e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 19 May 2025 13:53:21 +0300 Subject: [PATCH 07/36] feat(pubkey-banning): expirable bans (#2455) Implements expirable public key banning. For swaps public keys will be banned for one hour, which should be a sufficient penalty (and can be easily increased if needed). Manual bans will default to permanent but this can be changed by including the duration_min parameter in the request. --- Cargo.lock | 5 +- mm2src/coins/Cargo.toml | 4 +- mm2src/mm2_core/Cargo.toml | 4 +- mm2src/mm2_main/Cargo.toml | 4 +- mm2src/mm2_main/src/lp_swap.rs | 4 +- mm2src/mm2_main/src/lp_swap/pubkey_banning.rs | 61 +++++++++++++------ mm2src/mm2_p2p/Cargo.toml | 4 +- 7 files changed, 56 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92649835e2..f24abff6fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6969,11 +6969,12 @@ dependencies = [ [[package]] name = "timed-map" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07be2341cfbd1b8b9a84eb9212476ea383ef5cddeb85fa3ef89dc66666196619" +checksum = "ac74a5331850dc3b08de854b57674af757b6e286e7ef930baf71e0a196f53790" dependencies = [ "rustc-hash", + "serde", "web-time", ] diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 47a5baace0..26c3b39c97 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -125,7 +125,7 @@ mm2_db = { path = "../mm2_db" } mm2_metamask = { path = "../mm2_metamask" } mm2_test_helpers = { path = "../mm2_test_helpers" } time = { version = "0.3.20", features = ["wasm-bindgen"] } -timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } +timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } tonic = { version = "0.10", default-features = false, features = ["prost", "codegen", "gzip"] } tower-service = "0.3" wasm-bindgen = "0.2.86" @@ -148,7 +148,7 @@ lightning-net-tokio = "0.0.113" rust-ini = { version = "0.13" } rustls = { version = "0.21", features = ["dangerous_configuration"] } secp256k1v24 = { version = "0.24", package = "secp256k1" } -timed-map = { version = "1.3", features = ["rustc-hash"] } +timed-map = { version = "1.4", features = ["rustc-hash"] } tokio = { version = "1.20" } tokio-rustls = { version = "0.24" } tonic = { version = "0.10", features = ["tls", "tls-webpki-roots", "gzip"] } diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index b3756f9b94..37cf759a9d 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -37,10 +37,10 @@ uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] mm2_rpc = { path = "../mm2_rpc", features = [ "rpc_facilities" ] } -timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } +timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } wasm-bindgen-test = { version = "0.3.2" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rustls = { version = "0.21", default-features = false } tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } -timed-map = { version = "1.3", features = ["rustc-hash"] } +timed-map = { version = "1.4", features = ["rustc-hash"] } diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 28291cb210..f1a365d38a 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -108,7 +108,7 @@ instant = { version = "0.1.12", features = ["wasm-bindgen"] } js-sys = { version = "0.3.27" } mm2_db = { path = "../mm2_db" } mm2_test_helpers = { path = "../mm2_test_helpers" } -timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } +timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } wasm-bindgen = "0.2.86" wasm-bindgen-futures = { version = "0.4.1" } wasm-bindgen-test = { version = "0.3.1" } @@ -121,7 +121,7 @@ hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } rcgen = "0.10" rustls = { version = "0.21", default-features = false } rustls-pemfile = "1.0.2" -timed-map = { version = "1.3", features = ["rustc-hash"] } +timed-map = { version = "1.4", features = ["rustc-hash"] } tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net", "signal"] } [target.'cfg(windows)'.dependencies] diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index b15806c059..7f6cf266e8 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -532,7 +532,7 @@ struct LockedAmountInfo { struct SwapsContext { running_swaps: Mutex>>, active_swaps_v2_infos: Mutex>, - banned_pubkeys: Mutex>, + banned_pubkeys: Mutex>, swap_msgs: Mutex>, swap_v2_msgs: Mutex>, taker_swap_watchers: PaMutex, ()>>, @@ -548,7 +548,7 @@ impl SwapsContext { Ok(SwapsContext { running_swaps: Mutex::new(HashMap::new()), active_swaps_v2_infos: Mutex::new(HashMap::new()), - banned_pubkeys: Mutex::new(HashMap::new()), + banned_pubkeys: Mutex::new(TimedMap::new_with_map_kind(MapKind::FxHashMap)), swap_msgs: Mutex::new(HashMap::new()), swap_v2_msgs: Mutex::new(HashMap::new()), taker_swap_watchers: PaMutex::new(TimedMap::new_with_map_kind(MapKind::FxHashMap)), diff --git a/mm2src/mm2_main/src/lp_swap/pubkey_banning.rs b/mm2src/mm2_main/src/lp_swap/pubkey_banning.rs index 5aa8f94103..a9aded8a97 100644 --- a/mm2src/mm2_main/src/lp_swap/pubkey_banning.rs +++ b/mm2src/mm2_main/src/lp_swap/pubkey_banning.rs @@ -1,10 +1,12 @@ +use std::collections::HashMap; + use super::{SwapEvent, SwapsContext}; use chain::hash::H256; +use compatible_time::Duration; use http::Response; use mm2_core::mm_ctx::MmArc; use rpc::v1::types::H256 as H256Json; use serde_json::{self as json, Value as Json}; -use std::collections::hash_map::{Entry, HashMap}; use uuid::Uuid; #[derive(Serialize)] @@ -21,12 +23,19 @@ pub enum BanReason { } pub fn ban_pubkey_on_failed_swap(ctx: &MmArc, pubkey: H256, swap_uuid: &Uuid, event: SwapEvent) { + // Ban them for an hour. + const PENALTY: Duration = Duration::from_secs(60 * 60); + let ctx = SwapsContext::from_ctx(ctx).unwrap(); let mut banned = ctx.banned_pubkeys.lock().unwrap(); - banned.insert(pubkey.into(), BanReason::FailedSwap { - caused_by_swap: *swap_uuid, - caused_by_event: event, - }); + banned.insert_expirable( + pubkey.into(), + BanReason::FailedSwap { + caused_by_swap: *swap_uuid, + caused_by_event: event, + }, + PENALTY, + ); } pub fn is_pubkey_banned(ctx: &MmArc, pubkey: &H256Json) -> bool { @@ -47,6 +56,7 @@ pub async fn list_banned_pubkeys_rpc(ctx: MmArc) -> Result>, St struct BanPubkeysReq { pubkey: H256Json, reason: String, + duration_min: Option, } pub async fn ban_pubkey_rpc(ctx: MmArc, req: Json) -> Result>, String> { @@ -54,16 +64,25 @@ pub async fn ban_pubkey_rpc(ctx: MmArc, req: Json) -> Result>, let ctx = try_s!(SwapsContext::from_ctx(&ctx)); let mut banned_pubs = try_s!(ctx.banned_pubkeys.lock()); - match banned_pubs.entry(req.pubkey) { - Entry::Occupied(_) => ERR!("Pubkey is banned already"), - Entry::Vacant(entry) => { - entry.insert(BanReason::Manual { reason: req.reason }); - let res = try_s!(json::to_vec(&json!({ - "result": "success", - }))); - Ok(try_s!(Response::builder().body(res))) - }, + if banned_pubs.contains_key(&req.pubkey) { + return ERR!("Pubkey is banned already"); } + + if let Some(duration_min) = req.duration_min { + banned_pubs.insert_expirable( + req.pubkey, + BanReason::Manual { reason: req.reason }, + Duration::from_secs(duration_min as u64 * 60), + ); + } else { + banned_pubs.insert_constant(req.pubkey, BanReason::Manual { reason: req.reason }); + } + + let res = try_s!(json::to_vec(&json!({ + "result": "success", + }))); + + Response::builder().body(res).map_err(|e| e.to_string()) } #[derive(Deserialize)] @@ -77,13 +96,16 @@ pub async fn unban_pubkeys_rpc(ctx: MmArc, req: Json) -> Result let req: UnbanPubkeysReq = try_s!(json::from_value(req["unban_by"].clone())); let ctx = try_s!(SwapsContext::from_ctx(&ctx)); let mut banned_pubs = try_s!(ctx.banned_pubkeys.lock()); - let mut unbanned = HashMap::new(); let mut were_not_banned = vec![]; - match req { + + let unbanned = match req { UnbanPubkeysReq::All => { - unbanned = banned_pubs.drain().collect(); + let unbanned = json!(*banned_pubs); + banned_pubs.clear(); + unbanned }, UnbanPubkeysReq::Few(pubkeys) => { + let mut unbanned = HashMap::new(); for pubkey in pubkeys { match banned_pubs.remove(&pubkey) { Some(removed) => { @@ -92,8 +114,11 @@ pub async fn unban_pubkeys_rpc(ctx: MmArc, req: Json) -> Result None => were_not_banned.push(pubkey), } } + + json!(unbanned) }, - } + }; + let res = try_s!(json::to_vec(&json!({ "result": { "still_banned": *banned_pubs, diff --git a/mm2src/mm2_p2p/Cargo.toml b/mm2src/mm2_p2p/Cargo.toml index 6cc38db4d6..813e74dba6 100644 --- a/mm2src/mm2_p2p/Cargo.toml +++ b/mm2src/mm2_p2p/Cargo.toml @@ -40,13 +40,13 @@ void = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] futures-rustls = "0.24" libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } -timed-map = { version = "1.3", features = ["rustc-hash"] } +timed-map = { version = "1.4", features = ["rustc-hash"] } tokio = { version = "1.20", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] futures-rustls = "0.22" libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } -timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } +timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } [dev-dependencies] async-std = "1.6.2" From ecb17ecf1a8b8fdded17e68f378856285acea4e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 19 May 2025 21:17:04 +0300 Subject: [PATCH 08/36] chore(build-artifacts): remove duplicated mm2 build artifacts (#2448) * remove duplicated mm2 artifacts Signed-off-by: onur-ozkan * create `$SAFE_DIR_NAME` before moving artifacts Signed-off-by: onur-ozkan --------- Signed-off-by: onur-ozkan --- .github/workflows/dev-build.yml | 99 +++-------------------------- .github/workflows/release-build.yml | 76 +++------------------- mm2src/mm2_bin_lib/Cargo.toml | 7 -- 3 files changed, 16 insertions(+), 166 deletions(-) diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 585f9d4856..b44ec5bae4 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -51,17 +51,6 @@ jobs: - name: Build run: cargo build --release - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-linux-x86-64.zip" - zip $NAME target/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -70,6 +59,7 @@ jobs: NAME="kdf_$KDF_BUILD_TAG-linux-x86-64.zip" zip $NAME target/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -125,16 +115,6 @@ jobs: - name: Build run: cargo build --release --target x86_64-apple-darwin - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-x86-64.zip" - zip $NAME target/x86_64-apple-darwin/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -143,6 +123,7 @@ jobs: NAME="kdf_$KDF_BUILD_TAG-mac-x86-64.zip" zip $NAME target/x86_64-apple-darwin/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -186,17 +167,6 @@ jobs: - name: Build run: cargo build --release --target aarch64-apple-darwin - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-arm64.zip" - zip $NAME target/aarch64-apple-darwin/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -205,6 +175,7 @@ jobs: NAME="kdf_$KDF_BUILD_TAG-mac-arm64.zip" zip $NAME target/aarch64-apple-darwin/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -247,17 +218,6 @@ jobs: - name: Build run: cargo build --release - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - $NAME="mm2_$Env:KDF_BUILD_TAG-win-x86-64.zip" - 7z a $NAME .\target\release\mm2.exe .\target\release\*.dll - $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -266,6 +226,7 @@ jobs: $NAME="kdf_$Env:KDF_BUILD_TAG-win-x86-64.zip" 7z a $NAME .\target\release\kdf.exe .\target\release\*.dll $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -309,18 +270,6 @@ jobs: - name: Build run: cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-dylib-x86-64.zip" - cp target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libmm2.a - zip $NAME target/x86_64-apple-darwin/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -330,6 +279,7 @@ jobs: mv target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libkdf.a zip $NAME target/x86_64-apple-darwin/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -438,18 +388,6 @@ jobs: - name: Build run: cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-ios-aarch64.zip" - cp target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libmm2.a - zip $NAME target/aarch64-apple-ios/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -459,6 +397,7 @@ jobs: mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libkdf.a zip $NAME target/aarch64-apple-ios/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -516,18 +455,6 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-android-aarch64.zip" - cp target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libmm2.a - zip $NAME target/aarch64-linux-android/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -537,6 +464,7 @@ jobs: mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libkdf.a zip $NAME target/aarch64-linux-android/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -594,18 +522,6 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-android-armv7.zip" - cp target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libmm2.a - zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -615,6 +531,7 @@ jobs: mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libkdf.a zip $NAME target/armv7-linux-androideabi/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 7fec248c88..75e9262b55 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -51,19 +51,12 @@ jobs: - name: Build run: cargo build --release - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-linux-x86-64.zip" - zip $NAME target/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-linux-x86-64.zip" zip $NAME target/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -116,19 +109,12 @@ jobs: - name: Build run: cargo build --release --target x86_64-apple-darwin - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-x86-64.zip" - zip $NAME target/x86_64-apple-darwin/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-mac-x86-64.zip" zip $NAME target/x86_64-apple-darwin/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -172,19 +158,12 @@ jobs: - name: Build run: cargo build --release --target aarch64-apple-darwin - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-arm64.zip" - zip $NAME target/aarch64-apple-darwin/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-mac-arm64.zip" zip $NAME target/aarch64-apple-darwin/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -227,19 +206,12 @@ jobs: - name: Build run: cargo build --release - - name: Compress mm2 build output - run: | - $NAME="mm2_$Env:KDF_BUILD_TAG-win-x86-64.zip" - 7z a $NAME .\target\release\mm2.exe .\target\release\*.dll - $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | $NAME="kdf_$Env:KDF_BUILD_TAG-win-x86-64.zip" 7z a $NAME .\target\release\kdf.exe .\target\release\*.dll $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -283,21 +255,13 @@ jobs: - name: Build run: cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-dylib-x86-64.zip" - cp target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libmm2.a - zip $NAME target/x86_64-apple-darwin/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-mac-dylib-x86-64.zip" mv target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libkdf.a zip $NAME target/x86_64-apple-darwin/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -403,21 +367,13 @@ jobs: - name: Build run: cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-ios-aarch64.zip" - mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libmm2.a - zip $NAME target/aarch64-apple-ios/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-ios-aarch64.zip" mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libkdf.a zip $NAME target/aarch64-apple-ios/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -475,21 +431,13 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-android-aarch64.zip" - mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libmm2.a - zip $NAME target/aarch64-linux-android/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-android-aarch64.zip" mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libkdf.a zip $NAME target/aarch64-linux-android/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -547,21 +495,13 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-android-armv7.zip" - mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libmm2.a - zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-android-armv7.zip" mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libkdf.a zip $NAME target/armv7-linux-androideabi/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index 2097703ed7..0d267ca91f 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -17,13 +17,6 @@ track-ctx-pointer = ["mm2_main/track-ctx-pointer"] zhtlc-native-tests = ["mm2_main/zhtlc-native-tests"] test-ext-api = ["mm2_main/test-ext-api"] -[[bin]] -name = "mm2" -path = "src/mm2_bin.rs" -test = false -doctest = false -bench = false - [[bin]] name = "kdf" path = "src/mm2_bin.rs" From b8b98cf3248203e90a1fce5b384e0864e8d504d4 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 21 May 2025 05:40:06 +0300 Subject: [PATCH 09/36] feat(db-arch): more dbdir to address_dir replacements (#2398) address_dir() will now default to dbdir() if the new-db-arch feature is disabled. That won't affect dev environment while this feature is in progress. --- Cargo.lock | 2 + mm2src/coins/Cargo.toml | 2 +- mm2src/coins/coin_errors.rs | 5 + mm2src/coins/eth.rs | 27 +++-- mm2src/coins/eth/v2_activation.rs | 4 +- mm2src/coins/lightning.rs | 10 +- mm2src/coins/lightning/ln_utils.rs | 34 +++--- mm2src/coins/lp_coins.rs | 7 +- mm2src/coins/qrc20.rs | 10 +- mm2src/coins/siacoin.rs | 25 +++-- mm2src/coins/tendermint/tendermint_coin.rs | 10 +- mm2src/coins/tendermint/tendermint_token.rs | 8 +- mm2src/coins/test_coin.rs | 6 +- mm2src/coins/utxo.rs | 1 + mm2src/coins/utxo/bch.rs | 8 +- mm2src/coins/utxo/qtum.rs | 8 +- mm2src/coins/utxo/slp.rs | 11 +- .../utxo/utxo_builder/utxo_coin_builder.rs | 17 +-- mm2src/coins/utxo/utxo_standard.rs | 8 +- mm2src/coins/z_coin.rs | 101 +++++++++--------- .../storage/blockdb/blockdb_sql_storage.rs | 1 + .../storage/walletdb/wallet_sql_storage.rs | 10 +- .../z_coin/storage/walletdb/wasm/storage.rs | 5 +- mm2src/coins/z_coin/z_coin_native_tests.rs | 31 ++---- mm2src/coins/z_coin/z_rpc.rs | 8 +- mm2src/coins_activation/Cargo.toml | 1 + .../src/lightning_activation.rs | 16 ++- mm2src/mm2_core/Cargo.toml | 1 + mm2src/mm2_core/src/mm_ctx.rs | 34 +++--- mm2src/mm2_io/src/file_lock.rs | 6 ++ mm2src/mm2_io/src/fs.rs | 83 +++++++++++++- mm2src/mm2_main/Cargo.toml | 2 +- mm2src/mm2_main/src/lp_swap.rs | 31 +++--- mm2src/mm2_main/src/lp_swap/maker_swap.rs | 14 ++- .../src/lp_swap/recreate_swap_data.rs | 38 ++++++- mm2src/mm2_main/src/lp_swap/saved_swap.rs | 51 +++++++-- mm2src/mm2_main/src/lp_swap/swap_lock.rs | 10 +- mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs | 2 +- mm2src/mm2_main/src/lp_swap/taker_swap.rs | 12 ++- .../tests/docker_tests/z_coin_docker_tests.rs | 12 +-- 40 files changed, 454 insertions(+), 218 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f24abff6fc..13ad126fd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -970,6 +970,7 @@ dependencies = [ "parking_lot", "rpc", "rpc_task", + "secp256k1 0.24.3", "ser_error", "ser_error_derive", "serde", @@ -3883,6 +3884,7 @@ dependencies = [ "libp2p", "mm2_err_handle", "mm2_event_stream", + "mm2_io", "mm2_metrics", "mm2_rpc", "primitives", diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 26c3b39c97..64cc91c03d 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -13,7 +13,7 @@ enable-sia = [ default = [] run-docker-tests = [] for-tests = ["dep:mocktopus"] -new-db-arch = [] +new-db-arch = ["mm2_core/new-db-arch"] [lib] path = "lp_coins.rs" diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index 3e9bbc7349..486a07fc83 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -108,3 +108,8 @@ pub enum MyAddressError { UnexpectedDerivationMethod(String), InternalError(String), } + +#[derive(Debug, Display)] +pub enum AddressFromPubkeyError { + InternalError(String), +} diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 6fe52c0fa5..0bb6d7b1b2 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -102,7 +102,7 @@ use web3::{self, Web3}; cfg_wasm32! { use common::{now_ms, wait_until_ms}; use crypto::MetamaskArc; - use ethereum_types::{H264, H520}; + use ethereum_types::{H264 as EthH264, H520 as EthH520}; use mm2_metamask::MetamaskError; use web3::types::TransactionRequest; } @@ -940,7 +940,7 @@ macro_rules! tx_type_from_pay_for_gas_option { impl EthCoinImpl { #[cfg(not(target_arch = "wasm32"))] fn eth_traces_path(&self, ctx: &MmArc, my_address: Address) -> PathBuf { - ctx.dbdir() + ctx.address_dir(&my_address.display_address()) .join("TRANSACTIONS") .join(format!("{}_{:#02x}_trace.json", self.ticker, my_address)) } @@ -948,7 +948,8 @@ impl EthCoinImpl { /// Load saved ETH traces from local DB #[cfg(not(target_arch = "wasm32"))] fn load_saved_traces(&self, ctx: &MmArc, my_address: Address) -> Option { - let content = gstuff::slurp(&self.eth_traces_path(ctx, my_address)); + let path = self.eth_traces_path(ctx, my_address); + let content = gstuff::slurp(&path); if content.is_empty() { None } else { @@ -970,9 +971,8 @@ impl EthCoinImpl { #[cfg(not(target_arch = "wasm32"))] fn store_eth_traces(&self, ctx: &MmArc, my_address: Address, traces: &SavedTraces) { let content = json::to_vec(traces).unwrap(); - let tmp_file = format!("{}.tmp", self.eth_traces_path(ctx, my_address).display()); - std::fs::write(&tmp_file, content).unwrap(); - std::fs::rename(tmp_file, self.eth_traces_path(ctx, my_address)).unwrap(); + let path = self.eth_traces_path(ctx, my_address); + mm2_io::fs::write(&path, &content, true).unwrap(); } /// Store ETH traces to local DB @@ -984,7 +984,7 @@ impl EthCoinImpl { #[cfg(not(target_arch = "wasm32"))] fn erc20_events_path(&self, ctx: &MmArc, my_address: Address) -> PathBuf { - ctx.dbdir() + ctx.address_dir(&my_address.display_address()) .join("TRANSACTIONS") .join(format!("{}_{:#02x}_events.json", self.ticker, my_address)) } @@ -993,9 +993,8 @@ impl EthCoinImpl { #[cfg(not(target_arch = "wasm32"))] fn store_erc20_events(&self, ctx: &MmArc, my_address: Address, events: &SavedErc20Events) { let content = json::to_vec(events).unwrap(); - let tmp_file = format!("{}.tmp", self.erc20_events_path(ctx, my_address).display()); - std::fs::write(&tmp_file, content).unwrap(); - std::fs::rename(tmp_file, self.erc20_events_path(ctx, my_address)).unwrap(); + let path = self.erc20_events_path(ctx, my_address); + mm2_io::fs::write(&path, &content, true).unwrap(); } /// Store ERC20 events to local DB @@ -1008,7 +1007,8 @@ impl EthCoinImpl { /// Load saved ERC20 events from local DB #[cfg(not(target_arch = "wasm32"))] fn load_saved_erc20_events(&self, ctx: &MmArc, my_address: Address) -> Option { - let content = gstuff::slurp(&self.erc20_events_path(ctx, my_address)); + let path = self.erc20_events_path(ctx, my_address); + let content = gstuff::slurp(&path); if content.is_empty() { None } else { @@ -2329,6 +2329,11 @@ impl MarketCoinOps for EthCoin { } } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let addr = addr_from_raw_pubkey(&pubkey.0).map_err(AddressFromPubkeyError::InternalError)?; + Ok(addr.display_address()) + } + async fn get_public_key(&self) -> Result> { match self.priv_key_policy { EthPrivKeyPolicy::Iguana(ref key_pair) diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index ecdb4a5c37..bc00a35cba 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -984,9 +984,9 @@ async fn check_metamask_supports_chain_id( } #[cfg(target_arch = "wasm32")] -fn compress_public_key(uncompressed: H520) -> MmResult { +fn compress_public_key(uncompressed: EthH520) -> MmResult { let public_key = PublicKey::from_slice(uncompressed.as_bytes()) .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; let compressed = public_key.serialize(); - Ok(H264::from(compressed)) + Ok(EthH264::from(compressed)) } diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 949c39a857..64aa153aaa 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -10,7 +10,7 @@ mod ln_sql; pub mod ln_storage; pub mod ln_utils; -use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::lightning::ln_utils::{filter_channels, pay_invoice_with_max_total_cltv_expiry_delta, PaymentError}; use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::utxo::utxo_common::{big_decimal_from_sat, big_decimal_from_sat_unsigned}; @@ -65,7 +65,7 @@ use mm2_err_handle::prelude::*; use mm2_net::ip_addr::myipaddr; use mm2_number::{BigDecimal, MmNumber}; use parking_lot::Mutex as PaMutex; -use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264 as H264Json}; use script::TransactionInputSigner; use secp256k1v24::PublicKey; use serde::Deserialize; @@ -942,6 +942,12 @@ impl MarketCoinOps for LightningCoin { fn my_address(&self) -> MmResult { Ok(self.my_node_id()) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + PublicKey::from_slice(&pubkey.0) + .map(|pubkey| pubkey.to_string()) + .map_to_mm(|e| AddressFromPubkeyError::InternalError(format!("Couldn't parse bytes into secp pubkey: {e}"))) + } + async fn get_public_key(&self) -> Result> { Ok(self.my_node_id()) } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index 79868908fa..68f3c7f7ab 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -22,7 +22,7 @@ use mm2_core::mm_ctx::MmArc; use std::collections::hash_map::Entry; use std::fs::File; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; pub const PAYMENT_RETRY_ATTEMPTS: usize = 5; @@ -54,13 +54,15 @@ impl From for RpcBestBlock { } #[inline] -fn ln_data_dir(ctx: &MmArc, ticker: &str) -> PathBuf { ctx.dbdir().join("LIGHTNING").join(ticker) } +fn ln_data_dir(ctx: &MmArc, platform_coin_address: &str, ticker: &str) -> PathBuf { + ctx.address_dir(platform_coin_address).join("LIGHTNING").join(ticker) +} #[inline] -fn ln_data_backup_dir(ctx: &MmArc, path: Option, ticker: &str) -> Option { +fn ln_data_backup_dir(path: Option, platform_coin_address: &str, ticker: &str) -> Option { path.map(|p| { PathBuf::from(&p) - .join(hex::encode(ctx.rmd160().as_slice())) + .join(platform_coin_address) .join("LIGHTNING") .join(ticker) }) @@ -68,11 +70,12 @@ fn ln_data_backup_dir(ctx: &MmArc, path: Option, ticker: &str) -> Option pub async fn init_persister( ctx: &MmArc, + platform_coin_address: &str, ticker: String, backup_path: Option, ) -> EnableLightningResult> { - let ln_data_dir = ln_data_dir(ctx, &ticker); - let ln_data_backup_dir = ln_data_backup_dir(ctx, backup_path, &ticker); + let ln_data_dir = ln_data_dir(ctx, platform_coin_address, &ticker); + let ln_data_backup_dir = ln_data_backup_dir(backup_path, platform_coin_address, &ticker); let persister = Arc::new(LightningFilesystemPersister::new(ln_data_dir, ln_data_backup_dir)); let is_initialized = persister.is_fs_initialized().await?; @@ -83,16 +86,15 @@ pub async fn init_persister( Ok(persister) } -pub async fn init_db(ctx: &MmArc, ticker: String) -> EnableLightningResult { - let db = SqliteLightningDB::new( - ticker, - ctx.sqlite_connection - .get() - .ok_or(MmError::new(EnableLightningError::DbError( - "sqlite_connection is not initialized".into(), - )))? - .clone(), - )?; +pub async fn init_db( + ctx: &MmArc, + platform_coin_address: &str, + ticker: String, +) -> EnableLightningResult { + let conn = ctx + .address_db(platform_coin_address) + .map_err(|e| EnableLightningError::IOError(e.to_string()))?; + let db = SqliteLightningDB::new(ticker, Arc::new(Mutex::new(conn)))?; if !db.is_db_initialized().await? { db.init_db().await?; diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 83f1aa0026..d55add2c2a 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -71,7 +71,7 @@ use mm2_rpc::data::legacy::{EnabledCoin, GetEnabledResponse, Mm2RpcResult}; #[cfg(any(test, feature = "for-tests"))] use mocktopus::macros::*; use parking_lot::Mutex as PaMutex; -use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264 as H264Json}; use rpc_command::tendermint::ibc::ChannelId; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::{self as json, Value as Json}; @@ -214,7 +214,8 @@ pub mod lp_price; pub mod watcher_common; pub mod coin_errors; -use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentResult}; +use coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentResult}; use crypto::secret_hash_algo::SecretHashAlgo; pub mod eth; @@ -2076,6 +2077,8 @@ pub trait MarketCoinOps { fn my_address(&self) -> MmResult; + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult; + async fn get_public_key(&self) -> Result>; fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]>; diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index ac10ba3151..ad01bba16f 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1,4 +1,4 @@ -use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::eth::{self, u256_to_big_decimal, wei_from_big_decimal, TryToAddress}; use crate::qrc20::rpc_clients::{LogEntry, Qrc20ElectrumOps, Qrc20NativeOps, Qrc20RpcOps, TopicFilter, TxReceipt, ViewContractCallType}; @@ -44,7 +44,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; -use rpc::v1::types::{Bytes as BytesJson, ToTxHash, Transaction as RpcTransaction, H160 as H160Json, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, ToTxHash, Transaction as RpcTransaction, H160 as H160Json, H256 as H256Json, + H264 as H264Json}; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use script_pubkey::generate_contract_call_script_pubkey; use serde_json::{self as json, Value as Json}; @@ -1024,6 +1025,11 @@ impl MarketCoinOps for Qrc20Coin { fn my_address(&self) -> MmResult { utxo_common::my_address(self) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let pubkey = Public::Compressed((*pubkey).into()); + Ok(UtxoCommonOps::address_from_pubkey(self, &pubkey).to_string()) + } + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.as_ref())?; Ok(pubkey.to_string()) diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index bb5ec12353..08458e5c0d 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -1,12 +1,13 @@ use super::{BalanceError, CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, SwapOps, TradeFee, TransactionEnum}; -use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, - DexFee, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, PrivKeyPolicy, - RawTransactionResult, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, - SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentInput, ValidatePaymentResult, VerificationResult, - WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawFut, WithdrawRequest}; +use crate::{coin_errors::MyAddressError, AddressFromPubkeyError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, + ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, + PrivKeyBuildPolicy, PrivKeyPolicy, RawTransactionResult, RefundPaymentArgs, SearchForSwapTxSpendInput, + SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, TradePreimageFut, + TradePreimageResult, TradePreimageValue, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentInput, + ValidatePaymentResult, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawFut, + WithdrawRequest}; use async_trait::async_trait; use common::executor::AbortedError; pub use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature}; @@ -16,7 +17,7 @@ use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, BigInt, MmNumber}; -use rpc::v1::types::Bytes as BytesJson; +use rpc::v1::types::{Bytes as BytesJson, H264 as H264Json}; use serde_json::Value as Json; use std::ops::Deref; use std::sync::Arc; @@ -312,6 +313,14 @@ impl MarketCoinOps for SiaCoin { Ok(address.to_string()) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let pubkey = PublicKey::from_bytes(&pubkey.0[..32]).map_err(|e| { + AddressFromPubkeyError::InternalError(format!("Couldn't parse bytes into ed25519 pubkey: {e:?}")) + })?; + let address = SpendPolicy::PublicKey(pubkey).address(); + Ok(address.to_string()) + } + async fn get_public_key(&self) -> Result> { unimplemented!() } fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 201e215c19..b43c54bdf2 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -4,7 +4,7 @@ use super::htlc::{ClaimHtlcMsg, ClaimHtlcProto, CreateHtlcMsg, CreateHtlcProto, use super::ibc::transfer_v1::MsgTransfer; use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::rpc::*; -use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; +use crate::coin_errors::{AddressFromPubkeyError, 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, @@ -78,7 +78,7 @@ use num_traits::Zero; use parking_lot::Mutex as PaMutex; use primitives::hash::H256; use regex::Regex; -use rpc::v1::types::Bytes as BytesJson; +use rpc::v1::types::{Bytes as BytesJson, H264 as H264Json}; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; @@ -3320,6 +3320,12 @@ impl MarketCoinOps for TendermintCoin { fn my_address(&self) -> MmResult { Ok(self.account_id.to_string()) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let address = account_id_from_raw_pubkey(&self.account_prefix, &pubkey.0) + .map_err(|e| AddressFromPubkeyError::InternalError(e.to_string()))?; + Ok(address.to_string()) + } + async fn get_public_key(&self) -> Result> { let key = SigningKey::from_slice(self.activation_policy.activated_key_or_err()?.as_slice()) .expect("privkey validity is checked on coin creation"); diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index b30d07c1a5..d70b316d26 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -3,7 +3,7 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{create_withdraw_msg_as_any, TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATOSHIS, TIMEOUT_HEIGHT_DELTA, TX_DEFAULT_MEMO}; -use crate::coin_errors::ValidatePaymentResult; +use crate::coin_errors::{AddressFromPubkeyError, ValidatePaymentResult}; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, @@ -29,7 +29,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::MmNumber; use primitives::hash::H256; -use rpc::v1::types::Bytes as BytesJson; +use rpc::v1::types::{Bytes as BytesJson, H264 as H264Json}; use serde_json::Value as Json; use std::ops::Deref; use std::str::FromStr; @@ -269,6 +269,10 @@ impl MarketCoinOps for TendermintToken { fn my_address(&self) -> MmResult { self.platform_coin.my_address() } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + self.platform_coin.address_from_pubkey(pubkey) + } + async fn get_public_key(&self) -> Result> { self.platform_coin.get_public_key().await } diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 278492638c..0faae14886 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -3,7 +3,7 @@ use super::{CoinBalance, CommonSwapOpsV2, FindPaymentSpendError, FundingTxSpend, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, RefundTakerPaymentArgs, SearchForFundingSpendErr, SwapOps, TradeFee, TransactionEnum, TransactionFut}; -use crate::coin_errors::ValidatePaymentResult; +use crate::coin_errors::{AddressFromPubkeyError, ValidatePaymentResult}; use crate::hd_wallet::AddrToString; use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, @@ -28,7 +28,7 @@ use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; #[cfg(any(test, feature = "for-tests"))] use mocktopus::macros::*; -use rpc::v1::types::Bytes as BytesJson; +use rpc::v1::types::{Bytes as BytesJson, H264 as H264Json}; use serde_json::Value as Json; use std::fmt::{Display, Formatter}; use std::ops::Deref; @@ -65,6 +65,8 @@ impl MarketCoinOps for TestCoin { fn my_address(&self) -> MmResult { unimplemented!() } + fn address_from_pubkey(&self, _pubkey: &H264Json) -> MmResult { unimplemented!() } + async fn get_public_key(&self) -> Result> { unimplemented!() } fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 6fd2f28fe4..53e9bc7cdb 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -624,6 +624,7 @@ pub struct UtxoCoinFields { /// The cache of recently send transactions used to track the spent UTXOs and replace them with new outputs /// The daemon needs some time to update the listunspent list for address which makes it return already spent UTXOs /// This cache helps to prevent UTXO reuse in such cases + // TODO: change the type of `recently_spent_outpoints` to `AsyncMutex>` to better support HD wallets. pub recently_spent_outpoints: AsyncMutex, pub tx_hash_algo: TxHashAlgo, /// The flag determines whether to use mature unspent outputs *only* to generate transactions. diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 76a2f5d708..7e18dfc913 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -1,6 +1,6 @@ use super::*; use crate::coin_balance::{EnableCoinBalanceError, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; -use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxDetailsBuilder, @@ -34,6 +34,7 @@ use keys::CashAddress; pub use keys::NetworkPrefix as CashAddrPrefix; use mm2_metrics::MetricsArc; use mm2_number::MmNumber; +use rpc::v1::types::H264 as H264Json; use serde_json::{self as json, Value as Json}; use serialization::{deserialize, CoinVariant}; use std::sync::MutexGuard; @@ -1134,6 +1135,11 @@ impl MarketCoinOps for BchCoin { fn my_address(&self) -> MmResult { utxo_common::my_address(self) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let pubkey = Public::Compressed((*pubkey).into()); + Ok(UtxoCommonOps::address_from_pubkey(self, &pubkey).to_string()) + } + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; Ok(pubkey.to_string()) diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index e3214552e6..cab1d3c5d5 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -2,7 +2,7 @@ use super::utxo_common::utxo_prepare_addresses_for_balance_stream_if_enabled; use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; -use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; @@ -40,6 +40,7 @@ use futures::{FutureExt, TryFutureExt}; use keys::AddressHashEnum; use mm2_metrics::MetricsArc; use mm2_number::MmNumber; +use rpc::v1::types::H264 as H264Json; use serde::Serialize; use serialization::CoinVariant; use utxo_signer::UtxoSignerOps; @@ -757,6 +758,11 @@ impl MarketCoinOps for QtumCoin { fn my_address(&self) -> MmResult { utxo_common::my_address(self) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let pubkey = Public::Compressed((*pubkey).into()); + Ok(UtxoCommonOps::address_from_pubkey(self, &pubkey).to_string()) + } + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; Ok(pubkey.to_string()) diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 5229f07180..a0b29006cc 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -3,7 +3,7 @@ //! Tracking issue: https://github.com/KomodoPlatform/atomicDEX-API/issues/701 //! More info about the protocol and implementation guides can be found at https://slp.dev/ -use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::bch::BchCoin; @@ -44,7 +44,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; use primitives::hash::H256; -use rpc::v1::types::{Bytes as BytesJson, ToTxHash, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, ToTxHash, H256 as H256Json, H264 as H264Json}; use script::bytes::Bytes; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; @@ -1102,6 +1102,11 @@ impl MarketCoinOps for SlpToken { slp_address.encode().map_to_mm(MyAddressError::InternalError) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + // TODO: We have two `address_from_pubkey`s, one in MarketCoinOps and one in UtxoCommonOps. We should give them different names. + MarketCoinOps::address_from_pubkey(&self.platform_coin, pubkey) + } + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.platform_coin.as_ref())?; Ok(pubkey.to_string()) @@ -1122,7 +1127,7 @@ impl MarketCoinOps for SlpToken { let signature = CompactSignature::try_from(STANDARD.decode(signature)?) .map_to_mm(|err| VerificationError::SignatureDecodingError(err.to_string()))?; let pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; - let address_from_pubkey = self.platform_coin.address_from_pubkey(&pubkey); + let address_from_pubkey = UtxoCommonOps::address_from_pubkey(&self.platform_coin, &pubkey); let slp_address = self .platform_coin .slp_address(&address_from_pubkey) diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 80fd41c3ee..03ccd6fd8b 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -1,4 +1,4 @@ -use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDWallet, HDWalletCoinStorage, +use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDWallet, HDWalletCoinStorage, HDWalletOps, HDWalletStorageError, DEFAULT_GAP_LIMIT}; use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientSettings, ElectrumConnectionSettings, EstimateFeeMethod, UtxoRpcClientEnum}; @@ -19,7 +19,6 @@ use derive_more::Display; use futures::channel::mpsc::{channel, Receiver as AsyncReceiver}; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; -use keys::bytes::Bytes; pub use keys::{Address, AddressBuilder, AddressFormat as UtxoAddressFormat, AddressHashEnum, AddressScriptType, KeyPair, Private, Public, Secret}; use mm2_core::mm_ctx::MmArc; @@ -298,11 +297,6 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { } let hd_wallet_rmd160 = self.trezor_wallet_rmd160()?; - // For now, use a default script pubkey. - // TODO change the type of `recently_spent_outpoints` to `AsyncMutex>` - let my_script_pubkey = Bytes::new(); - let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)); - let address_format = self.address_format()?; let path_to_coin = conf .derivation_path @@ -327,6 +321,13 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { address_format, }; + let my_address = hd_wallet + .get_enabled_address() + .await + .ok_or_else(|| UtxoCoinBuildError::Internal("Failed to get enabled address from HD wallet".to_owned()))?; + let my_script_pubkey = output_script(&my_address.address).map(|script| script.to_bytes())?; + let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)); + // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, // all spawned futures related to this `UTXO` coin will be aborted as well. let abortable_system: AbortableQueue = self.ctx().abortable_system.create_subsystem()?; @@ -681,7 +682,7 @@ pub trait UtxoCoinBuilderCommonOps { } #[cfg(not(target_arch = "wasm32"))] - fn tx_cache_path(&self) -> PathBuf { self.ctx().dbdir().join("TX_CACHE") } + fn tx_cache_path(&self) -> PathBuf { self.ctx().global_dir().join("TX_CACHE") } fn block_header_status_channel( &self, diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index d8191dfef6..ec9fd8adb9 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -2,7 +2,7 @@ use super::utxo_common::utxo_prepare_addresses_for_balance_stream_if_enabled; use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; -use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; @@ -43,6 +43,7 @@ use futures::{FutureExt, TryFutureExt}; use mm2_metrics::MetricsArc; use mm2_number::MmNumber; #[cfg(test)] use mocktopus::macros::*; +use rpc::v1::types::H264 as H264Json; use script::Opcode; use utxo_signer::UtxoSignerOps; @@ -848,6 +849,11 @@ impl MarketCoinOps for UtxoStandardCoin { fn my_address(&self) -> MmResult { utxo_common::my_address(self) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let pubkey = Public::Compressed((*pubkey).into()); + Ok(UtxoCommonOps::address_from_pubkey(self, &pubkey).to_string()) + } + fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { utxo_common::sign_message_hash(self.as_ref(), message) } diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index ffad4d09ea..d1487fc029 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -8,7 +8,7 @@ mod z_htlc; mod z_rpc; mod z_tx_history; -use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::hd_wallet::HDPathAccountToAddressId; use crate::my_tx_history_v2::{MyTxHistoryErrorV2, MyTxHistoryRequestV2, MyTxHistoryResponseV2}; use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawInProgressStatus, WithdrawTaskHandleShared}; @@ -57,7 +57,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; -use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json, H264 as H264Json}; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; use serialization::CoinVariant; @@ -396,7 +396,7 @@ impl ZCoin { let spendable_notes = self .spendable_notes_ordered() .await - .map_err(|err| GenTxError::SpendableNotesError(err.to_string()))?; + .mm_err(|err| GenTxError::SpendableNotesError(err.to_string()))?; let mut total_input_amount = BigDecimal::from(0); let mut change = BigDecimal::from(0); @@ -826,10 +826,6 @@ pub async fn z_coin_from_conf_and_params( protocol_info: ZcoinProtocolInfo, priv_key_policy: PrivKeyBuildPolicy, ) -> Result> { - #[cfg(target_arch = "wasm32")] - let db_dir_path = PathBuf::new(); - #[cfg(not(target_arch = "wasm32"))] - let db_dir_path = ctx.dbdir(); let z_spending_key = None; let builder = ZCoinBuilder::new( ctx, @@ -837,10 +833,9 @@ pub async fn z_coin_from_conf_and_params( conf, params, priv_key_policy, - db_dir_path, z_spending_key, protocol_info, - ); + )?; builder.build().await } @@ -872,10 +867,9 @@ pub struct ZCoinBuilder<'a> { z_coin_params: &'a ZcoinActivationParams, utxo_params: UtxoActivationParams, priv_key_policy: PrivKeyBuildPolicy, - #[cfg_attr(target_arch = "wasm32", allow(unused))] - db_dir_path: PathBuf, - /// `Some` if `ZCoin` should be initialized with a forced spending key. - z_spending_key: Option, + z_spending_key: ExtendedSpendingKey, + my_z_addr: PaymentAddress, + my_z_addr_encoded: String, protocol_info: ZcoinProtocolInfo, } @@ -908,19 +902,6 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { let utxo = self.build_utxo_fields().await?; let utxo_arc = UtxoArc::new(utxo); - let z_spending_key = match self.z_spending_key { - Some(ref z_spending_key) => z_spending_key.clone(), - None => extended_spending_key_from_protocol_info_and_policy( - &self.protocol_info, - &self.priv_key_policy, - self.z_coin_params.account, - )?, - }; - - let (_, my_z_addr) = z_spending_key - .default_address() - .map_err(|_| MmError::new(ZCoinBuildError::GetAddressError))?; - let dex_fee_addr = decode_payment_address( self.protocol_info.consensus_params.hrp_sapling_payment_address(), DEX_FEE_Z_ADDR, @@ -936,18 +917,11 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { .expect("DEX_BURN_Z_ADDR is a valid z-address"); let z_tx_prover = self.z_tx_prover().await?; - let my_z_addr_encoded = encode_payment_address( - self.protocol_info.consensus_params.hrp_sapling_payment_address(), - &my_z_addr, - ); - let blocks_db = self.init_blocks_db().await?; let (sync_state_connector, light_wallet_db) = match &self.z_coin_params.mode { #[cfg(not(target_arch = "wasm32"))] - ZcoinRpcMode::Native => { - init_native_client(&self, self.native_client()?, blocks_db, &z_spending_key).await? - }, + ZcoinRpcMode::Native => init_native_client(&self, self.native_client()?, blocks_db).await?, ZcoinRpcMode::Light { light_wallet_d_servers, sync_params, @@ -960,7 +934,6 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { blocks_db, sync_params, skip_sync_params.unwrap_or_default(), - &z_spending_key, ) .await? }, @@ -969,10 +942,10 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { let z_fields = Arc::new(ZCoinFields { dex_fee_addr, dex_burn_addr, - my_z_addr, - my_z_addr_encoded, - evk: ExtendedFullViewingKey::from(&z_spending_key), - z_spending_key, + my_z_addr: self.my_z_addr, + my_z_addr_encoded: self.my_z_addr_encoded, + evk: ExtendedFullViewingKey::from(&self.z_spending_key), + z_spending_key: self.z_spending_key, z_tx_prover: Arc::new(z_tx_prover), light_wallet_db, consensus_params: self.protocol_info.consensus_params, @@ -991,10 +964,9 @@ impl<'a> ZCoinBuilder<'a> { conf: &'a Json, z_coin_params: &'a ZcoinActivationParams, priv_key_policy: PrivKeyBuildPolicy, - db_dir_path: PathBuf, z_spending_key: Option, protocol_info: ZcoinProtocolInfo, - ) -> ZCoinBuilder<'a> { + ) -> MmResult, ZCoinBuildError> { let utxo_mode = match &z_coin_params.mode { #[cfg(not(target_arch = "wasm32"))] ZcoinRpcMode::Native => UtxoRpcMode::Native, @@ -1023,27 +995,49 @@ impl<'a> ZCoinBuilder<'a> { // This is not used for Zcoin so we just provide a default value path_to_address: HDPathAccountToAddressId::default(), }; - ZCoinBuilder { + + let z_spending_key = match z_spending_key { + Some(ref z_spending_key) => z_spending_key.clone(), + None => extended_spending_key_from_protocol_info_and_policy( + &protocol_info, + &priv_key_policy, + z_coin_params.account, + )?, + }; + + let (_, my_z_addr) = z_spending_key + .default_address() + .map_to_mm(|_| ZCoinBuildError::GetAddressError)?; + + let my_z_addr_encoded = + encode_payment_address(protocol_info.consensus_params.hrp_sapling_payment_address(), &my_z_addr); + + Ok(ZCoinBuilder { ctx, ticker, conf, z_coin_params, utxo_params, priv_key_policy, - db_dir_path, z_spending_key, + my_z_addr, + my_z_addr_encoded, protocol_info, - } + }) } async fn init_blocks_db(&self) -> Result> { - let cache_db_path = self.db_dir_path.join(format!("{}_cache.db", self.ticker)); - let ctx = self.ctx.clone(); + let ctx = &self.ctx; let ticker = self.ticker.to_string(); - BlockDbImpl::new(&ctx, ticker, cache_db_path) - .map_err(|err| MmError::new(ZcoinClientInitError::ZcoinStorageError(err.to_string()))) + #[cfg(target_arch = "wasm32")] + let cache_db_path = PathBuf::new(); + #[cfg(not(target_arch = "wasm32"))] + let cache_db_path = self.ctx.global_dir().join(format!("{}_cache.db", self.ticker)); + + BlockDbImpl::new(ctx, ticker, cache_db_path) .await + .mm_err(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string())) } #[cfg(not(target_arch = "wasm32"))] @@ -1102,7 +1096,6 @@ pub async fn z_coin_from_conf_and_params_with_docker( conf: &Json, params: &ZcoinActivationParams, priv_key_policy: PrivKeyBuildPolicy, - db_dir_path: PathBuf, protocol_info: ZcoinProtocolInfo, spending_key: &str, ) -> Result> { @@ -1118,10 +1111,9 @@ pub async fn z_coin_from_conf_and_params_with_docker( conf, params, priv_key_policy, - db_dir_path, Some(z_spending_key), protocol_info, - ); + )?; println!("ZOMBIE_wallet.db will be synch'ed with the chain, this may take a while for the first time."); println!("You may also run prepare_zombie_sapling_cache test to update ZOMBIE_wallet.db before running tests."); @@ -1134,6 +1126,11 @@ impl MarketCoinOps for ZCoin { fn my_address(&self) -> MmResult { Ok(self.z_fields.my_z_addr_encoded.clone()) } + fn address_from_pubkey(&self, _pubkey: &H264Json) -> MmResult { + // NOTE: We can't derive a z-address from pubkey, so we will just return our own z_address. + Ok(self.z_fields.my_z_addr_encoded.clone()) + } + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.as_ref())?; Ok(pubkey.to_string()) @@ -1430,7 +1427,7 @@ impl SwapOps for ZCoin { .get_verbose_transaction(&tx_hash.into()) .compat() .await - .map_err(|e| MmError::new(ValidatePaymentError::InvalidRpcResponse(e.into_inner().to_string())))?; + .mm_err(|e| ValidatePaymentError::InvalidRpcResponse(e.to_string()))?; let mut encoded = Vec::with_capacity(1024); z_tx.write(&mut encoded).expect("Writing should not fail"); diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs index 44721b4364..8dd4dd39f7 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs @@ -48,6 +48,7 @@ impl BlockDbImpl { #[cfg(not(test))] pub async fn new(_ctx: &MmArc, ticker: String, path: PathBuf) -> ZcoinStorageRes { async_blocking(move || { + mm2_io::fs::create_parents(&path).map_err(|err| ZcoinStorageError::IoError(err.to_string()))?; let conn = Connection::open(path).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; let conn = Arc::new(Mutex::new(conn)); let conn_lock = conn.lock().unwrap(); diff --git a/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs b/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs index 3a957d375f..0408182ae0 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs @@ -11,7 +11,7 @@ use zcash_extras::{WalletRead, WalletWrite}; use zcash_primitives::block::BlockHash; use zcash_primitives::consensus::BlockHeight; use zcash_primitives::transaction::TxId; -use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; +use zcash_primitives::zip32::ExtendedFullViewingKey; /// `create_wallet_db` is responsible for creating a new Zcoin wallet database, initializing it /// with the provided parameters, and executing various initialization steps. These steps include checking and @@ -24,6 +24,9 @@ pub async fn create_wallet_db( evk: ExtendedFullViewingKey, continue_from_prev_sync: bool, ) -> Result, MmError> { + mm2_io::fs::create_parents_async(&wallet_db_path) + .await + .map_err(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string()))?; let db = async_blocking(move || { WalletDbAsync::for_path(wallet_db_path, consensus_params) .map_to_mm(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string())) @@ -81,16 +84,15 @@ impl<'a> WalletDbShared { pub async fn new( builder: &ZCoinBuilder<'a>, checkpoint_block: Option, - z_spending_key: &ExtendedSpendingKey, continue_from_prev_sync: bool, ) -> ZcoinStorageRes { let ticker = builder.ticker; let consensus_params = builder.protocol_info.consensus_params.clone(); let wallet_db = create_wallet_db( - builder.db_dir_path.join(format!("{ticker}_wallet.db")), + builder.ctx.wallet_dir().join(format!("{ticker}_wallet.db")), consensus_params, checkpoint_block, - ExtendedFullViewingKey::from(z_spending_key), + ExtendedFullViewingKey::from(&builder.z_spending_key), continue_from_prev_sync, ) .await diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs index d9ac5ec322..06d06c4d32 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs @@ -33,7 +33,7 @@ use zcash_primitives::merkle_tree::{CommitmentTree, IncrementalWitness}; use zcash_primitives::sapling::{Node, Nullifier, PaymentAddress}; use zcash_primitives::transaction::components::Amount; use zcash_primitives::transaction::{Transaction, TxId}; -use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; +use zcash_primitives::zip32::ExtendedFullViewingKey; const DB_NAME: &str = "wallet_db_cache"; const DB_VERSION: u32 = 1; @@ -54,7 +54,6 @@ impl<'a> WalletDbShared { pub async fn new( builder: &ZCoinBuilder<'a>, checkpoint_block: Option, - z_spending_key: &ExtendedSpendingKey, continue_from_prev_sync: bool, ) -> ZcoinStorageRes { let ticker = builder.ticker; @@ -62,7 +61,7 @@ impl<'a> WalletDbShared { let db = WalletIndexedDb::new(builder.ctx, ticker, consensus_params).await?; let extrema = db.block_height_extrema().await?; let get_evk = db.get_extended_full_viewing_keys().await?; - let evk = ExtendedFullViewingKey::from(z_spending_key); + let evk = ExtendedFullViewingKey::from(&builder.z_spending_key); let min_sync_height = extrema.map(|(min, _)| u32::from(min)); let init_block_height = checkpoint_block.clone().map(|block| block.height); diff --git a/mm2src/coins/z_coin/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs index 4e5ffc4325..892da5401e 100644 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ b/mm2src/coins/z_coin/z_coin_native_tests.rs @@ -26,7 +26,6 @@ use bitcrypto::dhash160; use common::{block_on, now_sec}; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_test_helpers::for_tests::zombie_conf; -use std::path::PathBuf; use std::time::Duration; use zcash_client_backend::encoding::decode_extended_spending_key; @@ -50,7 +49,6 @@ async fn zombie_coin_send_and_refund_maker_payment() { let mut conf = zombie_conf(); let params = native_zcoin_activation_params(); let pk_data = [1; 32]; - let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { CoinProtocol::ZHTLC(protocol_info) => protocol_info, @@ -63,7 +61,6 @@ async fn zombie_coin_send_and_refund_maker_payment() { &conf, ¶ms, PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), - db_dir, z_key, protocol_info, ) @@ -115,7 +112,6 @@ async fn zombie_coin_send_and_spend_maker_payment() { let mut conf = zombie_conf(); let params = native_zcoin_activation_params(); let pk_data = [1; 32]; - let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { CoinProtocol::ZHTLC(protocol_info) => protocol_info, @@ -128,7 +124,6 @@ async fn zombie_coin_send_and_spend_maker_payment() { &conf, ¶ms, PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), - db_dir, z_key, protocol_info, ) @@ -184,17 +179,15 @@ async fn zombie_coin_send_dex_fee() { let mut conf = zombie_conf(); let params = native_zcoin_activation_params(); let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { CoinProtocol::ZHTLC(protocol_info) => protocol_info, other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), }; - let coin = - z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) - .await - .unwrap(); + let coin = z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, z_key, protocol_info) + .await + .unwrap(); let dex_fee = DexFee::WithBurn { fee_amount: "0.0075".into(), @@ -211,17 +204,15 @@ async fn zombie_coin_send_standard_dex_fee() { let mut conf = zombie_conf(); let params = native_zcoin_activation_params(); let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { CoinProtocol::ZHTLC(protocol_info) => protocol_info, other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), }; - let coin = - z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) - .await - .unwrap(); + let coin = z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, z_key, protocol_info) + .await + .unwrap(); let dex_fee = DexFee::Standard("0.01".into()); let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); @@ -235,7 +226,6 @@ fn prepare_zombie_sapling_cache() { let mut conf = zombie_conf(); let params = native_zcoin_activation_params(); let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { CoinProtocol::ZHTLC(protocol_info) => protocol_info, @@ -248,7 +238,6 @@ fn prepare_zombie_sapling_cache() { &conf, ¶ms, priv_key, - db_dir, z_key, protocol_info, )) @@ -265,17 +254,15 @@ async fn zombie_coin_validate_dex_fee() { let mut conf = zombie_conf(); let params = native_zcoin_activation_params(); let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { CoinProtocol::ZHTLC(protocol_info) => protocol_info, other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), }; - let coin = - z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) - .await - .unwrap(); + let coin = z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, z_key, protocol_info) + .await + .unwrap(); // https://zombie.explorer.lordofthechains.com/tx/9390a26810342151f48f455b09e5d087a5429cbba08f2381b02c43b76f813e29 let tx_hex = "0400008085202f8900000000000001030c00e8030000000000000169e7017fbd969be53da2c1b8812002baaf59ce98b230a9c1001397ba7f4db8676bd77e8ea644b67067d1f996d8d81c279961343f00a10095bccbddc341c98539287c900cf969688ddc574786e0e34bd6d3ec2ffaab5e2d472848781b116906669786c14c5c608b20dc23c9566fd46861f6a258b5ffc6de73495b56f4823e098c8664eab895d5cd31c013428ae2cbe940dc236ca40465ea2b912ce6c36555b2affb1f38b99b28dc593d865b0b948d567f9315df666d2e65e666d829b9823154bae0410bd885582b4a8a6eb4b9ae214b59ffd9b1167b7cd48f48a11cbd67c08f4e01ed4fd78fc91d0c9e70baa4f25761ef6c78cd7268b307aaa6ece2b443937eb4beac2c8843279a8879adbe0b381e65d0b674f2feeb54b78f80b377f66baab72c4cf9f10dde48f343c001df91a1a6d252ad8eca26eea0fdee49ad7024b505e55b4e082e94616794ddd7c2b852594b4b7af2292f0aa9e34f38322f548f1a21c015e92dbfd239ce18144f3b8045e9efa3de6b4c6b338f01d0adeb26a088a3c8c00503b67b2980b7663e97541e2944e4ad3588554966b6a930d2dc01d9fc7f8a846583fcf3b721f979705eff5bb9bb1fb0cad9ad941ceb3f581710efd8c50713a53751a0a196322ef8618bf1e097383666e91b5133ba81645d2b542181476eba2326cd02fb29a9f09edc46ea04b32ed9243597318d23b955a2570d78cbfb46cc26c1807eddd1de4785b6e752f859f7e25fc67f9e8a00feafac6fd7781eb72a663d9b80c10e9c387abc4d41294b3573785fd53bc56ccac2edf5c7bbb99cb3bcf87161fa893d2e1aabfee75754767cef07a12e44bb707720e727e585a258356cc797ecee8263c0f61cfc8ffa0360c758f1348ac44c186e12ce0f4faad43b4638abd4a0bc9fd4a6fa4352c20cc771241f95c26f1671ca95c8f4a63a8318dc43299f54e8a899df78ccfd3112a0d5ea637847dd2e3b05be8c0658dd0d7d814473fa5369957c00e84df600df23faaee5faa17b9ededad4731e5e9c1099dfddf5264756800dcfcad4b006b736d1d47c59a019acde4dc22249fc40846b77b43294e32a21db745e1bec790324c3d505edc79388a6e44b02841b26306ed48cfce1e941642c30792315016dba03797c8e4e279eec5b78aad602620471f24c25aea3aaa57509aa9eef2057f11bc95bad708918f2f0df74ac179d7dffc772b2c603dd89e7aea0e8f94f1a8bab4a4fba10bf05c88fbe4b021b3faff3d558e32e4bc20be4bed62d653674ce697390e098e590a3e354cb4a1e703474de8aab30cd76cf7e237f2e66bf486c4fc6c22028764e95adf7d8fa018f44b51ae6acfa3bf80f14c45c06623b916d79649abe0a2b229f96e60e421f6e734160da37f01e915cf73d1cacd1eb7f06c26c33b4d8e4dde264f3cfe84bada0601d1c03aa31c5938750ca0b852f3177883cae9f285d582a4eb38c05f8ef6e5cff5be0745e1ec66e20752bfd5bd5a1590fa280ace3e9786e0022e7ae3c48bcca14e9c5513bc8b57e15820a685f8348159862be0579a35d8ac9d1abaf36d9274c7e750fd9ad265c0d8f08c95ed9ce69eef3a55aef05f2d5d601f80f472689f3428e4f0095829a459813d5dace7e6137a752ae5567982e67b2092afeba99561fbe4e716f67bd1b4e8de1f376dec30eed27371bcc42d7de2ea0f4288054618e9afa002a2d1996b7a70a9683229f28bab811b67629dad527f325c0f12e19d92bac51e5924f27048fa118673b52b296b3642ec946d9915ded0ae84e1a2236da65f672bdad75a22cc0ea751c07e56d2ec22caa41afc98ec6b37a8c1b6a5378a81f2cdb2228f4efb8d7f35c0086a955e1b04bd09bd7e056c949fab1805f733a8b2061adad0c2b7fae33d21363de911e517b21a1539dfa1b3cbb1ea0dbfa3ffff23bbac01183f852de41e798fca5a278b711893175aeaded90873574d8de30b360f39ea239492c630eda4a811d3bb7a125054d5ca74bb6698aeea1a417ad19415ca0e5ca36abc2f96725986f73bcbe3113e391010d08f58f05979c7cef26ff92506c5d1eb2a2f6f5689e9a39957f0723bef3262f5190de996234d4f00b73ed74d78fdf1e6bf31161e16bd083bc6fbddc4eba85c17067e15f08019e5ed943de8e23a974d516abc641e85e641b03779816c30b3449a16b142417c1ff93ab7fa8f96a175e9ef73b3f06ac76788c27889d426efa78d5b8ce35be4591902f7766fe579a0aa28229235a920d26264c09625dea807f619a040f08931d6e1fe57ff0c48ea476be93a16d1fc8de3617984eeebcf14b63c839b41f8f9305402d1288c8e481a4fa5c3302bb1f83e3f0dc8ff9550f9bacb44bccb58f3de152abef5d578afed1c29dc89495b9e54a0c6d00f1dba45a2cf68c9512d9a9ff0b2531e58e47428a99cb246ca23f867b660dc71785b57407cc292f735634c602409792c4640831809f1f1e51903273b623aa0ae0cdd335c7b9db360b0bceb0d15f2313e1944800f30f82ed5bb07cfa1c4740c2bf2806539a4afac1f79d779b923ad8dc2493ebb2d2fce9aea58a009d64e7d1b71ca6893b076e41f7e88a4b51b5402e3fa6c60fa65a686adea229f0164318c9fa1b6d2d2218e5ada710daffecb6b7dd8bf7447658795c4c7a0ad710c4f02fd19017a0575f9467600cdca019793f2f49d197dbfc937828e5790b90929e5ca16037ec79734b64feec36b36c220a2979c45dd51e24c9fb21d8634471aac20c6f179f90c0d61c7b3d89826d146b157bedd8f6b66f6edfabfe04b49f2f2d999fc2e578a440bafd524c82ae614dc8017e379cf926e042f4fbd6f0628fde52de18d764ba8385b77569eda30d5a3617fb0a0c7fd26c821308c3ae98498d33b974cb318a04af3ea3fbcb13fc62fc952aaef095423da9ec7bdc7b77adbd403931189ddc98fe19a06711415b40a9a68812bb7c5453b7b2377910c7b89c99b379e038a7940487c0fd2405456ee55ab6ead3ef25a8a5b1abcae479c24f5e6869057e0bdabcdf352b4a64a3e385171a6e14c8102b2a187034e21705e3a457167fe0dc0d63d6e8d489c9a18c9d84b541504d36b086c2c63cc1a34c0080122c5d60ca33ab60289d16f21e1ded753607267c2093b1c587b89da9df65584fbe3ff9eb7f91d64e33912b8e91adc27191d22f8e835be6bb24546f21488f7abcb29339c34058d4f4093096144b17b8ab76a346275b7e7c80bca59d20e0bb482bb2a9cc3c9515cc1b5be17348c65c73e9fb1ed77d423c509f7cff0e355a34d080d310f3b848dbc209bbba6b6b109fb8d9556dca0fab086e197327ab423d5d762b68961244d8d22c30a8a3a116770bb15b5a0a347091a843b68d6a8e0f1c79f12523a7561c1233cd44db90f6cd3c1ce5fc13f8382177b5522aae028379269b71ae2a42f41dff7374ed7e83c89566f57297b82478b04359a2c199ce8f842112b7450cc1e2e2e394cda4c67e0b2302e21f6af997607ceefd067f77be8900bb3ecb3e30782477aa76861b286b9ddc9e36fcebb50f04f9516e02da31e6219bb5bcb81ee673d95be14c1bd2be4909556d6dbca0365292c582dedcafcc60b255ab7bcd9d977a4139f394ca1da81040e784fd8e7534f230bc5201e7f1db47eadc30f37609d5bbaba624157d98d65029bbab766b6c23c3049a32b894c0cfcb40913ba1cd2d5acda7d2acc920fd01c36f28fc6b7ffd01a37b17fc3235d0dbe9b8098530bed6894b288604b8689f4aafc22cdf211fb95ef5c90cae62a250234e6f790e9a15012acac88305dc4f91fd564a9ab8bb27c057ec5dd46fe952a7be557caea9b7b1d6118aa42df79b8c207e2bae6c34d67dc32b4360ad20b3e609e9caeb7f432ad51cfce139f2d4eb9ed219f4323acd5685e0e0409939eb662175a83fa083f500516dbcb091a3448cb24c3198c8fc547fbda3cb0894edeceef7ccb4ad746aa06f4038b63ab4095a9c390656520561ba3763b1057b3af7cb548342a2bfc2ab725b01b12a7adfc30d7d9632acafd2595cde406b8637a911b7c86f7b09b11f58acec3f1a1bd7cf6853331b48d7907ed699d91fbdbcab8001e3d8d3a26b491b6e2d98c5e149847a07a2b7faa1f567cd4bc9c83ad553339632f3dcacb890c5222656b3349ddd5c8eacaa490ac0b2b38f8a26da9ce7789f5601769a7f10b93125cb93b589bda4ddb4e8795817b60cc149af7c0699b2bbbf655f2f5ec170d6af51213e8c725e699d181923ecf10c6f1069f46e6bc89c7a29d2ebe133b5c0c4b67826a93add7d4824e60b4c5f0cee358abedb50c54a59e95185d7a80081f2dddba5c7c7c637b2dfe8575ddaa71306a2725c9ec17b8e4e1f271a442f6798cc21bbd55c2d69819ddde37a8e8d6a812c41a3e58719b7c96e9375155c4a873ed698ad37144ef32e3fe41cce9c48bbe31441dbbeec7b97734769063d6d04cd8d4963f09f7101bf57cb97a83452cc5de873c5ac0ce001c471c9fcd3275d90a118dd4c25a525d9fb358ff85104b98136850786b387fa17cc1a1d128bc5f7c365ec7920ea677e4c8023071a958647d9fbd27e29d7d099b4dfbbac086ac2af00407fd12092ef1f4847bf8988d839e49a6b5b42482c3dde77022ace66e1ca15b46f2df88d053c1bc3623110b3be74b08749eba6d22f87a44cf7cc1997e7e45d0e"; diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index 40eef387c0..aba2d0d55f 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -29,7 +29,6 @@ use z_coin_grpc::{BlockId, BlockRange, TreeState, TxFilter}; use zcash_extras::{WalletRead, WalletWrite}; use zcash_primitives::consensus::BlockHeight; use zcash_primitives::transaction::TxId; -use zcash_primitives::zip32::ExtendedSpendingKey; pub(crate) mod z_coin_grpc { tonic::include_proto!("pirate.wallet.sdk.rpc"); @@ -508,7 +507,6 @@ pub(super) async fn init_light_client<'a>( blocks_db: BlockDbImpl, sync_params: &Option, skip_sync_params: bool, - z_spending_key: &ExtendedSpendingKey, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); @@ -541,8 +539,7 @@ pub(super) async fn init_light_client<'a>( // check if no sync_params was provided and continue syncing from last height in db if it's > 0 or skip_sync_params is true. let continue_from_prev_sync = (min_height > 0 && sync_params.is_none()) || (skip_sync_params && min_height < sapling_activation_height); - let wallet_db = - WalletDbShared::new(builder, maybe_checkpoint_block, z_spending_key, continue_from_prev_sync).await?; + let wallet_db = WalletDbShared::new(builder, maybe_checkpoint_block, continue_from_prev_sync).await?; // Check min_height in blocks_db and rewind blocks_db to 0 if sync_height != min_height if !continue_from_prev_sync && (sync_height != min_height) { // let user know we're clearing cache and re-syncing from new provided height. @@ -586,7 +583,6 @@ pub(super) async fn init_native_client<'a>( builder: &ZCoinBuilder<'a>, native_client: NativeClient, blocks_db: BlockDbImpl, - z_spending_key: &ExtendedSpendingKey, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); @@ -600,7 +596,7 @@ pub(super) async fn init_native_client<'a>( is_pre_sapling: false, actual: checkpoint_height, }; - let wallet_db = WalletDbShared::new(builder, checkpoint_block, z_spending_key, true) + let wallet_db = WalletDbShared::new(builder, checkpoint_block, true) .await .mm_err(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string()))?; diff --git a/mm2src/coins_activation/Cargo.toml b/mm2src/coins_activation/Cargo.toml index be951c67d4..3ed8213e3b 100644 --- a/mm2src/coins_activation/Cargo.toml +++ b/mm2src/coins_activation/Cargo.toml @@ -28,6 +28,7 @@ mm2_number = { path = "../mm2_number" } parking_lot = { version = "0.12.0", features = ["nightly"] } rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } +secp256k1 = { version = "0.24" } ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } serde = "1.0" diff --git a/mm2src/coins_activation/src/lightning_activation.rs b/mm2src/coins_activation/src/lightning_activation.rs index 1d2f9ec232..105fb2c422 100644 --- a/mm2src/coins_activation/src/lightning_activation.rs +++ b/mm2src/coins_activation/src/lightning_activation.rs @@ -20,7 +20,7 @@ use common::executor::{SpawnFuture, Timer}; use crypto::hw_rpc_task::{HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use derive_more::Display; use futures::compat::Future01CompatExt; -use lightning::chain::keysinterface::KeysInterface; +use lightning::chain::keysinterface::{KeysInterface, Recipient}; use lightning::chain::Access; use lightning::routing::gossip; use lightning::routing::router::DefaultRouter; @@ -29,6 +29,7 @@ use lightning_invoice::payment; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use parking_lot::Mutex as PaMutex; +use secp256k1::Secp256k1; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; use serde_json::{self as json, Value as Json}; @@ -350,11 +351,16 @@ async fn start_lightning( // Initialize the Logger let logger = ctx.log.0.clone(); - // Initialize Persister - let persister = init_persister(ctx, conf.ticker.clone(), params.backup_path).await?; - // Initialize the KeysManager let keys_manager = init_keys_manager(&platform)?; + let node_id = keys_manager + .get_node_secret(Recipient::Node) + .map_err(|e| EnableLightningError::Internal(format!("Error while getting node id: {:?}", e)))? + .public_key(&Secp256k1::new()); + let node_id = node_id.to_string(); + + // Initialize Persister + let persister = init_persister(ctx, &node_id, conf.ticker.clone(), params.backup_path).await?; // Initialize the P2PGossipSync. This is used for providing routes to send payments over task_handle.update_in_progress_status(LightningInProgressStatus::ReadingNetworkGraphFromFile)?; @@ -371,7 +377,7 @@ async fn start_lightning( )); // Initialize DB - let db = init_db(ctx, conf.ticker.clone()).await?; + let db = init_db(ctx, &node_id, conf.ticker.clone()).await?; // Initialize the ChannelManager task_handle.update_in_progress_status(LightningInProgressStatus::InitializingChannelManager)?; diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index 37cf759a9d..747875d5b8 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -41,6 +41,7 @@ timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } wasm-bindgen-test = { version = "0.3.2" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +mm2_io = { path = "../mm2_io" } rustls = { version = "0.21", default-features = false } tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } timed-map = { version = "1.4", features = ["rustc-hash"] } diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 292dc69ca6..5d01b81245 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -38,6 +38,7 @@ cfg_native! { use mm2_metrics::MmMetricsError; use std::net::{IpAddr, SocketAddr, AddrParseError}; use std::path::{Path, PathBuf}; + use derive_more::Display; use std::sync::MutexGuard; } @@ -348,8 +349,13 @@ impl MmCtx { /// /// Such directory isn't bound to a specific seed/wallet or address. /// Data that should be stored there is public and shared between all seeds and addresses (e.g. stats, block headers, etc...). - #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] - pub fn global_dir(&self) -> PathBuf { self.db_root().join("global") } + #[cfg(not(target_arch = "wasm32"))] + pub fn global_dir(&self) -> PathBuf { + if cfg!(not(feature = "new-db-arch")) { + return self.dbdir(); + } + self.db_root().join("global") + } /// Returns the path to wallet's data directory. /// @@ -357,8 +363,11 @@ impl MmCtx { /// For HD wallets, this `rmd160` is derived from `mm2_internal_derivation_path`. /// For Iguana, this `rmd160` is simply a hash of the seed. /// Use this directory to store seed/wallet related data rather than address related data (e.g. HD wallet accounts, HD wallet tx history, etc...) - #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + #[cfg(not(target_arch = "wasm32"))] pub fn wallet_dir(&self) -> PathBuf { + if cfg!(not(feature = "new-db-arch")) { + return self.dbdir(); + } self.db_root() .join("wallets") .join(hex::encode(self.rmd160().as_slice())) @@ -368,13 +377,12 @@ impl MmCtx { /// /// Use this directory for data related to a specific address and only that specific address (e.g. swap data, order data, etc...). /// This makes sure that when this address is activated using a different technique, this data is still accessible. - #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] - pub fn address_dir(&self, address: &str) -> Result { - let path = self.db_root().join("addresses").join(address); - if !path.exists() { - std::fs::create_dir_all(&path).map_err(AddressDataError::CreateAddressDirFailure)?; + #[cfg(not(target_arch = "wasm32"))] + pub fn address_dir(&self, address: &str) -> PathBuf { + if cfg!(not(feature = "new-db-arch")) { + return self.dbdir(); } - Ok(path) + self.db_root().join("addresses").join(address) } /// Returns a SQL connection to the global database. @@ -396,9 +404,10 @@ impl MmCtx { } /// Returns a SQL connection to the address database. - #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + #[cfg(not(target_arch = "wasm32"))] pub fn address_db(&self, address: &str) -> Result { - let path = self.address_dir(address)?.join("MM2.db"); + let path = self.address_dir(address).join("MM2.db"); + mm2_io::fs::create_parents(&path).map_err(|err| AddressDataError::CreateAddressDirFailure(err.into_inner()))?; log_sqlite_file_open_attempt(&path); let connection = Connection::open(path).map_err(AddressDataError::SqliteConnectionFailure)?; Ok(connection) @@ -533,7 +542,8 @@ impl Drop for MmCtx { } } -#[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] +#[cfg(not(target_arch = "wasm32"))] +#[derive(Debug, Display)] pub enum AddressDataError { CreateAddressDirFailure(std::io::Error), SqliteConnectionFailure(db_common::sqlite::rusqlite::Error), diff --git a/mm2src/mm2_io/src/file_lock.rs b/mm2src/mm2_io/src/file_lock.rs index 9a2e04bdef..334919a44f 100644 --- a/mm2src/mm2_io/src/file_lock.rs +++ b/mm2src/mm2_io/src/file_lock.rs @@ -4,6 +4,8 @@ use gstuff::now_float; use mm2_err_handle::prelude::*; use std::path::{Path, PathBuf}; +use crate::fs::create_parents; + pub type FileLockResult = std::result::Result>; #[derive(Debug, Display)] @@ -45,6 +47,10 @@ fn read_timestamp(path: &dyn AsRef) -> FileLockResult> { impl> FileLock { pub fn lock(lock_path: T, ttl_sec: f64) -> FileLockResult>> { + create_parents(&lock_path.as_ref()).map_err(|e| FileLockError::ErrorCreatingLockFile { + path: lock_path.as_ref().to_path_buf(), + error: e.to_string(), + })?; match std::fs::OpenOptions::new() .write(true) .create_new(true) diff --git a/mm2src/mm2_io/src/fs.rs b/mm2src/mm2_io/src/fs.rs index 960886a2b2..739e1c950d 100644 --- a/mm2src/mm2_io/src/fs.rs +++ b/mm2src/mm2_io/src/fs.rs @@ -111,11 +111,6 @@ pub async fn remove_file_async>(path: P) -> IoResult<()> { Ok(async_fs::remove_file(path.as_ref()).await?) } -pub fn write(path: &dyn AsRef, contents: &dyn AsRef<[u8]>) -> Result<(), String> { - try_s!(fs::write(path, contents)); - Ok(()) -} - /// Read a folder asynchronously and return a list of files. pub async fn read_dir_async>(dir: P) -> IoResult> { use futures::StreamExt; @@ -276,10 +271,88 @@ where read_files_with_extension(dir_path, "json").await } +/// Creates all the directories along the path to a file if they do not exist. +pub fn create_parents(path: &impl AsRef) -> IoResult<()> { + let parent_dir = path.as_ref().parent(); + let Some(parent_dir) = parent_dir else { + return MmError::err( + io::Error::new( + io::ErrorKind::InvalidInput, + format!("{} has no parent directory", path.as_ref().display()), + )) + }; + match fs::metadata(parent_dir) { + // Path exists, make sure it's a directory (and not a file for example). + Ok(metadata) => { + if !metadata.is_dir() { + return MmError::err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("{} is not a directory", parent_dir.display()), + )); + } + }, + // This path doesn't exist, create it. + Err(_) => fs::create_dir_all(parent_dir)?, + } + Ok(()) +} + +/// Similar to [`create_parents`], but using non-blocking async IO operations. +/// +/// Creates all the directories along the path to a file if they do not exist. +pub async fn create_parents_async(path: &Path) -> IoResult<()> { + let parent_dir = path.parent(); + let Some(parent_dir) = parent_dir else { + return MmError::err( + io::Error::new( + io::ErrorKind::InvalidInput, + format!("{} has no parent directory", path.display()), + )) + }; + match async_fs::metadata(parent_dir).await { + // Path exists, make sure it's a directory (and not a file, for instance). + Ok(metadata) => { + if !metadata.is_dir() { + return MmError::err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("{} is not a directory", parent_dir.display()), + )); + } + }, + // This path doesn't exist, try to create it. + Err(_) => async_fs::create_dir_all(parent_dir).await?, + } + Ok(()) +} + +/// Writes the `content` to the file at `path`. +/// +/// This also creates any intermediary directories up to the file itself if they do not exist. +/// If `use_tmp_file` is true, it writes to a temporary file first and then renames it to the final file name +/// to ensure atomicity. +pub fn write(path: &impl AsRef, content: &[u8], use_tmp_file: bool) -> IoResult<()> { + // Create all the directories in the path. + create_parents(path)?; + let path_tmp = if use_tmp_file { + PathBuf::from(format!("{}.tmp", path.as_ref().display())) + } else { + path.as_ref().to_path_buf() + }; + // Write the file content into the temp file and then rename the temp file into the desired name. + fs::write(&path_tmp, content)?; + if use_tmp_file { + fs::rename(&path_tmp, path.as_ref()).error_log_passthrough()? + } + Ok(()) +} + pub async fn write_json(t: &T, path: &Path, use_tmp_file: bool) -> FsJsonResult<()> where T: Serialize, { + create_parents_async(path) + .await + .map_err(|err| FsJsonError::IoWriting(err.into_inner()))?; let content = json::to_vec(t).map_to_mm(FsJsonError::Serializing)?; let path_tmp = if use_tmp_file { diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index f1a365d38a..fe2717b3d2 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -25,7 +25,7 @@ enable-sia = ["coins/enable-sia", "coins_activation/enable-sia"] sepolia-maker-swap-v2-tests = [] sepolia-taker-swap-v2-tests = [] test-ext-api = ["trading_api/test-ext-api"] -new-db-arch = [] # A temporary feature to integrate the new db architecture incrementally +new-db-arch = ["mm2_core/new-db-arch"] # A temporary feature to integrate the new db architecture incrementally [dependencies] async-std = { version = "1.5", features = ["unstable"] } diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 7f6cf266e8..2c41131927 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -962,10 +962,12 @@ pub struct TransactionIdentifier { } #[cfg(not(target_arch = "wasm32"))] -pub fn my_swaps_dir(ctx: &MmArc) -> PathBuf { ctx.dbdir().join("SWAPS").join("MY") } +pub fn my_swaps_dir(ctx: &MmArc, address: &str) -> PathBuf { ctx.address_dir(address).join("SWAPS").join("MY") } #[cfg(not(target_arch = "wasm32"))] -pub fn my_swap_file_path(ctx: &MmArc, uuid: &Uuid) -> PathBuf { my_swaps_dir(ctx).join(format!("{}.json", uuid)) } +pub fn my_swap_file_path(ctx: &MmArc, address: &str, uuid: &Uuid) -> PathBuf { + my_swaps_dir(ctx, address).join(format!("{}.json", uuid)) +} pub async fn insert_new_swap_to_db( ctx: MmArc, @@ -1080,7 +1082,7 @@ pub async fn my_swap_status(ctx: MmArc, req: Json) -> Result>, match swap_type { Some(LEGACY_SWAP_TYPE) => { - let status = match SavedSwap::load_my_swap_from_db(&ctx, uuid).await { + let status = match SavedSwap::load_my_swap_from_db(&ctx, None, uuid).await { Ok(Some(status)) => status, Ok(None) => return Err("swap data is not found".to_owned()), Err(e) => return ERR!("{}", e), @@ -1142,7 +1144,7 @@ struct SwapStatus { /// Broadcasts `my` swap status to P2P network async fn broadcast_my_swap_status(ctx: &MmArc, uuid: Uuid) -> Result<(), String> { - let mut status = match try_s!(SavedSwap::load_my_swap_from_db(ctx, uuid).await) { + let mut status = match try_s!(SavedSwap::load_my_swap_from_db(ctx, None, uuid).await) { Some(status) => status, None => return ERR!("swap data is not found"), }; @@ -1251,7 +1253,7 @@ pub async fn latest_swaps_for_pair( let mut swaps = Vec::with_capacity(db_result.uuids_and_types.len()); // TODO this is needed for trading bot, which seems not used as of now. Remove the code? for (uuid, _) in db_result.uuids_and_types.iter() { - let swap = match SavedSwap::load_my_swap_from_db(&ctx, *uuid).await { + let swap = match SavedSwap::load_my_swap_from_db(&ctx, None, *uuid).await { Ok(Some(swap)) => swap, Ok(None) => { error!("No such swap with the uuid '{}'", uuid); @@ -1278,7 +1280,7 @@ pub async fn my_recent_swaps_rpc(ctx: MmArc, req: Json) -> Result match SavedSwap::load_my_swap_from_db(&ctx, *uuid).await { + LEGACY_SWAP_TYPE => match SavedSwap::load_my_swap_from_db(&ctx, None, *uuid).await { Ok(Some(swap)) => { let swap_json = try_s!(json::to_value(MySwapStatusResponse::from(swap))); swaps.push(swap_json) @@ -1329,7 +1331,7 @@ pub async fn swap_kick_starts(ctx: MmArc) -> Result, String> { let mut coins = HashSet::new(); let legacy_unfinished_uuids = try_s!(get_unfinished_swaps_uuids(ctx.clone(), LEGACY_SWAP_TYPE).await); for uuid in legacy_unfinished_uuids { - let swap = match SavedSwap::load_my_swap_from_db(&ctx, uuid).await { + let swap = match SavedSwap::load_my_swap_from_db(&ctx, None, uuid).await { Ok(Some(s)) => s, Ok(None) => { warn!("Swap {} is indexed, but doesn't exist in DB", uuid); @@ -1477,7 +1479,7 @@ pub async fn coins_needed_for_kick_start(ctx: MmArc) -> Result> pub async fn recover_funds_of_swap(ctx: MmArc, req: Json) -> Result>, String> { let uuid: Uuid = try_s!(json::from_value(req["params"]["uuid"].clone())); - let swap = match SavedSwap::load_my_swap_from_db(&ctx, uuid).await { + let swap = match SavedSwap::load_my_swap_from_db(&ctx, None, uuid).await { Ok(Some(swap)) => swap, Ok(None) => return ERR!("swap data is not found"), Err(e) => return ERR!("{}", e), @@ -1554,7 +1556,7 @@ pub async fn active_swaps_rpc(ctx: MmArc, req: Json) -> Result> for (uuid, swap_type) in uuids_with_types.iter() { match *swap_type { LEGACY_SWAP_TYPE => { - let status = match SavedSwap::load_my_swap_from_db(&ctx, *uuid).await { + let status = match SavedSwap::load_my_swap_from_db(&ctx, None, *uuid).await { Ok(Some(status)) => status, Ok(None) => continue, Err(e) => { @@ -2240,7 +2242,7 @@ mod lp_swap_tests { uuid, None, conf_settings, - rick_maker.into(), + rick_maker.clone().into(), morty_maker.into(), lock_duration, None, @@ -2261,7 +2263,7 @@ mod lp_swap_tests { uuid, None, conf_settings, - rick_taker.into(), + rick_taker.clone().into(), morty_taker.into(), lock_duration, None, @@ -2274,15 +2276,18 @@ mod lp_swap_tests { run_taker_swap(RunTakerSwapInput::StartNew(taker_swap), taker_ctx.clone()), )); + let makers_maker_coin_address = rick_maker.my_address().unwrap(); + let takers_maker_coin_address = rick_taker.my_address().unwrap(); + println!( "Maker swap path {}", - std::fs::canonicalize(my_swap_file_path(&maker_ctx, &uuid)) + std::fs::canonicalize(my_swap_file_path(&maker_ctx, &makers_maker_coin_address, &uuid)) .unwrap() .display() ); println!( "Taker swap path {}", - std::fs::canonicalize(my_swap_file_path(&taker_ctx, &uuid)) + std::fs::canonicalize(my_swap_file_path(&taker_ctx, &takers_maker_coin_address, &uuid)) .unwrap() .display() ); diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index c7e5a43329..7ec3a86245 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -80,7 +80,7 @@ pub const MAKER_ERROR_EVENTS: [&str; 15] = [ pub const MAKER_PAYMENT_SENT_LOG: &str = "Maker payment sent"; #[cfg(not(target_arch = "wasm32"))] -pub fn stats_maker_swap_dir(ctx: &MmArc) -> PathBuf { ctx.dbdir().join("SWAPS").join("STATS").join("MAKER") } +pub fn stats_maker_swap_dir(ctx: &MmArc) -> PathBuf { ctx.global_dir().join("SWAPS").join("STATS").join("MAKER") } #[cfg(not(target_arch = "wasm32"))] pub fn stats_maker_swap_file_path(ctx: &MmArc, uuid: &Uuid) -> PathBuf { @@ -88,10 +88,14 @@ pub fn stats_maker_swap_file_path(ctx: &MmArc, uuid: &Uuid) -> PathBuf { } async fn save_my_maker_swap_event(ctx: &MmArc, swap: &MakerSwap, event: MakerSavedEvent) -> Result<(), String> { - let swap = match SavedSwap::load_my_swap_from_db(ctx, swap.uuid).await { + let maker_coin_pub = swap.my_maker_coin_htlc_pub(); + let maker_coin_address = try_s!(swap.maker_coin.address_from_pubkey(&maker_coin_pub)); + let swap = match SavedSwap::load_my_swap_from_db(ctx, Some(&maker_coin_address), swap.uuid).await { Ok(Some(swap)) => swap, Ok(None) => SavedSwap::Maker(MakerSavedSwap { uuid: swap.uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + maker_address: maker_coin_address, my_order_uuid: swap.my_order_uuid, maker_amount: Some(swap.maker_amount.clone()), maker_coin: Some(swap.maker_coin.ticker().to_owned()), @@ -1360,7 +1364,7 @@ impl MakerSwap { taker_coin: MmCoinEnum, swap_uuid: &Uuid, ) -> Result<(Self, Option), String> { - let saved = match SavedSwap::load_my_swap_from_db(&ctx, *swap_uuid).await { + let saved = match SavedSwap::load_my_swap_from_db(&ctx, None, *swap_uuid).await { Ok(Some(saved)) => saved, Ok(None) => return ERR!("Couldn't find a swap with the uuid '{}'", swap_uuid), Err(e) => return ERR!("{}", e), @@ -1855,6 +1859,8 @@ impl MakerSwapStatusChanged { #[derive(Debug, Default, PartialEq, Serialize, Deserialize)] pub struct MakerSavedSwap { pub uuid: Uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + pub maker_address: String, pub my_order_uuid: Option, pub events: Vec, pub maker_amount: Option, @@ -1911,6 +1917,8 @@ impl MakerSavedSwap { MakerSavedSwap { uuid: Default::default(), + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + maker_address: "".to_string(), my_order_uuid: None, events, maker_amount: Some(maker_amount.to_decimal()), diff --git a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs index e389597f92..3d9edf5c69 100644 --- a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs +++ b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs @@ -73,15 +73,18 @@ pub async fn recreate_swap_data(ctx: MmArc, args: RecreateSwapRequest) -> Recrea }, InputSwap::SavedSwap(SavedSwap::Taker(taker_swap)) | InputSwap::TakerSavedSwap(taker_swap) => { recreate_maker_swap(ctx, taker_swap) + .await .map(SavedSwap::from) .map(|swap| RecreateSwapResponse { swap }) }, } } -fn recreate_maker_swap(ctx: MmArc, taker_swap: TakerSavedSwap) -> RecreateSwapResult { +async fn recreate_maker_swap(ctx: MmArc, taker_swap: TakerSavedSwap) -> RecreateSwapResult { let mut maker_swap = MakerSavedSwap { uuid: taker_swap.uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + maker_address: String::new(), my_order_uuid: taker_swap.my_order_uuid, events: Vec::new(), maker_amount: taker_swap.maker_amount, @@ -121,7 +124,7 @@ fn recreate_maker_swap(ctx: MmArc, taker_swap: TakerSavedSwap) -> RecreateSwapRe taker_p2p_pubkey.copy_from_slice(&started_event.my_persistent_pub.0[1..33]); let maker_started_event = MakerSwapEvent::Started(MakerSwapData { taker_coin: started_event.taker_coin, - maker_coin: started_event.maker_coin, + maker_coin: started_event.maker_coin.clone(), taker_pubkey: H256Json::from(taker_p2p_pubkey), // We could parse the `TakerSwapEvent::TakerPaymentSpent` event. // As for now, don't try to find the secret in the events since we can refund without it. @@ -176,6 +179,23 @@ fn recreate_maker_swap(ctx: MmArc, taker_swap: TakerSavedSwap) -> RecreateSwapRe .events .extend(convert_taker_to_maker_events(event_it, wait_refund_until)); + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + { + // TODO(new-db-arch): Execute this plan: https://github.com/KomodoPlatform/komodo-defi-framework/pull/2398#discussion_r2036035916 + // instead of making the maker_address/address_dir available for the importer (i.e. let them find it themselves). + let maker_coin_ticker = started_event.maker_coin; + let maker_coin = lp_coinfind(&ctx, &maker_coin_ticker) + .await + .map_to_mm(RecreateSwapError::Internal)? + .or_mm_err(move || RecreateSwapError::NoSuchCoin { + coin: maker_coin_ticker, + })?; + maker_swap.maker_address = negotiated_event + .maker_coin_htlc_pubkey + .and_then(|pubkey| maker_coin.address_from_pubkey(&pubkey).ok()) + .unwrap_or("Couldn't get the maker coin address. Please set it manually.".to_string()); + } + Ok(maker_swap) } @@ -285,6 +305,8 @@ fn convert_taker_to_maker_events( async fn recreate_taker_swap(ctx: MmArc, maker_swap: MakerSavedSwap) -> RecreateSwapResult { let mut taker_swap = TakerSavedSwap { uuid: maker_swap.uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + maker_address: String::new(), my_order_uuid: Some(maker_swap.uuid), events: Vec::new(), maker_amount: maker_swap.maker_amount, @@ -382,6 +404,14 @@ async fn recreate_taker_swap(ctx: MmArc, maker_swap: MakerSavedSwap) -> Recreate coin: maker_coin_ticker, })?; + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + { + taker_swap.maker_address = negotiated_event + .maker_coin_htlc_pubkey + .and_then(|pubkey| maker_coin.address_from_pubkey(&pubkey).ok()) + .unwrap_or("Couldn't get the maker coin address. Please set it manually.".to_string()); + } + // Then we can continue to process success Maker events. let wait_refund_until = negotiated_event.taker_payment_locktime + 3700; taker_swap @@ -509,7 +539,7 @@ mod tests { let ctx = MmCtxBuilder::default().into_mm_arc(); - let maker_actual_swap = recreate_maker_swap(ctx, taker_saved_swap).expect("!recreate_maker_swap"); + let maker_actual_swap = block_on(recreate_maker_swap(ctx, taker_saved_swap)).expect("!recreate_maker_swap"); println!("{}", json::to_string(&maker_actual_swap).unwrap()); assert_eq!(maker_actual_swap, maker_expected_swap); } @@ -527,7 +557,7 @@ mod tests { let ctx = MmCtxBuilder::default().into_mm_arc(); - let maker_actual_swap = recreate_maker_swap(ctx, taker_saved_swap).expect("!recreate_maker_swap"); + let maker_actual_swap = block_on(recreate_maker_swap(ctx, taker_saved_swap)).expect("!recreate_maker_swap"); println!("{}", json::to_string(&maker_actual_swap).unwrap()); assert_eq!(maker_actual_swap, maker_expected_swap); } diff --git a/mm2src/mm2_main/src/lp_swap/saved_swap.rs b/mm2src/mm2_main/src/lp_swap/saved_swap.rs index 185edd1584..158843dadb 100644 --- a/mm2src/mm2_main/src/lp_swap/saved_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/saved_swap.rs @@ -75,6 +75,14 @@ impl SavedSwap { } } + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + pub fn maker_address(&self) -> &str { + match self { + SavedSwap::Maker(swap) => &swap.maker_address, + SavedSwap::Taker(swap) => &swap.maker_address, + } + } + pub fn maker_coin_ticker(&self) -> Result { match self { SavedSwap::Maker(swap) => swap.maker_coin(), @@ -159,7 +167,11 @@ impl SavedSwap { #[async_trait] pub trait SavedSwapIo { - async fn load_my_swap_from_db(ctx: &MmArc, uuid: Uuid) -> SavedSwapResult>; + async fn load_my_swap_from_db( + ctx: &MmArc, + address_dir: Option<&str>, + uuid: Uuid, + ) -> SavedSwapResult>; async fn load_all_my_swaps_from_db(ctx: &MmArc) -> SavedSwapResult>; @@ -208,13 +220,30 @@ mod native_impl { #[async_trait] impl SavedSwapIo for SavedSwap { - async fn load_my_swap_from_db(ctx: &MmArc, uuid: Uuid) -> SavedSwapResult> { - let path = my_swap_file_path(ctx, &uuid); + async fn load_my_swap_from_db( + ctx: &MmArc, + address_dir: Option<&str>, + uuid: Uuid, + ) -> SavedSwapResult> { + // TODO(new-db-arch): Set the correct address directory for the new db arch branch (via a query to the global DB). + #[cfg(feature = "new-db-arch")] + let address_dir = address_dir.unwrap_or("Fetch the address directory from the global DB given the UUID."); + #[cfg(not(feature = "new-db-arch"))] + let address_dir = address_dir.unwrap_or("no address directory for old DB architecture (has no effect)"); + let path = my_swap_file_path(ctx, address_dir, &uuid); Ok(read_json(&path).await?) } + #[cfg_attr(feature = "new-db-arch", allow(unreachable_code, unused_variables))] async fn load_all_my_swaps_from_db(ctx: &MmArc) -> SavedSwapResult> { - let path = my_swaps_dir(ctx); + #[cfg(feature = "new-db-arch")] + { + // This method is solely used for migrations. Which we should ditch or refactor with the new DB architecture. + // If we ditch the old migrations, this method should never be called (and should be deleted when we are + // done with the incremental architecture change). + todo!("Fix the dummy address directory in `my_swaps_dir` below or remove this method all together"); + } + let path = my_swaps_dir(ctx, "has no effect in not(feature = 'new-db-arch')"); Ok(read_dir_json(&path).await?) } @@ -239,7 +268,11 @@ mod native_impl { } async fn save_to_db(&self, ctx: &MmArc) -> SavedSwapResult<()> { - let path = my_swap_file_path(ctx, self.uuid()); + #[cfg(feature = "new-db-arch")] + let address_dir = self.maker_address(); + #[cfg(not(feature = "new-db-arch"))] + let address_dir = "no address directory for old DB architecture (has no effect)"; + let path = my_swap_file_path(ctx, address_dir, self.uuid()); write_json(self, &path, USE_TMP_FILE).await?; Ok(()) } @@ -376,7 +409,11 @@ mod wasm_impl { #[async_trait] impl SavedSwapIo for SavedSwap { - async fn load_my_swap_from_db(ctx: &MmArc, uuid: Uuid) -> SavedSwapResult> { + async fn load_my_swap_from_db( + ctx: &MmArc, + _address_dir: Option<&str>, + uuid: Uuid, + ) -> SavedSwapResult> { let swaps_ctx = SwapsContext::from_ctx(ctx).map_to_mm(SavedSwapError::InternalError)?; let db = swaps_ctx.swap_db().await?; let transaction = db.transaction().await?; @@ -486,7 +523,7 @@ mod tests { assert_eq!(item, second_saved_item); } - let actual_saved_swap = SavedSwap::load_my_swap_from_db(&ctx, *saved_swap.uuid()) + let actual_saved_swap = SavedSwap::load_my_swap_from_db(&ctx, None, *saved_swap.uuid()) .await .expect("!load_from_db") .expect("Swap not found"); diff --git a/mm2src/mm2_main/src/lp_swap/swap_lock.rs b/mm2src/mm2_main/src/lp_swap/swap_lock.rs index f4347dde3b..38591deeff 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_lock.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_lock.rs @@ -32,7 +32,6 @@ pub trait SwapLockOps: Sized { #[cfg(not(target_arch = "wasm32"))] mod native_lock { use super::*; - use crate::lp_swap::my_swaps_dir; use mm2_io::file_lock::{FileLock, FileLockError}; use std::path::PathBuf; @@ -57,7 +56,14 @@ mod native_lock { #[async_trait] impl SwapLockOps for SwapLock { async fn lock(ctx: &MmArc, swap_uuid: Uuid, ttl_sec: f64) -> SwapLockResult> { - let lock_path = my_swaps_dir(ctx).join(format!("{}.lock", swap_uuid)); + let lock_path = if cfg!(feature = "new-db-arch") { + ctx.global_dir().join("swap_locks").join(format!("{}.lock", swap_uuid)) + } else { + ctx.global_dir() + .join("SWAPS") + .join("MY") + .join(format!("{}.lock", swap_uuid)) + }; let file_lock = some_or_return_ok_none!(FileLock::lock(lock_path, ttl_sec)?); Ok(Some(SwapLock { file_lock })) diff --git a/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs b/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs index 669d17a492..92a305f252 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs @@ -301,7 +301,7 @@ async fn get_swap_data_by_uuid_and_type( ) -> MmResult, GetSwapDataErr> { match swap_type { LEGACY_SWAP_TYPE => { - let saved_swap = SavedSwap::load_my_swap_from_db(ctx, uuid).await?; + let saved_swap = SavedSwap::load_my_swap_from_db(ctx, None, uuid).await?; Ok(saved_swap.map(|swap| match swap { SavedSwap::Maker(m) => SwapRpcData::MakerV1(m), SavedSwap::Taker(t) => SwapRpcData::TakerV1(t), diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 54f5ab3bfe..08657cc572 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -103,7 +103,7 @@ pub const WATCHER_MESSAGE_SENT_LOG: &str = "Watcher message sent..."; pub const MAKER_PAYMENT_SPENT_BY_WATCHER_LOG: &str = "Maker payment is spent by the watcher..."; #[cfg(not(target_arch = "wasm32"))] -pub fn stats_taker_swap_dir(ctx: &MmArc) -> PathBuf { ctx.dbdir().join("SWAPS").join("STATS").join("TAKER") } +pub fn stats_taker_swap_dir(ctx: &MmArc) -> PathBuf { ctx.global_dir().join("SWAPS").join("STATS").join("TAKER") } #[cfg(not(target_arch = "wasm32"))] pub fn stats_taker_swap_file_path(ctx: &MmArc, uuid: &Uuid) -> PathBuf { @@ -111,10 +111,14 @@ pub fn stats_taker_swap_file_path(ctx: &MmArc, uuid: &Uuid) -> PathBuf { } async fn save_my_taker_swap_event(ctx: &MmArc, swap: &TakerSwap, event: TakerSavedEvent) -> Result<(), String> { - let swap = match SavedSwap::load_my_swap_from_db(ctx, swap.uuid).await { + let maker_coin_pub = swap.my_maker_coin_htlc_pub(); + let maker_coin_address = try_s!(swap.maker_coin.address_from_pubkey(&maker_coin_pub)); + let swap = match SavedSwap::load_my_swap_from_db(ctx, Some(&maker_coin_address), swap.uuid).await { Ok(Some(swap)) => swap, Ok(None) => SavedSwap::Taker(TakerSavedSwap { uuid: swap.uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + maker_address: maker_coin_address, my_order_uuid: swap.my_order_uuid, maker_amount: Some(swap.maker_amount.to_decimal()), maker_coin: Some(swap.maker_coin.ticker().to_owned()), @@ -204,6 +208,8 @@ impl TakerSavedEvent { #[derive(Debug, Deserialize, PartialEq, Serialize)] pub struct TakerSavedSwap { pub uuid: Uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + pub maker_address: String, pub my_order_uuid: Option, pub events: Vec, pub maker_amount: Option, @@ -2085,7 +2091,7 @@ impl TakerSwap { taker_coin: MmCoinEnum, swap_uuid: &Uuid, ) -> Result<(Self, Option), String> { - let saved = match SavedSwap::load_my_swap_from_db(&ctx, *swap_uuid).await { + let saved = match SavedSwap::load_my_swap_from_db(&ctx, None, *swap_uuid).await { Ok(Some(saved)) => saved, Ok(None) => return ERR!("Couldn't find a swap with the uuid '{}'", swap_uuid), Err(e) => return ERR!("{}", e), diff --git a/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs index 9abcfbe506..f9cd8e99af 100644 --- a/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs @@ -7,9 +7,7 @@ use common::now_sec; use lazy_static::lazy_static; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_number::MmNumber; -use mm2_test_helpers::for_tests::{new_mm2_temp_folder_path, zombie_conf_for_docker}; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; +use mm2_test_helpers::for_tests::zombie_conf_for_docker; use tokio::sync::Mutex; // https://github.com/KomodoPlatform/librustzcash/blob/4e030a0f44cc17f100bf5f019563be25c5b8755f/zcash_client_backend/src/data_api/wallet.rs#L72-L73 @@ -26,13 +24,6 @@ pub async fn z_coin_from_spending_key(spending_key: &str) -> (MmArc, ZCoin) { ..Default::default() }; let pk_data = [1; 32]; - let salt: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(4) - .map(char::from) - .collect(); - let db_folder = new_mm2_temp_folder_path(None).join(format!("ZOMBIE_DB_{}", salt)); - std::fs::create_dir_all(&db_folder).unwrap(); let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { CoinProtocol::ZHTLC(protocol_info) => protocol_info, other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), @@ -44,7 +35,6 @@ pub async fn z_coin_from_spending_key(spending_key: &str) -> (MmArc, ZCoin) { &conf, ¶ms, PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), - db_folder, protocol_info, spending_key, ) From e42a9af80a616e290c6b4f45442132e536a4b365 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 23 May 2025 02:39:52 +0100 Subject: [PATCH 10/36] chore(core): organize deps using workspace.dependencies (#2449) This commit organize multiple packages under a single workspace, share dependencies, etc. It also led to some compile time improvements. --- Cargo.lock | 217 +++++++------------ Cargo.toml | 178 ++++++++++++++- mm2src/adex_cli/Cargo.toml | 45 ++-- mm2src/coins/Cargo.toml | 197 ++++++++--------- mm2src/coins/utxo_signer/Cargo.toml | 6 +- mm2src/coins_activation/Cargo.toml | 26 +-- mm2src/common/Cargo.toml | 102 ++++----- mm2src/common/shared_ref_counter/Cargo.toml | 2 +- mm2src/crypto/Cargo.toml | 63 +++--- mm2src/db_common/Cargo.toml | 16 +- mm2src/derives/enum_derives/Cargo.toml | 8 +- mm2src/derives/ser_error/Cargo.toml | 2 +- mm2src/derives/ser_error_derive/Cargo.toml | 6 +- mm2src/hw_common/Cargo.toml | 26 +-- mm2src/ledger/Cargo.toml | 22 +- mm2src/mm2_bin_lib/Cargo.toml | 26 +-- mm2src/mm2_bitcoin/chain/Cargo.toml | 4 +- mm2src/mm2_bitcoin/crypto/Cargo.toml | 10 +- mm2src/mm2_bitcoin/keys/Cargo.toml | 18 +- mm2src/mm2_bitcoin/primitives/Cargo.toml | 8 +- mm2src/mm2_bitcoin/rpc/Cargo.toml | 12 +- mm2src/mm2_bitcoin/script/Cargo.toml | 6 +- mm2src/mm2_bitcoin/serialization/Cargo.toml | 4 +- mm2src/mm2_bitcoin/spv_validation/Cargo.toml | 18 +- mm2src/mm2_bitcoin/test_helpers/Cargo.toml | 2 +- mm2src/mm2_core/Cargo.toml | 38 ++-- mm2src/mm2_db/Cargo.toml | 30 +-- mm2src/mm2_err_handle/Cargo.toml | 12 +- mm2src/mm2_eth/Cargo.toml | 18 +- mm2src/mm2_event_stream/Cargo.toml | 18 +- mm2src/mm2_git/Cargo.toml | 6 +- mm2src/mm2_gui_storage/Cargo.toml | 12 +- mm2src/mm2_io/Cargo.toml | 19 +- mm2src/mm2_main/Cargo.toml | 134 ++++++------ mm2src/mm2_metamask/Cargo.toml | 28 +-- mm2src/mm2_metrics/Cargo.toml | 24 +- mm2src/mm2_net/Cargo.toml | 66 +++--- mm2src/mm2_number/Cargo.toml | 14 +- mm2src/mm2_p2p/Cargo.toml | 55 +++-- mm2src/mm2_rpc/Cargo.toml | 14 +- mm2src/mm2_state_machine/Cargo.toml | 4 +- mm2src/proxy_signature/Cargo.toml | 12 +- mm2src/rpc_task/Cargo.toml | 12 +- mm2src/trading_api/Cargo.toml | 19 +- mm2src/trezor/Cargo.toml | 40 ++-- 45 files changed, 852 insertions(+), 747 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13ad126fd9..59a803a59a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,7 +268,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0de5164e5edbf51c45fb8c2d9664ae1c095cce1b265ecf7569093c0d66ef690" dependencies = [ - "bytes 1.4.0", + "bytes", "futures-sink", "futures-util", "memchr", @@ -307,12 +307,12 @@ dependencies = [ "async-trait", "axum-core", "bitflags 1.3.2", - "bytes 1.4.0", + "bytes", "futures-util", - "http 0.2.12", - "http-body 0.4.5", + "http", + "http-body", "hyper", - "itoa 1.0.10", + "itoa", "matchit", "memchr", "mime", @@ -333,10 +333,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", - "bytes 1.4.0", + "bytes", "futures-util", - "http 0.2.12", - "http-body 0.4.5", + "http", + "http-body", "mime", "rustversion", "tower-layer", @@ -675,16 +675,6 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "iovec", -] - [[package]] name = "bytes" version = "1.4.0" @@ -832,7 +822,7 @@ dependencies = [ "blake2b_simd", "bs58 0.4.0", "byteorder", - "bytes 0.4.12", + "bytes", "cfg-if 1.0.0", "chain", "chrono", @@ -857,7 +847,7 @@ dependencies = [ "group 0.8.0", "gstuff", "hex", - "http 0.2.12", + "http", "hyper", "hyper-rustls 0.24.2", "itertools", @@ -987,7 +977,7 @@ dependencies = [ "arrayref", "async-trait", "backtrace", - "bytes 1.4.0", + "bytes", "cc", "cfg-if 1.0.0", "chrono", @@ -1001,8 +991,8 @@ dependencies = [ "futures-timer", "gstuff", "hex", - "http 0.2.12", - "http-body 0.1.0", + "http", + "http-body", "hyper", "hyper-rustls 0.24.2", "itertools", @@ -1327,7 +1317,7 @@ dependencies = [ "futures 0.3.28", "hex", "hmac 0.12.1", - "http 0.2.12", + "http", "hw_common", "keys", "lazy_static", @@ -1828,7 +1818,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "blake2b_simd", "byteorder", @@ -2386,12 +2376,12 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ - "bytes 1.4.0", + "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http 0.2.12", + "http", "indexmap 2.2.3", "slab", "tokio", @@ -2463,9 +2453,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ "base64 0.21.7", - "bytes 1.4.0", + "bytes", "headers-core", - "http 0.2.12", + "http", "httpdate", "mime", "sha1", @@ -2477,7 +2467,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" dependencies = [ - "http 0.2.12", + "http", ] [[package]] @@ -2576,38 +2566,15 @@ dependencies = [ "winapi", ] -[[package]] -name = "http" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" -dependencies = [ - "bytes 0.4.12", - "fnv", - "itoa 0.4.6", -] - [[package]] name = "http" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.4.0", + "bytes", "fnv", - "itoa 1.0.10", -] - -[[package]] -name = "http-body" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.29", - "http 0.1.21", - "tokio-buf", + "itoa", ] [[package]] @@ -2616,8 +2583,8 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes 1.4.0", - "http 0.2.12", + "bytes", + "http", "pin-project-lite 0.2.9", ] @@ -2666,16 +2633,16 @@ version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ - "bytes 1.4.0", + "bytes", "futures-channel", "futures-core", "futures-util", "h2", - "http 0.2.12", - "http-body 0.4.5", + "http", + "http-body", "httparse", "httpdate", - "itoa 1.0.10", + "itoa", "pin-project-lite 0.2.9", "socket2 0.4.9", "tokio", @@ -2690,7 +2657,7 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" dependencies = [ - "http 0.2.12", + "http", "hyper", "rustls 0.20.4", "tokio", @@ -2704,7 +2671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http 0.2.12", + "http", "hyper", "rustls 0.21.10", "tokio", @@ -2904,15 +2871,6 @@ dependencies = [ "windows-sys 0.45.0", ] -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - [[package]] name = "ipconfig" version = "0.3.0" @@ -2940,12 +2898,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" - [[package]] name = "itoa" version = "1.0.10" @@ -3107,7 +3059,7 @@ name = "libp2p" version = "0.52.1" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ - "bytes 1.4.0", + "bytes", "futures 0.3.28", "futures-timer", "getrandom 0.2.9", @@ -3225,7 +3177,7 @@ dependencies = [ "asynchronous-codec", "base64 0.21.7", "byteorder", - "bytes 1.4.0", + "bytes", "either", "fnv", "futures 0.3.28", @@ -3329,7 +3281,7 @@ name = "libp2p-noise" version = "0.43.0" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ - "bytes 1.4.0", + "bytes", "curve25519-dalek 3.2.0", "futures 0.3.28", "libp2p-core", @@ -3936,7 +3888,7 @@ dependencies = [ "common", "derive_more", "futures 0.1.29", - "http 0.2.12", + "http", "itertools", "ser_error", "ser_error_derive", @@ -3981,7 +3933,7 @@ version = "0.1.0" dependencies = [ "async-trait", "common", - "http 0.2.12", + "http", "mm2_err_handle", "mm2_net", "serde", @@ -4033,7 +3985,7 @@ dependencies = [ "async-trait", "bitcrypto", "blake2", - "bytes 0.4.12", + "bytes", "cfg-if 1.0.0", "chain", "chrono", @@ -4059,7 +4011,7 @@ dependencies = [ "hash-db", "hash256-std-hasher", "hex", - "http 0.2.12", + "http", "hw_common", "hyper", "instant", @@ -4180,7 +4132,7 @@ dependencies = [ "async-stream", "async-trait", "base64 0.21.7", - "bytes 1.4.0", + "bytes", "cfg-if 1.0.0", "common", "derive_more", @@ -4188,8 +4140,8 @@ dependencies = [ "futures 0.3.28", "futures-util", "gstuff", - "http 0.2.12", - "http-body 0.4.5", + "http", + "http-body", "httparse", "hyper", "js-sys", @@ -4259,7 +4211,6 @@ dependencies = [ "serde_json", "sha2 0.10.7", "smallvec 1.6.1", - "syn 2.0.38", "timed-map", "tokio", "void", @@ -4274,7 +4225,7 @@ dependencies = [ "derive_more", "futures 0.3.28", "gstuff", - "http 0.2.12", + "http", "mm2_err_handle", "mm2_number", "rpc", @@ -4298,7 +4249,7 @@ dependencies = [ name = "mm2_test_helpers" version = "0.1.0" dependencies = [ - "bytes 1.4.0", + "bytes", "cfg-if 1.0.0", "chrono", "common", @@ -4306,7 +4257,7 @@ dependencies = [ "db_common", "futures 0.3.28", "gstuff", - "http 0.2.12", + "http", "lazy_static", "mm2_core", "mm2_io", @@ -4394,7 +4345,7 @@ name = "multistream-select" version = "0.13.0" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ - "bytes 1.4.0", + "bytes", "futures 0.3.28", "log", "pin-project", @@ -4445,7 +4396,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b33524dc0968bfad349684447bfce6db937a9ac3332a1fe60c0c5a5ce63f21" dependencies = [ - "bytes 1.4.0", + "bytes", "futures 0.3.28", "log", "netlink-packet-core", @@ -4460,7 +4411,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411" dependencies = [ - "bytes 1.4.0", + "bytes", "futures 0.3.28", "libc", "log", @@ -4982,7 +4933,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c2f43e8969d51935d2a7284878ae053ba30034cd563f673cde37ba5205685e" dependencies = [ "dtoa", - "itoa 1.0.10", + "itoa", "parking_lot", "prometheus-client-derive-encode", ] @@ -5004,7 +4955,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ - "bytes 1.4.0", + "bytes", "prost-derive", ] @@ -5014,7 +4965,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ - "bytes 1.4.0", + "bytes", "heck", "itertools", "log", @@ -5081,7 +5032,7 @@ name = "proxy_signature" version = "0.1.0" dependencies = [ "chrono", - "http 0.2.12", + "http", "libp2p", "rand 0.7.3", "serde", @@ -5125,7 +5076,7 @@ version = "0.2.0" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "asynchronous-codec", - "bytes 1.4.0", + "bytes", "quick-protobuf", "thiserror", "unsigned-varint", @@ -5493,13 +5444,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" dependencies = [ "base64 0.13.0", - "bytes 1.4.0", + "bytes", "encoding_rs", "futures-core", "futures-util", "h2", - "http 0.2.12", - "http-body 0.4.5", + "http", + "http-body", "hyper", "hyper-rustls 0.23.0", "ipnet", @@ -5599,7 +5550,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" dependencies = [ - "bytes 1.4.0", + "bytes", "rustc-hex", ] @@ -6144,7 +6095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" dependencies = [ "indexmap 1.9.3", - "itoa 1.0.10", + "itoa", "ryu", "serde", ] @@ -6176,7 +6127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.10", + "itoa", "ryu", "serde", ] @@ -6456,7 +6407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "083624472e8817d44d02c0e55df043737ff11f279af924abdf93845717c2b75c" dependencies = [ "base64 0.13.0", - "bytes 1.4.0", + "bytes", "futures 0.3.28", "httparse", "log", @@ -6792,7 +6743,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43f8a10105d0a7c4af0a242e23ed5a12519afe5cc0e68419da441bb5981a6802" dependencies = [ - "bytes 1.4.0", + "bytes", "digest 0.10.7", "ed25519 2.2.3", "ed25519-consensus", @@ -6837,7 +6788,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff525d5540a9fc535c38dc0d92a98da3ee36fcdfbda99cecb9f3cce5cd4d41d7" dependencies = [ - "bytes 1.4.0", + "bytes", "flex-error", "num-derive", "num-traits", @@ -6856,7 +6807,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d8fe61b1772cd50038bdeeadf53773bb37a09e639dd8e6d996668fd220ddb29" dependencies = [ "async-trait", - "bytes 1.4.0", + "bytes", "flex-error", "getrandom 0.2.9", "peg", @@ -6947,7 +6898,7 @@ version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ - "itoa 1.0.10", + "itoa", "js-sys", "serde", "time-core", @@ -7020,7 +6971,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" dependencies = [ "autocfg 1.1.0", - "bytes 1.4.0", + "bytes", "libc", "mio", "num_cpus", @@ -7031,16 +6982,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "tokio-buf" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.29", -] - [[package]] name = "tokio-io-timeout" version = "1.2.0" @@ -7117,7 +7058,7 @@ source = "git+https://github.com/KomodoPlatform/tokio-tungstenite-wasm?rev=d20ab dependencies = [ "futures-channel", "futures-util", - "http 0.2.12", + "http", "httparse", "js-sys", "thiserror", @@ -7133,7 +7074,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" dependencies = [ - "bytes 1.4.0", + "bytes", "futures-core", "futures-sink", "pin-project-lite 0.2.9", @@ -7194,11 +7135,11 @@ dependencies = [ "async-trait", "axum", "base64 0.21.7", - "bytes 1.4.0", + "bytes", "flate2", "h2", - "http 0.2.12", - "http-body 0.4.5", + "http", + "http-body", "hyper", "hyper-timeout", "percent-encoding", @@ -7432,8 +7373,8 @@ checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1" dependencies = [ "base64 0.13.0", "byteorder", - "bytes 1.4.0", - "http 0.2.12", + "bytes", + "http", "httparse", "log", "rand 0.8.4", @@ -7524,7 +7465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86a8dc7f45e4c1b0d30e43038c38f274e77af056aa5f74b93c2cf9eb3c1c836" dependencies = [ "asynchronous-codec", - "bytes 1.4.0", + "bytes", ] [[package]] @@ -7812,11 +7753,11 @@ dependencies = [ [[package]] name = "web3" version = "0.19.0" -source = "git+https://github.com/KomodoPlatform/rust-web3?tag=v0.20.0#01de1d732e61c920cfb2fb1533db7d7110c8a457" +source = "git+https://github.com/komodoplatform/rust-web3?tag=v0.20.0#01de1d732e61c920cfb2fb1533db7d7110c8a457" dependencies = [ "arrayvec 0.7.1", "base64 0.13.0", - "bytes 1.4.0", + "bytes", "derive_more", "ethabi", "ethereum-types", @@ -8308,7 +8249,7 @@ checksum = "0f9079049688da5871a7558ddacb7f04958862c703e68258594cb7a862b5e33f" [[package]] name = "zcash_client_backend" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "async-trait", "base64 0.13.0", @@ -8333,7 +8274,7 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.3.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "async-trait", "bech32", @@ -8355,7 +8296,7 @@ dependencies = [ [[package]] name = "zcash_extras" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "async-trait", "ff 0.8.0", @@ -8371,7 +8312,7 @@ dependencies = [ [[package]] name = "zcash_note_encryption" version = "0.0.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "blake2b_simd", "byteorder", @@ -8385,7 +8326,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "aes 0.8.3", "bitvec 0.18.5", @@ -8415,7 +8356,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "bellman", "blake2b_simd", diff --git a/Cargo.toml b/Cargo.toml index 507c2e5c31..dd4931df38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,6 @@ [workspace] +# https://doc.rust-lang.org/beta/cargo/reference/features.html#feature-resolver-version-2 +resolver = "2" members = [ "mm2src/coins_activation", "mm2src/coins", @@ -47,8 +49,180 @@ exclude = [ "mm2src/mm2_test_helpers", ] -# https://doc.rust-lang.org/beta/cargo/reference/features.html#feature-resolver-version-2 -resolver = "2" +[workspace.dependencies] +aes = "0.8.3" +argon2 = { version = "0.5.2", features = ["zeroize"] } +arrayref = "0.3" +anyhow = "1.0" +async-std = "1.5" +async-trait = "0.1" +async-stream = "0.3" +backtrace = "0.3" +base64 = "0.21.2" +bech32 = "0.9.1" +bs58 = "0.4.0" +bigdecimal = { version = "0.3", features = ["serde"] } +bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } +bip39 = { version = "2.0.0", features = ["rand_core", "zeroize"], default-features = false } +bitcoin = "0.29" +bitcoin_hashes = "0.11" +blake2 = "0.10.6" +blake2b_simd = "0.5.10" +bytes = "1.1" +byteorder = "1.3" +cbc = "0.1.2" +cc = "1.0" +cipher = "0.4.4" +chrono = "0.4.23" +cfg-if = "1.0" +clap = { version = "4.2", features = ["derive"] } +cosmrs = { version = "0.16", default-features = false } +crossbeam = "0.8" +crossbeam-channel = "0.5.1" +compatible-time = { version = "1.1.0", package = "web-time" } +crc32fast = { version = "1.3.2", features = ["std", "nightly"] } +derive_more = "0.99" +directories = "5.0" +dirs = "1" +ed25519-dalek = { version = "1.0.1", features = ["serde"] } +either = "1.6" +enum-primitive-derive = "0.2" +env_logger = "0.9.3" +ethabi = "17.0.0" +ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } +ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } +# Waiting for https://github.com/rust-lang/rust/issues/54725 to use on Stable. +#enum_dispatch = "0.1" +ff = "0.8" +findshlibs = "0.5" +# using select macro requires the crate to be named futures, compilation failed with futures03 name +futures = { version = "0.3.1", default-features = false } +futures01 = { version = "0.1", package = "futures" } +futures-rustls = { version = "0.24", default-features = false } +futures-ticker = "0.0.3" +futures-timer = "3.0" +futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } +fnv = "1.0.6" +group = "0.8.0" +gstuff = { version = "0.7", features = ["nightly"] } +hash256-std-hasher = "0.15.2" +hash-db = "0.15.2" +hex = "0.4.2" +hmac = "0.12.1" +http = "0.2" +http-body = "0.4" +httparse = "1.8.0" +hyper = "0.14.26" +hyper-rustls = { version = "0.24", default-features = false } +indexmap = "1.7.0" +inquire = "0.6" +itertools = "0.10" +jemallocator = "0.5.0" +jubjub = "0.5.1" +js-sys = "0.3.27" +# Same version as `web3` depends on. +jsonrpc-core = "18.0.0" +lazy_static = "1.4" +libc = "0.2" +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false } +lightning = "0.0.113" +lightning-background-processor = "0.0.113" +lightning-invoice = { version = "0.21.0", features = ["serde"] } +lightning-net-tokio = "0.0.113" +instant = "0.1.12" +log = "0.4" +metrics = "0.21" +metrics-exporter-prometheus = "0.12.1" +metrics-util = "0.15" +mocktopus = "0.8.0" +nom = "6.1.2" +num-bigint = { version = "0.4", features = ["serde", "std"] } +num-rational = { version = "0.4", features = ["serde"] } +parity-util-mem = "0.11" +num-traits = "0.2" +parking_lot = { version = "0.12.0", default-features = false } +parking_lot_core = { version = "0.6", features = ["nightly"] } +passwords = "3.1" +paste = "1.0" +pin-project = "1.1.2" +primitive-types = "0.11.1" +prost = "0.12" +prost-build = { version = "0.12", default-features = false } +protobuf = "2.20" +proc-macro2 = "1.0" +quote = "1.0" +regex = "1" +reqwest = { version = "0.11.9", default-features = false, features = ["json"] } +rand = { version = "0.7", default-features = false, features = ["std", "small_rng", "wasm-bindgen"] } +rcgen = "0.10" +ripemd160 = "0.9.0" +rlp = "0.5" +rmp-serde = "0.14.3" +rusb = { version = "0.7.0", features = ["vendored"] } +rustc-hash = "2.0" +rustc-hex = "2" +rust-ini = "0.13" +rustls = { version = "0.21", default-features = false } +rustls-pemfile = "1.0.2" +rusqlite = { version = "0.28", features = ["bundled"] } +secp256k1 = "0.20" +secp256k1v24 = { version = "0.24", package = "secp256k1" } +serde = { version = "1", default-features = false } +serde_bytes = "0.11.5" +serde_derive = { version = "1", default-features = false } +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +serde_with = "1.14.0" +serde_repr = "0.1.6" +serde-wasm-bindgen = "0.4.3" +sha-1 = "0.9" +sha2 = "0.10" +sha3 = "0.9" +sia-rust = { git = "https://github.com/KomodoPlatform/sia-rust", rev = "9f188b80b3213bcb604e7619275251ce08fae808" } +siphasher = "0.1.1" +smallvec = "1.6.1" +sp-runtime-interface = { version = "6.0.0", default-features = false, features = ["disable_target_static_assertions"] } +sp-trie = { version = "6.0", default-features = false } +sql-builder = "3.1.1" +syn = "1.0" +sysinfo = "0.28" +# using the same version as cosmrs +tendermint-rpc = { version = "0.35", default-features = false } +testcontainers = "0.15.0" +tiny-bip39 = "0.8.0" +thiserror = "1.0.30" +time = "0.3.20" +timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } +tokio = { version = "1.20", default-features = false } +tokio-rustls = { version = "0.24", default-features = false } +tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "d20abdb", defautl-features = false, features = ["rustls-tls-native-roots"]} +tonic = { version = "0.10", default-features = false } +tonic-build = { version = "0.10", default-features = false, features = ["prost"] } +tower-service = "0.3" +trie-db = { version = "0.23.1", default-features = false } +trie-root = "0.16.0" +url = { version = "2.2.2", features = ["serde"] } +uint = "0.9.3" +uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +void = "1.0" +wagyu-zcash-parameters = { version = "0.2" } +wasm-bindgen = "0.2.86" +wasm-bindgen-futures = "0.4.21" +wasm-bindgen-test = "0.3.2" +webpki-roots = "0.25" +web-sys = {version = "0.3.55", default-features = false } +# we don't need the default web3 features at all since we added our own web3 transport using shared httparse.workspace = true instance. +# one of web3 dependencies is the old `tokio-uds 0.1.7` which fails cross-compiling to arm. +# we don't need the default web3 features at all since we added our own web3 transport using shared hyper instance. +web3 = { git = "https://github.com/komodoplatform/rust-web3", tag = "v0.20.0", default-features = false } +winapi = "0.3" +zbase32 = "0.1.2" +zcash_client_backend = { git = "https://github.com/komodoplatform/librustzcash.git", tag = "k-1.4.2" } +zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } +zcash_extras = { git = "https://github.com/komodoplatform/librustzcash.git", tag = "k-1.4.2" } +zcash_primitives = { git = "https://github.com/komodoplatform/librustzcash.git", tag = "k-1.4.2", features = ["transparent-inputs"] } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2", default-features = false, features = ["local-prover", "multicore"] } +zeroize = { version = "1.5", features = ["zeroize_derive"] } [profile.release] debug = 0 diff --git a/mm2src/adex_cli/Cargo.toml b/mm2src/adex_cli/Cargo.toml index cb477cacb0..cc05303755 100644 --- a/mm2src/adex_cli/Cargo.toml +++ b/mm2src/adex_cli/Cargo.toml @@ -7,33 +7,34 @@ description = "Provides a CLI interface and facilitates interoperating to komodo # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -anyhow = { version = "1.0", features = ["std"] } -async-trait = "0.1" -clap = { version = "4.2", features = ["derive"] } +anyhow = { workspace = true, features = ["std"] } +async-trait.workspace = true +clap.workspace = true common = { path = "../common" } -derive_more = "0.99" -directories = "5.0" -env_logger = "0.9.3" -http = "0.2" -hyper = { version = "0.14.26", features = ["client", "http2", "tcp"] } -hyper-rustls = "0.24" -gstuff = { version = "0.7" , features = [ "nightly" ]} -inquire = "0.6" -itertools = "0.10" -log = "0.4.21" +derive_more.workspace = true +directories.workspace = true +env_logger.workspace = true +http.workspace = true +hyper = { workspace = true, features = ["client", "http2", "tcp"] } +hyper-rustls.workspace = true +stuff.workspace = true +inquire.workspace = true +itertools.workspace = true +log.workspace = true mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } mm2_rpc = { path = "../mm2_rpc"} mm2_core = { path = "../mm2_core" } -passwords = "3.1" +passwords.workspace = true rpc = { path = "../mm2_bitcoin/rpc" } -rustls = { version = "0.21", features = [ "dangerous_configuration" ] } -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -sysinfo = "0.28" -tiny-bip39 = "0.8.0" -tokio = { version = "1.20.0", features = [ "macros" ] } -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +rustls = { workspace = true, features = [ "dangerous_configuration" ] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +sysinfo.workspace = true +tiny-bip39.workspace = true +tokio = { workspace = true, features = [ "macros" ] } +uuid.workspace = true [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.3", features = ["processthreadsapi", "winnt"] } +winapi = { workspace = true, features = ["processthreadsapi", "winnt"] } + diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 64cc91c03d..fd4d0bfbcf 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -20,49 +20,46 @@ path = "lp_coins.rs" doctest = false [dependencies] -async-std = { version = "1.5", features = ["unstable"] } -async-trait = "0.1.52" -base64 = "0.21.2" -bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } -bitcoin_hashes = "0.11" +async-std = { workspace = true, features = ["unstable"] } +async-trait.workspace = true +base64.workspace = true +bip32.workspace = true +bitcoin_hashes.workspace = true bitcrypto = { path = "../mm2_bitcoin/crypto" } -blake2b_simd = { version = "0.5.10", optional = true } -bs58 = "0.4.0" -byteorder = "1.3" -bytes = "0.4" -cfg-if = "1.0" +blake2b_simd = { workspace = true, optional = true } +bs58.workspace = true +byteorder.workspace = true +bytes.workspace = true +cfg-if.workspace = true chain = { path = "../mm2_bitcoin/chain" } -chrono = { version = "0.4.23", "features" = ["serde"] } +chrono = { workspace = true, "features" = ["serde"] } common = { path = "../common" } -compatible-time = { version = "1.1.0", package = "web-time" } -cosmrs = { version = "0.16", default-features = false } -crossbeam = "0.8" +compatible-time.workspace = true +cosmrs.workspace = true +crossbeam.workspace = true crypto = { path = "../crypto" } db_common = { path = "../db_common" } -derive_more = "0.99" -ed25519-dalek = { version = "1.0.1", features = ["serde"] } +derive_more.workspace = true +ed25519-dalek.workspace = true enum_derives = { path = "../derives/enum_derives" } -ethabi = { version = "17.0.0" } -ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } -ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -# Waiting for https://github.com/rust-lang/rust/issues/54725 to use on Stable. -#enum_dispatch = "0.1" -futures01 = { version = "0.1", package = "futures" } -futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } -futures-ticker = "0.0.3" -# using select macro requires the crate to be named futures, compilation failed with futures03 name -futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } -group = "0.8.0" -gstuff = { version = "0.7", features = ["nightly"] } -hex = "0.4.2" -http = "0.2" -itertools = { version = "0.10", features = ["use_std"] } -jsonrpc-core = "18.0.0" +ethabi.workspace = true +ethcore-transaction.workspace = true +ethereum-types.workspace = true +ethkey.workspace = true +futures01.workspace = true +futures-util.workspace = true +futures-ticker.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } +group.workspace = true +gstuff.workspace = true +hex.workspace = true +http.workspace = true +itertools = { workspace = true, features = ["use_std"] } +jsonrpc-core.workspace = true keys = { path = "../mm2_bitcoin/keys" } -lazy_static = "1.4" -libc = "0.2" -nom = "6.1.2" +lazy_static.workspace = true +libc.workspace = true +nom.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } @@ -73,100 +70,98 @@ mm2_number = { path = "../mm2_number"} mm2_p2p = { path = "../mm2_p2p", default-features = false } mm2_rpc = { path = "../mm2_rpc" } mm2_state_machine = { path = "../mm2_state_machine" } -mocktopus = { version = "0.8.0", optional = true } -num-traits = "0.2" -parking_lot = { version = "0.12.0", features = ["nightly"] } +mocktopus = { workspace = true, optional = true } +num-traits.workspace = true +parking_lot = { workspace = true, features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } -prost = "0.12" -protobuf = "2.20" +prost.workspace = true +protobuf.workspace = true proxy_signature = { path = "../proxy_signature" } -rand = { version = "0.7", features = ["std", "small_rng"] } -regex = "1" -reqwest = { version = "0.11.9", default-features = false, features = ["json"], optional = true } -rlp = { version = "0.5" } -rmp-serde = "0.14.3" +rand = { workspace = true, features = ["std", "small_rng"] } +regex.workspace = true +reqwest = { workspace = true, optional = true } +rlp.workspace = true +rmp-serde.workspace = true rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } script = { path = "../mm2_bitcoin/script" } -secp256k1 = { version = "0.20" } +secp256k1.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -serde = "1.0" -serde_derive = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -serde_with = "1.14.0" +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +serde_with.workspace = true serialization = { path = "../mm2_bitcoin/serialization" } serialization_derive = { path = "../mm2_bitcoin/serialization_derive" } sia-rust = { git = "https://github.com/KomodoPlatform/sia-rust", rev = "9f188b80b3213bcb604e7619275251ce08fae808", optional = true } spv_validation = { path = "../mm2_bitcoin/spv_validation" } -sha2 = "0.10" -sha3 = "0.9" +sha2.workspace = true +sha3.workspace = true utxo_signer = { path = "utxo_signer" } # using the same version as cosmrs -tendermint-rpc = { version = "0.35", default-features = false } -tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "d20abdb", features = ["rustls-tls-native-roots"]} -url = { version = "2.2.2", features = ["serde"] } -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +tendermint-rpc.workspace = true +tokio-tungstenite-wasm = { workspace = true, features = ["rustls-tls-native-roots"]} +url.workspace = true +uuid.workspace = true # One of web3 dependencies is the old `tokio-uds 0.1.7` which fails cross-compiling to ARM. # We don't need the default web3 features at all since we added our own web3 transport using shared HYPER instance. -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false } -zbase32 = "0.1.2" -zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } -zcash_extras = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } -zcash_primitives = {features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } +web3 = { workspace = true, default-features = false } +zbase32.workspace = true +zcash_client_backend.workspace = true +zcash_extras.workspace = true +zcash_primitives.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -blake2b_simd = "0.5" -ff = "0.8" -futures-util = "0.3" -jubjub = "0.5.1" -js-sys = { version = "0.3.27" } +blake2b_simd.workspace = true +ff.workspace = true +futures-util.workspace = true +jubjub.workspace = true +js-sys.workspace = true mm2_db = { path = "../mm2_db" } mm2_metamask = { path = "../mm2_metamask" } mm2_test_helpers = { path = "../mm2_test_helpers" } -time = { version = "0.3.20", features = ["wasm-bindgen"] } -timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } -tonic = { version = "0.10", default-features = false, features = ["prost", "codegen", "gzip"] } -tower-service = "0.3" -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.2" } -web-sys = { version = "0.3.55", features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } -zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2", default-features = false, features = ["local-prover"] } +time = { workspace = true, features = ["wasm-bindgen"] } +timed-map = { workspace = true, features = ["rustc-hash", "wasm"] } +tonic = { workspace = true, default-features = false, features = ["prost", "codegen", "gzip"] } +tower-service.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys = { workspace = true, features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } +zcash_proofs.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -dirs = { version = "1" } -bitcoin = "0.29" -hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } -# using webpki-tokio to avoid rejecting valid certificates -# got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features -hyper-rustls = { version = "0.24", default-features = false, features = ["http1", "http2", "webpki-tokio"] } -lightning = "0.0.113" -lightning-background-processor = "0.0.113" -lightning-invoice = { version = "0.21.0", features = ["serde"] } -lightning-net-tokio = "0.0.113" -rust-ini = { version = "0.13" } -rustls = { version = "0.21", features = ["dangerous_configuration"] } -secp256k1v24 = { version = "0.24", package = "secp256k1" } -timed-map = { version = "1.4", features = ["rustc-hash"] } -tokio = { version = "1.20" } -tokio-rustls = { version = "0.24" } -tonic = { version = "0.10", features = ["tls", "tls-webpki-roots", "gzip"] } -webpki-roots = { version = "0.25" } -zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } -zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2", default-features = false, features = ["local-prover", "multicore"] } +dirs.workspace = true +bitcoin.workspace = true +hyper = { workspace = true, features = ["client", "http2", "server", "tcp"] } +hyper-rustls = { workspace = true, default-features = false, features = ["http1", "http2", "webpki-tokio"] } +lightning.workspace = true +lightning-background-processor.workspace = true +lightning-invoice.workspace = true +lightning-net-tokio.workspace = true +rust-ini.workspace = true +rustls = { workspace = true, features = ["dangerous_configuration"] } +secp256k1v24.workspace = true +timed-map = { workspace = true, features = ["rustc-hash"] } +tokio.workspace = true +tokio-rustls.workspace = true +tonic = { workspace = true, features = ["codegen", "prost", "gzip", "tls", "tls-webpki-roots"] } +webpki-roots.workspace = true +zcash_client_sqlite.workspace = true +zcash_proofs.workspace = true [target.'cfg(windows)'.dependencies] -winapi = "0.3" +winapi.workspace = true [dev-dependencies] mm2_test_helpers = { path = "../mm2_test_helpers" } -mocktopus = { version = "0.8.0" } +mocktopus.workspace = true mm2_p2p = { path = "../mm2_p2p", features = ["application"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wagyu-zcash-parameters = { version = "0.2" } +wagyu-zcash-parameters.workspace = true [build-dependencies] -prost-build = { version = "0.12", default-features = false } -tonic-build = { version = "0.10", default-features = false, features = ["prost"] } +prost-build.workspace = true +tonic-build.workspace = true diff --git a/mm2src/coins/utxo_signer/Cargo.toml b/mm2src/coins/utxo_signer/Cargo.toml index 7e44ad011e..c37b1c675b 100644 --- a/mm2src/coins/utxo_signer/Cargo.toml +++ b/mm2src/coins/utxo_signer/Cargo.toml @@ -7,13 +7,13 @@ edition = "2018" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true chain = { path = "../../mm2_bitcoin/chain" } common = { path = "../../common" } mm2_err_handle = { path = "../../mm2_err_handle" } crypto = { path = "../../crypto" } -derive_more = "0.99" -hex = "0.4.2" +derive_more.workspace = true +hex.workspace = true keys = { path = "../../mm2_bitcoin/keys" } primitives = { path = "../../mm2_bitcoin/primitives" } rpc = { path = "../../mm2_bitcoin/rpc" } diff --git a/mm2src/coins_activation/Cargo.toml b/mm2src/coins_activation/Cargo.toml index 3ed8213e3b..215a4067c6 100644 --- a/mm2src/coins_activation/Cargo.toml +++ b/mm2src/coins_activation/Cargo.toml @@ -12,34 +12,34 @@ default = [] for-tests = [] [dependencies] -async-trait = "0.1" +async-trait.workspace = true coins = { path = "../coins" } common = { path = "../common" } crypto = { path = "../crypto" } -derive_more = "0.99" -ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } -futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } -hex = "0.4.2" +derive_more.workspace = true +ethereum-types.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } +hex.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_metrics = { path = "../mm2_metrics" } mm2_number = { path = "../mm2_number" } -parking_lot = { version = "0.12.0", features = ["nightly"] } +parking_lot = { workspace = true, features = ["nightly"] } rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } secp256k1 = { version = "0.24" } ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -serde = "1.0" -serde_derive = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -url = { version = "2.2.2", features = ["serde"] } +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +url.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] mm2_metamask = { path = "../mm2_metamask" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -lightning = "0.0.113" -lightning-background-processor = "0.0.113" -lightning-invoice = { version = "0.21.0", features = ["serde"] } +lightning.workspace = true +lightning-background-processor.workspace = true +lightning-invoice.workspace = true diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index d515ee5f18..39590862d8 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -14,69 +14,69 @@ for-tests = [] track-ctx-pointer = ["shared_ref_counter/enable", "shared_ref_counter/log"] [dependencies] -arrayref = "0.3" -async-trait = "0.1" -backtrace = "0.3" -bytes = "1.1" -cfg-if = "1.0" -compatible-time = { version = "1.1.0", package = "web-time" } -crossbeam = "0.8" -env_logger = "0.9.3" -derive_more = "0.99" -fnv = "1.0.6" -futures01 = { version = "0.1", package = "futures" } -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -futures-timer = "3.0" -gstuff = "0.7" -hex = "0.4.2" -http = "0.2" -http-body = "0.1" -itertools = "0.10" -lazy_static = "1.4" -log = "0.4.17" -parking_lot = { version = "0.12.0", features = ["nightly"] } -parking_lot_core = { version = "0.6", features = ["nightly"] } -paste = "1.0" -primitive-types = "0.11.1" -rand = { version = "0.7", features = ["std", "small_rng"] } -rustc-hash = "2.0" -regex = "1" -serde = "1" -serde_derive = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +arrayref.workspace = true +async-trait.workspace = true +backtrace.workspace = true +bytes.workspace = true +cfg-if.workspace = true +compatible-time.workspace = true +crossbeam.workspace = true +env_logger.workspace = true +derive_more.workspace = true +fnv.workspace = true +futures01.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +futures-timer.workspace = true +gstuff.workspace = true +hex.workspace = true +http.workspace = true +http-body.workspace = true +itertools.workspace = true +lazy_static.workspace = true +log.workspace = true +parking_lot = { workspace = true, features = ["nightly"] } +parking_lot_core.workspace = true +paste.workspace = true +primitive-types.workspace = true +rand = { workspace = true, features = ["std", "small_rng"] } +rustc-hash.workspace = true +regex.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -sha2 = "0.10" +sha2.workspace = true shared_ref_counter = { path = "shared_ref_counter", optional = true } -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +uuid.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -chrono = { version = "0.4", features = ["wasmbind"] } -js-sys = "0.3.27" -serde_repr = "0.1.6" -serde-wasm-bindgen = "0.4.3" -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = "0.4.21" -wasm-bindgen-test = { version = "0.3.2" } -web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "MessageEvent", "WebSocket"] } +chrono = { workspace = true, features = ["wasmbind"] } +js-sys.workspace = true +serde_repr.workspace = true +serde-wasm-bindgen.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys = { workspace = true, features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "MessageEvent", "WebSocket"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -anyhow = "1.0" -chrono = "0.4" -hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } +anyhow.workspace = true +chrono.workspace = true +hyper = { workspace = true, features = ["client", "http2", "server", "tcp"] } # using webpki-tokio to avoid rejecting valid certificates # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features -hyper-rustls = { version = "0.24", default-features = false, features = ["http1", "http2", "webpki-tokio"] } -libc = { version = "0.2" } -lightning = "0.0.113" -tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } +hyper-rustls = { workspace = true, default-features = false, features = ["http1", "http2", "webpki-tokio"] } +libc.workspace = true +lightning.workspace = true +tokio = { workspace = true, features = ["io-util", "rt-multi-thread", "net"] } [target.'cfg(windows)'.dependencies] -winapi = "0.3" +winapi.workspace = true [target.'cfg(not(windows))'.dependencies] -findshlibs = "0.5" +findshlibs.workspace = true [build-dependencies] -cc = "1.0" -gstuff = "0.7" +cc.workspace = true +gstuff.workspace = true diff --git a/mm2src/common/shared_ref_counter/Cargo.toml b/mm2src/common/shared_ref_counter/Cargo.toml index 36fe1e2807..ee510ed9fc 100644 --- a/mm2src/common/shared_ref_counter/Cargo.toml +++ b/mm2src/common/shared_ref_counter/Cargo.toml @@ -10,4 +10,4 @@ doctest = false enable = [] [dependencies] -log = { version = "0.4.17", optional = true } +log = { workspace = true, optional = true } diff --git a/mm2src/crypto/Cargo.toml b/mm2src/crypto/Cargo.toml index dd3bbec752..52c36a6cdb 100644 --- a/mm2src/crypto/Cargo.toml +++ b/mm2src/crypto/Cargo.toml @@ -7,56 +7,57 @@ edition = "2018" doctest = false [dependencies] -aes = "0.8.3" -argon2 = { version = "0.5.2", features = ["zeroize"] } -arrayref = "0.3" -async-trait = "0.1" -base64 = "0.21.2" -bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } -bip39 = { version = "2.0.0", features = ["rand_core", "zeroize"], default-features = false } +aes.workspace = true +argon2.workspace = true +arrayref.workspace = true +async-trait.workspace = true +base64.workspace = true +bip32.workspace = true +bip39.workspace = true bitcrypto = { path = "../mm2_bitcoin/crypto" } -bs58 = "0.4.0" -cbc = "0.1.2" -cipher = "0.4.4" +bs58.workspace = true +cbc.workspace = true +cipher.workspace = true common = { path = "../common" } -derive_more = "0.99" +derive_more.workspace = true enum_derives = { path = "../derives/enum_derives" } -enum-primitive-derive = "0.2" -futures = "0.3" -hex = "0.4.2" -hmac = "0.12.1" -http = "0.2" +enum-primitive-derive.workspace = true +futures.workspace = true +hex.workspace = true +hmac.workspace = true +http.workspace = true hw_common = { path = "../hw_common" } keys = { path = "../mm2_bitcoin/keys" } -lazy_static = "1.4" +lazy_static.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } -num-traits = "0.2" -parking_lot = { version = "0.12.0", features = ["nightly"] } +num-traits.workspace = true +parking_lot = { workspace = true, features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } -rustc-hex = "2" -secp256k1 = "0.20" +rustc-hex.workspace = true +secp256k1.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -serde = "1.0" -serde_derive = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -sha2 = "0.10" +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +sha2.workspace = true trezor = { path = "../trezor" } -zeroize = { version = "1.5", features = ["zeroize_derive"] } +zeroize.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -cfg-if = "1.0" +cfg-if.workspace = true mm2_eth = { path = "../mm2_eth" } mm2_metamask = { path = "../mm2_metamask" } -wasm-bindgen-test = { version = "0.3.2" } -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false } +wasm-bindgen-test.workspace = true +web3 = { workspace = true, default-features = false } + [dev-dependencies] -cfg-if = "1.0" -tokio = { version = "1.20", default-features = false } +cfg-if.workspace = true +tokio.workspace = true [features] trezor-udp = ["trezor/trezor-udp"] diff --git a/mm2src/db_common/Cargo.toml b/mm2src/db_common/Cargo.toml index e161be857e..b21615141b 100644 --- a/mm2src/db_common/Cargo.toml +++ b/mm2src/db_common/Cargo.toml @@ -8,13 +8,13 @@ doctest = false [dependencies] common = { path = "../common" } -hex = "0.4.2" -log = "0.4.17" -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +hex.workspace = true +log.workspace = true +uuid.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -crossbeam-channel = "0.5.1" -futures = "0.3.1" -rusqlite = { version = "0.28", features = ["bundled"] } -sql-builder = "3.1.1" -tokio = { version = "1.20", default-features = false, features = ["macros"] } +crossbeam-channel.workspace = true +futures.workspace = true +rusqlite.workspace = true +sql-builder.workspace = true +tokio = { workspace = true, default-features = false, features = ["macros"] } diff --git a/mm2src/derives/enum_derives/Cargo.toml b/mm2src/derives/enum_derives/Cargo.toml index 9518bc6d1a..d4f378c6f2 100644 --- a/mm2src/derives/enum_derives/Cargo.toml +++ b/mm2src/derives/enum_derives/Cargo.toml @@ -8,7 +8,7 @@ proc-macro = true doctest = false [dependencies] -syn = { version = "1.0", features = ["full"] } -quote = "1.0" -proc-macro2 = "1.0" -itertools = "0.10" +syn = { workspace = true, features = ["full"] } +quote.workspace = true +proc-macro2.workspace = true +itertools.workspace = true diff --git a/mm2src/derives/ser_error/Cargo.toml b/mm2src/derives/ser_error/Cargo.toml index ace2ba57fd..b9c5197109 100644 --- a/mm2src/derives/ser_error/Cargo.toml +++ b/mm2src/derives/ser_error/Cargo.toml @@ -8,4 +8,4 @@ edition = "2018" doctest = false [dependencies] -serde = "1.0" +serde.workspace = true diff --git a/mm2src/derives/ser_error_derive/Cargo.toml b/mm2src/derives/ser_error_derive/Cargo.toml index 640becbeed..5461242ce0 100644 --- a/mm2src/derives/ser_error_derive/Cargo.toml +++ b/mm2src/derives/ser_error_derive/Cargo.toml @@ -9,7 +9,7 @@ proc-macro = true doctest = false [dependencies] -proc-macro2 = "1.0" -quote = "1.0" +proc-macro2.workspace = true +quote.workspace = true ser_error = { path = "../ser_error" } -syn = { version = "1.0", features = ["full"] } +syn = { workspace = true, features = ["full"] } diff --git a/mm2src/hw_common/Cargo.toml b/mm2src/hw_common/Cargo.toml index 7d2b43ac12..f3390fb087 100644 --- a/mm2src/hw_common/Cargo.toml +++ b/mm2src/hw_common/Cargo.toml @@ -7,22 +7,22 @@ edition = "2018" doctest = false [dependencies] -async-trait = "0.1" -bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } +async-trait.workspace = true +bip32.workspace = true common = { path = "../common" } mm2_err_handle = { path = "../mm2_err_handle" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } -secp256k1 = { version = "0.20", features = ["rand"] } -serde = "1.0" -serde_derive = "1.0" +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } +secp256k1 = { workspace = true, features = ["rand"] } +serde.workspace = true +serde_derive.workspace = true [target.'cfg(all(not(target_arch = "wasm32"), not(target_os = "ios")))'.dependencies] -rusb = { version = "0.7.0", features = ["vendored"] } +rusb.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = { version = "0.3.27" } -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.1" } -web-sys = { version = "0.3.55", features = ["console", "Navigator", "Usb", "UsbDevice", "UsbDeviceRequestOptions", "UsbInTransferResult"] } +js-sys.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys = { workspace = true, features = ["console", "Navigator", "Usb", "UsbDevice", "UsbDeviceRequestOptions", "UsbInTransferResult"] } diff --git a/mm2src/ledger/Cargo.toml b/mm2src/ledger/Cargo.toml index c6883fc75c..10c5178b2a 100644 --- a/mm2src/ledger/Cargo.toml +++ b/mm2src/ledger/Cargo.toml @@ -4,18 +4,18 @@ version = "0.1.0" edition = "2018" [dependencies] -async-trait = "0.1" -byteorder = "1.3.2" +async-trait.workspace = true +byteorder.workspace = true common = { path = "../common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } hw_common = { path = "../hw_common" } -serde = "1.0" -serde_derive = "1.0" +serde.workspace = true +serde_derive.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = { version = "0.3.27" } -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.1" } -web-sys = { version = "0.3.55" } +js-sys.workspace = true +wasm-bindgen.workspace.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys.workspace = true diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index 0d267ca91f..002592e745 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -33,27 +33,27 @@ bench = false [dependencies] common = { path = "../common" } -enum-primitive-derive = "0.2" -libc = "0.2" +enum-primitive-derive.workspace = true +libc.workspace = true mm2_core = { path = "../mm2_core" } mm2_main = { path = "../mm2_main" } -num-traits = "0.2" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +num-traits.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -gstuff = { version = "0.7", features = ["nightly"] } +gstuff.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = { version = "0.3.27" } +js-sys.workspace = true mm2_rpc = { path = "../mm2_rpc", features=["rpc_facilities"] } -serde = "1.0" -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } +serde.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true [target.x86_64-unknown-linux-gnu.dependencies] -jemallocator = "0.5.0" +jemallocator.workspace = true [build-dependencies] -chrono = "0.4" -gstuff = { version = "0.7", features = ["nightly"] } -regex = "1" +chrono.workspace = true +gstuff.workspace = true +regex.workspace = true diff --git a/mm2src/mm2_bitcoin/chain/Cargo.toml b/mm2src/mm2_bitcoin/chain/Cargo.toml index 58a1929c96..3f85ae7db7 100644 --- a/mm2src/mm2_bitcoin/chain/Cargo.toml +++ b/mm2src/mm2_bitcoin/chain/Cargo.toml @@ -7,11 +7,11 @@ authors = ["debris "] doctest = false [dependencies] -rustc-hex = "2" +rustc-hex.workspace = true bitcrypto = { path = "../crypto" } primitives = { path = "../primitives" } serialization = { path = "../serialization" } serialization_derive = { path = "../serialization_derive" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -bitcoin = "0.29" +bitcoin.workspace = true diff --git a/mm2src/mm2_bitcoin/crypto/Cargo.toml b/mm2src/mm2_bitcoin/crypto/Cargo.toml index 20a62aa693..1ca5ee8257 100644 --- a/mm2src/mm2_bitcoin/crypto/Cargo.toml +++ b/mm2src/mm2_bitcoin/crypto/Cargo.toml @@ -9,9 +9,9 @@ doctest = false [dependencies] groestl = "0.9" primitives = { path = "../primitives" } -ripemd160 = "0.9.0" -sha-1 = "0.9" -sha2 = "0.10" -sha3 = "0.9" -siphasher = "0.1.1" +ripemd160.workspace = true +sha-1.workspace = true +sha2.workspace = true +sha3.workspace = true +siphasher.workspace = true serialization = { path = "../serialization" } diff --git a/mm2src/mm2_bitcoin/keys/Cargo.toml b/mm2src/mm2_bitcoin/keys/Cargo.toml index 7002e711a8..e840322fd1 100644 --- a/mm2src/mm2_bitcoin/keys/Cargo.toml +++ b/mm2src/mm2_bitcoin/keys/Cargo.toml @@ -7,14 +7,14 @@ authors = ["debris "] doctest = false [dependencies] -bs58 = "0.4.0" -rustc-hex = "2" -bech32 = "0.9.1" +bech32.workspace = true +bs58.workspace = true bitcrypto = { path = "../crypto" } -derive_more = "0.99" -lazy_static = "1.4" -rand = {version = "0.6", features = ["wasm-bindgen"] } +derive_more.workspace = true +lazy_static.workspace = true primitives = { path = "../primitives" } -secp256k1 = { version = "0.20", features = ["rand", "recovery"] } -serde = { version = "1.0", features = ["derive"] } -serde_derive = "1.0" +rand = { version = "0.6", features = ["wasm-bindgen"] } +rustc-hex.workspace = true +secp256k1 = { workspace = true, features = ["rand", "recovery"] } +serde = { workspace = true, features = ["derive"] } +serde_derive.workspace = true diff --git a/mm2src/mm2_bitcoin/primitives/Cargo.toml b/mm2src/mm2_bitcoin/primitives/Cargo.toml index 3da53cdf33..ac1000a9fb 100644 --- a/mm2src/mm2_bitcoin/primitives/Cargo.toml +++ b/mm2src/mm2_bitcoin/primitives/Cargo.toml @@ -7,7 +7,7 @@ authors = ["debris "] doctest = false [dependencies] -rustc-hex = "2" -bitcoin_hashes = "0.11" -byteorder = "1.0" -uint = "0.9.3" +rustc-hex.workspace = true +bitcoin_hashes.workspace = true +byteorder.workspace = true +uint.workspace = true diff --git a/mm2src/mm2_bitcoin/rpc/Cargo.toml b/mm2src/mm2_bitcoin/rpc/Cargo.toml index 4fbba4129f..a9e0841695 100644 --- a/mm2src/mm2_bitcoin/rpc/Cargo.toml +++ b/mm2src/mm2_bitcoin/rpc/Cargo.toml @@ -7,11 +7,11 @@ authors = ["Ethcore "] doctest = false [dependencies] -log = "0.4.17" -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -serde_derive = "1.0" -rustc-hex = "2" +log.workspace = true +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +serde_derive.workspace = true +rustc-hex.workspace = true serialization = { path = "../serialization" } chain = { path = "../chain" } @@ -20,4 +20,4 @@ keys = { path = "../keys" } script = { path = "../script" } [dev-dependencies] -lazy_static = "1.4" \ No newline at end of file +lazy_static.workspace = true diff --git a/mm2src/mm2_bitcoin/script/Cargo.toml b/mm2src/mm2_bitcoin/script/Cargo.toml index e340cd8944..6a97b950b2 100644 --- a/mm2src/mm2_bitcoin/script/Cargo.toml +++ b/mm2src/mm2_bitcoin/script/Cargo.toml @@ -11,7 +11,7 @@ bitcrypto = { path = "../crypto" } chain = { path = "../chain" } keys = { path = "../keys" } primitives = { path = "../primitives" } -serde = "1.0" +serde.workspace = true serialization = { path = "../serialization" } -log = "0.4.17" -blake2b_simd = "0.5" \ No newline at end of file +log.workspace = true +blake2b_simd.workspace = true diff --git a/mm2src/mm2_bitcoin/serialization/Cargo.toml b/mm2src/mm2_bitcoin/serialization/Cargo.toml index 590b18296b..6a843442a2 100644 --- a/mm2src/mm2_bitcoin/serialization/Cargo.toml +++ b/mm2src/mm2_bitcoin/serialization/Cargo.toml @@ -7,7 +7,7 @@ authors = ["debris "] doctest = false [dependencies] -byteorder = "1.0" +byteorder.workspace = true primitives = { path = "../primitives" } -derive_more = "0.99" +derive_more.workspace = true test_helpers = { path = "../test_helpers" } diff --git a/mm2src/mm2_bitcoin/spv_validation/Cargo.toml b/mm2src/mm2_bitcoin/spv_validation/Cargo.toml index 53e840955b..1c3bd46ef7 100644 --- a/mm2src/mm2_bitcoin/spv_validation/Cargo.toml +++ b/mm2src/mm2_bitcoin/spv_validation/Cargo.toml @@ -8,20 +8,20 @@ edition = "2018" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true chain = {path = "../chain"} -derive_more = "0.99" +derive_more.workspace = true keys = {path = "../keys"} primitives = { path = "../primitives" } -ripemd160 = "0.9.0" -rustc-hex = "2" -serde = "1.0" +ripemd160.workspace = true +rustc-hex.workspace = true +serde.workspace = true serialization = { path = "../serialization" } -sha2 = "0.10" +sha2.workspace = true test_helpers = { path = "../test_helpers" } [dev-dependencies] common = { path = "../../common" } -lazy_static = "1.4" -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +lazy_static.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } diff --git a/mm2src/mm2_bitcoin/test_helpers/Cargo.toml b/mm2src/mm2_bitcoin/test_helpers/Cargo.toml index 1528506ab5..f91e832519 100644 --- a/mm2src/mm2_bitcoin/test_helpers/Cargo.toml +++ b/mm2src/mm2_bitcoin/test_helpers/Cargo.toml @@ -7,4 +7,4 @@ edition = "2018" doctest = false [dependencies] -hex = "0.4.2" +hex.workspace = true diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index 747875d5b8..d0350c766a 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -10,38 +10,38 @@ doctest = false new-db-arch = [] [dependencies] -arrayref = "0.3" -async-std = { version = "1.5", features = ["unstable"] } -async-trait = "0.1" -cfg-if = "1.0" +arrayref.workspace = true +async-std = { workspace = true, features = ["unstable"] } +async-trait.workspace = true +cfg-if.workspace = true common = { path = "../common" } -compatible-time = { version = "1.1.0", package = "web-time" } +compatible-time.workspace = true db_common = { path = "../db_common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -gstuff = { version = "0.7", features = ["nightly"] } -hex = "0.4.2" -lazy_static = "1.4" +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +gstuff.workspace = true +hex.workspace = true +lazy_static.workspace = true libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["identify"] } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_metrics = { path = "../mm2_metrics" } primitives = { path = "../mm2_bitcoin/primitives" } -rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } -serde = "1" +rand.workspace = true +serde.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } shared_ref_counter = { path = "../common/shared_ref_counter" } -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +uuid.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] mm2_rpc = { path = "../mm2_rpc", features = [ "rpc_facilities" ] } -timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } -wasm-bindgen-test = { version = "0.3.2" } +timed-map = { workspace = true, features = ["rustc-hash", "wasm"] } +wasm-bindgen-test.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] mm2_io = { path = "../mm2_io" } -rustls = { version = "0.21", default-features = false } -tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } -timed-map = { version = "1.4", features = ["rustc-hash"] } +rustls.workspace = true +tokio = { workspace = true, features = ["io-util", "rt-multi-thread", "net"] } +timed-map = { workspace = true, features = ["rustc-hash"] } diff --git a/mm2src/mm2_db/Cargo.toml b/mm2src/mm2_db/Cargo.toml index 5f5374acad..0bfa1795ad 100644 --- a/mm2src/mm2_db/Cargo.toml +++ b/mm2src/mm2_db/Cargo.toml @@ -7,24 +7,24 @@ edition = "2021" doctest = false [target.'cfg(target_arch = "wasm32")'.dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } -derive_more = "0.99" +derive_more.workspace = true enum_derives = { path = "../derives/enum_derives" } -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -itertools = "0.10" -hex = "0.4.2" -js-sys = "0.3.27" -lazy_static = "1.4" +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +itertools.workspace = true +hex.workspace = true +js-sys.workspace = true +lazy_static.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_number = { path = "../mm2_number" } -num-traits = "0.2" +num-traits.workspace = true primitives = { path = "../mm2_bitcoin/primitives" } -rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } -serde = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.2" } -web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbCursorDirection", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "MessageEvent", "WebSocket"] } +rand.workspace = true +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys = { workspace = true, features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbCursorDirection", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "MessageEvent", "WebSocket"] } diff --git a/mm2src/mm2_err_handle/Cargo.toml b/mm2src/mm2_err_handle/Cargo.toml index 0e2faaa8d2..247ad2d161 100644 --- a/mm2src/mm2_err_handle/Cargo.toml +++ b/mm2src/mm2_err_handle/Cargo.toml @@ -7,12 +7,12 @@ edition = "2018" doctest = false [dependencies] -futures01 = { version = "0.1", package = "futures" } -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -derive_more = "0.99" -itertools = "0.10" -serde = { version = "1.0", features = ["derive"] } +futures01.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +derive_more.workspace = true +itertools.workspace = true +serde = { workspace = true, features = ["derive"] } ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } common = { path = "../common" } -http = "0.2" +http.workspace = true diff --git a/mm2src/mm2_eth/Cargo.toml b/mm2src/mm2_eth/Cargo.toml index 5992896424..7c4e2e68d2 100644 --- a/mm2src/mm2_eth/Cargo.toml +++ b/mm2src/mm2_eth/Cargo.toml @@ -7,13 +7,13 @@ edition = "2021" doctest = false [dependencies] -ethabi = { version = "17.0.0" } -ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -hex = "0.4.2" -indexmap = "1.7.0" -itertools = "0.10" +ethabi.workspace = true +ethkey.workspace = true +hex.workspace = true +indexmap.workspace = true +itertools.workspace = true mm2_err_handle = { path = "../mm2_err_handle" } -secp256k1 = { version = "0.20", features = ["recovery"] } -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false } +secp256k1 = { workspace = true, features = ["recovery"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +web3 = { workspace = true, default-features = false } diff --git a/mm2src/mm2_event_stream/Cargo.toml b/mm2src/mm2_event_stream/Cargo.toml index 5b1677fa0e..d6787e4346 100644 --- a/mm2src/mm2_event_stream/Cargo.toml +++ b/mm2src/mm2_event_stream/Cargo.toml @@ -4,17 +4,17 @@ version = "0.1.0" edition = "2021" [dependencies] -async-trait = "0.1" -cfg-if = "1.0" +async-trait.workspace = true +cfg-if.workspace = true common = { path = "../common" } -futures = { version = "0.3", default-features = false } -parking_lot = "0.12" -serde = { version = "1", features = ["derive", "rc"] } -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -tokio = "1.20" +futures.workspace = true +parking_lot = { workspace = true } +serde = { workspace = true, features = ["derive", "rc"] } +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +tokio.workspace = true [dev-dependencies] -tokio = { version = "1.20", features = ["macros"] } +tokio = { workspace = true, features = ["macros"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen-test = { version = "0.3.2" } +wasm-bindgen-test.workspace = true diff --git a/mm2src/mm2_git/Cargo.toml b/mm2src/mm2_git/Cargo.toml index ee06101400..cf90802e90 100644 --- a/mm2src/mm2_git/Cargo.toml +++ b/mm2src/mm2_git/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } -http = "0.2" +http.workspace = true mm2_err_handle = { path = "../mm2_err_handle" } mm2_net = { path = "../mm2_net" } -serde = "1" +serde.workspace = true serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } diff --git a/mm2src/mm2_gui_storage/Cargo.toml b/mm2src/mm2_gui_storage/Cargo.toml index 8beb78b863..96ded12dd2 100644 --- a/mm2src/mm2_gui_storage/Cargo.toml +++ b/mm2src/mm2_gui_storage/Cargo.toml @@ -7,24 +7,24 @@ edition = "2021" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } db_common = { path = "../db_common" } -derive_more = "0.99" +derive_more.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_number = { path = "../mm2_number" } rpc = { path = "../mm2_bitcoin/rpc" } -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -serde_repr = "0.1" +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +serde_repr.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } [target.'cfg(target_arch = "wasm32")'.dependencies] mm2_db = { path = "../mm2_db" } mm2_test_helpers = { path = "../mm2_test_helpers" } -wasm-bindgen-test = { version = "0.3.2" } +wasm-bindgen-test.workspace = true [dev-dependencies] mm2_test_helpers = { path = "../mm2_test_helpers" } diff --git a/mm2src/mm2_io/Cargo.toml b/mm2src/mm2_io/Cargo.toml index 925e0944b0..b8b5bc101d 100644 --- a/mm2src/mm2_io/Cargo.toml +++ b/mm2src/mm2_io/Cargo.toml @@ -7,17 +7,12 @@ edition = "2018" doctest = false [dependencies] +async-std = { workspace = true, features = ["unstable"] } common = { path = "../common" } +gstuff.workspace = true mm2_err_handle = { path = "../mm2_err_handle" } -serde = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -derive_more = "0.99" -async-std = { version = "1.5", features = ["unstable"] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -gstuff = { version = "0.7", features = ["nightly"] } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -gstuff = { version = "0.7", features = ["nightly"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +rand.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +derive_more.workspace = true diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index fe2717b3d2..ad0109793c 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -28,40 +28,40 @@ test-ext-api = ["trading_api/test-ext-api"] new-db-arch = ["mm2_core/new-db-arch"] # A temporary feature to integrate the new db architecture incrementally [dependencies] -async-std = { version = "1.5", features = ["unstable"] } -async-trait = "0.1" +async-std = { workspace = true, features = ["unstable"] } +async-trait.workspace = true bitcrypto = { path = "../mm2_bitcoin/crypto" } -blake2 = "0.10.6" -bytes = "0.4" +blake2.workspace = true +bytes.workspace = true chain = { path = "../mm2_bitcoin/chain" } -chrono = "0.4" -cfg-if = "1.0" +chrono.workspace = true +cfg-if.workspace = true coins = { path = "../coins" } coins_activation = { path = "../coins_activation" } common = { path = "../common" } -compatible-time = { version = "1.1.0", package = "web-time" } -crc32fast = { version = "1.3.2", features = ["std", "nightly"] } -crossbeam = "0.8" +compatible-time.workspace = true +crc32fast.workspace = true +crossbeam.workspace = true crypto = { path = "../crypto" } db_common = { path = "../db_common" } -derive_more = "0.99" -either = "1.6" -ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +derive_more.workspace = true +either.workspace = true +ethereum-types.workspace = true enum_derives = { path = "../derives/enum_derives" } -enum-primitive-derive = "0.2" -futures01 = { version = "0.1", package = "futures" } -futures = { version = "0.3.1", package = "futures", features = ["compat", "async-await"] } -gstuff = { version = "0.7", features = ["nightly"] } -hash256-std-hasher = "0.15.2" -hash-db = "0.15.2" -hex = "0.4.2" -http = "0.2" +enum-primitive-derive.workspace = true +futures01.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } +gstuff.workspace = true +hash256-std-hasher.workspace = true +hash-db.workspace = true +hex.workspace = true +http.workspace = true hw_common = { path = "../hw_common" } -itertools = "0.10" +itertools.workspace = true keys = { path = "../mm2_bitcoin/keys" } -lazy_static = "1.4" +lazy_static.workspace = true # ledger = { path = "../ledger" } -libc = "0.2" +libc.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } @@ -74,58 +74,58 @@ mm2_number = { path = "../mm2_number" } mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"]} mm2_state_machine = { path = "../mm2_state_machine" } trading_api = { path = "../trading_api" } -num-traits = "0.2" -parity-util-mem = "0.11" -parking_lot = { version = "0.12.0", features = ["nightly"] } +num-traits.workspace = true +parity-util-mem.workspace = true +parking_lot = { workspace = true, features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } -primitive-types = "0.11.1" -prost = "0.12" -rand = { version = "0.7", features = ["std", "small_rng"] } +primitive-types.workspace = true +prost.workspace = true +rand = { workspace = true, features = ["std", "small_rng"] } rand6 = { version = "0.6", package = "rand" } -rmp-serde = "0.14.3" +rmp-serde.workspace = true rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } script = { path = "../mm2_bitcoin/script" } -secp256k1 = { version = "0.20", features = ["rand"] } -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -serde_derive = "1.0" +secp256k1 = { workspace = true, features = ["rand"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +serde_derive.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } serialization = { path = "../mm2_bitcoin/serialization" } serialization_derive = { path = "../mm2_bitcoin/serialization_derive" } spv_validation = { path = "../mm2_bitcoin/spv_validation" } -sp-runtime-interface = { version = "6.0.0", default-features = false, features = ["disable_target_static_assertions"] } -sp-trie = { version = "6.0", default-features = false } -trie-db = { version = "0.23.1", default-features = false } -trie-root = "0.16.0" -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +sp-runtime-interface.workspace = true +sp-trie.workspace = true +trie-db.workspace = true +trie-root.workspace = true +uuid.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Removing this causes `wasm-pack` to fail when starting a web session (even though we don't use this crate). # Investigate why. -instant = { version = "0.1.12", features = ["wasm-bindgen"] } -js-sys = { version = "0.3.27" } +instant = { workspace = true, features = ["wasm-bindgen"] } +js-sys.workspace = true mm2_db = { path = "../mm2_db" } mm2_test_helpers = { path = "../mm2_test_helpers" } -timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.1" } -web-sys = { version = "0.3.55", features = ["console"] } +timed-map = { workspace = true, features = ["rustc-hash", "wasm"] } +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys = { workspace = true, features = ["console"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -dirs = { version = "1" } -futures-rustls = { version = "0.24" } -hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } -rcgen = "0.10" -rustls = { version = "0.21", default-features = false } -rustls-pemfile = "1.0.2" -timed-map = { version = "1.4", features = ["rustc-hash"] } -tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net", "signal"] } +dirs.workspace = true +futures-rustls.workspace = true +hyper = { workspace = true, features = ["client", "http2", "server", "tcp"] } +rcgen.workspace = true +rustls = { workspace = true, default-features = false } +rustls-pemfile.workspace = true +timed-map = { workspace = true, features = ["rustc-hash"] } +tokio = { workspace = true, features = ["io-util", "rt-multi-thread", "net", "signal"] } [target.'cfg(windows)'.dependencies] -winapi = "0.3" +winapi.workspace = true [dev-dependencies] coins = { path = "../coins", features = ["for-tests"] } @@ -133,19 +133,19 @@ coins_activation = { path = "../coins_activation", features = ["for-tests"] } common = { path = "../common", features = ["for-tests"] } mm2_test_helpers = { path = "../mm2_test_helpers" } trading_api = { path = "../trading_api", features = ["for-tests"] } -mocktopus = "0.8.0" -testcontainers = "0.15.0" -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false, features = ["http-rustls-tls"] } -ethabi = { version = "17.0.0" } -rlp = { version = "0.5" } -ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -rustc-hex = "2" -sia-rust = { git = "https://github.com/KomodoPlatform/sia-rust", rev = "9f188b80b3213bcb604e7619275251ce08fae808" } -url = { version = "2.2.2", features = ["serde"] } +mocktopus.workspace = true +testcontainers.workspace = true +web3 = { workspace = true, default-features = false, features = ["http-rustls-tls"] } +ethabi.workspace = true +rlp.workspace = true +ethcore-transaction.workspace = true +rustc-hex.workspace = true +sia-rust.workspace = true +url.workspace = true [build-dependencies] -chrono = "0.4" -gstuff = { version = "0.7", features = ["nightly"] } +chrono.workspace = true +gstuff.workspace = true prost-build = { version = "0.12", default-features = false } -regex = "1" +regex.workspace = true diff --git a/mm2src/mm2_metamask/Cargo.toml b/mm2src/mm2_metamask/Cargo.toml index e26be8c434..925f424236 100644 --- a/mm2src/mm2_metamask/Cargo.toml +++ b/mm2src/mm2_metamask/Cargo.toml @@ -4,20 +4,20 @@ version = "0.1.0" edition = "2021" [target.'cfg(target_arch = "wasm32")'.dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures" } -itertools = "0.10" -js-sys = { version = "0.3.27" } -jsonrpc-core = "18.0.0" # Same version as `web3` depends on. -lazy_static = "1.4" +derive_more.workspace = true +futures = { workspace = true } +itertools.workspace = true +js-sys.workspace = true +jsonrpc-core.workspace = true +lazy_static.workspace = true mm2_err_handle = { path = "../mm2_err_handle" } mm2_eth = { path = "../mm2_eth" } -parking_lot = { version = "0.12.0", features = ["nightly"] } -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -serde_derive = "1.0" -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false, features = ["eip-1193"] } +parking_lot = { workspace = true, features = ["nightly"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +serde_derive.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +web3 = { workspace = true, features = ["eip-1193"] } diff --git a/mm2src/mm2_metrics/Cargo.toml b/mm2src/mm2_metrics/Cargo.toml index f7a5fabc52..4a30e7f0b3 100644 --- a/mm2src/mm2_metrics/Cargo.toml +++ b/mm2src/mm2_metrics/Cargo.toml @@ -7,21 +7,21 @@ edition = "2021" doctest = false [dependencies] -base64 = "0.21.2" +base64.workspace = true common = { path = "../common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -itertools = "0.10" -metrics = { version = "0.21" } -metrics-util = { version = "0.15" } +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +itertools.workspace = true +metrics.workspace = true +metrics-util.workspace = true mm2_err_handle = { path = "../mm2_err_handle" } -serde = "1" -serde_derive = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } +hyper = { workspace = true, features = ["client", "http2", "server", "tcp"] } # using webpki-tokio to avoid rejecting valid certificates # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features -hyper-rustls = { version = "0.24", default-features = false, features = ["http1", "http2", "webpki-tokio"] } -metrics-exporter-prometheus = "0.12.1" +hyper-rustls = { workspace = true, default-features = false, features = ["http1", "http2", "webpki-tokio"] } +metrics-exporter-prometheus.workspace = true diff --git a/mm2src/mm2_net/Cargo.toml b/mm2src/mm2_net/Cargo.toml index a720327d96..3512d89e9b 100644 --- a/mm2src/mm2_net/Cargo.toml +++ b/mm2src/mm2_net/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "mm2_net" version = "0.1.0" -edition = "2018" +edition = "2021" [lib] doctest = false @@ -9,48 +9,48 @@ doctest = false [features] [dependencies] -async-stream = { version = "0.3" } -async-trait = "0.1" -bytes = "1.1" -cfg-if = "1.0" +async-stream.workspace = true +async-trait.workspace = true +bytes.workspace = true +cfg-if.workspace = true common = { path = "../common" } -derive_more = "0.99" -ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -gstuff = "0.7" -http = "0.2" -lazy_static = "1.4" +derive_more.workspace = true +ethkey.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +gstuff.workspace = true +http.workspace = true +lazy_static.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_number = { path = "../mm2_number" } -prost = "0.12" -rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } -serde = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -thiserror = "1.0.30" +prost.workspace = true +rand.workspace = true +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +thiserror.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -base64 = "0.21.7" -futures-util = "0.3" +base64.workspace = true +futures-util.workspace = true mm2_state_machine = { path = "../mm2_state_machine"} -http-body = "0.4" -httparse = "1.8.0" -js-sys = "0.3.27" -pin-project = "1.1.2" -tonic = { version = "0.10", default-features = false, features = ["prost", "codegen"] } -tower-service = "0.3" -wasm-bindgen = "0.2.86" -wasm-bindgen-test = { version = "0.3.2" } -wasm-bindgen-futures = "0.4.21" -web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", +http-body.workspace = true +httparse.workspace = true +js-sys.workspace = true +pin-project.workspace = true +tonic = { workspace = true, default-features = false, features = ["prost", "codegen"] } +tower-service.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-test.workspace = true +wasm-bindgen-futures.workspace = true +web-sys = { workspace = true, features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "MessageEvent", "MessagePort", "ReadableStreamDefaultReader", "ReadableStream", "SharedWorker", "Url", "WebSocket"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -futures-util = { version = "0.3" } -hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp", "stream"] } -rustls = { version = "0.21", default-features = false } -tokio = { version = "1.20" } -tokio-rustls = { version = "0.24", default-features = false } +futures-util.workspace = true +hyper = { workspace = true, features = ["client", "http2", "server", "tcp", "stream"] } +rustls.workspace = true +tokio.workspace = true +tokio-rustls = { workspace = true, default-features = false } diff --git a/mm2src/mm2_number/Cargo.toml b/mm2src/mm2_number/Cargo.toml index 0303fd20d8..83ca5ed5bc 100644 --- a/mm2src/mm2_number/Cargo.toml +++ b/mm2src/mm2_number/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" doctest = false [dependencies] -bigdecimal = { version = "0.3", features = ["serde"] } -num-bigint = { version = "0.4", features = ["serde", "std"] } -num-rational = { version = "0.4", features = ["serde"] } -num-traits = "0.2" -paste = "1.0" -serde = { version = "1", features = ["serde_derive"] } -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +bigdecimal.workspace = true +num-bigint.workspace = true +num-rational.workspace = true +num-traits.workspace = true +paste.workspace = true +serde = { workspace = true, features = ["serde_derive"] } +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } diff --git a/mm2src/mm2_p2p/Cargo.toml b/mm2src/mm2_p2p/Cargo.toml index 813e74dba6..c0d9a3761f 100644 --- a/mm2src/mm2_p2p/Cargo.toml +++ b/mm2src/mm2_p2p/Cargo.toml @@ -11,44 +11,43 @@ application = ["dep:mm2_number"] doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } -compatible-time = { version = "1.1.0", package = "web-time" } -derive_more = "0.99" -futures = { version = "0.3.1", default-features = false } -futures-ticker = "0.0.3" -hex = "0.4.2" -lazy_static = "1.4" -log = "0.4" +compatible-time.workspace = true +derive_more.workspace = true +futures.workspace = true +futures-ticker.workspace = true +hex.workspace = true +lazy_static.workspace = true +log.workspace = true mm2_core = { path = "../mm2_core" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number", optional = true } -parking_lot = { version = "0.12.0", features = ["nightly"] } -rand = { version = "0.7", default-features = false, features = ["wasm-bindgen"] } -regex = "1" -rmp-serde = "0.14.3" -secp256k1 = { version = "0.20", features = ["rand"] } -serde = { version = "1.0", default-features = false } -serde_bytes = "0.11.5" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -sha2 = "0.10" -smallvec = "1.6.1" -syn = "2.0.18" -void = "1.0" +parking_lot = { workspace = true, features = ["nightly"] } +rand = { workspace = true, features = ["wasm-bindgen"] } +regex.workspace = true +rmp-serde.workspace = true +secp256k1 = { workspace = true, features = ["rand"] } +serde = { workspace = true, default-features = false } +serde_bytes.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +sha2.workspace = true +smallvec.workspace = true +void.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -futures-rustls = "0.24" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } -timed-map = { version = "1.4", features = ["rustc-hash"] } -tokio = { version = "1.20", default-features = false } +futures-rustls.workspace = true +libp2p = { workspace = true, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } +timed-map = { workspace = true, features = ["rustc-hash"] } +tokio.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] futures-rustls = "0.22" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } -timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } +libp2p = { workspace = true, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } +timed-map = { workspace = true, features = ["rustc-hash", "wasm"] } [dev-dependencies] -async-std = "1.6.2" -env_logger = "0.9.3" +async-std.workspace = true +env_logger.workspace = true common = { path = "../common", features = ["for-tests"] } diff --git a/mm2src/mm2_rpc/Cargo.toml b/mm2src/mm2_rpc/Cargo.toml index 6f84bb201a..d82cc9dd81 100644 --- a/mm2src/mm2_rpc/Cargo.toml +++ b/mm2src/mm2_rpc/Cargo.toml @@ -8,18 +8,18 @@ doctest = false [dependencies] common = { path = "../common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"], optional = true } -gstuff = { version = "0.7", features = ["nightly"], optional = true} -http = {version = "0.2", optional = true} +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"], optional = true } +gstuff = { workspace = true, optional = true} +http = { workspace = true, optional = true} mm2_err_handle = { path = "../mm2_err_handle", optional = true } mm2_number = { path = "../mm2_number" } rpc = { path = "../mm2_bitcoin/rpc" } -serde = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } ser_error = { path = "../derives/ser_error", optional = true} ser_error_derive = { path = "../derives/ser_error_derive", optional=true } -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +uuid.workspace = true [features] default = [] diff --git a/mm2src/mm2_state_machine/Cargo.toml b/mm2src/mm2_state_machine/Cargo.toml index 9683850ba0..f6a03acb9b 100644 --- a/mm2src/mm2_state_machine/Cargo.toml +++ b/mm2src/mm2_state_machine/Cargo.toml @@ -9,8 +9,8 @@ edition = "2021" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true [dev-dependencies] common = { path = "../common" } -futures = { version = "0.3" } \ No newline at end of file +futures.workspace = true diff --git a/mm2src/proxy_signature/Cargo.toml b/mm2src/proxy_signature/Cargo.toml index 260aa1a568..524dd1d39e 100644 --- a/mm2src/proxy_signature/Cargo.toml +++ b/mm2src/proxy_signature/Cargo.toml @@ -4,11 +4,11 @@ version = "0.1.0" edition = "2018" [dependencies] -chrono = "0.4" -http = "0.2" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["identify"] } -serde = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +chrono.workspace = true +http.workspace = true +libp2p = { workspace = true, features = ["identify"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } [dev-dependencies] -rand = { version = "0.7", features = ["std", "small_rng"] } +rand = { workspace = true, features = ["std", "small_rng"] } diff --git a/mm2src/rpc_task/Cargo.toml b/mm2src/rpc_task/Cargo.toml index c542159f25..ebe7f1b4b3 100644 --- a/mm2src/rpc_task/Cargo.toml +++ b/mm2src/rpc_task/Cargo.toml @@ -7,14 +7,14 @@ edition = "2018" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } -derive_more = "0.99" -futures = "0.3" +derive_more.workspace = true +futures.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -serde = "1" -serde_derive = "1" -serde_json = "1" +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml index 4f714ed34d..bc30874dfc 100644 --- a/mm2src/trading_api/Cargo.toml +++ b/mm2src/trading_api/Cargo.toml @@ -11,19 +11,18 @@ mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } -mocktopus = { version = "0.8.0", optional = true } - -derive_more = "0.99" -ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } -lazy_static = "1.4" -serde = "1.0" -serde_derive = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -url = { version = "2.2.2", features = ["serde"] } +mocktopus = { workspace = true, optional = true } +derive_more.workspace = true +ethereum-types.workspace = true +lazy_static.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +url.workspace = true [features] test-ext-api = [] # use test config to connect to an external api for-tests = ["dep:mocktopus"] [dev-dependencies] -mocktopus = { version = "0.8.0" } \ No newline at end of file +mocktopus.workspace = true diff --git a/mm2src/trezor/Cargo.toml b/mm2src/trezor/Cargo.toml index 36e5abb0ec..55f40b5d3d 100644 --- a/mm2src/trezor/Cargo.toml +++ b/mm2src/trezor/Cargo.toml @@ -7,34 +7,34 @@ edition = "2018" doctest = false [dependencies] -async-trait = "0.1" -byteorder = "1.3.2" +async-trait.workspace = true +byteorder.workspace = true common = { path = "../common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } hw_common = { path = "../hw_common" } mm2_err_handle = { path = "../mm2_err_handle" } -prost = "0.12" -rand = { version = "0.7", features = ["std", "wasm-bindgen"] } +prost.workspace = true +rand = { workspace = true, features = ["std", "wasm-bindgen"] } rpc_task = { path = "../rpc_task" } -serde = "1.0" -serde_derive = "1.0" -ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } -ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } -lazy_static = "1.4" +serde.workspace = true +serde_derive.workspace = true +ethcore-transaction.workspace = true +ethereum-types.workspace = true +ethkey.workspace = true +bip32.workspace = true +lazy_static.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } -async-std = { version = "1.5" } +bip32.workspace = true +async-std.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = { version = "0.3.27" } -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.1" } -web-sys = { version = "0.3.55" } +js-sys.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys.workspace = true [features] trezor-udp = [] # use for tests to connect to trezor emulator over udp From 07b70a87146eb9129f7039827e3f786f0790175a Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Sat, 24 May 2025 01:56:58 +0300 Subject: [PATCH 11/36] chore(docs): add DeepWiki badge to README (#2463) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 441c23ba8c..d175302c39 100755 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ release version + Ask DeepWiki chat on Discord From 68319c08dcb946e23a9618f8d5b1054d8909b967 Mon Sep 17 00:00:00 2001 From: Alina Sharon <52405288+laruh@users.noreply.github.com> Date: Mon, 26 May 2025 18:09:03 +0700 Subject: [PATCH 12/36] fix(evm-api): find enabled erc20 token using platform ticker (#2445) This renames the function `get_enabled_erc20_by_contract` to `get_enabled_erc20_by_platform_and_contract` and adds a new parameter, namely the platform ticker. The platform ticker is used to distinguish which erc20 token we are interested in, as erc20 tokens share the same contract address over different platforms, which means contract address alone isn't enough to identify the erc20 token. --- mm2src/coins/eth/erc20.rs | 14 +- mm2src/coins/eth/eth_tests.rs | 149 ++++++++++++++++-- mm2src/coins/eth/v2_activation.rs | 4 +- mm2src/coins/rpc_command/get_enabled_coins.rs | 10 +- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 4 +- 5 files changed, 152 insertions(+), 29 deletions(-) diff --git a/mm2src/coins/eth/erc20.rs b/mm2src/coins/eth/erc20.rs index 75f7033fda..3ab3d89a03 100644 --- a/mm2src/coins/eth/erc20.rs +++ b/mm2src/coins/eth/erc20.rs @@ -1,6 +1,6 @@ use crate::eth::web3_transport::Web3Transport; use crate::eth::{EthCoin, ERC20_CONTRACT}; -use crate::{CoinsContext, MmCoinEnum}; +use crate::{CoinsContext, MarketCoinOps, MmCoinEnum}; use ethabi::Token; use ethereum_types::Address; use futures_util::TryFutureExt; @@ -90,16 +90,20 @@ pub fn get_erc20_ticker_by_contract_address(ctx: &MmArc, platform: &str, contrac }) } -/// Finds an enabled ERC20 token by its contract address and returns it as `MmCoinEnum`. -pub async fn get_enabled_erc20_by_contract( +/// Finds an enabled ERC20 token by contract address and platform coin ticker and returns it as `MmCoinEnum`. +pub async fn get_enabled_erc20_by_platform_and_contract( ctx: &MmArc, - contract_address: Address, + platform: &str, + contract_address: &Address, ) -> MmResult, String> { let cctx = CoinsContext::from_ctx(ctx)?; let coins = cctx.coins.lock().await; Ok(coins.values().find_map(|coin| match &coin.inner { - MmCoinEnum::EthCoin(eth_coin) if eth_coin.erc20_token_address() == Some(contract_address) => { + MmCoinEnum::EthCoin(eth_coin) + if eth_coin.platform_ticker() == platform + && eth_coin.erc20_token_address().as_ref() == Some(contract_address) => + { Some(coin.inner.clone()) }, _ => None, diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 64cd3aecb0..ef7a126c4a 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -28,6 +28,8 @@ cfg_native!( const GAS_PRICE_PERCENT: u64 = 10; const MATIC_CHAIN_ID: u64 = 137; +const ETH: &str = "ETH"; + fn check_sum(addr: &str, expected: &str) { let actual = checksum_address(addr); assert_eq!(expected, actual); @@ -219,7 +221,7 @@ fn test_withdraw_impl_manual_fee() { let withdraw_req = WithdrawRequest { amount: 1.into(), to: "0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94".to_string(), - coin: "ETH".to_string(), + coin: ETH.to_string(), fee: Some(WithdrawFee::EthGas { gas: gas_limit::ETH_MAX_TRADE_GAS, gas_price: 1.into(), @@ -231,7 +233,7 @@ fn test_withdraw_impl_manual_fee() { let tx_details = block_on(withdraw_impl(coin, withdraw_req)).unwrap(); let expected = Some( EthTxFeeDetails { - coin: "ETH".into(), + coin: ETH.into(), gas_price: "0.000000001".parse().unwrap(), gas: gas_limit::ETH_MAX_TRADE_GAS, total_fee: "0.00015".parse().unwrap(), @@ -248,7 +250,7 @@ fn test_withdraw_impl_manual_fee() { fn test_withdraw_impl_fee_details() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str(ETH_SEPOLIA_TOKEN_CONTRACT).unwrap(), }, &["http://dummy.dummy"], @@ -277,7 +279,7 @@ fn test_withdraw_impl_fee_details() { let tx_details = block_on(withdraw_impl(coin, withdraw_req)).unwrap(); let expected = Some( EthTxFeeDetails { - coin: "ETH".into(), + coin: ETH.into(), gas_price: "0.000000001".parse().unwrap(), gas: gas_limit::ETH_MAX_TRADE_GAS, total_fee: "0.00015".parse().unwrap(), @@ -314,7 +316,7 @@ fn get_sender_trade_preimage() { fn expected_fee(gas_price: u64, gas_limit: u64) -> TradeFee { let amount = u256_to_big_decimal((gas_limit * gas_price).into(), 18).expect("!u256_to_big_decimal"); TradeFee { - coin: "ETH".to_owned(), + coin: ETH.to_owned(), amount: amount.into(), paid_from_trading_vol: false, } @@ -382,7 +384,7 @@ fn get_erc20_sender_trade_preimage() { fn expected_trade_fee(gas_limit: u64, gas_price: u64) -> TradeFee { let amount = u256_to_big_decimal((gas_limit * gas_price).into(), 18).expect("!u256_to_big_decimal"); TradeFee { - coin: "ETH".to_owned(), + coin: ETH.to_owned(), amount: amount.into(), paid_from_trading_vol: false, } @@ -390,7 +392,7 @@ fn get_erc20_sender_trade_preimage() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::default(), }, &["http://dummy.dummy"], @@ -475,7 +477,7 @@ fn get_receiver_trade_preimage() { let amount = u256_to_big_decimal((gas_limit::ETH_RECEIVER_SPEND * GAS_PRICE).into(), 18).expect("!u256_to_big_decimal"); let expected_fee = TradeFee { - coin: "ETH".to_owned(), + coin: ETH.to_owned(), amount: amount.into(), paid_from_trading_vol: false, }; @@ -498,7 +500,7 @@ fn test_get_fee_to_send_taker_fee() { // fee to send taker fee is `TRANSFER_GAS_LIMIT * gas_price` always. let amount = u256_to_big_decimal((TRANSFER_GAS_LIMIT * GAS_PRICE).into(), 18).expect("!u256_to_big_decimal"); let expected_fee = TradeFee { - coin: "ETH".to_owned(), + coin: ETH.to_owned(), amount: amount.into(), paid_from_trading_vol: false, }; @@ -515,7 +517,7 @@ fn test_get_fee_to_send_taker_fee() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str("0xaD22f63404f7305e4713CcBd4F296f34770513f4").unwrap(), }, &["http://dummy.dummy"], @@ -545,7 +547,7 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(40.into())))); let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str("0xaD22f63404f7305e4713CcBd4F296f34770513f4").unwrap(), }, ETH_MAINNET_NODES, @@ -598,7 +600,7 @@ fn validate_dex_fee_invalid_sender_eth() { fn validate_dex_fee_invalid_sender_erc() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str("0xa1d6df714f91debf4e0802a542e13067f31b8262").unwrap(), }, ETH_MAINNET_NODES, @@ -672,7 +674,7 @@ fn validate_dex_fee_eth_confirmed_before_min_block() { fn validate_dex_fee_erc_confirmed_before_min_block() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str("0xa1d6df714f91debf4e0802a542e13067f31b8262").unwrap(), }, ETH_MAINNET_NODES, @@ -882,7 +884,7 @@ fn test_sign_verify_message() { fn test_eth_extract_secret() { let key_pair = Random.generate().unwrap(); let coin_type = EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str("0xc0eb7aed740e1796992a08962c15661bdeb58003").unwrap(), }; let (_ctx, coin) = eth_coin_from_keypair(coin_type, &["http://dummy.dummy"], None, key_pair, ETH_SEPOLIA_CHAIN_ID); @@ -984,6 +986,123 @@ fn test_eth_validate_valid_and_invalid_pubkey() { assert!(coin.validate_other_pubkey(&[1u8; 8]).is_err()); } +#[test] +fn test_get_enabled_erc20_by_contract_and_platform() { + use super::erc20::get_enabled_erc20_by_platform_and_contract; + use crate::rpc_command::get_enabled_coins::{get_enabled_coins_rpc, GetEnabledCoinsRequest}; + const BNB_TOKEN: &str = "1INCH-BEP20"; + const ETH_TOKEN: &str = "1INCH-ERC20"; + + let conf = json!({ + "coins": [{ + "coin": "BNB", + "name": "binancesmartchain", + "fname": "Binance Coin", + "avg_blocktime": 3, + "rpcport": 80, + "mm2": 1, + "use_access_list": true, + "max_eth_tx_type": 2, + "required_confirmations": 3, + "protocol": { + "type": "ETH", + "protocol_data": { + "chain_id": 56 + } + }, + "derivation_path": "m/44'/60'", + "trezor_coin": "Binance Smart Chain", + "links": { + "homepage": "https://www.binance.org" + } + },{ + "coin": BNB_TOKEN, + "name": "1inch_bep20", + "fname": "1Inch", + "rpcport": 80, + "mm2": 1, + "avg_blocktime": 3, + "required_confirmations": 3, + "protocol": { + "type": "ERC20", + "protocol_data": { + "platform": "BNB", + "contract_address": "0x111111111117dC0aa78b770fA6A738034120C302" + } + }, + "derivation_path": "m/44'/60'", + "use_access_list": true, + "max_eth_tx_type": 2, + "gas_limit": { + "eth_send_erc20": 60000, + "erc20_payment": 110000, + "erc20_receiver_spend": 85000, + "erc20_sender_refund": 85000 + } + },{ + "coin": "ETH", + "name": "ethereum", + "fname": "Ethereum", + "rpcport": 80, + "mm2": 1, + "sign_message_prefix": "Ethereum Signed Message:\n", + "required_confirmations": 3, + "avg_blocktime": 15, + "protocol": { + "type": "ETH", + "protocol_data": { + "chain_id": 1 + } + }, + "derivation_path": "m/44'/60'" + },{ + "coin": ETH_TOKEN, + "name": "1inch_erc20", + "fname": "1Inch", + "rpcport": 80, + "mm2": 1, + "avg_blocktime": 15, + "required_confirmations": 3, + "decimals": 18, + "protocol": { + "type": "ERC20", + "protocol_data": { + "platform": "ETH", + "contract_address": "0x111111111117dC0aa78b770fA6A738034120C302" + } + }, + "derivation_path": "m/44'/60'" + }] + }); + + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); + CryptoCtx::init_with_iguana_passphrase( + ctx.clone(), + "spice describe gravity federal blast come thank unfair canal monkey style afraid", + ) + .unwrap(); + + let req_bnb_token = json!({ + "urls":["https://bsc-dataseed1.binance.org","https://bsc-dataseed1.defibit.io"], + "swap_contract_address":"0x9130b257d37a52e52f21054c4da3450c72f595ce", + }); + block_on(lp_coininit(&ctx, BNB_TOKEN, &req_bnb_token)).unwrap(); + + let req_eth_token = json!({ + "urls":["https://ethereum-rpc.publicnode.com", "https://eth.drpc.org"], + "swap_contract_address":"0x9130b257d37a52e52f21054c4da3450c72f595ce", + }); + block_on(lp_coininit(&ctx, ETH_TOKEN, &req_eth_token)).unwrap(); + + let coins = block_on(get_enabled_coins_rpc(ctx.clone(), GetEnabledCoinsRequest)).unwrap(); + assert_eq!(coins.coins.len(), 2); + + let contract_address = Address::from_str("0x111111111117dC0aa78b770fA6A738034120C302").unwrap(); + let res = block_on(get_enabled_erc20_by_platform_and_contract(&ctx, ETH, &contract_address)).unwrap(); + assert!(res.is_some()); + assert_eq!(res.unwrap().platform_ticker(), ETH); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_fee_history() { @@ -1028,7 +1147,7 @@ fn test_gas_limit_conf() { "urls":ETH_SEPOLIA_NODES, "swap_contract_address":ETH_SEPOLIA_SWAP_CONTRACT }); - let coin = block_on(lp_coininit(&ctx, "ETH", &req)).unwrap(); + let coin = block_on(lp_coininit(&ctx, ETH, &req)).unwrap(); let eth_coin = match coin { MmCoinEnum::EthCoin(eth_coin) => eth_coin, _ => panic!("not eth coin"), diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index bc00a35cba..a3317ad809 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -1,5 +1,5 @@ use super::*; -use crate::eth::erc20::{get_enabled_erc20_by_contract, get_token_decimals}; +use crate::eth::erc20::{get_enabled_erc20_by_platform_and_contract, get_token_decimals}; use crate::eth::web3_transport::http_transport::HttpTransport; use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDPathAccountToAddressId, HDWalletCoinStorage, HDWalletStorageError, DEFAULT_GAP_LIMIT}; @@ -402,7 +402,7 @@ impl EthCoin { // Todo: when custom token config storage is added, this might not be needed // `is_custom` was added to avoid this unnecessary check for non-custom tokens if is_custom { - match get_enabled_erc20_by_contract(&ctx, protocol.token_addr).await { + match get_enabled_erc20_by_platform_and_contract(&ctx, &protocol.platform, &protocol.token_addr).await { Ok(Some(token)) => { return MmError::err(EthTokenActivationError::CustomTokenError( CustomTokenError::TokenWithSameContractAlreadyActivated { diff --git a/mm2src/coins/rpc_command/get_enabled_coins.rs b/mm2src/coins/rpc_command/get_enabled_coins.rs index 543f6160eb..d746d298df 100644 --- a/mm2src/coins/rpc_command/get_enabled_coins.rs +++ b/mm2src/coins/rpc_command/get_enabled_coins.rs @@ -5,7 +5,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmResult; use mm2_err_handle::prelude::*; -#[derive(Serialize, Display, SerializeErrorType)] +#[derive(Debug, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum GetEnabledCoinsError { #[display(fmt = "Internal error: {}", _0)] @@ -23,17 +23,17 @@ impl HttpStatusCode for GetEnabledCoinsError { #[derive(Deserialize)] pub struct GetEnabledCoinsRequest; -#[derive(Serialize)] +#[derive(Debug, Serialize)] pub struct GetEnabledCoinsResponse { - coins: Vec, + pub coins: Vec, } -#[derive(Serialize)] +#[derive(Debug, Serialize)] pub struct EnabledCoinV2 { ticker: String, } -pub async fn get_enabled_coins( +pub async fn get_enabled_coins_rpc( ctx: MmArc, _req: GetEnabledCoinsRequest, ) -> MmResult { diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index bbbeca9050..bc5fb414ee 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -27,7 +27,7 @@ use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, - get_enabled_coins::get_enabled_coins, + get_enabled_coins::get_enabled_coins_rpc, get_new_address::{cancel_get_new_address, get_new_address, init_get_new_address, init_get_new_address_status, init_get_new_address_user_action}, init_account_balance::{cancel_account_balance, init_account_balance, @@ -208,7 +208,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, enable_token::).await, "get_current_mtp" => handle_mmrpc(ctx, request, get_current_mtp_rpc).await, - "get_enabled_coins" => handle_mmrpc(ctx, request, get_enabled_coins).await, + "get_enabled_coins" => handle_mmrpc(ctx, request, get_enabled_coins_rpc).await, "get_locked_amount" => handle_mmrpc(ctx, request, get_locked_amount_rpc).await, "get_mnemonic" => handle_mmrpc(ctx, request, get_mnemonic_rpc).await, "get_my_address" => handle_mmrpc(ctx, request, get_my_address).await, From 0e6d246d9aa0cd649fbc12fbf8105a63e6c0971c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Wed, 28 May 2025 10:03:30 +0300 Subject: [PATCH 13/36] improvement(p2p): remove hardcoded seeds (#2439) - Removes all hardcoded seed nodes from KDF. Users must now supply their own seed node addresses in the `seednodes` config field; KDF will no longer auto-connect to 8762 mainnet by default. - Adds two explicit boolean config fields: `is_bootstrap_node` and `disable_p2p`. - Implements a P2P precheck flow that validates the combination of `i_am_seed`, `disable_p2p`, `is_bootstrap_node`, `seednodes`, `p2p_in_memory`, etc, and prevents misconfiguration. **BREAKING:** KDF will not connect to any seed nodes by default. If `seednodes` is not properly set, mainnet connections will fail. Fixes #2409 --- README.md | 3 +- mm2src/mm2_core/src/mm_ctx.rs | 23 +++ mm2src/mm2_main/src/lp_native_dex.rs | 149 ++++++++---------- mm2src/mm2_main/src/lp_swap.rs | 1 + .../docker_tests/docker_ordermatch_tests.rs | 5 + .../tests/docker_tests/docker_tests_common.rs | 2 + .../tests/docker_tests/docker_tests_inner.rs | 35 +++- .../tests/docker_tests/qrc20_tests.rs | 9 ++ .../swaps_confs_settings_sync_tests.rs | 2 + .../docker_tests/swaps_file_lock_tests.rs | 3 + .../tests/docker_tests/tendermint_tests.rs | 3 + .../tests/mm2_tests/bch_and_slp_tests.rs | 5 + .../tests/mm2_tests/best_orders_tests.rs | 4 + .../tests/mm2_tests/lightning_tests.rs | 3 + .../mm2_main/tests/mm2_tests/lp_bot_tests.rs | 1 + .../tests/mm2_tests/mm2_tests_inner.rs | 54 ++++++- .../tests/mm2_tests/orderbook_sync_tests.rs | 11 ++ mm2src/mm2_p2p/src/behaviours/atomicdex.rs | 27 ++-- mm2src/mm2_p2p/src/lib.rs | 2 - mm2src/mm2_p2p/src/network.rs | 80 ---------- mm2src/mm2_test_helpers/src/for_tests.rs | 8 + 21 files changed, 247 insertions(+), 183 deletions(-) delete mode 100644 mm2src/mm2_p2p/src/network.rs diff --git a/README.md b/README.md index d175302c39..7f6e9fbcf7 100755 --- a/README.md +++ b/README.md @@ -117,7 +117,8 @@ For example: "gui": "core_readme", "netid": 8762, "rpc_password": "Ent3r_Un1Qu3_Pa$$w0rd", - "passphrase": "ENTER_UNIQUE_SEED_PHRASE_DONT_USE_THIS_CHANGE_IT_OR_FUNDS_NOT_SAFU" + "passphrase": "ENTER_UNIQUE_SEED_PHRASE_DONT_USE_THIS_CHANGE_IT_OR_FUNDS_NOT_SAFU", + "seednodes": ["example-seed-address1.com", "example-seed-address2.com", "example-seed-address3.com", "example-seed-address4.com"] } ``` diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 5d01b81245..83944e2d5a 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -425,6 +425,29 @@ impl MmCtx { netid as u16 } + pub fn disable_p2p(&self) -> bool { + if let Some(disable_p2p) = self.conf["disable_p2p"].as_bool() { + return disable_p2p; + } + + let default = !self.conf["is_bootstrap_node"].as_bool().unwrap_or(false) + && self.conf["seednodes"].as_array().is_none() + && !self.p2p_in_memory(); + + default + } + + pub fn is_bootstrap_node(&self) -> bool { + if let Some(is_bootstrap_node) = self.conf["is_bootstrap_node"].as_bool() { + return is_bootstrap_node; + } + + let default = !self.conf["disable_p2p"].as_bool().unwrap_or(false) + && self.conf["seednodes"].as_array().map_or(true, |t| t.is_empty()); + + default + } + pub fn p2p_in_memory(&self) -> bool { self.conf["p2p_in_memory"].as_bool().unwrap_or(false) } pub fn p2p_in_memory_port(&self) -> Option { self.conf["p2p_in_memory_port"].as_u64() } diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index fc37bc8624..a24a75d8ce 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -40,8 +40,8 @@ use mm2_err_handle::common_errors::InternalError; use mm2_err_handle::prelude::*; use mm2_libp2p::behaviours::atomicdex::{generate_ed25519_keypair, GossipsubConfig, DEPRECATED_NETID_LIST}; use mm2_libp2p::p2p_ctx::P2PContext; -use mm2_libp2p::{spawn_gossipsub, AdexBehaviourError, NodeType, RelayAddress, RelayAddressError, SeedNodeInfo, - SwarmRuntime, WssCerts}; +use mm2_libp2p::{spawn_gossipsub, AdexBehaviourError, NodeType, RelayAddress, RelayAddressError, SwarmRuntime, + WssCerts}; use mm2_metrics::mm_gauge; use rpc_task::RpcTaskError; use serde_json as json; @@ -69,45 +69,6 @@ cfg_wasm32! { pub mod init_metamask; } -const DEFAULT_NETID_SEEDNODES: &[SeedNodeInfo] = &[ - SeedNodeInfo::new( - "12D3KooWHKkHiNhZtKceQehHhPqwU5W1jXpoVBgS1qst899GjvTm", - "viserion.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWAToxtunEBWCoAHjefSv74Nsmxranw8juy3eKEdrQyGRF", - "rhaegal.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWSmEi8ypaVzFA1AGde2RjxNW5Pvxw3qa2fVe48PjNs63R", - "drogon.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWMrjLmrv8hNgAoVf1RfumfjyPStzd4nv5XL47zN4ZKisb", - "falkor.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWEWzbYcosK2JK9XpFXzumfgsWJW1F7BZS15yLTrhfjX2Z", - "smaug.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWJWBnkVsVNjiqUEPjLyHpiSmQVAJ5t6qt1Txv5ctJi9Xd", - "balerion.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWPR2RoPi19vQtLugjCdvVmCcGLP2iXAzbDfP3tp81ZL4d", - "kalessin.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWEaZpH61H4yuQkaNG5AsyGdpBhKRppaLdAY52a774ab5u", - "seed01.kmdefi.net", - ), - SeedNodeInfo::new( - "12D3KooWAd5gPXwX7eDvKWwkr2FZGfoJceKDCA53SHmTFFVkrN7Q", - "seed02.kmdefi.net", - ), -]; - pub type P2PResult = Result>; pub type MmInitResult = Result>; @@ -132,8 +93,10 @@ pub enum P2PInitError { #[display(fmt = "Invalid relay address: '{}'", _0)] InvalidRelayAddress(RelayAddressError), #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] - #[display(fmt = "WASM node can be a seed if only 'p2p_in_memory' is true")] + #[display(fmt = "WASM node can be a seed only if 'p2p_in_memory' is true")] WasmNodeCannotBeSeed, + #[display(fmt = "Precheck failed: '{}'", reason)] + Precheck { reason: String }, #[display(fmt = "Internal error: '{}'", _0)] Internal(String), } @@ -295,31 +258,6 @@ impl MmInitError { } } -#[cfg(target_arch = "wasm32")] -fn default_seednodes(netid: u16) -> Vec { - if netid == 8762 { - DEFAULT_NETID_SEEDNODES - .iter() - .map(|SeedNodeInfo { domain, .. }| RelayAddress::Dns(domain.to_string())) - .collect() - } else { - Vec::new() - } -} - -#[cfg(not(target_arch = "wasm32"))] -fn default_seednodes(netid: u16) -> Vec { - if netid == 8762 { - DEFAULT_NETID_SEEDNODES - .iter() - .filter_map(|SeedNodeInfo { domain, .. }| mm2_net::ip_addr::addr_to_ipv4_string(domain).ok()) - .map(RelayAddress::IPv4) - .collect() - } else { - Vec::new() - } -} - #[cfg(not(target_arch = "wasm32"))] pub fn fix_directories(ctx: &MmCtx) -> MmInitResult<()> { fix_shared_dbdir(ctx)?; @@ -535,9 +473,9 @@ async fn kick_start(ctx: MmArc) -> MmInitResult<()> { Ok(()) } -fn get_p2p_key(ctx: &MmArc, i_am_seed: bool) -> P2PResult<[u8; 32]> { +fn get_p2p_key(ctx: &MmArc, is_seed_node: bool) -> P2PResult<[u8; 32]> { // TODO: Use persistent peer ID regardless the node type. - if i_am_seed { + if is_seed_node { if let Ok(crypto_ctx) = CryptoCtx::from_ctx(ctx) { let key = sha256(crypto_ctx.mm2_internal_privkey_slice()); return Ok(key.take()); @@ -549,21 +487,74 @@ fn get_p2p_key(ctx: &MmArc, i_am_seed: bool) -> P2PResult<[u8; 32]> { Ok(p2p_key) } -pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { - let i_am_seed = ctx.is_seed_node(); +fn p2p_precheck(ctx: &MmArc) -> P2PResult<()> { + let is_seed_node = ctx.is_seed_node(); + let is_bootstrap_node = ctx.is_bootstrap_node(); + let disable_p2p = ctx.disable_p2p(); + let p2p_in_memory = ctx.p2p_in_memory(); let netid = ctx.netid(); if DEPRECATED_NETID_LIST.contains(&netid) { return MmError::err(P2PInitError::InvalidNetId(NetIdError::Deprecated { netid })); } + let seednodes = seednodes(ctx)?; + + let precheck_err = |reason: &str| { + MmError::err(P2PInitError::Precheck { + reason: reason.to_owned(), + }) + }; + + if is_bootstrap_node { + if !is_seed_node { + return precheck_err("Bootstrap node must also be a seed node."); + } + + if !seednodes.is_empty() { + return precheck_err("Bootstrap node cannot have seed nodes to connect."); + } + } + + if !is_bootstrap_node && seednodes.is_empty() && !disable_p2p { + return precheck_err("Non-bootstrap node must have seed nodes configured to connect."); + } + + if disable_p2p { + if !seednodes.is_empty() { + return precheck_err("Cannot disable P2P while seed nodes are configured."); + } + + if p2p_in_memory { + return precheck_err("Cannot disable P2P while using in-memory P2P mode."); + } + + if is_seed_node { + return precheck_err("Seed nodes cannot disable P2P."); + } + } + + Ok(()) +} + +pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { + p2p_precheck(&ctx)?; + + if ctx.disable_p2p() { + warn!("P2P is disabled. Features that require a P2P network (like swaps, peer health checks, etc.) will not work."); + return Ok(()); + } + + let is_seed_node = ctx.is_seed_node(); + let netid = ctx.netid(); + let seednodes = seednodes(&ctx)?; let ctx_on_poll = ctx.clone(); - let p2p_key = get_p2p_key(&ctx, i_am_seed)?; + let p2p_key = get_p2p_key(&ctx, is_seed_node)?; - let node_type = if i_am_seed { + let node_type = if is_seed_node { relay_node_type(&ctx).await? } else { light_node_type(&ctx)? @@ -616,7 +607,7 @@ pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { let p2p_context = P2PContext::new(cmd_tx, generate_ed25519_keypair(p2p_key)); p2p_context.store_to_mm_arc(&ctx); - let fut = p2p_event_process_loop(ctx.weak(), event_rx, i_am_seed); + let fut = p2p_event_process_loop(ctx.weak(), event_rx, is_seed_node); ctx.spawner().spawn(fut); // Listen for health check messages. @@ -626,15 +617,9 @@ pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { } fn seednodes(ctx: &MmArc) -> P2PResult> { - if ctx.conf["seednodes"].is_null() { - if ctx.p2p_in_memory() { - // If the network is in memory, there is no need to use default seednodes. - return Ok(Vec::new()); - } - return Ok(default_seednodes(ctx.netid())); - } + let seednodes_value = ctx.conf.get("seednodes").unwrap_or(&json!([])).clone(); - json::from_value(ctx.conf["seednodes"].clone()).map_to_mm(|e| P2PInitError::ErrorDeserializingConfig { + json::from_value(seednodes_value).map_to_mm(|e| P2PInitError::ErrorDeserializingConfig { field: "seednodes".to_owned(), error: e.to_string(), }) diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 2c41131927..7744b555d8 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -2154,6 +2154,7 @@ mod lp_swap_tests { "p2p_in_memory": true, "p2p_in_memory_port": 777, "i_am_seed": true, + "is_bootstrap_node": true }); let maker_ctx = MmCtxBuilder::default().with_conf(maker_ctx_conf).into_mm_arc(); diff --git a/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs b/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs index 8f090c91f2..45345ef4d5 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs @@ -199,6 +199,7 @@ fn test_ordermatch_custom_orderbook_ticker_both_on_maker() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -334,6 +335,7 @@ fn test_ordermatch_custom_orderbook_ticker_both_on_taker() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -467,6 +469,7 @@ fn test_ordermatch_custom_orderbook_ticker_mixed_case_one() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -608,6 +611,7 @@ fn test_ordermatch_custom_orderbook_ticker_mixed_case_two() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1183,6 +1187,7 @@ fn test_zombie_order_after_balance_reduce_and_mm_restart() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }); let mm_seed = MarketMakerIt::start(seed_conf, "pass".to_string(), None).unwrap(); diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs index 2425bf8fbb..d513ada2d2 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -978,6 +978,7 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1154,6 +1155,7 @@ pub fn slp_supplied_node() -> MarketMakerIt { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs index 936272b063..c34a2afe55 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -405,6 +405,7 @@ fn order_should_be_cancelled_when_entire_balance_is_withdrawn() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -521,6 +522,7 @@ fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_after_upda "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -654,6 +656,7 @@ fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_before_upd "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -801,6 +804,7 @@ fn test_order_should_be_updated_when_matched_partially() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -906,6 +910,7 @@ fn test_match_and_trade_setprice_max() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1005,6 +1010,7 @@ fn test_max_taker_vol_swap() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1125,6 +1131,7 @@ fn test_buy_when_coins_locked_by_other_swap() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1218,6 +1225,7 @@ fn test_sell_when_coins_locked_by_other_swap() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1310,6 +1318,7 @@ fn test_buy_max() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1374,6 +1383,7 @@ fn test_maker_trade_preimage() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1511,6 +1521,7 @@ fn test_taker_trade_preimage() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1652,6 +1663,7 @@ fn test_trade_preimage_not_sufficient_balance() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1771,6 +1783,7 @@ fn test_trade_preimage_additional_validation() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1910,6 +1923,7 @@ fn test_trade_preimage_legacy() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1980,6 +1994,7 @@ fn test_get_max_taker_vol() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2032,6 +2047,7 @@ fn test_get_max_taker_vol_dex_fee_min_tx_amount() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2093,6 +2109,7 @@ fn test_get_max_taker_vol_dust_threshold() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2144,6 +2161,7 @@ fn test_get_max_taker_vol_with_kmd() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2250,6 +2268,7 @@ fn test_set_price_max() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2305,6 +2324,7 @@ fn swaps_should_stop_on_stop_rpc() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2395,6 +2415,7 @@ fn test_maker_order_should_kick_start_and_appear_in_orderbook_on_restart() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }); let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -2452,6 +2473,7 @@ fn test_maker_order_should_not_kick_start_and_appear_in_orderbook_if_balance_is_ "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }); let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -2547,6 +2569,7 @@ fn test_maker_order_kick_start_should_trigger_subscription_and_match() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }); let relay = MarketMakerIt::start(relay_conf, "pass".to_string(), None).unwrap(); let (_relay_dump_log, _relay_dump_dashboard) = mm_dump(&relay.log_path); @@ -2559,7 +2582,6 @@ fn test_maker_order_kick_start_should_trigger_subscription_and_match() { "coins": coins, "rpc_password": "pass", "seednodes": vec![format!("{}", relay.ip)], - "i_am_seed": false, }); let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -2638,6 +2660,7 @@ fn test_orders_should_match_on_both_nodes_with_same_priv() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2741,6 +2764,7 @@ fn test_maker_and_taker_order_created_with_same_priv_should_not_match() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2812,6 +2836,7 @@ fn test_taker_order_converted_to_maker_should_cancel_properly_when_matched() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2935,6 +2960,7 @@ fn test_utxo_merge() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2988,6 +3014,7 @@ fn test_utxo_merge_max_merge_at_once() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3036,6 +3063,7 @@ fn test_withdraw_not_sufficient_balance() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3122,6 +3150,7 @@ fn test_taker_should_match_with_best_price_buy() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3255,6 +3284,7 @@ fn test_taker_should_match_with_best_price_sell() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3393,6 +3423,7 @@ fn test_match_utxo_with_eth_taker_sell() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3469,6 +3500,7 @@ fn test_match_utxo_with_eth_taker_buy() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3972,6 +4004,7 @@ fn test_withdraw_and_send_eth_erc20() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index 7f759443a5..dfc6f3fabf 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -888,6 +888,7 @@ fn test_check_balance_on_order_post_base_coin_locked() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -987,6 +988,7 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1162,6 +1164,7 @@ fn test_trade_preimage_not_sufficient_base_coin_balance_for_ticker() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1221,6 +1224,7 @@ fn test_trade_preimage_dynamic_fee_not_sufficient_balance() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1282,6 +1286,7 @@ fn test_trade_preimage_deduct_fee_from_output_failed() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1341,6 +1346,7 @@ fn test_segwit_native_balance() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1387,6 +1393,7 @@ fn test_withdraw_and_send_from_segwit() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1435,6 +1442,7 @@ fn test_withdraw_and_send_legacy_to_segwit() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1635,6 +1643,7 @@ fn segwit_address_in_the_orderbook() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, diff --git a/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs b/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs index 31c6c264e3..bda9b8fdfd 100644 --- a/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs @@ -37,6 +37,7 @@ fn test_confirmation_settings_sync_correctly_on_buy( "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -202,6 +203,7 @@ fn test_confirmation_settings_sync_correctly_on_sell( "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, diff --git a/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs b/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs index 785eb0a849..6446fa3a4f 100644 --- a/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs @@ -45,6 +45,7 @@ fn swap_file_lock_prevents_double_swap_start_on_kick_start(swap_json: &str) { "rpc_password": "pass", "i_am_seed": true, "dbdir": db_folder.to_str().unwrap(), + "is_bootstrap_node": true }); let mut mm_bob = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -88,6 +89,7 @@ fn test_swaps_should_kick_start_if_process_was_killed() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }); let mut mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -211,6 +213,7 @@ fn swap_should_not_kick_start_if_finished_during_waiting_for_file_lock( "rpc_password": "pass", "i_am_seed": true, "dbdir": db_folder.to_str().unwrap(), + "is_bootstrap_node": true }); let mut mm_bob = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index d51bc50d34..58ad67e645 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -850,6 +850,7 @@ mod swap { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -941,6 +942,7 @@ mod swap { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -1036,6 +1038,7 @@ mod swap { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, diff --git a/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs b/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs index cc1094e668..f03cb18e06 100644 --- a/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs @@ -34,6 +34,7 @@ fn test_withdraw_cashaddresses() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -238,6 +239,7 @@ fn test_withdraw_to_different_cashaddress_network_should_fail() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -299,6 +301,7 @@ fn test_common_cashaddresses() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -502,6 +505,7 @@ fn test_sign_verify_message_bch() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -571,6 +575,7 @@ fn test_sign_verify_message_slp() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, diff --git a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs index f1149c15f3..705dec58f4 100644 --- a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs @@ -30,6 +30,7 @@ fn test_best_orders_v2_exclude_mine() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -137,6 +138,7 @@ fn test_best_orders_no_duplicates_after_update() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -291,6 +293,7 @@ fn test_best_orders_address_and_confirmations() { "coins": bob_coins_config, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -603,6 +606,7 @@ fn zhtlc_best_orders() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, diff --git a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs index 4fde665bd6..22c52114ce 100644 --- a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs @@ -112,6 +112,7 @@ fn start_lightning_nodes(enable_0_confs: bool) -> (MarketMakerIt, MarketMakerIt, "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -368,6 +369,7 @@ fn test_enable_lightning() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -1055,6 +1057,7 @@ fn test_sign_verify_message_lightning() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, diff --git a/mm2src/mm2_main/tests/mm2_tests/lp_bot_tests.rs b/mm2src/mm2_main/tests/mm2_tests/lp_bot_tests.rs index 846508d2f2..e95fbcff38 100644 --- a/mm2src/mm2_main/tests/mm2_tests/lp_bot_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/lp_bot_tests.rs @@ -19,6 +19,7 @@ fn test_start_and_stop_simple_market_maker_bot() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 7ad99c3b8c..988f7f6a4d 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -118,6 +118,7 @@ fn orders_of_banned_pubkeys_should_not_be_displayed() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -222,6 +223,7 @@ fn test_my_balance() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -363,6 +365,7 @@ fn test_check_balance_on_order_post() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -423,6 +426,7 @@ fn test_rpc_password_from_json() { "rpc_password": "", "i_am_seed": true, "skip_startup_checks": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -440,6 +444,7 @@ fn test_rpc_password_from_json() { "rpc_password": {"key":"value"}, "i_am_seed": true, "skip_startup_checks": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -455,6 +460,7 @@ fn test_rpc_password_from_json() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -549,6 +555,7 @@ fn test_mmrpc_v2() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -653,7 +660,7 @@ fn test_rpc_password_from_json_no_userpass() { "netid": 9998, "passphrase": "bob passphrase", "coins": coins, - "i_am_seed": true, + "disable_p2p": true }), "password".into(), None, @@ -943,6 +950,7 @@ fn test_withdraw_and_send() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -1262,6 +1270,7 @@ fn test_swap_status() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -1315,6 +1324,7 @@ fn test_order_errors_when_base_equal_rel() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1374,6 +1384,7 @@ fn startup_passphrase(passphrase: &str, expected_address: &str) { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1441,6 +1452,7 @@ fn test_cancel_order() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -1586,6 +1598,7 @@ fn test_cancel_all_orders() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -1734,6 +1747,7 @@ fn test_electrum_enable_conn_errors() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -1780,6 +1794,7 @@ fn test_order_should_not_be_displayed_when_node_is_down() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -1892,6 +1907,7 @@ fn test_own_orders_should_not_be_removed_from_orderbook() { "i_am_seed": true, "rpc_password": "pass", "maker_order_timeout": 5, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1968,6 +1984,7 @@ fn test_show_priv_key() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -2008,6 +2025,7 @@ fn test_electrum_and_enable_response() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -2111,6 +2129,7 @@ fn set_price_with_cancel_previous_should_broadcast_cancelled_message() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -2236,6 +2255,7 @@ fn test_batch_requests() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -2311,6 +2331,7 @@ fn test_metrics_method() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -2361,7 +2382,8 @@ fn test_electrum_tx_history() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", - "metrics_interval": 30. + "metrics_interval": 30., + "is_bootstrap_node": true }), "pass".into(), None, @@ -2462,6 +2484,7 @@ fn test_convert_utxo_address() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -2673,6 +2696,7 @@ fn test_convert_eth_address() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -2779,6 +2803,7 @@ fn test_add_delegation_qtum() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -2865,6 +2890,7 @@ fn test_remove_delegation_qtum() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -2922,6 +2948,7 @@ fn test_query_delegations_info_qtum() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -2978,6 +3005,7 @@ fn test_convert_qrc20_address() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -3125,6 +3153,7 @@ fn test_validateaddress() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -3339,6 +3368,7 @@ fn qrc20_activate_electrum() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -3387,6 +3417,7 @@ fn test_qrc20_withdraw() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -3469,6 +3500,7 @@ fn test_qrc20_withdraw_error() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -3552,6 +3584,7 @@ fn test_get_raw_transaction() { "i_am_seed": true, "rpc_password": "pass", "metrics_interval": 30., + "is_bootstrap_node": true }), "pass".into(), None, @@ -3947,6 +3980,7 @@ fn test_update_maker_order() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4087,6 +4121,7 @@ fn test_update_maker_order_fail() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4290,6 +4325,7 @@ fn test_trade_fee_returns_numbers_in_various_formats() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -4332,6 +4368,7 @@ fn test_orderbook_is_mine_orders() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -4511,6 +4548,7 @@ fn test_mm2_db_migration() { "rpc_password": "password", "i_am_seed": true, "dbdir": mm2_folder.display().to_string(), + "is_bootstrap_node": true }), "password".into(), None, @@ -4570,6 +4608,7 @@ fn test_get_public_key() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4612,6 +4651,7 @@ fn test_get_public_key_hash() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4652,6 +4692,7 @@ fn test_get_orderbook_with_same_orderbook_ticker() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4699,6 +4740,7 @@ fn test_conf_settings_in_orderbook() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4822,6 +4864,7 @@ fn alice_can_see_confs_in_orderbook_after_sync() { "rpc_password": "password", "coins": bob_coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4973,6 +5016,7 @@ fn test_sign_verify_message_utxo() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -5040,6 +5084,7 @@ fn test_sign_verify_message_utxo_segwit() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -5121,6 +5166,7 @@ fn test_sign_verify_message_eth() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -5568,6 +5614,7 @@ fn test_enable_btc_with_sync_starting_header() { "passphrase": "bob passphrase", "coins": coins, "rpc_password": "pass", + "disable_p2p": true }), "pass".into(), None, @@ -5598,6 +5645,7 @@ fn test_btc_block_header_sync() { "passphrase": "bob passphrase", "coins": coins, "rpc_password": "pass", + "disable_p2p": true }), "pass".into(), None, @@ -5636,6 +5684,7 @@ fn test_tbtc_block_header_sync() { "passphrase": "bob passphrase", "coins": coins, "rpc_password": "pass", + "disable_p2p": true }), "pass".into(), None, @@ -6056,6 +6105,7 @@ fn test_connection_healthcheck_rpc() { thread::sleep(Duration::from_secs(2)); let mut alice_conf = Mm2TestConf::seednode(ALICE_SEED, &json!([])); + alice_conf.conf["is_bootstrap_node"] = json!(false); alice_conf.conf["seednodes"] = json!([bob_mm.my_seed_addr()]); alice_conf.conf["skip_startup_checks"] = json!(true); let alice_mm = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); diff --git a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs index 187404a580..832f23cfae 100644 --- a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs @@ -36,6 +36,7 @@ fn alice_can_see_the_active_order_after_connection() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -254,6 +255,7 @@ fn alice_can_see_the_active_order_after_orderbook_sync_segwit() { "coins": bob_coins_config, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -427,6 +429,7 @@ fn test_orderbook_segwit() { "coins": bob_coins_config, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -540,6 +543,7 @@ fn test_get_orderbook_with_same_orderbook_ticker() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -586,6 +590,7 @@ fn test_conf_settings_in_orderbook() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -708,6 +713,7 @@ fn alice_can_see_confs_in_orderbook_after_sync() { "rpc_password": "password", "coins": bob_coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -848,6 +854,7 @@ fn orderbook_extended_data() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -960,6 +967,7 @@ fn orderbook_should_display_base_rel_volumes() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1058,6 +1066,7 @@ fn orderbook_should_work_without_coins_activation() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1139,6 +1148,7 @@ fn test_all_orders_per_pair_per_node_must_be_displayed_in_orderbook() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1210,6 +1220,7 @@ fn setprice_min_volume_should_be_displayed_in_orderbook() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, diff --git a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs index ad649328b4..5d5806582c 100644 --- a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs +++ b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs @@ -22,7 +22,6 @@ use rand::seq::SliceRandom; use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; use std::hash::{Hash, Hasher}; -use std::iter; use std::net::IpAddr; use std::sync::{Mutex, MutexGuard}; use std::task::{Context, Poll}; @@ -34,7 +33,6 @@ use super::request_response::{build_request_response_behaviour, PeerRequest, Pee RequestResponseSender}; use crate::application::request_response::network_info::NetworkInfoRequest; use crate::application::request_response::P2PRequest; -use crate::network::{get_all_network_seednodes, DEFAULT_NETID}; use crate::relay_address::{RelayAddress, RelayAddressError}; use crate::swarm_runtime::SwarmRuntime; use crate::{decode_message, encode_message, NetworkInfo, NetworkPorts, RequestResponseBehaviourEvent}; @@ -54,6 +52,7 @@ const CONNECTED_RELAYS_CHECK_INTERVAL: Duration = Duration::from_secs(30); const ANNOUNCE_INTERVAL: Duration = Duration::from_secs(600); const ANNOUNCE_INITIAL_DELAY: Duration = Duration::from_secs(60); const CHANNEL_BUF_SIZE: usize = 1024 * 8; +const DEFAULT_NETID: u16 = 8762; /// Used in time validation logic for each peer which runs immediately after the /// `ConnectionEstablished` event. @@ -714,23 +713,12 @@ fn start_gossipsub( .map_err(|e| AdexBehaviourError::InitializationError(e.to_owned()))?; // build a gossipsub network behaviour - let mut gossipsub = Gossipsub::new(MessageAuthenticity::Author(local_peer_id), gossipsub_config) + let gossipsub = Gossipsub::new(MessageAuthenticity::Author(local_peer_id), gossipsub_config) .map_err(|e| AdexBehaviourError::InitializationError(e.to_owned()))?; let floodsub = Floodsub::new(local_peer_id, config.netid != DEFAULT_NETID); - let mut peers_exchange = PeersExchange::new(network_info); - if !network_info.in_memory() { - // Please note WASM nodes don't support `PeersExchange` currently, - // so `get_all_network_seednodes` returns an empty list. - for (peer_id, addr, _domain) in get_all_network_seednodes(config.netid) { - let multiaddr = addr.try_to_multiaddr(network_info)?; - peers_exchange.add_peer_addresses_to_known_peers(&peer_id, iter::once(multiaddr).collect()); - if peer_id != local_peer_id { - gossipsub.add_explicit_relay(peer_id); - } - } - } + let peers_exchange = PeersExchange::new(network_info); // build a request-response network behaviour let request_response = build_request_response_behaviour(); @@ -795,6 +783,14 @@ fn start_gossipsub( Err(e) => error!("Dial {:?} failed: {:?}", relay, e), } } + + // All currently connected peers come from the config file (because we didn't connect any other + // ones yet), so it's safe to treat them as trusted nodes. + let peers: Vec<_> = libp2p::Swarm::connected_peers(&swarm).cloned().collect(); + for peer in peers { + swarm.behaviour_mut().core.gossipsub.add_explicit_peer(&peer); + } + drop(recently_dialed_peers); let mut check_connected_relays_interval = @@ -817,6 +813,7 @@ fn start_gossipsub( if swarm.disconnect_peer_id(peer_id).is_err() { error!("Disconnection from `{peer_id}` failed unexpectedly, which should never happen."); } + swarm.behaviour_mut().core.gossipsub.remove_explicit_peer(&peer_id); } loop { diff --git a/mm2src/mm2_p2p/src/lib.rs b/mm2src/mm2_p2p/src/lib.rs index b1d0283be0..ff05a6d4f6 100644 --- a/mm2src/mm2_p2p/src/lib.rs +++ b/mm2src/mm2_p2p/src/lib.rs @@ -2,7 +2,6 @@ pub mod behaviours; -mod network; mod relay_address; mod swarm_runtime; @@ -39,7 +38,6 @@ pub use libp2p::identity::{secp256k1::PublicKey as Libp2pSecpPublic, Keypair, Pu pub use libp2p::{Multiaddr, PeerId}; // relay-address related re-exports -pub use network::SeedNodeInfo; pub use relay_address::RelayAddress; pub use relay_address::RelayAddressError; diff --git a/mm2src/mm2_p2p/src/network.rs b/mm2src/mm2_p2p/src/network.rs deleted file mode 100644 index 6d5524a31e..0000000000 --- a/mm2src/mm2_p2p/src/network.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::relay_address::RelayAddress; -use libp2p::PeerId; - -pub const DEFAULT_NETID: u16 = 8762; - -pub struct SeedNodeInfo { - pub id: &'static str, - pub domain: &'static str, -} - -impl SeedNodeInfo { - pub const fn new(id: &'static str, domain: &'static str) -> Self { Self { id, domain } } -} - -#[cfg_attr(target_arch = "wasm32", allow(dead_code))] -const ALL_DEFAULT_NETID_SEEDNODES: &[SeedNodeInfo] = &[ - SeedNodeInfo::new( - "12D3KooWHKkHiNhZtKceQehHhPqwU5W1jXpoVBgS1qst899GjvTm", - "viserion.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWAToxtunEBWCoAHjefSv74Nsmxranw8juy3eKEdrQyGRF", - "rhaegal.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWSmEi8ypaVzFA1AGde2RjxNW5Pvxw3qa2fVe48PjNs63R", - "drogon.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWMrjLmrv8hNgAoVf1RfumfjyPStzd4nv5XL47zN4ZKisb", - "falkor.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWEWzbYcosK2JK9XpFXzumfgsWJW1F7BZS15yLTrhfjX2Z", - "smaug.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWJWBnkVsVNjiqUEPjLyHpiSmQVAJ5t6qt1Txv5ctJi9Xd", - "balerion.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWPR2RoPi19vQtLugjCdvVmCcGLP2iXAzbDfP3tp81ZL4d", - "kalessin.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWJDoV9vJdy6PnzwVETZ3fWGMhV41VhSbocR1h2geFqq9Y", - "icefyre.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWEaZpH61H4yuQkaNG5AsyGdpBhKRppaLdAY52a774ab5u", - "seed01.kmdefi.net", - ), - SeedNodeInfo::new( - "12D3KooWAd5gPXwX7eDvKWwkr2FZGfoJceKDCA53SHmTFFVkrN7Q", - "seed02.kmdefi.net", - ), -]; - -#[cfg(target_arch = "wasm32")] -pub fn get_all_network_seednodes(_netid: u16) -> Vec<(PeerId, RelayAddress, String)> { Vec::new() } - -#[cfg(not(target_arch = "wasm32"))] -pub fn get_all_network_seednodes(netid: u16) -> Vec<(PeerId, RelayAddress, String)> { - use std::str::FromStr; - - if netid != DEFAULT_NETID { - return Vec::new(); - } - ALL_DEFAULT_NETID_SEEDNODES - .iter() - .map(|SeedNodeInfo { id, domain }| { - let peer_id = PeerId::from_str(id).unwrap_or_else(|e| panic!("Valid peer id {id}: {e}")); - let ip = - mm2_net::ip_addr::addr_to_ipv4_string(domain).unwrap_or_else(|e| panic!("Valid domain {domain}: {e}")); - let address = RelayAddress::IPv4(ip); - let domain = domain.to_string(); - (peer_id, address, domain) - }) - .collect() -} diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 5f5fa8edfc..7fd4dc4c35 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -275,6 +275,7 @@ impl Mm2TestConf { "coins": coins, "rpc_password": DEFAULT_RPC_PASSWORD, "i_am_seed": true, + "is_bootstrap_node": true }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -291,6 +292,7 @@ impl Mm2TestConf { "rpc_password": DEFAULT_RPC_PASSWORD, "i_am_seed": true, "use_trading_proto_v2": true, + "is_bootstrap_node": true }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -306,6 +308,7 @@ impl Mm2TestConf { "rpc_password": DEFAULT_RPC_PASSWORD, "i_am_seed": true, "enable_hd": true, + "is_bootstrap_node": true }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -322,6 +325,7 @@ impl Mm2TestConf { "i_am_seed": true, "enable_hd": true, "use_trading_proto_v2": true, + "is_bootstrap_node": true }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -337,6 +341,7 @@ impl Mm2TestConf { "i_am_seed": true, "wallet_name": wallet_name, "wallet_password": wallet_password, + "is_bootstrap_node": true }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -1863,6 +1868,7 @@ pub fn mm_spat() -> (&'static str, MarketMakerIt, RaiiDump, RaiiDump) { ], "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true, }), "pass".into(), None, @@ -3722,6 +3728,8 @@ pub async fn test_qrc20_history_impl(local_start: Option) { "coins": coins, "rpc_password": "pass", "metrics_interval": 30., + "disable_p2p": true, + "p2p_in_memory": false }), "pass".into(), local_start, From 015b731daee74f5efb324bcf6f43da331e5c41d3 Mon Sep 17 00:00:00 2001 From: dragonhound <35845239+smk762@users.noreply.github.com> Date: Wed, 28 May 2025 16:46:27 +0800 Subject: [PATCH 14/36] chore(docs): update old urls referencing atomicdex or old docs pages (#2428) --- README.md | 24 +++++++++++++----------- mm2src/mm2_main/src/mm2.rs | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7f6e9fbcf7..6b67fc4086 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

@@ -49,13 +49,13 @@ ## What is the Komodo DeFi Framework? -The Komodo DeFi Framework is open-source [atomic-swap](https://komodoplatform.com/en/academy/atomic-swaps/) software for seamless, decentralized, peer to peer trading between almost every blockchain asset in existence. This software works with propagation of orderbooks and swap states through the [libp2p](https://libp2p.io/) protocol and uses [Hash Time Lock Contracts (HTLCs)](https://en.bitcoinwiki.org/wiki/Hashed_Timelock_Contracts) for ensuring that the two parties in a swap either mutually complete a trade, or funds return to thier original owner. +The Komodo DeFi Framework is open-source [atomic-swap](https://komodoplatform.com/en/docs/komodo-defi-framework/tutorials/#technical-comparisons) software for seamless, decentralized, peer to peer trading between almost every blockchain asset in existence. This software works with propagation of orderbooks and swap states through the [libp2p](https://libp2p.io/) protocol and uses [Hash Time Lock Contracts (HTLCs)](https://en.bitcoinwiki.org/wiki/Hashed_Timelock_Contracts) for ensuring that the two parties in a swap either mutually complete a trade, or funds return to thier original owner. There is no 3rd party intermediary, no proxy tokens, and at all times users remain in sole possession of their private keys. -A [well documented API](https://developers.komodoplatform.com/basic-docs/atomicdex/introduction-to-atomicdex.html) offers simple access to the underlying services using simple language agnostic JSON structured methods and parameters such that users can communicate with the core in a variety of methods such as [curl](https://developers.komodoplatform.com/basic-docs/atomicdex-api-legacy/buy.html) in CLI, or fully functioning [desktop and mobile applications](https://atomicdex.io/) like [Komodo Wallet Desktop](https://github.com/KomodoPlatform/komodo-wallet-desktop). +A [well documented API](https://komodoplatform.com/en/docs/komodo-defi-framework/tutorials/) offers simple access to the underlying services using simple language agnostic JSON structured methods and parameters such that users can communicate with the core in a variety of methods such as [curl](https://komodoplatform.com/en/docs/komodo-defi-framework/api/legacy/buy/) in CLI, or fully functioning [browser, desktop and mobile wallet apps](https://komodoplatform.com/en/downloads/) like [Komodo Wallet](https://github.com/KomodoPlatform/komodo-wallet). -For a curated list of Komodo DeFi Framework based projects and resources, check out [Awesome AtomicDEX](https://github.com/KomodoPlatform/awesome-atomicdex). +For a curated list of Komodo DeFi Framework based projects and resources, check out [Awesome KomoDeFi]( https://github.com/KomodoPlatform/awesome-komodefi). ## Features @@ -63,13 +63,13 @@ For a curated list of Komodo DeFi Framework based projects and resources, check - Perform blockchain transactions without a local native chain (e.g. via Electrum servers) - Query orderbooks for all pairs within the [supported coins](https://github.com/KomodoPlatform/coins/blob/master/coins) - Buy/sell from the orderbook, or create maker orders -- Configure automated ["makerbot" trading](https://developers.komodoplatform.com/basic-docs/atomicdex-api-20/start_simple_market_maker_bot.html) with periodic price updates and optional [telegram](https://telegram.org/) alerts +- Configure automated ["makerbot" trading](https://komodoplatform.com/en/docs/komodo-defi-framework/api/v20/swaps_and_orders/start_simple_market_maker_bot/) with periodic price updates and optional [telegram](https://telegram.org/) alerts ## Building from source ### On Host System: -[Pre-built release binaries](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/get-started-atomicdex.html) are available for OSX, Linux or Windows. +[Pre-built release binaries](https://github.com/KomodoPlatform/komodo-defi-framework/releases) are available for Android, iOS, OSX, Linux, Windows and WASM. If you want to build from source, the following prerequisites are required: - [Rustup](https://rustup.rs/) @@ -81,7 +81,7 @@ If you want to build from source, the following prerequisites are required: To build, run `cargo build` (or `cargo build -vv` to get verbose build output). -For more detailed instructions, please refer to the [Installation Guide](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/get-started-atomicdex.html). +For more detailed instructions, please refer to the [Installation Guide](https://komodoplatform.com/en/docs/komodo-defi-framework/setup/). ### From Container: @@ -101,6 +101,8 @@ docker run -v "$(pwd)":/app -w /app kdf-build-container cargo build Just like building it on your host system, you will now have the target directory containing the build files. +Alternatively, container images are available on [DockerHub](https://hub.docker.com/r/komodoofficial/komodo-defi-framework) + ## Building WASM binary Please refer to the [WASM Build Guide](./docs/WASM_BUILD.md). @@ -109,7 +111,7 @@ Please refer to the [WASM Build Guide](./docs/WASM_BUILD.md). Basic config is contained in two files, `MM2.json` and `coins` -The user configuration [MM2.json file](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/configure-mm2-json.html) contains rpc credentials, your mnemonic seed phrase, a `netid` (8762 is the current main network) and some extra [optional parameters](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/get-started-atomicdex.html). +The user configuration `MM2.json` file contains rpc credentials, your mnemonic seed phrase, a `netid` (8762 is the current main network) and some extra [optional parameters](https://komodoplatform.com/en/docs/komodo-defi-framework/setup/configure-mm2-json/). For example: ```json @@ -169,7 +171,7 @@ curl --url "http://127.0.0.1:7783" --data '{ }' ``` -Refer to the [Komodo Developer Docs](https://developers.komodoplatform.com/basic-docs/atomicdex/introduction-to-atomicdex.html) for details of additional RPC methods and parameters +Refer to the [Komodo Developer Docs](https://komodoplatform.com/en/docs/komodo-defi-framework/api/) for details of additional RPC methods and parameters ## Project structure @@ -182,7 +184,7 @@ Refer to the [Komodo Developer Docs](https://developers.komodoplatform.com/basic - [Contribution guide](./docs/CONTRIBUTING.md) - [Setting up the environment to run the full tests suite](./docs/DEV_ENVIRONMENT.md) - [Git flow and general workflow](./docs/GIT_FLOW_AND_WORKING_PROCESS.md) -- [Komodo Developer Docs](https://developers.komodoplatform.com/basic-docs/atomicdex/introduction-to-atomicdex.html) +- [Komodo Developer Docs](https://komodoplatform.com/en/docs/komodo-defi-framework/) ## Disclaimer @@ -195,5 +197,5 @@ The current state can be considered as an alpha version. ## Help and troubleshooting -If you have any question/want to report a bug/suggest an improvement feel free to [open an issue](https://github.com/KomodoPlatform/komodo-defi-framework/issues/new/choose) or join the [Komodo Platform Discord](https://discord.gg/PGxVm2y) `dev-marketmaker` channel. +If you have any question/want to report a bug/suggest an improvement feel free to [open an issue](https://github.com/KomodoPlatform/komodo-defi-framework/issues/new/choose) or join the [Komodo Platform Discord](https://discord.gg/PGxVm2y) `dev-general` channel. diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index a7c9063725..37ad202ed8 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -247,12 +247,12 @@ Environment variables: Defaults to `MM2.json` MM_COINS_PATH .. File path. MM2 will try to load coins data from this file. File must contain valid json. - Recommended: https://github.com/jl777/coins/blob/master/coins. + Recommended: https://github.com/komodoplatform/coins/blob/master/coins. Defaults to `coins`. MM_LOG .. File path. Must end with '.log'. MM will log to this file. See also the online documentation at -https://developers.atomicdex.io +https://komodoplatform.com/en/docs "#; println!("{}", HELP_MSG); From 82144f69f0427503558dfa07879c5b92c6547507 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 28 May 2025 10:51:22 +0200 Subject: [PATCH 15/36] fix(p2pk): validate expected pubkey correctly for p2pk inputs (#2408) For p2pk inputs, the pubkey isn't included in the scriptSig. The scriptSig only includes the signature and that's it. This commit verifies the signature in the scriptSig using the expected pubkey. --- mm2src/coins/qrc20.rs | 3 +- .../utxo/rpc_clients/electrum_rpc/client.rs | 15 +- mm2src/coins/utxo/utxo_common.rs | 229 ++++++++++++++++-- mm2src/mm2_bitcoin/keys/src/public.rs | 9 +- mm2src/mm2_bitcoin/script/src/script.rs | 33 +++ 5 files changed, 255 insertions(+), 34 deletions(-) diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index ad01bba16f..07aa04aad2 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -842,7 +842,8 @@ impl SwapOps for Qrc20Coin { }, }; let fee_tx_hash = fee_tx.hash().reversed().into(); - let inputs_signed_by_pub = check_all_utxo_inputs_signed_by_pub(fee_tx, validate_fee_args.expected_sender)?; + let inputs_signed_by_pub = + check_all_utxo_inputs_signed_by_pub(self, fee_tx, validate_fee_args.expected_sender).await?; if !inputs_signed_by_pub { return MmError::err(ValidatePaymentError::WrongPaymentTx( "The dex fee was sent from wrong address".to_string(), diff --git a/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs index e7c5311db2..3cf3996f09 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs @@ -806,8 +806,12 @@ impl UtxoRpcClientOps for ElectrumClient { // If the plain pubkey is available, fetch the UTXOs found in P2PK outputs as well (if any). if let Some(pubkey) = address.pubkey() { - let p2pk_output_script = output_script_p2pk(pubkey); - output_scripts.push(p2pk_output_script); + // We don't want to show P2PK outputs along with segwit ones (P2WPKH). + // Allow listing the P2PK outputs only if the address is not segwit (i.e. show P2PK outputs along with P2PKH). + if !address.addr_format().is_segwit() { + let p2pk_output_script = output_script_p2pk(pubkey); + output_scripts.push(p2pk_output_script); + } } let this = self.clone(); @@ -937,8 +941,11 @@ impl UtxoRpcClientOps for ElectrumClient { // If the plain pubkey is available, fetch the balance found in P2PK output as well (if any). if let Some(pubkey) = address.pubkey() { - let p2pk_output_script = output_script_p2pk(pubkey); - hashes.push(hex::encode(electrum_script_hash(&p2pk_output_script))); + // Show the balance in P2PK outputs only for the non-segwit legacy addresses (P2PKH). + if !address.addr_format().is_segwit() { + let p2pk_output_script = output_script_p2pk(pubkey); + hashes.push(hex::encode(electrum_script_hash(&p2pk_output_script))); + } } let this = self.clone(); diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 2e792f05ac..c6eed83761 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2019,19 +2019,100 @@ pub async fn send_maker_refunds_payment( refund_htlc_payment(coin, args).await.map(|tx| tx.into()) } +/// Sets the amount of the input at the given index to the value of the corresponding output in the previous transaction. +/// +/// This invokes the RPC client to fetch the previous transaction and extract the output value. +pub async fn set_index_amount_from_prev_tx( + rpc_client: &UtxoRpcClientEnum, + signer: &mut TransactionInputSigner, + idx: usize, +) -> Result<(), String> { + let inputs_len = signer.inputs.len(); + let input = signer.inputs.get_mut(idx).ok_or_else(|| { + format!( + "Input index {} out of bounds for transaction with {} inputs", + idx, inputs_len + ) + })?; + let prev_output_tx_hash = input.previous_output.hash.reversed().into(); + let prev_output_index = input.previous_output.index as usize; + let prev_tx_hex = rpc_client + .get_transaction_bytes(&prev_output_tx_hash) + .compat() + .await + .map_err(|e| format!("Failed to get prev tx hex: {e}"))?; + let prev_tx: UtxoTx = deserialize(prev_tx_hex.0.as_slice()) + .map_err(|e| format!("Failed to deserialize prev tx {}: {}", prev_output_tx_hash, e))?; + let prev_output = prev_tx.outputs.get(prev_output_index).ok_or_else(|| { + format!( + "Prev tx output index {} out of bounds for tx {}", + input.previous_output.index, + prev_tx.hash() + ) + })?; + input.amount = prev_output.value; + Ok(()) +} + +/// Verifies that the script that spends a P2PK is signed by the expected pubkey. +fn verify_p2pk_input_pubkey( + script: &Script, + expected_pubkey: &Public, + unsigned_tx: &TransactionInputSigner, + index: usize, + signature_version: SignatureVersion, + fork_id: u32, +) -> Result { + // Extract the signature from the scriptSig. + let signature = script.extract_signature()?; + // Validate the signature. + try_s!(SecpSignature::from_der(&signature[..signature.len() - 1])); + let signature = signature.into(); + // Make sure we have no more instructions. P2PK scriptSigs consist of a single instruction only containing the signature. + if script.get_instruction(1).is_some() { + return ERR!("Unexpected instruction at position 2 of script {:?}", script); + }; + // Get the scriptPub for this input. We need it to get the transaction sig_hash to sign (but actually "to verify" in this case). + let pubkey = expected_pubkey + .to_secp256k1_pubkey() + .map_err(|e| ERRL!("Error converting plain pubkey to secp256k1 pubkey: {}", e))?; + // P2PK scriptPub has two valid possible formats depending on whether the public key is written in compressed or uncompressed form. + let possible_pubkey_scripts = [ + Builder::build_p2pk(&Public::Compressed(pubkey.serialize().into())), + Builder::build_p2pk(&Public::Normal(pubkey.serialize_uncompressed().into())), + ]; + for pubkey_script in possible_pubkey_scripts { + // Get the transaction hash that has been signed in the scriptSig. + let hash = match signature_hash_to_sign( + unsigned_tx, + index, + &pubkey_script, + signature_version, + SIGHASH_ALL, + fork_id, + ) { + Ok(hash) => hash, + Err(e) => return ERR!("Error calculating signature hash: {}", e), + }; + // Verify that the signature is valid for the transaction hash with respect to the expected public key. + return match expected_pubkey.verify(&hash, &signature) { + Ok(true) => Ok(true), + // The signature is invalid for this pubkey, try the other possible pubkey script. + Ok(false) => continue, + Err(e) => ERR!("Error verifying signature: {}", e), + }; + } + + // Both possible pubkey scripts failed to verify the signature. + Ok(false) +} + /// Extracts pubkey from script sig fn pubkey_from_script_sig(script: &Script) -> Result { - match script.get_instruction(0) { - Some(Ok(instruction)) => match instruction.opcode { - Opcode::OP_PUSHBYTES_70 | Opcode::OP_PUSHBYTES_71 | Opcode::OP_PUSHBYTES_72 => match instruction.data { - Some(bytes) => try_s!(SecpSignature::from_der(&bytes[..bytes.len() - 1])), - None => return ERR!("No data at instruction 0 of script {:?}", script), - }, - _ => return ERR!("Unexpected opcode {:?}", instruction.opcode), - }, - Some(Err(e)) => return ERR!("Error {} on getting instruction 0 of script {:?}", e, script), - None => return ERR!("None instruction 0 of script {:?}", script), - }; + // Extract the signature from the scriptSig. + let signature = script.extract_signature()?; + // Validate the signature. + try_s!(SecpSignature::from_der(&signature[..signature.len() - 1])); let pubkey = match script.get_instruction(1) { Some(Ok(instruction)) => match instruction.opcode { @@ -2087,18 +2168,65 @@ where } } -pub fn check_all_utxo_inputs_signed_by_pub( +/// This function is used to check that all inputs are signed/owned by the expected pubkey. +/// +/// It's used to verify that all the inputs of the taker-sent dex fee are signed/owned by the taker's pubkey. +/// It's used also by watcher to verify that all the taker payment inputs are signed/owned by the taker's pubkey. +/// The `expected_pub` should be the taker's pubkey in compressed (33-byte) format. +pub async fn check_all_utxo_inputs_signed_by_pub( + coin: &T, tx: &UtxoTx, expected_pub: &[u8], ) -> Result> { - for input in &tx.inputs { + let expected_pub = + H264::from_slice(expected_pub).map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; + let mut unsigned_tx: Option = None; + + for (idx, input) in tx.inputs.iter().enumerate() { + let script = Script::from(input.script_sig.clone()); + + // This handles the case where the input is a P2PK input. + if !input.has_witness() && script.does_script_spend_p2pk() { + let unsigned_tx = unsigned_tx.get_or_insert_with(|| tx.clone().into()); + // If the transaction is overwintered, we need to set the consensus branch id and the input's amount. + // This is needed for the sighash calculation. + if unsigned_tx.overwintered { + set_index_amount_from_prev_tx(&coin.as_ref().rpc_client, unsigned_tx, idx) + .await + .map_err(|e| { + ValidatePaymentError::TxDeserializationError(format!( + "Failed to set index amount for input {}: {}", + idx, e + )) + })?; + unsigned_tx.consensus_branch_id = coin.as_ref().conf.consensus_branch_id; + } + // Verfiy that the P2PK input's scriptSig corresponds to the expected public key. + let successful_verification = verify_p2pk_input_pubkey( + &script, + &Public::Compressed(expected_pub), + unsigned_tx, + idx, + coin.as_ref().conf.signature_version, + coin.as_ref().conf.fork_id, + ) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if successful_verification { + // No pubkey extraction for P2PK inputs. Continue. + continue; + } + return Ok(false); + } + let pubkey = if input.has_witness() { + // Extract the pubkey from a P2WPKH scriptSig. pubkey_from_witness_script(&input.script_witness).map_to_mm(ValidatePaymentError::TxDeserializationError)? } else { - let script: Script = input.script_sig.clone().into(); + // Extract the pubkey from a P2PKH scriptSig. pubkey_from_script_sig(&script).map_to_mm(ValidatePaymentError::TxDeserializationError)? }; - if *pubkey != expected_pub { + + if pubkey != expected_pub { return Ok(false); } } @@ -2146,7 +2274,8 @@ pub fn watcher_validate_taker_fee( }; let taker_fee_tx: UtxoTx = deserialize(tx_from_rpc.hex.0.as_slice())?; - let inputs_signed_by_pub = check_all_utxo_inputs_signed_by_pub(&taker_fee_tx, &sender_pubkey)?; + let inputs_signed_by_pub = + check_all_utxo_inputs_signed_by_pub(&coin, &taker_fee_tx, &sender_pubkey).await?; if !inputs_signed_by_pub { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Taker fee does not belong to the verified public key", @@ -2278,17 +2407,18 @@ pub fn validate_fee( ) -> ValidatePaymentFut<()> { let dex_address = try_f!(dex_address(&coin).map_to_mm(ValidatePaymentError::InternalError)); let burn_address = try_f!(burn_address(&coin).map_to_mm(ValidatePaymentError::InternalError)); - let inputs_signed_by_pub = try_f!(check_all_utxo_inputs_signed_by_pub(&tx, sender_pubkey)); - if !inputs_signed_by_pub { - return Box::new(futures01::future::err( - ValidatePaymentError::WrongPaymentTx(format!( - "{INVALID_SENDER_ERR_LOG}: Taker payment does not belong to the verified public key" - )) - .into(), - )); - } + let sender_pubkey = sender_pubkey.to_vec(); let fut = async move { + match check_all_utxo_inputs_signed_by_pub(&coin, &tx, &sender_pubkey).await { + Ok(true) => {}, + Ok(false) => { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{INVALID_SENDER_ERR_LOG}: Taker payment does not belong to the verified public key" + ))) + }, + Err(e) => return Err(e), + }; let tx_from_rpc = coin .as_ref() .rpc_client @@ -2394,7 +2524,8 @@ pub fn watcher_validate_taker_payment( let coin = coin.clone(); let fut = async move { - let inputs_signed_by_pub = check_all_utxo_inputs_signed_by_pub(&taker_payment_tx, &input.taker_pub)?; + let inputs_signed_by_pub = + check_all_utxo_inputs_signed_by_pub(&coin, &taker_payment_tx, &input.taker_pub).await?; if !inputs_signed_by_pub { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{INVALID_SENDER_ERR_LOG}: Taker payment does not belong to the verified public key" @@ -5288,6 +5419,52 @@ fn test_pubkey_from_script_sig() { pubkey_from_script_sig(&script_sig_err).unwrap_err(); } +#[test] +fn test_verify_p2pk_input_pubkey() { + // 65-byte (uncompressed) pubkey example. + // https://mempool.space/tx/1db6251a9afce7025a2061a19e63c700dffc3bec368bd1883decfac353357a9d + let tx: UtxoTx = "0100000001740443e82e526cef440ed590d1c43a67f509424134542de092e5ae68721575d60100000049483045022078e86c021003cca23842d4b2862dfdb68d2478a98c08c10dcdffa060e55c72be022100f6a41da12cdc2e350045f4c97feeab76a7c0ab937bd8a9e507293ce6d37c9cc201ffffffff0200f2052a010000001976a91431891996d28cc0214faa3760a765b40846bd035888ac00ba1dd2050000004341049464205950188c29d377eebca6535e0f3699ce4069ecd77ffebfbd0bcf95e3c134cb7d2742d800a12df41413a09ef87a80516353a2f0a280547bb5512dc03da8ac00000000".into(); + let script_sig = tx.inputs[0].script_sig.clone().into(); + let expected_pub = Public::Normal("049464205950188c29d377eebca6535e0f3699ce4069ecd77ffebfbd0bcf95e3c134cb7d2742d800a12df41413a09ef87a80516353a2f0a280547bb5512dc03da8".into()); + let unsigned_tx: TransactionInputSigner = tx.into(); + let successful_verification = + verify_p2pk_input_pubkey(&script_sig, &expected_pub, &unsigned_tx, 0, SignatureVersion::Base, 0).unwrap(); + assert!(successful_verification); + + // 33-byte (compressed) pubkey example. + // https://kmdexplorer.io/tx/07ceb50f9eedc3b820e48dc1e5250f6625115afe4ace3089bfcc66b34f5d4344 + let tx: UtxoTx = "0400008085202f89013683897bf3bfb1e217663aa9591bd73c9eb105f8c8471e88dbe7152ca7627a19050000004948304502210087100bf4a665ebab3cc6d3472068905bdc6c6def37e432597e78e2ccc4da017a02205b5f0800cabe84bc49b5eb0997926b48dfee3b8ca5a31623ae9506272f8a5cd501ffffffff0288130000000000002321020e46e79a2a8d12b9b5d12c7a91adb4e454edfae43c0a0cb805427d2ac7613fd9ac0000000000000000226a20976bd7ad5596ac3521fd90295e753b1096e4eb90ad9ded1170b2ed81f810df5fc0dbf36752ea42000000000000000000000000".into(); + let script_sig = tx.inputs[0].script_sig.clone().into(); + let expected_pub = Public::Compressed("02f9a7b49282885cd03969f1f5478287497bc8edfceee9eac676053c107c5fcdaf".into()); + let mut unsigned_tx: TransactionInputSigner = tx.into(); + // For overwintered transactions, the amount must be set, as wel as the consensus branch id. + unsigned_tx.inputs[0].amount = 10000; + unsigned_tx.consensus_branch_id = 0x76b8_09bb; + let successful_verification = + verify_p2pk_input_pubkey(&script_sig, &expected_pub, &unsigned_tx, 0, SignatureVersion::Base, 0).unwrap(); + assert!(successful_verification); +} + +#[test] +fn test_check_all_utxo_inputs_signed_by_pub_overwintered() { + use super::utxo_tests::electrum_client_for_test; + use common::block_on; + + // We need a running electrum client for this test to test the functionality of fetching a tx from the network, parsing it, and using its input amount for sig_hash calculations. + let client = UtxoRpcClientEnum::Electrum(electrum_client_for_test(&[ + "electrum3.cipig.net:10001", + "electrum1.cipig.net:10001", + "electrum2.cipig.net:10001", + ])); + let mut fields = utxo_coin_fields_for_test(client, None, false); + fields.conf.ticker = "KMD".to_owned(); + let coin = utxo_coin_from_fields(fields); + + let tx: UtxoTx = "0400008085202f89013683897bf3bfb1e217663aa9591bd73c9eb105f8c8471e88dbe7152ca7627a19050000004948304502210087100bf4a665ebab3cc6d3472068905bdc6c6def37e432597e78e2ccc4da017a02205b5f0800cabe84bc49b5eb0997926b48dfee3b8ca5a31623ae9506272f8a5cd501ffffffff0288130000000000002321020e46e79a2a8d12b9b5d12c7a91adb4e454edfae43c0a0cb805427d2ac7613fd9ac0000000000000000226a20976bd7ad5596ac3521fd90295e753b1096e4eb90ad9ded1170b2ed81f810df5fc0dbf36752ea42000000000000000000000000".into(); + let expected_pub = Public::Compressed("02f9a7b49282885cd03969f1f5478287497bc8edfceee9eac676053c107c5fcdaf".into()); + assert!(block_on(check_all_utxo_inputs_signed_by_pub(&coin, &tx, &expected_pub)).unwrap()); +} + #[test] fn test_tx_v_size() { // Multiple legacy inputs with P2SH and P2PKH output diff --git a/mm2src/mm2_bitcoin/keys/src/public.rs b/mm2src/mm2_bitcoin/keys/src/public.rs index cb801585d8..c21eda0259 100644 --- a/mm2src/mm2_bitcoin/keys/src/public.rs +++ b/mm2src/mm2_bitcoin/keys/src/public.rs @@ -3,8 +3,8 @@ use crypto::dhash160; use hash::{H160, H264, H520}; use hex::ToHex; use secp256k1::{recovery::{RecoverableSignature, RecoveryId}, - Message as SecpMessage, PublicKey, Signature as SecpSignature}; -use std::{fmt, ops}; + Error as SecpError, Message as SecpMessage, PublicKey, Signature as SecpSignature}; +use std::{fmt, ops::Deref}; use {CompactSignature, Error, Message, Signature}; /// Secret public key @@ -80,9 +80,12 @@ impl Public { Public::Normal(_) => None, } } + + #[inline(always)] + pub fn to_secp256k1_pubkey(&self) -> Result { PublicKey::from_slice(self.deref()) } } -impl ops::Deref for Public { +impl Deref for Public { type Target = [u8]; fn deref(&self) -> &Self::Target { diff --git a/mm2src/mm2_bitcoin/script/src/script.rs b/mm2src/mm2_bitcoin/script/src/script.rs index c5e4b57d0f..8614961879 100644 --- a/mm2src/mm2_bitcoin/script/src/script.rs +++ b/mm2src/mm2_bitcoin/script/src/script.rs @@ -513,6 +513,30 @@ impl Script { script.sigops_count(true) } + + /// Extracts the signature from a scriptSig at instruction 0. + /// + /// Usable for P2PK and P2PKH scripts. + pub fn extract_signature(&self) -> Result, String> { + match self.get_instruction(0) { + Some(Ok(instruction)) => match instruction.opcode { + Opcode::OP_PUSHBYTES_70 | Opcode::OP_PUSHBYTES_71 | Opcode::OP_PUSHBYTES_72 => match instruction.data { + Some(bytes) => Ok(bytes.to_vec()), + None => Err(format!("No data at instruction 0 of script {:?}", self)), + }, + opcode => Err(format!("Unexpected opcode {:?}", opcode)), + }, + Some(Err(e)) => Err(format!("Error {} on getting instruction 0 of script {:?}", e, self)), + None => Err(format!("None instruction 0 of script {:?}", self)), + } + } + + /// Checks if a scriptSig is a script that spends a P2PK output. + pub fn does_script_spend_p2pk(&self) -> bool { + // P2PK scriptSig is just a single signature. The script should consist of a single push bytes + // instruction with the data as the signature. + self.extract_signature().is_ok() && self.get_instruction(1).is_none() + } } pub struct Instructions<'a> { @@ -971,4 +995,13 @@ OP_ADD } assert_eq!(max_idx, 3); } + + #[test] + fn test_does_script_spend_p2pk() { + let script_sig = Script::from("473044022071edae37cf518e98db3f7637b9073a7a980b957b0c7b871415dbb4898ec3ebdc022031b402a6b98e64ffdf752266449ca979a9f70144dba77ed7a6a25bfab11648f6012103ad6f89abc2e5beaa8a3ac28e22170659b3209fe2ddf439681b4b8f31508c36fa"); + assert!(!script_sig.does_script_spend_p2pk()); + // The scriptSig of the input spent from: https://mempool.space/tx/1db6251a9afce7025a2061a19e63c700dffc3bec368bd1883decfac353357a9d + let script_sig = Script::from("483045022078e86c021003cca23842d4b2862dfdb68d2478a98c08c10dcdffa060e55c72be022100f6a41da12cdc2e350045f4c97feeab76a7c0ab937bd8a9e507293ce6d37c9cc201"); + assert!(script_sig.does_script_spend_p2pk()); + } } From b5e034aecced98d3ecfe187384fdedfd19a41890 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Thu, 29 May 2025 18:41:42 +0100 Subject: [PATCH 16/36] feat(wallet): implement HD multi-address support for message signing (#2432) This commit allows users with HD wallets to sign messages from any derived address using `sign_message` RPC. --- mm2src/coins/eth.rs | 21 +- mm2src/coins/eth/eth_tests.rs | 2 +- mm2src/coins/eth/eth_withdraw.rs | 8 +- mm2src/coins/hd_wallet/mod.rs | 59 ++- mm2src/coins/hd_wallet/withdraw_ops.rs | 47 +- mm2src/coins/lightning.rs | 8 +- mm2src/coins/lp_coins.rs | 25 +- mm2src/coins/qrc20.rs | 5 +- .../coins/rpc_command/tendermint/staking.rs | 5 +- mm2src/coins/siacoin.rs | 14 +- mm2src/coins/tendermint/tendermint_coin.rs | 6 +- mm2src/coins/tendermint/tendermint_token.rs | 5 +- mm2src/coins/test_coin.rs | 6 +- mm2src/coins/utxo/bch.rs | 10 +- mm2src/coins/utxo/qtum.rs | 6 +- mm2src/coins/utxo/slp.rs | 7 +- mm2src/coins/utxo/utxo_common.rs | 30 +- mm2src/coins/utxo/utxo_standard.rs | 6 +- mm2src/coins/utxo/utxo_tests.rs | 4 +- mm2src/coins/z_coin.rs | 4 +- mm2src/crypto/src/bip32_child.rs | 4 +- mm2src/crypto/src/standard_hd_path.rs | 21 + mm2src/mm2_core/src/mm_ctx.rs | 2 + mm2src/mm2_main/src/lp_wallet.rs | 2 +- .../tests/mm2_tests/bch_and_slp_tests.rs | 4 +- .../tests/mm2_tests/lightning_tests.rs | 2 +- .../tests/mm2_tests/mm2_tests_inner.rs | 425 +++++++++++++++++- mm2src/mm2_test_helpers/src/for_tests.rs | 10 +- mm2src/mm2_test_helpers/src/structs.rs | 2 +- 29 files changed, 640 insertions(+), 110 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 0bb6d7b1b2..f79c2b05c0 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -2378,10 +2378,25 @@ impl MarketCoinOps for EthCoin { Some(keccak256(&stream.out()).take()) } - fn sign_message(&self, message: &str) -> SignatureResult { + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { let message_hash = self.sign_message_hash(message).ok_or(SignatureError::PrefixNotFound)?; - let privkey = &self.priv_key_policy.activated_key_or_err()?.secret(); - let signature = sign(privkey, &H256::from(message_hash))?; + + let secret = if let Some(address) = address { + let path_to_coin = self.priv_key_policy.path_to_coin_or_err()?; + let derivation_path = address + .valid_derivation_path(path_to_coin) + .mm_err(|err| SignatureError::InvalidRequest(err.to_string()))?; + let privkey = self + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(&derivation_path)?; + ethkey::Secret::from_slice(privkey.as_slice()).ok_or(MmError::new(SignatureError::InternalError( + "failed to derive ethkey::Secret".to_string(), + )))? + } else { + self.priv_key_policy.activated_key_or_err()?.secret().clone() + }; + let signature = sign(&secret, &H256::from(message_hash))?; + Ok(format!("0x{}", signature)) } diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index ef7a126c4a..ce5d58f58a 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -870,7 +870,7 @@ fn test_sign_verify_message() { ); let message = "test"; - let signature = coin.sign_message(message).unwrap(); + let signature = coin.sign_message(message, None).unwrap(); assert_eq!(signature, "0xcdf11a9c4591fb7334daa4b21494a2590d3f7de41c7d2b333a5b61ca59da9b311b492374cc0ba4fbae53933260fa4b1c18f15d95b694629a7b0620eec77a938600"); let is_valid = coin diff --git a/mm2src/coins/eth/eth_withdraw.rs b/mm2src/coins/eth/eth_withdraw.rs index 05efef9804..1f61734b55 100644 --- a/mm2src/coins/eth/eth_withdraw.rs +++ b/mm2src/coins/eth/eth_withdraw.rs @@ -3,7 +3,7 @@ use super::{checksum_address, u256_to_big_decimal, wei_from_big_decimal, ChainSp use crate::eth::{calc_total_fee, get_eth_gas_details_from_withdraw_fee, tx_builder_with_pay_for_gas_option, tx_type_from_pay_for_gas_option, Action, Address, EthTxFeeDetails, KeyPair, PayForGasOption, SignedEthTx, TransactionWrapper, UnSignedEthTxBuilder}; -use crate::hd_wallet::{HDCoinWithdrawOps, HDWalletOps, WithdrawFrom, WithdrawSenderAddress}; +use crate::hd_wallet::{HDAddressSelector, HDCoinWithdrawOps, HDWalletOps, WithdrawSenderAddress}; use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::{BytesJson, CoinWithDerivationMethod, EthCoin, GetWithdrawSenderAddress, PrivKeyPolicy, TransactionData, TransactionDetails}; @@ -89,10 +89,12 @@ where /// Gets the derivation path for the address from which the withdrawal is made using the `from` parameter. #[allow(clippy::result_large_err)] - fn get_from_derivation_path(&self, from: &WithdrawFrom) -> Result> { + fn get_from_derivation_path(&self, from: &HDAddressSelector) -> Result> { let coin = self.coin(); let path_to_coin = &coin.deref().derivation_method.hd_wallet_or_err()?.derivation_path; - let path_to_address = from.to_address_path(path_to_coin.coin_type())?; + let path_to_address = from + .to_address_path(path_to_coin.coin_type()) + .mm_err(|err| WithdrawError::UnexpectedFromAddress(err.to_string()))?; let derivation_path = path_to_address.to_derivation_path(path_to_coin)?; Ok(derivation_path) } diff --git a/mm2src/coins/hd_wallet/mod.rs b/mm2src/coins/hd_wallet/mod.rs index 666293cb60..1fedf35fcd 100644 --- a/mm2src/coins/hd_wallet/mod.rs +++ b/mm2src/coins/hd_wallet/mod.rs @@ -47,7 +47,7 @@ mod wallet_ops; pub use wallet_ops::HDWalletOps; mod withdraw_ops; -pub use withdraw_ops::{HDCoinWithdrawOps, WithdrawFrom, WithdrawSenderAddress}; +pub use withdraw_ops::{HDCoinWithdrawOps, WithdrawSenderAddress}; pub(crate) type HDAccountsMap = BTreeMap; pub(crate) type HDAccountsMutex = AsyncMutex>; @@ -485,6 +485,63 @@ impl HDPathAccountToAddressId { Ok(account_der_path) } } +/// Represents how a hierarchical deterministic (HD) address is selected. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum HDAddressSelector { + /// Specifies the HD address using its structured account, chain, and address ID. + AddressId(HDPathAccountToAddressId), + /// Specifies the HD address directly using a BIP-44,84 and other compliant derivation path. + /// + /// IMPORTANT: Don't use `Bip44DerivationPath` or `RpcDerivationPath` because if there is an error in the path, + /// `serde::Deserialize` returns "data did not match any variant of untagged enum HDAddressSelector". + /// It's better to show the user an informative error. + DerivationPath { derivation_path: String }, +} + +impl HDAddressSelector { + pub fn to_address_path(&self, expected_coin_type: u32) -> MmResult { + match self { + HDAddressSelector::AddressId(address_id) => Ok(*address_id), + HDAddressSelector::DerivationPath { derivation_path } => { + let derivation_path = StandardHDPath::from_str(derivation_path).map_to_mm(StandardHDPathError::from)?; + let coin_type = derivation_path.coin_type(); + + if coin_type != expected_coin_type { + return MmError::err(StandardHDPathError::InvalidCoinType { + expected: expected_coin_type, + found: coin_type, + }); + } + + Ok(HDPathAccountToAddressId::from(derivation_path)) + }, + } + } + + pub fn valid_derivation_path(self, path_to_coin: &HDPathToCoin) -> MmResult { + match self { + HDAddressSelector::AddressId(id) => id + .to_derivation_path(path_to_coin) + .mm_err(StandardHDPathError::Bip32Error), + HDAddressSelector::DerivationPath { derivation_path } => { + let standard_hd_path = StandardHDPath::from_str(&derivation_path) + .map_to_mm(|_| StandardHDPathError::Bip32Error(Bip32Error::Decode))?; + let rpc_path_to_coin = standard_hd_path.path_to_coin(); + + // validate rpc path_to_coin against activated coin. + if &rpc_path_to_coin != path_to_coin { + return MmError::err(StandardHDPathError::InvalidPathToCoin { + expected: rpc_path_to_coin.to_string(), + found: path_to_coin.to_string(), + }); + }; + + Ok(standard_hd_path.to_derivation_path()) + }, + } + } +} pub(crate) mod inner_impl { use super::*; diff --git a/mm2src/coins/hd_wallet/withdraw_ops.rs b/mm2src/coins/hd_wallet/withdraw_ops.rs index 5b7ddf48cf..3b3d6ac7d9 100644 --- a/mm2src/coins/hd_wallet/withdraw_ops.rs +++ b/mm2src/coins/hd_wallet/withdraw_ops.rs @@ -1,51 +1,12 @@ use super::{DisplayAddress, HDPathAccountToAddressId, HDWalletOps, HDWithdrawError}; -use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDWalletCoinOps}; +use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDAddressSelector, HDCoinAddress, HDWalletCoinOps}; use async_trait::async_trait; use bip32::DerivationPath; -use crypto::{StandardHDPath, StandardHDPathError}; use mm2_err_handle::prelude::*; -use std::str::FromStr; type HDCoinPubKey = <<<::HDWallet as HDWalletOps>::HDAccount as HDAccountOps>::HDAddress as HDAddressOps>::Pubkey; -/// Represents the source of the funds for a withdrawal operation. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub enum WithdrawFrom { - /// The address id of the sender address which is specified by the account id, chain, and address id. - AddressId(HDPathAccountToAddressId), - /// The derivation path of the sender address in the BIP-44 format. - /// - /// IMPORTANT: Don't use `Bip44DerivationPath` or `RpcDerivationPath` because if there is an error in the path, - /// `serde::Deserialize` returns "data did not match any variant of untagged enum WithdrawFrom". - /// It's better to show the user an informative error. - DerivationPath { derivation_path: String }, -} - -impl WithdrawFrom { - #[allow(clippy::result_large_err)] - pub fn to_address_path(&self, expected_coin_type: u32) -> MmResult { - match self { - WithdrawFrom::AddressId(address_id) => Ok(*address_id), - WithdrawFrom::DerivationPath { derivation_path } => { - let derivation_path = StandardHDPath::from_str(derivation_path) - .map_to_mm(StandardHDPathError::from) - .mm_err(|e| HDWithdrawError::UnexpectedFromAddress(e.to_string()))?; - let coin_type = derivation_path.coin_type(); - if coin_type != expected_coin_type { - let error = format!( - "Derivation path '{}' must have '{}' coin type", - derivation_path, expected_coin_type - ); - return MmError::err(HDWithdrawError::UnexpectedFromAddress(error)); - } - Ok(HDPathAccountToAddressId::from(derivation_path)) - }, - } - } -} - /// Contains the details of the sender address for a withdraw operation. pub struct WithdrawSenderAddress { pub(crate) address: Address, @@ -61,13 +22,15 @@ pub trait HDCoinWithdrawOps: HDWalletCoinOps { async fn get_withdraw_hd_sender( &self, hd_wallet: &Self::HDWallet, - from: &WithdrawFrom, + from: &HDAddressSelector, ) -> MmResult, HDCoinPubKey>, HDWithdrawError> { let HDPathAccountToAddressId { account_id, chain, address_id, - } = from.to_address_path(hd_wallet.coin_type())?; + } = from + .to_address_path(hd_wallet.coin_type()) + .mm_err(|err| HDWithdrawError::UnexpectedFromAddress(err.to_string()))?; let hd_account = hd_wallet .get_account(account_id) diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 64aa153aaa..d9c48ecbf3 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -11,6 +11,7 @@ pub mod ln_storage; pub mod ln_utils; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::HDAddressSelector; use crate::lightning::ln_utils::{filter_channels, pay_invoice_with_max_total_cltv_expiry_delta, PaymentError}; use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::utxo::utxo_common::{big_decimal_from_sat, big_decimal_from_sat_unsigned}; @@ -956,7 +957,12 @@ impl MarketCoinOps for LightningCoin { Some(dhash256(prefixed_message.as_bytes()).take()) } - fn sign_message(&self, message: &str) -> SignatureResult { + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + if address.is_some() { + return MmError::err(SignatureError::InvalidRequest( + "functionality not supported for Lightning yet.".into(), + )); + } let message_hash = self.sign_message_hash(message).ok_or(SignatureError::PrefixNotFound)?; let secret_key = self .keys_manager diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index d55add2c2a..8bca996ba9 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -225,9 +225,9 @@ use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetail GetEthAddressError, GetValidEthWithdrawAddError, SignedEthTx}; pub mod hd_wallet; -use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, - HDCoinHDAccount, HDExtractPubkeyError, HDPathAccountToAddressId, HDWalletAddress, HDWalletCoinOps, - HDWalletOps, HDWithdrawError, HDXPubExtractor, WithdrawFrom, WithdrawSenderAddress}; +use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, + HDAddressSelector, HDCoinAddress, HDCoinHDAccount, HDExtractPubkeyError, HDPathAccountToAddressId, + HDWalletAddress, HDWalletCoinOps, HDWalletOps, HDWithdrawError, HDXPubExtractor, WithdrawSenderAddress}; #[cfg(not(target_arch = "wasm32"))] pub mod lightning; #[cfg_attr(target_arch = "wasm32", allow(dead_code, unused_imports))] @@ -2083,7 +2083,7 @@ pub trait MarketCoinOps { fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]>; - fn sign_message(&self, _message: &str) -> SignatureResult; + fn sign_message(&self, _message: &str, _address: Option) -> SignatureResult; fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult; @@ -2210,7 +2210,7 @@ pub trait GetWithdrawSenderAddress { #[derive(Clone, Default, Deserialize)] pub struct WithdrawRequest { coin: String, - from: Option, + from: Option, to: String, #[serde(default)] amount: BigDecimal, @@ -2294,10 +2294,11 @@ pub enum ValidatorsInfoDetails { Cosmos(rpc_command::tendermint::staking::ValidatorsQuery), } -#[derive(Serialize, Deserialize)] +#[derive(Deserialize)] pub struct SignatureRequest { coin: String, message: String, + address: Option, } #[derive(Serialize, Deserialize)] @@ -5116,8 +5117,14 @@ pub async fn get_raw_transaction(ctx: MmArc, req: RawTransactionRequest) -> RawT } pub async fn sign_message(ctx: MmArc, req: SignatureRequest) -> SignatureResult { + if req.address.is_some() && !ctx.enable_hd() { + return MmError::err(SignatureError::InvalidRequest( + "You need to enable kdf with enable_hd to sign messages with a specific account/address".to_string(), + )); + }; let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - let signature = coin.sign_message(&req.message)?; + let signature = coin.sign_message(&req.message, req.address)?; + Ok(SignatureResponse { signature }) } @@ -6151,7 +6158,7 @@ mod tests { pub mod for_tests { use crate::rpc_command::init_withdraw::WithdrawStatusRequest; use crate::rpc_command::init_withdraw::{init_withdraw, withdraw_status}; - use crate::{TransactionDetails, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawRequest}; + use crate::{HDAddressSelector, TransactionDetails, WithdrawError, WithdrawFee, WithdrawRequest}; use common::executor::Timer; use common::{now_ms, wait_until_ms}; use mm2_core::mm_ctx::MmArc; @@ -6173,7 +6180,7 @@ pub mod for_tests { client_id: 0, inner: WithdrawRequest { amount: BigDecimal::from_str(amount).unwrap(), - from: from_derivation_path.map(|from_derivation_path| WithdrawFrom::DerivationPath { + from: from_derivation_path.map(|from_derivation_path| HDAddressSelector::DerivationPath { derivation_path: from_derivation_path.to_owned(), }), to: to.to_owned(), diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 07aa04aad2..3d6f8660b3 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1,5 +1,6 @@ use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::eth::{self, u256_to_big_decimal, wei_from_big_decimal, TryToAddress}; +use crate::hd_wallet::HDAddressSelector; use crate::qrc20::rpc_clients::{LogEntry, Qrc20ElectrumOps, Qrc20NativeOps, Qrc20RpcOps, TopicFilter, TxReceipt, ViewContractCallType}; use crate::utxo::qtum::QtumBasedCoin; @@ -1040,8 +1041,8 @@ impl MarketCoinOps for Qrc20Coin { utxo_common::sign_message_hash(self.as_ref(), message) } - fn sign_message(&self, message: &str) -> SignatureResult { - utxo_common::sign_message(self.as_ref(), message) + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message, address) } fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 8a83e0e6cc..519c18cc38 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -3,7 +3,8 @@ use cosmrs::staking::{Commission, Description, Validator}; use mm2_err_handle::prelude::MmError; use mm2_number::BigDecimal; -use crate::{hd_wallet::WithdrawFrom, tendermint::TendermintCoinRpcError, MmCoinEnum, StakingInfoError, WithdrawFee}; +use crate::{hd_wallet::HDAddressSelector, tendermint::TendermintCoinRpcError, MmCoinEnum, StakingInfoError, + WithdrawFee}; /// Represents current status of the validator. #[derive(Debug, Default, Deserialize)] @@ -131,7 +132,7 @@ pub async fn validators_rpc( pub struct DelegationPayload { pub validator_address: String, pub fee: Option, - pub withdraw_from: Option, + pub withdraw_from: Option, #[serde(default)] pub memo: String, #[serde(default)] diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index 08458e5c0d..a6cfb6ad7a 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -1,5 +1,6 @@ use super::{BalanceError, CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, SwapOps, TradeFee, TransactionEnum}; +use crate::hd_wallet::HDAddressSelector; use crate::{coin_errors::MyAddressError, AddressFromPubkeyError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, PrivKeyPolicy, RawTransactionResult, RefundPaymentArgs, SearchForSwapTxSpendInput, @@ -8,6 +9,7 @@ use crate::{coin_errors::MyAddressError, AddressFromPubkeyError, BalanceFut, Can ValidateAddressResult, ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentInput, ValidatePaymentResult, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawFut, WithdrawRequest}; +use crate::{SignatureError, VerificationError}; use async_trait::async_trait; use common::executor::AbortedError; pub use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature}; @@ -321,14 +323,18 @@ impl MarketCoinOps for SiaCoin { Ok(address.to_string()) } - async fn get_public_key(&self) -> Result> { unimplemented!() } + async fn get_public_key(&self) -> Result> { + MmError::err(UnexpectedDerivationMethod::InternalError("Not implemented".into())) + } - fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } + fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { None } - fn sign_message(&self, _message: &str) -> SignatureResult { unimplemented!() } + fn sign_message(&self, _message: &str, _address: Option) -> SignatureResult { + MmError::err(SignatureError::InternalError("Not implemented".into())) + } fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult { - unimplemented!() + MmError::err(VerificationError::InternalError("Not implemented".into())) } fn my_balance(&self) -> BalanceFut { diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index b43c54bdf2..ab067249b9 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -5,7 +5,7 @@ use super::ibc::transfer_v1::MsgTransfer; use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::rpc::*; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentResult}; -use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; +use crate::hd_wallet::{HDAddressSelector, HDPathAccountToAddressId}; use crate::rpc_command::tendermint::ibc::ChannelId; use crate::rpc_command::tendermint::staking::{ClaimRewardsPayload, Delegation, DelegationPayload, DelegationsQueryResponse, Undelegation, UndelegationEntry, @@ -1216,7 +1216,7 @@ impl TendermintCoin { pub(super) fn extract_account_id_and_private_key( &self, - withdraw_from: Option, + withdraw_from: Option, ) -> Result<(AccountId, Option), io::Error> { if let TendermintActivationPolicy::PublicKey(_) = self.activation_policy { return Ok((self.account_id.clone(), None)); @@ -3337,7 +3337,7 @@ impl MarketCoinOps for TendermintCoin { None } - fn sign_message(&self, _message: &str) -> SignatureResult { + fn sign_message(&self, _message: &str, _address: Option) -> SignatureResult { // TODO MmError::err(SignatureError::InternalError("Not implemented".into())) } diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index d70b316d26..89424daf4d 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -4,6 +4,7 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{create_withdraw_msg_as_any, TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATOSHIS, TIMEOUT_HEIGHT_DELTA, TX_DEFAULT_MEMO}; use crate::coin_errors::{AddressFromPubkeyError, ValidatePaymentResult}; +use crate::hd_wallet::HDAddressSelector; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, @@ -279,7 +280,9 @@ impl MarketCoinOps for TendermintToken { fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { self.platform_coin.sign_message_hash(message) } - fn sign_message(&self, message: &str) -> SignatureResult { self.platform_coin.sign_message(message) } + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + self.platform_coin.sign_message(message, address) + } fn verify_message(&self, signature: &str, message: &str, address: &str) -> VerificationResult { self.platform_coin.verify_message(signature, message, address) diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 0faae14886..13c8beb7a5 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -4,7 +4,7 @@ use super::{CoinBalance, CommonSwapOpsV2, FindPaymentSpendError, FundingTxSpend, MmCoin, RawTransactionFut, RawTransactionRequest, RefundTakerPaymentArgs, SearchForFundingSpendErr, SwapOps, TradeFee, TransactionEnum, TransactionFut}; use crate::coin_errors::{AddressFromPubkeyError, ValidatePaymentResult}; -use crate::hd_wallet::AddrToString; +use crate::hd_wallet::{AddrToString, HDAddressSelector}; use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MmCoinEnum, NegotiateSwapContractAddrErr, ParseCoinAssocTypes, PaymentInstructionArgs, @@ -71,7 +71,9 @@ impl MarketCoinOps for TestCoin { fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } - fn sign_message(&self, _message: &str) -> SignatureResult { unimplemented!() } + fn sign_message(&self, _message: &str, _address: Option) -> SignatureResult { + unimplemented!() + } fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult { unimplemented!() diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 7e18dfc913..8694710907 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -1,8 +1,8 @@ use super::*; use crate::coin_balance::{EnableCoinBalanceError, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; -use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDExtractPubkeyError, HDXPubExtractor, - TrezorCoinError, WithdrawSenderAddress}; +use crate::hd_wallet::{ExtractExtendedPubkey, HDAddressSelector, HDCoinAddress, HDCoinWithdrawOps, + HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxDetailsBuilder, TxHistoryStorage}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; @@ -1149,8 +1149,8 @@ impl MarketCoinOps for BchCoin { utxo_common::sign_message_hash(self.as_ref(), message) } - fn sign_message(&self, message: &str) -> SignatureResult { - utxo_common::sign_message(self.as_ref(), message) + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message, address) } fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { @@ -1705,7 +1705,7 @@ mod bch_tests { #[test] fn test_sign_message() { let (_ctx, coin) = tbch_coin_for_test(); - let signature = coin.sign_message("test").unwrap(); + let signature = coin.sign_message("test", None).unwrap(); assert_eq!( signature, "ILuePKMsycXwJiNDOT7Zb7TfIlUW7Iq+5ylKd15AK72vGVYXbnf7Gj9Lk9MFV+6Ub955j7MiAkp0wQjvuIoRPPA=" diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index cab1d3c5d5..a45b4a9491 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -3,7 +3,7 @@ use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; -use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, +use crate::hd_wallet::{ExtractExtendedPubkey, HDAddressSelector, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; @@ -772,8 +772,8 @@ impl MarketCoinOps for QtumCoin { utxo_common::sign_message_hash(self.as_ref(), message) } - fn sign_message(&self, message: &str) -> SignatureResult { - utxo_common::sign_message(self.as_ref(), message) + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message, address) } fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index a0b29006cc..5a537e42dc 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -4,6 +4,7 @@ //! More info about the protocol and implementation guides can be found at https://slp.dev/ use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentResult}; +use crate::hd_wallet::HDAddressSelector; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::bch::BchCoin; @@ -1116,8 +1117,8 @@ impl MarketCoinOps for SlpToken { utxo_common::sign_message_hash(self.as_ref(), message) } - fn sign_message(&self, message: &str) -> SignatureResult { - utxo_common::sign_message(self.as_ref(), message) + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message, address) } fn verify_message(&self, signature: &str, message: &str, address: &str) -> VerificationResult { @@ -2166,7 +2167,7 @@ mod slp_tests { let (_ctx, bch) = tbch_coin_for_test(); let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch, 0).unwrap(); - let signature = fusd.sign_message("test").unwrap(); + let signature = fusd.sign_message("test", None).unwrap(); assert_eq!( signature, "ILuePKMsycXwJiNDOT7Zb7TfIlUW7Iq+5ylKd15AK72vGVYXbnf7Gj9Lk9MFV+6Ub955j7MiAkp0wQjvuIoRPPA=" diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index c6eed83761..760731d28b 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2,7 +2,7 @@ use super::*; use crate::coin_balance::{HDAddressBalance, HDWalletBalanceObject, HDWalletBalanceOps}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::eth::EthCoinType; -use crate::hd_wallet::{HDCoinAddress, HDCoinHDAccount, HDCoinWithdrawOps, TrezorCoinError}; +use crate::hd_wallet::{HDAddressSelector, HDCoinAddress, HDCoinHDAccount, HDCoinWithdrawOps, TrezorCoinError}; use crate::lp_price::get_base_price_in_rel; use crate::rpc_command::init_withdraw::WithdrawTaskHandleShared; use crate::utxo::rpc_clients::{electrum_script_hash, BlockHashOrHeight, UnspentInfo, UnspentMap, UtxoRpcClientEnum, @@ -2886,10 +2886,32 @@ pub fn sign_message_hash(coin: &UtxoCoinFields, message: &str) -> Option<[u8; 32 Some(dhash256(&stream.out()).take()) } -pub fn sign_message(coin: &UtxoCoinFields, message: &str) -> SignatureResult { +pub fn sign_message( + coin: &UtxoCoinFields, + message: &str, + account: Option, +) -> SignatureResult { let message_hash = sign_message_hash(coin, message).ok_or(SignatureError::PrefixNotFound)?; - let private_key = coin.priv_key_policy.activated_key_or_err()?.private(); - let signature = private_key.sign_compact(&H256::from(message_hash))?; + + let private = if let Some(account) = account { + let path_to_coin = coin.priv_key_policy.path_to_coin_or_err()?; + let derivation_path = account + .valid_derivation_path(path_to_coin) + .mm_err(|err| SignatureError::InvalidRequest(err.to_string()))?; + let privkey = coin + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(&derivation_path)?; + Private { + prefix: coin.conf.wif_prefix, + secret: privkey, + compressed: true, + checksum_type: coin.conf.checksum_type, + } + } else { + *coin.priv_key_policy.activated_key_or_err()?.private() + }; + + let signature = private.sign_compact(&H256::from(message_hash))?; Ok(STANDARD.encode(&*signature)) } diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index ec9fd8adb9..49e4ea9d60 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -3,7 +3,7 @@ use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; -use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, +use crate::hd_wallet::{ExtractExtendedPubkey, HDAddressSelector, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; @@ -858,8 +858,8 @@ impl MarketCoinOps for UtxoStandardCoin { utxo_common::sign_message_hash(self.as_ref(), message) } - fn sign_message(&self, message: &str) -> SignatureResult { - utxo_common::sign_message(self.as_ref(), message) + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message, address) } fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 16831a0579..0c8d9a53f8 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -5038,7 +5038,7 @@ fn test_sign_verify_message() { ); let message = "test"; - let signature = coin.sign_message(message).unwrap(); + let signature = coin.sign_message(message, None).unwrap(); assert_eq!( signature, "HzetbqVj9gnUOznon9bvE61qRlmjH5R+rNgkxu8uyce3UBbOu+2aGh7r/GGSVFGZjRnaYC60hdwtdirTKLb7bE4=" @@ -5059,7 +5059,7 @@ fn test_sign_verify_message_segwit() { ); let message = "test"; - let signature = coin.sign_message(message).unwrap(); + let signature = coin.sign_message(message, None).unwrap(); assert_eq!( signature, "HzetbqVj9gnUOznon9bvE61qRlmjH5R+rNgkxu8uyce3UBbOu+2aGh7r/GGSVFGZjRnaYC60hdwtdirTKLb7bE4=" diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index d1487fc029..e31a8f8fa0 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -9,7 +9,7 @@ mod z_rpc; mod z_tx_history; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; -use crate::hd_wallet::HDPathAccountToAddressId; +use crate::hd_wallet::{HDAddressSelector, HDPathAccountToAddressId}; use crate::my_tx_history_v2::{MyTxHistoryErrorV2, MyTxHistoryRequestV2, MyTxHistoryResponseV2}; use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::utxo::rpc_clients::{ElectrumConnectionSettings, UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, @@ -1138,7 +1138,7 @@ impl MarketCoinOps for ZCoin { fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { None } - fn sign_message(&self, _message: &str) -> SignatureResult { + fn sign_message(&self, _message: &str, _address: Option) -> SignatureResult { MmError::err(SignatureError::InvalidRequest( "Message signing is not supported by the given coin type".to_string(), )) diff --git a/mm2src/crypto/src/bip32_child.rs b/mm2src/crypto/src/bip32_child.rs index 17b5064f97..a187398563 100644 --- a/mm2src/crypto/src/bip32_child.rs +++ b/mm2src/crypto/src/bip32_child.rs @@ -104,8 +104,8 @@ impl Bip32ChildValue for AnyValue { #[derive(Clone, PartialEq)] pub struct Bip32Child { - value: Value, - child: Child, + pub(crate) value: Value, + pub(crate) child: Child, } impl fmt::Debug for Bip32Child { diff --git a/mm2src/crypto/src/standard_hd_path.rs b/mm2src/crypto/src/standard_hd_path.rs index 9e51dc5f9f..a0fbbd6f66 100644 --- a/mm2src/crypto/src/standard_hd_path.rs +++ b/mm2src/crypto/src/standard_hd_path.rs @@ -41,6 +41,23 @@ impl StandardHDPath { pub fn chain(&self) -> Bip44Chain { self.child().child().child().value() } pub fn address_id(&self) -> u32 { self.child().child().child().child().value() } + + /// Derive `HDPathToCoin` from `StandardHDPath` + pub fn path_to_coin(&self) -> HDPathToCoin { + let Bip32Child { + value: purpose, + child: rest, + } = self; + let Bip32Child { value: coin_type, .. } = rest; + + Bip32Child { + value: purpose.clone(), + child: Bip32Child { + value: coin_type.clone(), + child: Bip44Tail, + }, + } + } } impl HDPathToCoin { @@ -78,6 +95,10 @@ pub enum StandardHDPathError { }, #[display(fmt = "Unknown BIP32 error: {}", _0)] Bip32Error(Bip32Error), + #[display(fmt = "Invalid coin type '{}', expected '{}'", found, expected)] + InvalidCoinType { expected: u32, found: u32 }, + #[display(fmt = "Invalid path to coin '{}', expected '{}'", found, expected)] + InvalidPathToCoin { expected: String, found: String }, } impl From for StandardHDPathError { diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 83944e2d5a..d0d6bfb87c 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -223,6 +223,8 @@ impl MmCtx { } } + pub fn enable_hd(&self) -> bool { self.conf["enable_hd"].as_bool().unwrap_or(false) } + pub fn rmd160(&self) -> &H160 { lazy_static! { static ref DEFAULT: H160 = [0; 20].into(); diff --git a/mm2src/mm2_main/src/lp_wallet.rs b/mm2src/mm2_main/src/lp_wallet.rs index b06318e56f..8bb64690e2 100644 --- a/mm2src/mm2_main/src/lp_wallet.rs +++ b/mm2src/mm2_main/src/lp_wallet.rs @@ -297,7 +297,7 @@ async fn process_passphrase_logic( fn initialize_crypto_context(ctx: &MmArc, passphrase: &str) -> WalletInitResult<()> { // This defaults to false to maintain backward compatibility. - match ctx.conf["enable_hd"].as_bool().unwrap_or(false) { + match ctx.enable_hd() { true => CryptoCtx::init_with_global_hd_account(ctx.clone(), passphrase)?, false => CryptoCtx::init_with_iguana_passphrase(ctx.clone(), passphrase)?, }; diff --git a/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs b/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs index f03cb18e06..564e2de8cf 100644 --- a/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs @@ -533,7 +533,7 @@ fn test_sign_verify_message_bch() { let electrum: Json = json::from_str(&electrum.1).unwrap(); log!("{:?}", electrum); - let response = block_on(sign_message(&mm, "BCH")); + let response = block_on(sign_message(&mm, "BCH", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; @@ -591,7 +591,7 @@ fn test_sign_verify_message_slp() { let enable_usdf = block_on(enable_slp(&mm, "USDF")); log!("enable_usdf: {:?}", enable_usdf); - let response = block_on(sign_message(&mm, "USDF")); + let response = block_on(sign_message(&mm, "USDF", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; diff --git a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs index 22c52114ce..f4317f4d26 100644 --- a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs @@ -1069,7 +1069,7 @@ fn test_sign_verify_message_lightning() { block_on(enable_electrum(&mm, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); block_on(enable_lightning(&mm, "tBTC-TEST-lightning", 600)); - let response = block_on(sign_message(&mm, "tBTC-TEST-lightning")); + let response = block_on(sign_message(&mm, "tBTC-TEST-lightning", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 988f7f6a4d..81ccbb3b7f 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -879,7 +879,7 @@ fn withdraw_and_send( use coins::TxFeeDetails; use std::ops::Sub; - let from = from.map(WithdrawFrom::AddressId); + let from = from.map(HDAddressSelector::AddressId); let withdraw = block_on(mm.rpc(&json! ({ "mmrpc": "2.0", "userpass": mm.userpass, @@ -5030,7 +5030,7 @@ fn test_sign_verify_message_utxo() { block_on(enable_coins_rick_morty_electrum(&mm_bob)) ); - let response = block_on(sign_message(&mm_bob, "RICK")); + let response = block_on(sign_message(&mm_bob, "RICK", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; @@ -5051,6 +5051,75 @@ fn test_sign_verify_message_utxo() { assert!(response.is_valid); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_sign_verify_message_utxo_with_derivation_path() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let coins = json!([rick_conf()]); + + let path_to_address = HDAccountAddressId::default(); + let conf_0 = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd_0 = MarketMakerIt::start(conf_0.conf, conf_0.rpc_password, None).unwrap(); + let (_dump_log, _dump_dashboard) = mm_hd_0.mm_dump(); + log!("log path: {}", mm_hd_0.log_path.display()); + + let rick = block_on(enable_utxo_v2_electrum( + &mm_hd_0, + "RICK", + doc_electrums(), + Some(path_to_address), + 60, + None, + )); + let balance = match rick.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let address0 = &balance.accounts.get(0).expect("Expected account at index 0").addresses[0].address; + let address1 = &balance.accounts.get(0).expect("Expected account at index 1").addresses[1].address; + + // Test address0 + let expected_signature = "ICnkSvQkAurwLvK6RYtCItrWMOS4ESjCf4GKp1DvBM8Xc2+dxM4si6NcSb0JJaJajYhPkwg5yWHmgR/9AmGB0KE="; + let response = block_on(sign_message( + &mm_hd_0, + "RICK", + Some(HDAddressSelector::DerivationPath { + derivation_path: "m/44'/141'/0'/0/0".to_owned(), + }), + )); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + assert_eq!(expected_signature, response.signature); + + let response = block_on(verify_message(&mm_hd_0, "RICK", expected_signature, address0)); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + assert!(response.is_valid); + + // Test address1. + let expected_signature = "IPGbtsPPz6u2DishjOcP0Lf6xqPfpvTcMnkP/rRUVddKPBtkN+SfUPVZcz1vagjhj95I2t4ctLzcc3vcRdQLxbY="; + let response = block_on(sign_message( + &mm_hd_0, + "RICK", + Some(HDAddressSelector::AddressId(HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, + })), + )); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + assert_eq!(expected_signature, response.signature); + + let response = block_on(verify_message(&mm_hd_0, "RICK", expected_signature, address1)); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + assert!(response.is_valid); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_sign_verify_message_utxo_segwit() { @@ -5098,7 +5167,7 @@ fn test_sign_verify_message_utxo_segwit() { block_on(enable_coins_rick_morty_electrum(&mm_bob)) ); - let response = block_on(sign_message(&mm_bob, "RICK")); + let response = block_on(sign_message(&mm_bob, "RICK", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; @@ -5130,6 +5199,224 @@ fn test_sign_verify_message_utxo_segwit() { assert!(response.is_valid); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_sign_verify_message_segwit_with_bip84_derivation_path() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let rick_segwit_conf = json!({ + "coin": "RICK", + "asset": "RICK", + "rpcport": 8923, + "sign_message_prefix": "Komodo Signed Message:\n", + "txversion": 4, + "overwintered": 1, + "segwit": true, + "address_format": {"format": "segwit"}, + "bech32_hrp": "rck", + "protocol": {"type": "UTXO"}, + "derivation_path": "m/84'/141'", + }); + + let coins = json!([rick_segwit_conf]); + + // Start MM with HD wallet + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_dump_log, _dump_dashboard) = mm_hd.mm_dump(); + log!("log path: {}", mm_hd.log_path.display()); + + // Enable RICK with BIP84 derivation path (segwit) + let path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, + }; + + // Enable with BIP84 path + let rick = block_on(enable_utxo_v2_electrum( + &mm_hd, + "RICK", + doc_electrums(), + Some(path_to_address), + 60, + None, + )); + + let balance = match rick.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + + let account0 = balance.accounts.get(0).expect("Expected account at index 0"); + let address0 = &account0.addresses[0].address; + let address1 = &account0.addresses[1].address; + + // Verify addresses are segwit (bech32) + assert!( + address0.starts_with("rck1"), + "Expected segwit address for address0: {}", + address0 + ); + assert!( + address1.starts_with("rck1"), + "Expected segwit address for address1: {}", + address1 + ); + + // Test 1: Sign with BIP84 path for address0 (m/84'/141'/0'/0/0) + let derivation_path_0 = "m/84'/141'/0'/0/0"; + let sign_response = block_on(sign_message( + &mm_hd, + "RICK", + Some(HDAddressSelector::DerivationPath { + derivation_path: derivation_path_0.to_owned(), + }), + )); + let sign_response: RpcV2Response = json::from_value(sign_response).unwrap(); + let signature0 = sign_response.result.signature; + + log!("Signature for {}: {}", derivation_path_0, signature0); + log!("Address0: {}", address0); + + // Verify with the segwit address + let verify_response = block_on(verify_message(&mm_hd, "RICK", &signature0, address0)); + let verify_response: RpcV2Response = json::from_value(verify_response).unwrap(); + assert!(verify_response.result.is_valid, "Verification failed for address0"); + + // Test 2: Sign with AddressId for address1 + let sign_response = block_on(sign_message( + &mm_hd, + "RICK", + Some(HDAddressSelector::AddressId(HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, + })), + )); + let sign_response: RpcV2Response = json::from_value(sign_response).unwrap(); + let signature1 = sign_response.result.signature; + + log!("Signature for address1: {}", signature1); + log!("Address1: {}", address1); + + // Verify with the segwit address + let verify_response = block_on(verify_message(&mm_hd, "RICK", &signature1, address1)); + let verify_response: RpcV2Response = json::from_value(verify_response).unwrap(); + assert!(verify_response.result.is_valid, "Verification failed for address1"); + + // Test 3: Cross-verification should fail + let verify_response = block_on(verify_message(&mm_hd, "RICK", &signature0, address1)); + let verify_response: RpcV2Response = json::from_value(verify_response).unwrap(); + assert!(!verify_response.result.is_valid, "Cross-verification should fail"); +} + +/// Needs attention after [issue #2470](https://github.com/KomodoPlatform/komodo-defi-framework/issues/2470) is resolved. +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_address_conflict_across_derivation_paths() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let rick_legacy_conf = json!({ + "coin": "RICK", + "asset": "RICK", + "rpcport": 8923, + "sign_message_prefix": "Komodo Signed Message:\n", + "txversion": 4, + "overwintered": 1, + "segwit": true, + "address_format": {"format": "segwit"}, + "bech32_hrp": "rck", + "protocol": {"type": "UTXO"}, + "derivation_path": "m/49'/141'", + }); + + let coins = json!([rick_legacy_conf]); + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 0, + }; + + // Enable RICK with BIP49 (legacy P2SH-SegWit) + let rick_legacy = block_on(enable_utxo_v2_electrum( + &mm_hd, + "RICK", + doc_electrums(), + Some(path_to_address.clone()), + 60, + None, + )); + + let legacy_address = match &rick_legacy.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd.accounts[0].addresses[0].address.clone(), + _ => panic!("Expected HD wallet balance"), + }; + log!("Legacy address: {}", legacy_address); + + // Sign a message using this legacy address + let sign_response = block_on(sign_message( + &mm_hd, + "RICK", + Some(HDAddressSelector::AddressId(path_to_address.clone())), + )); + let sign_response: RpcV2Response = json::from_value(sign_response).unwrap(); + let signature = sign_response.result.signature; + + // Shutdown MM and restart it with different(new native SegWit) derivation path (BIP84) + block_on(mm_hd.stop()).unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); + + let rick_bip84_conf = json!({ + "coin": "RICK", + "asset": "RICK", + "rpcport": 8923, + "sign_message_prefix": "Komodo Signed Message:\n", + "txversion": 4, + "overwintered": 1, + "segwit": true, + "address_format": {"format": "segwit"}, + "bech32_hrp": "rck", + "protocol": {"type": "UTXO"}, + "derivation_path": "m/84'/141'", + }); + + let coins = json!([rick_bip84_conf]); + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + // Re-enable RICK, but it will try to reuse address0 stored under old path + let rick_bip84 = block_on(enable_utxo_v2_electrum( + &mm_hd, + "RICK", + doc_electrums(), + Some(path_to_address), + 60, + None, + )); + + let bip84_address = match &rick_bip84.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd.accounts[0].addresses[0].address.clone(), + _ => panic!("Expected HD wallet balance"), + }; + + log!("BIP84 address: {}", bip84_address); + + // Try to verify the old signature using new BIP84-derived address + let verify_response = block_on(verify_message(&mm_hd, "RICK", &signature, &bip84_address)); + let verify_response: RpcV2Response = json::from_value(verify_response).unwrap(); + + // This should fail: signature was created with pubkey from m/49'/141'/0'/0/0, + // but we are now resolving address from m/84'/141'/0'/0/0 + assert!( + !verify_response.result.is_valid, + "Expected verification to fail due to derivation path mismatch" + ); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_sign_verify_message_eth() { @@ -5181,7 +5468,7 @@ fn test_sign_verify_message_eth() { block_on(enable_native(&mm_bob, "ETH", ETH_SEPOLIA_NODES, None)) ); - let response = block_on(sign_message(&mm_bob, "ETH")); + let response = block_on(sign_message(&mm_bob, "ETH", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; @@ -5199,6 +5486,136 @@ fn test_sign_verify_message_eth() { assert!(response.is_valid); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_sign_verify_message_eth_with_derivation_path() { + use mm2_test_helpers::for_tests::ETH_SEPOLIA_CHAIN_ID; + + let seed = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + let coins = json!([ + { + "coin": "ETH", + "name": "ethereum", + "fname": "Ethereum", + "sign_message_prefix": "Ethereum Signed Message:\n", + "rpcport": 80, + "mm2": 1, + "chain_id": 1, + "required_confirmations": 3, + "avg_blocktime": 0.25, + "protocol":{ + "type": "ETH", + "protocol_data": { + "chain_id": ETH_SEPOLIA_CHAIN_ID, + } + }, + + "derivation_path": "m/44'/60'" + } + ]); + + // start bob and immediately place the order + let mm_bob = MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9998, + "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": seed.to_string(), + "enable_hd": true, + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + }), + "pass".into(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log!("Bob log path: {}", mm_bob.log_path.display()); + + // Enable coins on Bob side. Print the replies in case we need the "address". + let enable = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "enable_eth_with_tokens", + "mmrpc": "2.0", + "params": { + "ticker": "ETH", + "priv_key_policy": "ContextPrivKey", + "mm2": 1, + "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, + "nodes": ETH_SEPOLIA_NODES.iter().map(|node| json!({ "url": node})).collect::>(), + "erc20_tokens_requests": [] + } + }))) + .unwrap(); + + assert_eq!( + enable.0, + StatusCode::OK, + "'enable_eth_with_tokens' failed: {}", + enable.1 + ); + let result: Json = json::from_str(&enable.1).unwrap(); + let result: HDEthWithTokensActivationResult = json::from_value(result["result"].clone()).unwrap(); + log!("enable_coins (bob): {result:?}"); + + let response = block_on(sign_message( + &mm_bob, + "ETH", + Some(HDAddressSelector::DerivationPath { + derivation_path: "m/44'/60'/0'/0/0".to_owned(), + }), + )); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + let expected_signature = + "0x36b91a54f905f2dd88ecfd7f4a539710c699eaab2b425ba79ad959c29ec26492011674981da72d68ac0ab72bb35661a13c42bce314ecdfff0e44174f82a7ee2501"; + assert_eq!(expected_signature, response.signature); + + let address0 = match result.wallet_balance { + EnableCoinBalanceMap::HD(bal) => bal.accounts[0].addresses[0].address.clone(), + EnableCoinBalanceMap::Iguana(_) => panic!("Expected HD"), + }; + let response = block_on(verify_message(&mm_bob, "ETH", expected_signature, &address0)); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + assert!(response.is_valid); + + // Test address 1. + let get_new_address = block_on(get_new_address(&mm_bob, "ETH", 0, Some(Bip44Chain::External))); + assert!(get_new_address.new_address.balance.contains_key("ETH")); + let response = block_on(sign_message( + &mm_bob, + "ETH", + Some(HDAddressSelector::AddressId(HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, + })), + )); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + let expected_signature = + "0xc8aa1d54c311e38edc815308dc67018aecbd6d4008a88b9af7aba9c98997b7b56f9e6eab64b3c496c6fff1762ae0eba8228370b369d505dd9087cded0a4d947a01"; + assert_eq!(expected_signature, response.signature); + + let response = block_on(verify_message( + &mm_bob, + "ETH", + expected_signature, + &get_new_address.new_address.address, + )); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + assert!(response.is_valid); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_no_login() { diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 7fd4dc4c35..f47fcde661 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -554,6 +554,7 @@ pub fn rick_conf() -> Json { "txversion":4, "overwintered":1, "derivation_path": "m/44'/141'", + "sign_message_prefix": "Komodo Signed Message:\n", "protocol":{ "type":"UTXO" } @@ -2879,7 +2880,7 @@ pub async fn init_z_coin_status(mm: &MarketMakerIt, task_id: u64) -> Json { json::from_str(&request.1).unwrap() } -pub async fn sign_message(mm: &MarketMakerIt, coin: &str) -> Json { +pub async fn sign_message(mm: &MarketMakerIt, coin: &str, derivation_path: Option) -> Json { let request = mm .rpc(&json!({ "userpass": mm.userpass, @@ -2888,7 +2889,8 @@ pub async fn sign_message(mm: &MarketMakerIt, coin: &str) -> Json { "id": 0, "params":{ "coin": coin, - "message":"test" + "message": "test", + "address": derivation_path } })) .await @@ -3327,13 +3329,15 @@ pub async fn init_utxo_electrum( "rpc": "Electrum", "rpc_data": { "servers": servers, - "path_to_address": path_to_address, } } }); if let Some(priv_key_policy) = priv_key_policy { activation_params["priv_key_policy"] = priv_key_policy.into(); } + if let Some(path_to_address) = path_to_address { + activation_params["path_to_address"] = json!(path_to_address); + } let request = mm .rpc(&json!({ "userpass": mm.userpass, diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index b5d3be28f5..fac660af8d 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -746,7 +746,7 @@ pub enum CreateNewAccountStatus { #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] #[serde(untagged)] -pub enum WithdrawFrom { +pub enum HDAddressSelector { AddressId(HDAccountAddressId), DerivationPath { derivation_path: String }, } From 0c39e5025107a835574da6ca6c2eca6738ad84bc Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Fri, 30 May 2025 03:05:07 +0300 Subject: [PATCH 17/36] improvement(builds): enable static CRT linking for MSVC builds (#2464) --- .cargo/config.toml | 3 +++ .github/workflows/dev-build.yml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 5704896780..6ad3bdd1c7 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -25,3 +25,6 @@ rustflags = [ "-Zshare-generics=y" ] [target.wasm32-unknown-unknown] runner = 'wasm-bindgen-test-runner' rustflags = [ "--cfg=web_sys_unstable_apis" ] + +[target.x86_64-pc-windows-msvc] +rustflags = ["-Ctarget-feature=+crt-static"] diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index b44ec5bae4..0849cca89a 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -224,7 +224,7 @@ jobs: if: ${{ env.AVAILABLE != '' }} run: | $NAME="kdf_$Env:KDF_BUILD_TAG-win-x86-64.zip" - 7z a $NAME .\target\release\kdf.exe .\target\release\*.dll + 7z a $NAME .\target\release\kdf.exe $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ From 923eb0be25495df1d5a2be357bd69504adf2006c Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Sat, 31 May 2025 03:56:26 +0100 Subject: [PATCH 18/36] fix(test): fix HD Wallet message signing tests (#2474) fixes two tests that were introduced in https://github.com/KomodoPlatform/komodo-defi-framework/pull/2432. 1- fixes `test_sign_verify_message_eth_with_derivation_path` by including a json parameter that was missing. 2- fixes `test_hd_address_conflict_across_derivation_paths` as it was not functioning correctly because it never re-used the same DB path, which is a critical step to show that the bug exists. this makes it so we reuse the same DB path for the second mm2 instance. the test was also simplified to omit message signing as it was not critical part of the test. --- .../tests/mm2_tests/mm2_tests_inner.rs | 72 +++++++++---------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 81ccbb3b7f..276ab5f9f7 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -5311,10 +5311,11 @@ fn test_sign_verify_message_segwit_with_bip84_derivation_path() { assert!(!verify_response.result.is_valid, "Cross-verification should fail"); } -/// Needs attention after [issue #2470](https://github.com/KomodoPlatform/komodo-defi-framework/issues/2470) is resolved. +///NOTE: Should not fail after [issue #2470](https://github.com/KomodoPlatform/komodo-defi-framework/issues/2470) is resolved. #[test] +#[ignore] #[cfg(not(target_arch = "wasm32"))] -fn test_address_conflict_across_derivation_paths() { +fn test_hd_address_conflict_across_derivation_paths() { const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; let rick_legacy_conf = json!({ @@ -5330,19 +5331,18 @@ fn test_address_conflict_across_derivation_paths() { "protocol": {"type": "UTXO"}, "derivation_path": "m/49'/141'", }); - let coins = json!([rick_legacy_conf]); - let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); - let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let mut conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf.clone(), conf.rpc_password.clone(), None).unwrap(); let path_to_address = HDAccountAddressId { account_id: 0, chain: Bip44Chain::External, address_id: 0, }; - - // Enable RICK with BIP49 (legacy P2SH-SegWit) - let rick_legacy = block_on(enable_utxo_v2_electrum( + // Enable RICK with m/49'/141' + let rick_1 = block_on(enable_utxo_v2_electrum( &mm_hd, "RICK", doc_electrums(), @@ -5350,27 +5350,18 @@ fn test_address_conflict_across_derivation_paths() { 60, None, )); - - let legacy_address = match &rick_legacy.wallet_balance { + let old_address = match &rick_1.wallet_balance { EnableCoinBalanceMap::HD(hd) => hd.accounts[0].addresses[0].address.clone(), _ => panic!("Expected HD wallet balance"), }; - log!("Legacy address: {}", legacy_address); - - // Sign a message using this legacy address - let sign_response = block_on(sign_message( - &mm_hd, - "RICK", - Some(HDAddressSelector::AddressId(path_to_address.clone())), - )); - let sign_response: RpcV2Response = json::from_value(sign_response).unwrap(); - let signature = sign_response.result.signature; + log!("Old address: {}", old_address); - // Shutdown MM and restart it with different(new native SegWit) derivation path (BIP84) + // Shutdown MM and restart RICK with derivation path m/84'/141' + log!("Conf log path: {}", mm_hd.log_path.display()); + conf.conf["dbdir"] = mm_hd.folder.join("DB").to_str().unwrap().into(); block_on(mm_hd.stop()).unwrap(); - std::thread::sleep(std::time::Duration::from_secs(1)); - let rick_bip84_conf = json!({ + let coin = json!({ "coin": "RICK", "asset": "RICK", "rpcport": 8923, @@ -5383,13 +5374,11 @@ fn test_address_conflict_across_derivation_paths() { "protocol": {"type": "UTXO"}, "derivation_path": "m/84'/141'", }); - - let coins = json!([rick_bip84_conf]); - let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + conf.conf["coins"] = json!([coin]); let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - // Re-enable RICK, but it will try to reuse address0 stored under old path - let rick_bip84 = block_on(enable_utxo_v2_electrum( + // Re-enable RICK, but it will try to reuse address0 stored under old path(m/49'/141') + let rick_2 = block_on(enable_utxo_v2_electrum( &mm_hd, "RICK", doc_electrums(), @@ -5397,23 +5386,27 @@ fn test_address_conflict_across_derivation_paths() { 60, None, )); - - let bip84_address = match &rick_bip84.wallet_balance { + let new_address = match &rick_2.wallet_balance { EnableCoinBalanceMap::HD(hd) => hd.accounts[0].addresses[0].address.clone(), _ => panic!("Expected HD wallet balance"), }; + log!("New address: {}", new_address); - log!("BIP84 address: {}", bip84_address); + // KDF has a bug and reuses the same account (and thus the same address) for derivation paths that use different `m/purpose'/coin'` fields. - // Try to verify the old signature using new BIP84-derived address - let verify_response = block_on(verify_message(&mm_hd, "RICK", &signature, &bip84_address)); - let verify_response: RpcV2Response = json::from_value(verify_response).unwrap(); + // This stems from the fact that KDF doesn't differentiate/store the "purpose" & "coin" derivation fields in the database, but it rather stores the whole xpub - // This should fail: signature was created with pubkey from m/49'/141'/0'/0/0, - // but we are now resolving address from m/84'/141'/0'/0/0 - assert!( - !verify_response.result.is_valid, - "Expected verification to fail due to derivation path mismatch" + // that repsresents `m/purpose'/coin'/account_id'` + + // Now, when KDF queries the database for already stored accounts, it specifies the specifies `COIN=ticker` in the SQL query, and since + + // we badly mutated the conf by changing the derivation path but not the coin ticker, it returns accounts belonging to the old coin ticker (old derivation path). + + // This wouldn't have happened if we gave the conf with `m/84'/141'` ticker="RICK-segwit" and `m/49'/141'` ticker="RICK-legacy", but we don't do that. + + assert_ne!( + old_address, new_address, + "Address from old derivation path(m/49'/141') should not match address from new derivation path(m/84'/141')" ); } @@ -5527,6 +5520,7 @@ fn test_sign_verify_message_eth_with_derivation_path() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, From 62f77afd5007067329541db143e678a92475695b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 2 Jun 2025 10:33:48 +0300 Subject: [PATCH 19/36] feat(ibc-routing-part-1): supporting entire Cosmos network for swaps (#2459) * add new feature `ibc-routing-for-swaps` to coins crate * re-implement `fn wallet_only` behind ibc-routing-for-swaps feature * add pre-check function `create_maker_order_pre_checks` to `MmCoins` * add default logic to `tendermint_coin::pre_check_for_order_creation` * enable conditional compilation * use single generic IBC error in `WithdrawError` --- mm2src/coins/Cargo.toml | 4 + mm2src/coins/lp_coins.rs | 54 ++++-- mm2src/coins/tendermint/tendermint_coin.rs | 205 +++++++++++++++++--- mm2src/coins/tendermint/tendermint_token.rs | 17 +- mm2src/mm2_main/src/lp_ordermatch.rs | 7 +- 5 files changed, 232 insertions(+), 55 deletions(-) diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index fd4d0bfbcf..93df96663d 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -15,6 +15,10 @@ run-docker-tests = [] for-tests = ["dep:mocktopus"] new-db-arch = ["mm2_core/new-db-arch"] +# Temporary feature for implementing IBC wrap/unwrap mechanism and will be removed +# once we consider it as stable. +ibc-routing-for-swaps = [] + [lib] path = "lp_coins.rs" doctest = false diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 8bca996ba9..abae7a4c85 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -3181,20 +3181,8 @@ pub enum WithdrawError { SigningError(String), #[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.", - target_address - )] - IBCChannelCouldNotFound { - target_address: String, - }, - #[display( - fmt = "IBC channel '{}' is not healthy. Provide a healthy one manually by including `ibc_source_channel` in the request.", - channel_id - )] - IBCChannelNotHealthy { - channel_id: ChannelId, - }, + #[display(fmt = "Tendermint IBC error: {}", _0)] + IBCError(tendermint::IBCError), } impl HttpStatusCode for WithdrawError { @@ -3223,8 +3211,7 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::NoChainIdSet { .. } | WithdrawError::TxTypeNotSupported | WithdrawError::SigningError(_) - | WithdrawError::IBCChannelCouldNotFound { .. } - | WithdrawError::IBCChannelNotHealthy { .. } + | WithdrawError::IBCError(_) | WithdrawError::MyAddressNotNftOwner { .. } => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] @@ -3421,6 +3408,16 @@ impl HttpStatusCode for VerificationError { } } +#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] +pub enum OrderCreationPreCheckError { + #[display(fmt = "'{ticker}' is a wallet only asset and can't be used in orders.")] + IsWalletOnly { ticker: String }, + #[display(fmt = "Pre-Check failed due to this reason: {reason}")] + PreCheckFailed { reason: String }, + #[display(fmt = "Internal error: {reason}")] + InternalError { reason: String }, +} + /// NB: Implementations are expected to follow the pImpl idiom, providing cheap reference-counted cloning and garbage collection. #[async_trait] pub trait MmCoin: SwapOps + WatcherOps + MarketCoinOps + Send + Sync + 'static { @@ -3532,6 +3529,31 @@ pub trait MmCoin: SwapOps + WatcherOps + MarketCoinOps + Send + Sync + 'static { stage: FeeApproxStage, ) -> TradePreimageResult; + /// TODO: It's weird that we implement this function on this trait. + /// + /// Move this into the `SwapOps` trait when possible (this function requires `MmCoins` + /// trait to be implemented, but it's currently not possible to do `SwapOps: MmCoins` + /// as `MmCoins` is already `MmCoins: SwapOps`. + async fn pre_check_for_order_creation( + &self, + ctx: &MmArc, + rel_coin: &MmCoinEnum, + ) -> MmResult<(), OrderCreationPreCheckError> { + if self.wallet_only(ctx) { + return MmError::err(OrderCreationPreCheckError::IsWalletOnly { + ticker: self.ticker().to_owned(), + }); + } + + if rel_coin.wallet_only(ctx) { + return MmError::err(OrderCreationPreCheckError::IsWalletOnly { + ticker: rel_coin.ticker().to_owned(), + }); + } + + Ok(()) + } + /// required transaction confirmations number to ensure double-spend safety fn required_confirmations(&self) -> u64; diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index ab067249b9..cefcd18ec2 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -459,6 +459,30 @@ pub enum TendermintCoinRpcError { NotFound(String), } +#[derive(Clone, Debug, Display, PartialEq, Serialize)] +pub enum IBCError { + #[display( + fmt = "IBC channel could not be found in coins file for '{}' address prefix. Provide it manually by including `ibc_source_channel` in the request.", + address_prefix + )] + IBCChannelCouldNotBeFound { address_prefix: String }, + #[display( + fmt = "IBC channel '{}' is not healthy. Provide a healthy one manually by including `ibc_source_channel` in the request.", + channel_id + )] + IBCChannelNotHealthy { channel_id: ChannelId }, + #[display(fmt = "IBC channel '{}' is not present on the target node.", channel_id)] + IBCChannelMissingOnNode { channel_id: ChannelId }, + #[display(fmt = "Transport error: {reason}")] + Transport { reason: String }, + #[display(fmt = "Internal error: {reason}")] + InternalError { reason: String }, +} + +impl From for WithdrawError { + fn from(err: IBCError) -> Self { WithdrawError::IBCError(err) } +} + impl From for TendermintCoinRpcError { fn from(err: DecodeError) -> Self { TendermintCoinRpcError::Prost(err.to_string()) } } @@ -756,7 +780,7 @@ impl TendermintCoin { &self, channel_id: ChannelId, port_id: &str, - ) -> MmResult { + ) -> Result { let payload = QueryChannelRequest { channel_id: channel_id.to_string(), port_id: port_id.to_string(), @@ -770,31 +794,34 @@ impl TendermintCoin { ABCI_REQUEST_PROVE, ); - let response = self.rpc_client().await?.perform(request).await?; - let response = QueryChannelResponse::decode(response.response.value.as_slice())?; + let response = self + .rpc_client() + .await + .map_err(|e| IBCError::Transport { reason: e.to_string() })? + .perform(request) + .await + .map_err(|e| IBCError::Transport { reason: e.to_string() })?; - response.channel.ok_or_else(|| { - MmError::new(TendermintCoinRpcError::NotFound(format!( - "No result for channel id: {channel_id}, port: {port_id}." - ))) - }) + let response = QueryChannelResponse::decode(response.response.value.as_slice()) + .map_err(|e| IBCError::InternalError { reason: e.to_string() })?; + + response.channel.ok_or(IBCError::IBCChannelMissingOnNode { channel_id }) } /// 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> { + address_prefix: &str, + ) -> 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_id = - *self - .ibc_channels - .get(target_address.prefix()) - .ok_or_else(|| WithdrawError::IBCChannelCouldNotFound { - target_address: target_address.to_string(), - })?; + let channel_id = *self + .ibc_channels + .get(address_prefix) + .ok_or_else(|| IBCError::IBCChannelCouldNotBeFound { + address_prefix: address_prefix.to_owned(), + })?; let channel = self.query_ibc_channel(channel_id, "transfer").await?; @@ -804,7 +831,7 @@ impl TendermintCoin { // - Verifying the total amount transferred since the channel was created // - Check the channel creation time if channel.state != STATE_OPEN { - return MmError::err(WithdrawError::IBCChannelNotHealthy { channel_id }); + return MmError::err(IBCError::IBCChannelNotHealthy { channel_id }); } Ok(channel_id) @@ -2995,6 +3022,33 @@ fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult bool { false } + #[cfg(feature = "ibc-routing-for-swaps")] + fn wallet_only(&self, ctx: &MmArc) -> bool { + // Keplr with Ledger does not support some transactions like HTLC due to + // the transaction format they use. As HTLC is part of our swap system's DNA, + // treat any Tendermint asset as wallet-only. + // + // TODO: Once `SIGN_MODE_DIRECT` is supported, we can remove this. + if self.is_keplr_from_ledger { + common::log::info!("Using Keplr with Ledger: operating in wallet only mode."); + return true; + } + + let coin_conf = crate::coin_conf(ctx, self.ticker()); + let wallet_only_conf = coin_conf + .get("wallet_only") + .unwrap_or(&json!(false)) + .as_bool() + .unwrap_or(false); + + if wallet_only_conf { + warn!("`wallet_only` option cannot be set to true for Tendermint assets. This setting will be ignored."); + } + + false + } + + #[cfg(not(feature = "ibc-routing-for-swaps"))] fn wallet_only(&self, ctx: &MmArc) -> bool { let coin_conf = crate::coin_conf(ctx, self.ticker()); // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only @@ -3047,7 +3101,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_healthy_ibc_channel_for_address(&to_address).await?), + None => Some(coin.get_healthy_ibc_channel_for_address(to_address.prefix()).await?), } } else { None @@ -3256,6 +3310,115 @@ impl MmCoin for TendermintCoin { .await } + /// Overrides the default `pre_check_for_order_creation` implementation with + /// additional IBC-related logic on top of the default behavior. + #[cfg(feature = "ibc-routing-for-swaps")] + async fn pre_check_for_order_creation( + &self, + ctx: &MmArc, + rel_coin: &crate::MmCoinEnum, + ) -> MmResult<(), crate::OrderCreationPreCheckError> { + use crate::{lp_coinfind, MmCoinEnum, OrderCreationPreCheckError}; + + /// Looks for a Tendermint platform coin by the given ticker. + /// + /// Returns `Ok(Some(...))` if the coin exists and is a Tendermint platform coin, + /// `Ok(None)` if it's not active, or an error if somethings goes wrong or the ticker + /// isn't belongs to a Tendermint platform coin. + async fn find_tendermint_platform_coin( + ctx: &MmArc, + ticker: &str, + ) -> Result, MmError> { + match lp_coinfind(ctx, ticker).await { + Ok(Some(MmCoinEnum::Tendermint(coin))) => Ok(Some(coin)), + Ok(Some(other)) => MmError::err(OrderCreationPreCheckError::InternalError { + reason: format!( + "Expected a Tendermint coin for '{}', but found '{}'.", + ticker, + other.ticker() + ), + }), + Ok(None) => Ok(None), + Err(reason) => MmError::err(OrderCreationPreCheckError::PreCheckFailed { reason }), + } + } + + /// Picks an HTLC coin (IRIS or NUCLEUS) based on which IBC channel is configured + /// and is healthy. + async fn get_htlc_coin( + coin: &TendermintCoin, + ctx: &MmArc, + ) -> Result, MmError> { + const IRIS_PREFIX: &str = "iaa"; + const IRIS_TICKER: &str = "IRIS"; + + const NUCLEUS_PREFIX: &str = "nuc"; + const NUCLEUS_TICKER: &str = "NUCLEUS"; + + if coin.get_healthy_ibc_channel_for_address(IRIS_PREFIX).await.is_ok() { + return find_tendermint_platform_coin(ctx, IRIS_TICKER).await; + } + + if coin.get_healthy_ibc_channel_for_address(NUCLEUS_PREFIX).await.is_ok() { + return find_tendermint_platform_coin(ctx, NUCLEUS_TICKER).await; + } + + MmError::err(OrderCreationPreCheckError::PreCheckFailed { + reason: format!("No healthy IBC channel found for {}.", coin.ticker()), + }) + } + + if self.wallet_only(ctx) { + return MmError::err(OrderCreationPreCheckError::IsWalletOnly { + ticker: self.ticker().to_owned(), + }); + } + + if rel_coin.wallet_only(ctx) { + return MmError::err(OrderCreationPreCheckError::IsWalletOnly { + ticker: rel_coin.ticker().to_owned(), + }); + } + + let supports_htlc = matches!(self.account_prefix.as_str(), "nuc" | "iaa"); + + if supports_htlc { + return Ok(()); + } + + // If `self` is not an HTLC-supported coin, we need to check a few things when creating the order: + // - Is there an HTLC coin enabled? + // - Does that HTLC network have an IBC channel configured to `self` network? + // - Does that HTLC coin have enough balance to handle IBC routing? + + let Some(htlc_coin) = get_htlc_coin(self, ctx).await? else { + return MmError::err(OrderCreationPreCheckError::PreCheckFailed { + reason: "No HTLC coin is currently enabled. Please enable either Iris or Nucleus.".into(), + }); + }; + + let my_balance = htlc_coin + .my_balance() + .compat() + .await + .map_err(|e| OrderCreationPreCheckError::InternalError { reason: e.to_string() })? + .spendable; + + // TODO: Take this value from the coins file. + let min = BigDecimal::from(2); + + if min > my_balance { + let htlc_ticker = htlc_coin.ticker(); + let self_ticker = self.ticker(); + let reason = format!( + "Insufficient balance on HTLC coin ({htlc_ticker}) for making orders with {self_ticker}. Minimum required expected balance {min}, current balance {my_balance}.", + ); + return MmError::err(OrderCreationPreCheckError::PreCheckFailed { reason }); + } + + Ok(()) + } + fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { let coin = self.clone(); let fut = async move { @@ -5051,9 +5214,7 @@ pub mod tendermint_coin_tests { 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_healthy_ibc_channel_for_address(&addr)).unwrap(); + let actual_channel = block_on(coin.get_healthy_ibc_channel_for_address("cosmos")).unwrap(); let actual_channel_str = actual_channel.to_string(); assert_eq!(expected_channel, actual_channel); diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 89424daf4d..fa065a1853 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -365,16 +365,7 @@ impl MarketCoinOps for TendermintToken { impl MmCoin for TendermintToken { fn is_asset_chain(&self) -> bool { false } - fn wallet_only(&self, ctx: &MmArc) -> bool { - let coin_conf = crate::coin_conf(ctx, self.ticker()); - // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only - if coin_conf.is_null() { - return true; - } - let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); - - wallet_only_conf || self.platform_coin.is_keplr_from_ledger - } + fn wallet_only(&self, ctx: &MmArc) -> bool { self.platform_coin.wallet_only(ctx) } fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } @@ -437,7 +428,11 @@ 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_healthy_ibc_channel_for_address(&to_address).await?), + None => Some( + platform + .get_healthy_ibc_channel_for_address(to_address.prefix()) + .await?, + ), } } else { None diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 9141dc6284..91b728c2a2 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -4936,12 +4936,7 @@ pub async fn create_maker_order(ctx: &MmArc, req: SetPriceReq) -> Result return ERR!("Rel coin {} is not found", req.rel), }; - if base_coin.wallet_only(ctx) { - return ERR!("Base coin {} is wallet only", req.base); - } - if rel_coin.wallet_only(ctx) { - return ERR!("Rel coin {} is wallet only", req.rel); - } + try_s!(base_coin.pre_check_for_order_creation(ctx, &rel_coin).await); let (volume, balance) = if req.max { let CoinVolumeInfo { volume, balance, .. } = try_s!( From 9ccb42f11b94b976da3ea847cb25251ecbf127d5 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Tue, 3 Jun 2025 04:21:46 +0100 Subject: [PATCH 20/36] feat(walletconnect): add WalletConnect v2 support for EVM and Cosmos (#2223) - Adds WalletConnect v2 integration for Ethereum (ETH, ERC20) and Cosmos-based coins, supporting both mobile and web wallets. - Activation requests for these coins now use `"priv_key_policy": { "type": "WalletConnect", "session_topic": ... }` to delegate signing and transaction broadcasting to the connected wallet. No local private key is stored. - New public APIs for session management: `wc_new_connection`, `wc_get_sessions`, `wc_get_session`, `wc_ping_session`, `wc_delete_session`. - BREAKING: Coin activation for EVM and Cosmos coins now requires `"priv_key_policy"` to be an object with a `"type"` field (e.g. `{ "type": "ContextPrivKey" }`). Using a plain string is no longer accepted. - BREAKING: Activation requests/responses for these coins are not backward compatible with previous formats. - NFT coins are **not** yet supported via WalletConnect. - UTXO coins remain unchanged; WalletConnect is not supported for them. - All swap and transaction signing for WalletConnect-activated coins requires user approval in the external wallet. This update enables secure, external signing with WalletConnect-compatible wallets and introduces new APIs for session lifecycle management. --- .cargo/config.toml | 2 +- Cargo.lock | 710 +++++++++++------- Cargo.toml | 15 +- mm2src/coins/Cargo.toml | 1 + mm2src/coins/coin_errors.rs | 7 + mm2src/coins/eth.rs | 127 +++- mm2src/coins/eth/eth_withdraw.rs | 57 +- mm2src/coins/eth/v2_activation.rs | 40 +- mm2src/coins/eth/wallet_connect.rs | 298 ++++++++ mm2src/coins/lp_coins.rs | 29 +- mm2src/coins/siacoin.rs | 6 + mm2src/coins/tendermint/mod.rs | 2 + mm2src/coins/tendermint/tendermint_coin.rs | 300 ++++++-- mm2src/coins/tendermint/tendermint_token.rs | 1 + mm2src/coins/tendermint/wallet_connect.rs | 306 ++++++++ mm2src/coins/utxo/utxo_common.rs | 7 +- mm2src/coins/utxo/utxo_tests.rs | 2 +- mm2src/coins/utxo/utxo_withdraw.rs | 5 + mm2src/coins_activation/Cargo.toml | 1 + .../src/eth_with_token_activation.rs | 28 +- .../src/platform_coin_with_tokens.rs | 16 +- .../src/tendermint_with_assets_activation.rs | 104 ++- mm2src/common/common.rs | 5 + mm2src/db_common/src/async_sql_conn.rs | 4 + mm2src/kdf_walletconnect/Cargo.toml | 50 ++ mm2src/kdf_walletconnect/src/chain.rs | 107 +++ .../src/connection_handler.rs | 98 +++ mm2src/kdf_walletconnect/src/error.rs | 186 +++++ .../kdf_walletconnect/src/inbound_message.rs | 98 +++ mm2src/kdf_walletconnect/src/lib.rs | 682 +++++++++++++++++ mm2src/kdf_walletconnect/src/metadata.rs | 20 + mm2src/kdf_walletconnect/src/pairing.rs | 48 ++ mm2src/kdf_walletconnect/src/session/key.rs | 197 +++++ mm2src/kdf_walletconnect/src/session/mod.rs | 376 ++++++++++ .../src/session/rpc/delete.rs | 58 ++ .../src/session/rpc/event.rs | 73 ++ .../src/session/rpc/extend.rs | 20 + .../kdf_walletconnect/src/session/rpc/mod.rs | 9 + .../kdf_walletconnect/src/session/rpc/ping.rs | 30 + .../src/session/rpc/propose.rs | 152 ++++ .../src/session/rpc/settle.rs | 83 ++ .../src/session/rpc/update.rs | 48 ++ .../src/storage/indexed_db.rs | 129 ++++ mm2src/kdf_walletconnect/src/storage/mod.rs | 202 +++++ .../kdf_walletconnect/src/storage/sqlite.rs | 176 +++++ mm2src/mm2_core/src/mm_ctx.rs | 6 +- mm2src/mm2_io/src/fs.rs | 18 +- mm2src/mm2_main/Cargo.toml | 3 +- mm2src/mm2_main/src/lp_native_dex.rs | 5 + mm2src/mm2_main/src/rpc.rs | 1 + .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 8 + .../src/rpc/lp_commands/one_inch/rpcs.rs | 2 +- mm2src/mm2_main/src/rpc/wc_commands/mod.rs | 34 + .../src/rpc/wc_commands/new_connection.rs | 32 + .../mm2_main/src/rpc/wc_commands/sessions.rs | 86 +++ .../tests/mm2_tests/mm2_tests_inner.rs | 10 +- mm2src/mm2_test_helpers/src/for_tests.rs | 3 + 57 files changed, 4696 insertions(+), 427 deletions(-) create mode 100644 mm2src/coins/eth/wallet_connect.rs create mode 100644 mm2src/coins/tendermint/wallet_connect.rs create mode 100644 mm2src/kdf_walletconnect/Cargo.toml create mode 100644 mm2src/kdf_walletconnect/src/chain.rs create mode 100644 mm2src/kdf_walletconnect/src/connection_handler.rs create mode 100644 mm2src/kdf_walletconnect/src/error.rs create mode 100644 mm2src/kdf_walletconnect/src/inbound_message.rs create mode 100644 mm2src/kdf_walletconnect/src/lib.rs create mode 100644 mm2src/kdf_walletconnect/src/metadata.rs create mode 100644 mm2src/kdf_walletconnect/src/pairing.rs create mode 100644 mm2src/kdf_walletconnect/src/session/key.rs create mode 100644 mm2src/kdf_walletconnect/src/session/mod.rs create mode 100644 mm2src/kdf_walletconnect/src/session/rpc/delete.rs create mode 100644 mm2src/kdf_walletconnect/src/session/rpc/event.rs create mode 100644 mm2src/kdf_walletconnect/src/session/rpc/extend.rs create mode 100644 mm2src/kdf_walletconnect/src/session/rpc/mod.rs create mode 100644 mm2src/kdf_walletconnect/src/session/rpc/ping.rs create mode 100644 mm2src/kdf_walletconnect/src/session/rpc/propose.rs create mode 100644 mm2src/kdf_walletconnect/src/session/rpc/settle.rs create mode 100644 mm2src/kdf_walletconnect/src/session/rpc/update.rs create mode 100644 mm2src/kdf_walletconnect/src/storage/indexed_db.rs create mode 100644 mm2src/kdf_walletconnect/src/storage/mod.rs create mode 100644 mm2src/kdf_walletconnect/src/storage/sqlite.rs create mode 100644 mm2src/mm2_main/src/rpc/wc_commands/mod.rs create mode 100644 mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs create mode 100644 mm2src/mm2_main/src/rpc/wc_commands/sessions.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 6ad3bdd1c7..d59b360d40 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,7 +2,7 @@ JEMALLOC_SYS_WITH_MALLOC_CONF = "background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" [target.'cfg(all())'] -rustflags = [ "-Zshare-generics=y" ] +rustflags = [ "-Zshare-generics=y", '--cfg=curve25519_dalek_backend="fiat"' ] # # Install lld using package manager # [target.x86_64-unknown-linux-gnu] diff --git a/Cargo.lock b/Cargo.lock index 59a803a59a..bbe2d395a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,25 +35,14 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "922b33332f54fc0ad13fa3e514601e8d30fb54e1f3eadc36643f6526db645621" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ + "crypto-common", "generic-array", ] -[[package]] -name = "aes" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" -dependencies = [ - "cfg-if 1.0.0", - "cipher 0.3.0", - "cpufeatures 0.2.11", - "opaque-debug", -] - [[package]] name = "aes" version = "0.8.3" @@ -61,19 +50,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if 1.0.0", - "cipher 0.4.4", - "cpufeatures 0.2.11", + "cipher", + "cpufeatures", ] [[package]] name = "aes-gcm" -version = "0.9.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", - "aes 0.7.5", - "cipher 0.3.0", + "aes", + "cipher", "ctr", "ghash", "subtle", @@ -130,9 +119,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.42" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "argon2" @@ -142,7 +131,7 @@ checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" dependencies = [ "base64ct", "blake2", - "cpufeatures 0.2.11", + "cpufeatures", "password-hash", "zeroize", ] @@ -241,7 +230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -258,8 +247,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531b97fb4cd3dfdce92c35dedbfdc1f0b9d8091c8ca943d6dae340ef5012d514" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -309,7 +298,7 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http", + "http 0.2.12", "http-body", "hyper", "itoa", @@ -335,7 +324,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 0.2.12", "http-body", "mime", "rustversion", @@ -696,7 +685,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher 0.4.4", + "cipher", ] [[package]] @@ -719,25 +708,24 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chacha20" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if 1.0.0", - "cipher 0.3.0", - "cpufeatures 0.2.11", - "zeroize", + "cipher", + "cpufeatures", ] [[package]] name = "chacha20poly1305" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", "chacha20", - "cipher 0.3.0", + "cipher", "poly1305", "zeroize", ] @@ -770,15 +758,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - [[package]] name = "cipher" version = "0.4.4" @@ -787,6 +766,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -833,7 +813,7 @@ dependencies = [ "db_common", "derive_more", "dirs", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "enum_derives", "ethabi", "ethcore-transaction", @@ -847,13 +827,14 @@ dependencies = [ "group 0.8.0", "gstuff", "hex", - "http", + "http 0.2.12", "hyper", "hyper-rustls 0.24.2", "itertools", "js-sys", "jsonrpc-core", "jubjub", + "kdf_walletconnect", "keys", "lazy_static", "libc", @@ -948,6 +929,7 @@ dependencies = [ "ethereum-types", "futures 0.3.28", "hex", + "kdf_walletconnect", "lightning", "lightning-background-processor", "lightning-invoice", @@ -991,7 +973,7 @@ dependencies = [ "futures-timer", "gstuff", "hex", - "http", + "http 0.2.12", "http-body", "hyper", "hyper-rustls 0.24.2", @@ -1122,15 +1104,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cpufeatures" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.2.11" @@ -1298,7 +1271,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" name = "crypto" version = "1.0.0" dependencies = [ - "aes 0.8.3", + "aes", "argon2", "arrayref", "async-trait", @@ -1309,7 +1282,7 @@ dependencies = [ "bs58 0.4.0", "cbc", "cfg-if 1.0.0", - "cipher 0.4.4", + "cipher", "common", "derive_more", "enum-primitive-derive", @@ -1317,7 +1290,7 @@ dependencies = [ "futures 0.3.28", "hex", "hmac 0.12.1", - "http", + "http 0.2.12", "hw_common", "keys", "lazy_static", @@ -1364,6 +1337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1404,11 +1378,11 @@ dependencies = [ [[package]] name = "ctr" -version = "0.7.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher 0.3.0", + "cipher", ] [[package]] @@ -1437,18 +1411,31 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0-rc.1" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if 1.0.0", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", "fiat-crypto", - "packed_simd_2", - "platforms", + "rustc_version 0.4.0", "subtle", "zeroize", ] +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote 1.0.37", + "syn 2.0.77", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1484,7 +1471,7 @@ dependencies = [ "codespan-reporting", "lazy_static", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "scratch", "syn 1.0.95", ] @@ -1502,7 +1489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1525,7 +1512,7 @@ dependencies = [ "fnv", "ident_case", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "strsim", "syn 1.0.95", ] @@ -1537,7 +1524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1599,7 +1586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1677,12 +1664,12 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ "serde", - "signature 1.4.0", + "signature 1.6.4", ] [[package]] @@ -1715,7 +1702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek 3.2.0", - "ed25519 1.5.2", + "ed25519 1.5.3", "rand 0.7.3", "serde", "serde_bytes", @@ -1723,6 +1710,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.7", + "subtle", + "zeroize", +] + [[package]] name = "edit-distance" version = "2.1.0" @@ -1775,9 +1777,9 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1788,7 +1790,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c375b9c5eadb68d0a6efee2999fef292f45854c3444c86f09d8ab086ba942b0e" dependencies = [ "num-traits", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1798,7 +1800,7 @@ version = "0.1.0" dependencies = [ "itertools", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1996,9 +1998,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.1.20" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "findshlibs" @@ -2017,7 +2019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ "byteorder", - "rand 0.8.4", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -2071,8 +2073,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26c4b37de5ae15812a764c958297cfc50f5c010438f60c6ce75d11b802abd404" dependencies = [ "cbc", - "cipher 0.4.4", - "libm 0.2.7", + "cipher", + "libm", "num-bigint", "num-integer", "num-traits", @@ -2184,8 +2186,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -2299,9 +2301,9 @@ dependencies = [ [[package]] name = "ghash" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bbd60caa311237d508927dbba7594b483db3ef05faa55172fcf89b1bcda7853" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", "polyval", @@ -2381,7 +2383,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap 2.2.3", "slab", "tokio", @@ -2455,7 +2457,7 @@ dependencies = [ "base64 0.21.7", "bytes", "headers-core", - "http", + "http 0.2.12", "httpdate", "mime", "sha1", @@ -2467,7 +2469,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" dependencies = [ - "http", + "http 0.2.12", ] [[package]] @@ -2476,6 +2478,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.14" @@ -2515,6 +2523,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.8.1" @@ -2577,6 +2594,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -2584,7 +2612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.12", "pin-project-lite 0.2.9", ] @@ -2638,7 +2666,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body", "httparse", "httpdate", @@ -2657,7 +2685,7 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" dependencies = [ - "http", + "http 0.2.12", "hyper", "rustls 0.20.4", "tokio", @@ -2671,7 +2699,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", + "http 0.2.12", "hyper", "rustls 0.21.10", "tokio", @@ -2809,7 +2837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -2949,6 +2977,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.7", + "pem", + "ring 0.16.20", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "jubjub" version = "0.5.1" @@ -2975,6 +3017,47 @@ dependencies = [ "sha2 0.10.7", ] +[[package]] +name = "kdf_walletconnect" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.21.7", + "cfg-if 1.0.0", + "chrono", + "common", + "db_common", + "derive_more", + "enum_derives", + "futures 0.3.28", + "hex", + "hkdf", + "js-sys", + "mm2_core", + "mm2_db", + "mm2_err_handle", + "mm2_test_helpers", + "pairing_api", + "parking_lot", + "rand 0.8.5", + "relay_client", + "relay_rpc", + "secp256k1 0.20.3", + "serde", + "serde_json", + "sha2 0.10.7", + "thiserror", + "timed-map", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "wc_common", + "web-sys", + "x25519-dalek 2.0.1", +] + [[package]] name = "keccak" version = "0.1.0" @@ -3042,12 +3125,6 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" -[[package]] -name = "libm" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" - [[package]] name = "libm" version = "0.2.7" @@ -3127,7 +3204,7 @@ dependencies = [ "parking_lot", "pin-project", "quick-protobuf", - "rand 0.8.4", + "rand 0.8.5", "rw-stream-sink", "smallvec 1.6.1", "thiserror", @@ -3164,7 +3241,7 @@ dependencies = [ "log", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "thiserror", ] @@ -3192,7 +3269,7 @@ dependencies = [ "prometheus-client", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.4", + "rand 0.8.5", "regex", "sha2 0.10.7", "smallvec 1.6.1", @@ -3229,12 +3306,12 @@ checksum = "d2874d9c6575f1d7a151022af5c42bb0ffdcdfbafe0a6fd039de870b384835a2" dependencies = [ "asn1_der", "bs58 0.5.0", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "libsecp256k1", "log", "multihash", "quick-protobuf", - "rand 0.8.4", + "rand 0.8.5", "sha2 0.10.7", "thiserror", "zeroize", @@ -3252,7 +3329,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "socket2 0.5.3", "tokio", @@ -3291,12 +3368,12 @@ dependencies = [ "multihash", "once_cell", "quick-protobuf", - "rand 0.8.4", + "rand 0.8.5", "sha2 0.10.7", "snow", "static_assertions", "thiserror", - "x25519-dalek", + "x25519-dalek 1.1.0", "zeroize", ] @@ -3313,7 +3390,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand 0.8.4", + "rand 0.8.5", "void", ] @@ -3329,7 +3406,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "void", ] @@ -3350,7 +3427,7 @@ dependencies = [ "log", "multistream-select", "once_cell", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "tokio", "void", @@ -3361,11 +3438,11 @@ name = "libp2p-swarm-derive" version = "0.33.0" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro-warning", "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -3443,7 +3520,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand 0.8.4", + "rand 0.8.5", "serde", "sha2 0.9.9", "typenum", @@ -3737,8 +3814,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -3888,7 +3965,7 @@ dependencies = [ "common", "derive_more", "futures 0.1.29", - "http", + "http 0.2.12", "itertools", "ser_error", "ser_error_derive", @@ -3933,7 +4010,7 @@ version = "0.1.0" dependencies = [ "async-trait", "common", - "http", + "http 0.2.12", "mm2_err_handle", "mm2_net", "serde", @@ -4011,12 +4088,13 @@ dependencies = [ "hash-db", "hash256-std-hasher", "hex", - "http", + "http 0.2.12", "hw_common", "hyper", "instant", "itertools", "js-sys", + "kdf_walletconnect", "keys", "lazy_static", "libc", @@ -4140,7 +4218,7 @@ dependencies = [ "futures 0.3.28", "futures-util", "gstuff", - "http", + "http 0.2.12", "http-body", "httparse", "hyper", @@ -4225,7 +4303,7 @@ dependencies = [ "derive_more", "futures 0.3.28", "gstuff", - "http", + "http 0.2.12", "mm2_err_handle", "mm2_number", "rpc", @@ -4257,7 +4335,7 @@ dependencies = [ "db_common", "futures 0.3.28", "gstuff", - "http", + "http 0.2.12", "lazy_static", "mm2_core", "mm2_io", @@ -4290,7 +4368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3048ef3680533a27f9f8e7d6a0bce44dc61e4895ea0f42709337fa1c8616fefe" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -4476,8 +4554,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -4558,16 +4636,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "packed_simd_2" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" -dependencies = [ - "cfg-if 1.0.0", - "libm 0.1.4", -] - [[package]] name = "pairing" version = "0.18.0" @@ -4578,6 +4646,27 @@ dependencies = [ "group 0.8.0", ] +[[package]] +name = "pairing_api" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.3#e2fb03ac19186fd2372a0eef71897ef8a6ae9653" +dependencies = [ + "anyhow", + "chrono", + "hex", + "lazy_static", + "paste", + "rand 0.8.5", + "regex", + "relay_client", + "relay_rpc", + "serde", + "serde_json", + "thiserror", + "url", + "wc_common", +] + [[package]] name = "parity-scale-codec" version = "3.1.2" @@ -4600,7 +4689,7 @@ checksum = "c45ed1f39709f5a89338fab50e59816b2e8815f5bb58276e7ddf9afd495f73f8" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -4696,9 +4785,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.7" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "peg" @@ -4718,7 +4807,7 @@ checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" dependencies = [ "peg-runtime", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", ] [[package]] @@ -4768,8 +4857,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -4806,12 +4895,6 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" -[[package]] -name = "platforms" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" - [[package]] name = "polling" version = "3.7.4" @@ -4829,23 +4912,23 @@ dependencies = [ [[package]] name = "poly1305" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe800695325da85083cd23b56826fccb2e2dc29b218e7811a6f33bc93f414be" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures 0.1.4", + "cpufeatures", "opaque-debug", "universal-hash", ] [[package]] name = "polyval" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e597450cbf209787f0e6de80bf3795c6b2356a380ee87837b545aded8dbc1823" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if 1.0.0", - "cpufeatures 0.1.4", + "cpufeatures", "opaque-debug", "universal-hash", ] @@ -4869,7 +4952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.38", + "syn 2.0.77", ] [[package]] @@ -4913,15 +4996,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70550716265d1ec349c41f70dd4f964b4fd88394efe4405f0c1da679c4799a07" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] @@ -4945,7 +5028,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b6a5217beb0ad503ee7fa752d451c905113d70721b937126158f3106a48cc1" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -4966,7 +5049,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck", + "heck 0.5.0", "itertools", "log", "multimap", @@ -4976,7 +5059,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.38", + "syn 2.0.77", "tempfile", ] @@ -4989,8 +5072,8 @@ dependencies = [ "anyhow", "itertools", "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -5032,7 +5115,7 @@ name = "proxy_signature" version = "0.1.0" dependencies = [ "chrono", - "http", + "http 0.2.12", "libp2p", "rand 0.7.3", "serde", @@ -5101,9 +5184,9 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -5171,14 +5254,13 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", - "rand_hc 0.3.1", ] [[package]] @@ -5262,15 +5344,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rand_isaac" version = "0.1.1" @@ -5416,7 +5489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c523ccaed8ac4b0288948849a350b37d3035827413c458b6a40ddb614bb4f72" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -5437,6 +5510,61 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "relay_client" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.3#e2fb03ac19186fd2372a0eef71897ef8a6ae9653" +dependencies = [ + "chrono", + "data-encoding", + "futures-util", + "getrandom 0.2.9", + "http 1.1.0", + "js-sys", + "pin-project", + "rand 0.8.5", + "relay_rpc", + "serde", + "serde_json", + "serde_qs", + "thiserror", + "tokio", + "tokio-tungstenite-wasm", + "tokio-util", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "relay_rpc" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.3#e2fb03ac19186fd2372a0eef71897ef8a6ae9653" +dependencies = [ + "anyhow", + "bs58 0.4.0", + "chrono", + "data-encoding", + "derive_more", + "ed25519-dalek 2.1.1", + "getrandom 0.2.9", + "hex", + "jsonwebtoken", + "once_cell", + "paste", + "rand 0.8.5", + "regex", + "serde", + "serde-aux", + "serde_json", + "sha2 0.10.7", + "strum", + "thiserror", + "url", +] + [[package]] name = "reqwest" version = "0.11.9" @@ -5449,7 +5577,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body", "hyper", "hyper-rustls 0.23.0", @@ -5859,7 +5987,7 @@ checksum = "50e334bb10a245e28e5fd755cabcafd96cfcd167c99ae63a46924ca8d8703a3c" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6043,20 +6171,30 @@ name = "ser_error_derive" version = "0.1.0" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "ser_error", "syn 1.0.95", ] [[package]] name = "serde" -version = "1.0.189" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-aux" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "serde-wasm-bindgen" version = "0.4.3" @@ -6079,27 +6217,39 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.2.3", "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "serde_qs" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_repr" version = "0.1.6" @@ -6107,7 +6257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6150,7 +6300,7 @@ checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ "darling", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6181,7 +6331,7 @@ checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.9.0", "opaque-debug", ] @@ -6193,7 +6343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.10.7", ] @@ -6205,7 +6355,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.9.0", "opaque-debug", ] @@ -6217,7 +6367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.10.7", ] @@ -6259,7 +6409,7 @@ dependencies = [ "blake2b_simd", "chrono", "derive_more", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "hex", "nom", "reqwest", @@ -6281,9 +6431,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.4.0" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "signature" @@ -6295,6 +6445,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.20", +] + [[package]] name = "siphasher" version = "0.1.3" @@ -6354,16 +6516,16 @@ dependencies = [ [[package]] name = "snow" -version = "0.9.2" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" dependencies = [ "aes-gcm", "blake2", "chacha20poly1305", - "curve25519-dalek 4.0.0-rc.1", + "curve25519-dalek 4.1.3", "rand_core 0.6.4", - "ring 0.16.20", + "ring 0.17.3", "rustc_version 0.4.0", "sha2 0.10.7", "subtle", @@ -6411,7 +6573,7 @@ dependencies = [ "futures 0.3.28", "httparse", "log", - "rand 0.8.4", + "rand 0.8.5", "sha-1", ] @@ -6447,7 +6609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d676664972e22a0796176e81e7bec41df461d1edf52090955cdab55f2c956ff2" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6477,7 +6639,7 @@ dependencies = [ "Inflector", "proc-macro-crate", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6598,7 +6760,7 @@ checksum = "2f9799e6d412271cb2414597581128b03f3285f260ea49f5363d07df6a332b3e" dependencies = [ "Inflector", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "serde", "serde_json", "unicode-xid 0.2.0", @@ -6616,6 +6778,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote 1.0.37", + "rustversion", + "syn 2.0.77", +] + [[package]] name = "subtle" version = "2.4.0" @@ -6655,18 +6839,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.38" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "unicode-ident", ] @@ -6692,7 +6876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", "unicode-xid 0.2.0", ] @@ -6812,7 +6996,7 @@ dependencies = [ "getrandom 0.2.9", "peg", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "semver 1.0.6", "serde", "serde_bytes", @@ -6856,7 +7040,7 @@ dependencies = [ "hex", "hmac 0.12.1", "log", - "rand 0.8.4", + "rand 0.8.5", "serde", "serde_json", "sha2 0.10.7", @@ -6878,8 +7062,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -6999,8 +7183,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -7054,11 +7238,11 @@ dependencies = [ [[package]] name = "tokio-tungstenite-wasm" version = "0.1.1-alpha.0" -source = "git+https://github.com/KomodoPlatform/tokio-tungstenite-wasm?rev=d20abdb#d20abdbbb2f03e302e3a8d11a1736ec8b50d0f58" +source = "git+https://github.com/KomodoPlatform/tokio-tungstenite-wasm?rev=8fc7e2f#8fc7e2ff4c970bee0c0867399cb9a941881ea183" dependencies = [ "futures-channel", "futures-util", - "http", + "http 0.2.12", "httparse", "js-sys", "thiserror", @@ -7138,7 +7322,7 @@ dependencies = [ "bytes", "flate2", "h2", - "http", + "http 0.2.12", "http-body", "hyper", "hyper-timeout", @@ -7166,8 +7350,8 @@ dependencies = [ "prettyplease", "proc-macro2", "prost-build", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -7181,7 +7365,7 @@ dependencies = [ "indexmap 1.9.3", "pin-project", "pin-project-lite 0.2.9", - "rand 0.8.4", + "rand 0.8.5", "slab", "tokio", "tokio-util", @@ -7221,8 +7405,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -7329,7 +7513,7 @@ dependencies = [ "idna 0.2.3", "ipnet", "lazy_static", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "socket2 0.4.9", "thiserror", @@ -7374,10 +7558,10 @@ dependencies = [ "base64 0.13.0", "byteorder", "bytes", - "http", + "http 0.2.12", "httparse", "log", - "rand 0.8.4", + "rand 0.8.5", "rustls 0.20.4", "sha-1", "thiserror", @@ -7388,9 +7572,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uint" @@ -7450,11 +7634,11 @@ checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" [[package]] name = "universal-hash" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "generic-array", + "crypto-common", "subtle", ] @@ -7523,7 +7707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.9", - "rand 0.8.4", + "rand 0.8.5", "serde", ] @@ -7660,8 +7844,8 @@ dependencies = [ "log", "once_cell", "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -7683,7 +7867,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ - "quote 1.0.33", + "quote 1.0.37", "wasm-bindgen-macro-support", ] @@ -7694,8 +7878,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7727,7 +7911,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c2e18093f11c19ca4e188c177fecc7c372304c311189f12c2f9bea5b7324ac7" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", +] + +[[package]] +name = "wc_common" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.3#e2fb03ac19186fd2372a0eef71897ef8a6ae9653" +dependencies = [ + "base64 0.21.7", + "chacha20poly1305", + "thiserror", ] [[package]] @@ -7772,7 +7966,7 @@ dependencies = [ "log", "parking_lot", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "reqwest", "rlp", "serde", @@ -8200,6 +8394,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "yamux" version = "0.12.1" @@ -8211,7 +8417,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "static_assertions", ] @@ -8227,7 +8433,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "static_assertions", ] @@ -8328,7 +8534,7 @@ name = "zcash_primitives" version = "0.5.0" source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ - "aes 0.8.3", + "aes", "bitvec 0.18.5", "blake2b_simd", "blake2s_simd", @@ -8387,7 +8593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index dd4931df38..6fdc0564bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "mm2src/derives/ser_error_derive", "mm2src/derives/ser_error", "mm2src/hw_common", + "mm2src/kdf_walletconnect", "mm2src/mm2_bin_lib", "mm2src/mm2_bitcoin/chain", "mm2src/mm2_bitcoin/crypto", @@ -53,9 +54,9 @@ exclude = [ aes = "0.8.3" argon2 = { version = "0.5.2", features = ["zeroize"] } arrayref = "0.3" -anyhow = "1.0" +anyhow = "1.0.89" async-std = "1.5" -async-trait = "0.1" +async-trait = "0.1.52" async-stream = "0.3" backtrace = "0.3" base64 = "0.21.2" @@ -110,6 +111,7 @@ hash256-std-hasher = "0.15.2" hash-db = "0.15.2" hex = "0.4.2" hmac = "0.12.1" +hkdf = "0.12.4" http = "0.2" http-body = "0.4" httparse = "1.8.0" @@ -141,6 +143,7 @@ num-bigint = { version = "0.4", features = ["serde", "std"] } num-rational = { version = "0.4", features = ["serde"] } parity-util-mem = "0.11" num-traits = "0.2" +pairing_api = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.3" } parking_lot = { version = "0.12.0", default-features = false } parking_lot_core = { version = "0.6", features = ["nightly"] } passwords = "3.1" @@ -153,6 +156,8 @@ protobuf = "2.20" proc-macro2 = "1.0" quote = "1.0" regex = "1" +relay_client = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.3" } +relay_rpc = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.3" } reqwest = { version = "0.11.9", default-features = false, features = ["json"] } rand = { version = "0.7", default-features = false, features = ["std", "small_rng", "wasm-bindgen"] } rcgen = "0.10" @@ -190,12 +195,12 @@ sysinfo = "0.28" tendermint-rpc = { version = "0.35", default-features = false } testcontainers = "0.15.0" tiny-bip39 = "0.8.0" -thiserror = "1.0.30" +thiserror = "1.0.40" time = "0.3.20" timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } tokio = { version = "1.20", default-features = false } tokio-rustls = { version = "0.24", default-features = false } -tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "d20abdb", defautl-features = false, features = ["rustls-tls-native-roots"]} +tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "8fc7e2f", defautl-features = false, features = ["rustls-tls-native-roots"]} tonic = { version = "0.10", default-features = false } tonic-build = { version = "0.10", default-features = false, features = ["prost"] } tower-service = "0.3" @@ -209,6 +214,7 @@ wagyu-zcash-parameters = { version = "0.2" } wasm-bindgen = "0.2.86" wasm-bindgen-futures = "0.4.21" wasm-bindgen-test = "0.3.2" +wc_common = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.3" } webpki-roots = "0.25" web-sys = {version = "0.3.55", default-features = false } # we don't need the default web3 features at all since we added our own web3 transport using shared httparse.workspace = true instance. @@ -222,6 +228,7 @@ zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.gi zcash_extras = { git = "https://github.com/komodoplatform/librustzcash.git", tag = "k-1.4.2" } zcash_primitives = { git = "https://github.com/komodoplatform/librustzcash.git", tag = "k-1.4.2", features = ["transparent-inputs"] } zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2", default-features = false, features = ["local-prover", "multicore"] } +x25519-dalek = { version = "2.0", features = ["static_secrets"] } zeroize = { version = "1.5", features = ["zeroize_derive"] } [profile.release] diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 93df96663d..1042df4966 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -46,6 +46,7 @@ db_common = { path = "../db_common" } derive_more.workspace = true ed25519-dalek.workspace = true enum_derives = { path = "../derives/enum_derives" } +kdf_walletconnect = { path = "../kdf_walletconnect" } ethabi.workspace = true ethcore-transaction.workspace = true ethereum-types.workspace = true diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index 486a07fc83..86a2c0da00 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -109,6 +109,13 @@ pub enum MyAddressError { InternalError(String), } +impl std::error::Error for MyAddressError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + // This error doesn't wrap another error, so we return None + None + } +} + #[derive(Debug, Display)] pub enum AddressFromPubkeyError { InternalError(String), diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index f79c2b05c0..91c3294c2d 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -20,6 +20,7 @@ // // Copyright © 2023 Pampex LTD and TillyHK LTD. All rights reserved. // +use self::wallet_connect::{send_transaction_with_walletconnect, WcEthTxParams}; use super::eth::Action::{Call, Create}; use super::watcher_common::{validate_watcher_reward, REWARD_GAS_AMOUNT}; use super::*; @@ -58,6 +59,7 @@ use common::executor::{abortable_queue::AbortableQueue, AbortSettings, Abortable Timer}; use common::log::{debug, error, info, warn}; use common::number_type_casting::SafeTypeCastingNumbers; +use common::wait_until_sec; use common::{now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::privkey::key_pair_from_secret; use crypto::{Bip44Chain, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; @@ -77,6 +79,7 @@ use futures::compat::Future01CompatExt; use futures::future::{join, join_all, select_ok, try_join_all, Either, FutureExt, TryFutureExt}; use futures01::Future; use http::Uri; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_number::bigdecimal_custom::CheckedDivision; use mm2_number::{BigDecimal, BigUint, MmNumber}; @@ -100,9 +103,8 @@ use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallReques use web3::{self, Web3}; cfg_wasm32! { - use common::{now_ms, wait_until_ms}; use crypto::MetamaskArc; - use ethereum_types::{H264 as EthH264, H520 as EthH520}; + use ethereum_types::H520; use mm2_metamask::MetamaskError; use web3::types::TransactionRequest; } @@ -137,6 +139,7 @@ mod eth_rpc; #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; #[cfg(any(test, target_arch = "wasm32"))] mod for_tests; pub(crate) mod nft_swap_v2; +pub mod wallet_connect; mod web3_transport; use web3_transport::{http_transport::HttpTransportNode, Web3Transport}; @@ -810,6 +813,11 @@ pub enum EthPrivKeyBuildPolicy { #[cfg(target_arch = "wasm32")] Metamask(MetamaskArc), Trezor, + WalletConnect { + address: Address, + public_key_uncompressed: H520, + session_topic: String, + }, } impl EthPrivKeyBuildPolicy { @@ -1572,7 +1580,7 @@ impl SwapOps for EthCoin { activated_key: ref key_pair, .. } => key_pair_from_secret(key_pair.secret().as_fixed_bytes()).expect("valid key"), - EthPrivKeyPolicy::Trezor => todo!(), + EthPrivKeyPolicy::Trezor | EthPrivKeyPolicy::WalletConnect { .. } => todo!(), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => todo!(), } @@ -1590,6 +1598,7 @@ impl SwapOps for EthCoin { .public_slice() .try_into() .expect("valid key length!"), + EthPrivKeyPolicy::WalletConnect { public_key, .. } => public_key.into(), EthPrivKeyPolicy::Trezor => todo!(), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(ref metamask_policy) => metamask_policy.public_key.0, @@ -2361,6 +2370,10 @@ impl MarketCoinOps for EthCoin { EthPrivKeyPolicy::Metamask(ref metamask_policy) => { Ok(format!("{:02x}", metamask_policy.public_key_uncompressed)) }, + EthPrivKeyPolicy::WalletConnect { + public_key_uncompressed, + .. + } => Ok(format!("{public_key_uncompressed:02x}")), } } @@ -2643,6 +2656,7 @@ impl MarketCoinOps for EthCoin { EthPrivKeyPolicy::Trezor => ERR!("'display_priv_key' is not supported for Hardware Wallets"), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' is not supported for MetaMask"), + EthPrivKeyPolicy::WalletConnect { .. } => ERR!("'display_priv_key' is not supported for WalletConnect"), } } @@ -2700,6 +2714,7 @@ async fn sign_transaction_with_keypair<'a>( if !coin.is_tx_type_supported(&tx_type) { return Err(TransactionErr::Plain("Eth transaction type not supported".into())); } + let tx_builder = UnSignedEthTxBuilder::new(tx_type, nonce, gas, action, value, data); let tx_builder = tx_builder_with_pay_for_gas_option(coin, tx_builder, pay_for_gas_option) .map_err(|e| TransactionErr::Plain(e.get_inner().to_string()))?; @@ -2713,11 +2728,9 @@ async fn sign_transaction_with_keypair<'a>( )) }, }; + let signed_tx = tx.sign(key_pair.secret(), Some(chain_id))?; - Ok(( - tx.sign(key_pair.secret(), Some(chain_id))?, - web3_instances_with_latest_nonce, - )) + Ok((signed_tx, web3_instances_with_latest_nonce)) } /// Sign and send eth transaction with provided keypair, @@ -2791,7 +2804,7 @@ async fn sign_and_send_transaction_with_metamask( // It's important to return the transaction hex for the swap, // so wait up to 60 seconds for the transaction to appear on the RPC node. - let wait_rpc_timeout = 60_000; + let wait_rpc_timeout = 60; let check_every = 1.; // Please note that this method may take a long time @@ -2858,13 +2871,70 @@ async fn sign_raw_eth_tx(coin: &EthCoin, args: &SignEthTransactionParams) -> Raw }) .map_to_mm(|err| RawTransactionError::TransactionError(err.get_plain_text_format())) }, + EthPrivKeyPolicy::WalletConnect { .. } => { + // NOTE: doesn't work with wallets that doesn't support `eth_signTransaction`. + // e.g Metamask + let wc = { + let ctx = MmArc::from_weak(&coin.ctx).expect("No context"); + WalletConnectCtx::from_ctx(&ctx) + .expect("TODO: handle error when enable kdf initialization without key.") + }; + // Todo: Tron will have to be set with `ChainSpec::Evm` to work with walletconnect. + // This means setting the protocol as `ETH` in coin config and having a different coin for this mode. + let chain_id = coin.chain_spec.chain_id().ok_or(RawTransactionError::InvalidParam( + "WalletConnect needs chain_id to be set".to_owned(), + ))?; + let my_address = coin + .derivation_method + .single_addr_or_err() + .await + .mm_err(|e| RawTransactionError::InternalError(e.to_string()))?; + let address_lock = coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; + let pay_for_gas_option = if let Some(ref pay_for_gas) = args.pay_for_gas { + pay_for_gas.clone().try_into()? + } else { + // use legacy gas_price() if not set + info!(target: "sign-and-send", "get_gas_price…"); + let gas_price = coin.get_gas_price().await?; + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) + }; + let (nonce, _) = coin + .clone() + .get_addr_nonce(my_address) + .compat() + .await + .map_to_mm(RawTransactionError::InvalidParam)?; + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); + + info!(target: "sign-and-send", "WalletConnect signing and sending tx…"); + let (signed_tx, _) = coin + .wc_sign_tx(&wc, WcEthTxParams { + my_address, + gas_price: pay_for_gas_option.get_gas_price(), + action, + value, + gas: args.gas_limit, + data: &data, + nonce, + chain_id, + max_fee_per_gas, + max_priority_fee_per_gas, + }) + .await + .mm_err(|err| RawTransactionError::TransactionError(err.to_string()))?; + + Ok(RawTransactionRes { + tx_hex: signed_tx.tx_hex().into(), + }) + }, + EthPrivKeyPolicy::Trezor => MmError::err(RawTransactionError::InvalidParam( + "sign raw eth tx not implemented for Trezor".into(), + )), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => MmError::err(RawTransactionError::InvalidParam( "sign raw eth tx not implemented for Metamask".into(), )), - EthPrivKeyPolicy::Trezor => MmError::err(RawTransactionError::InvalidParam( - "sign raw eth tx not implemented for Trezor".into(), - )), } } @@ -3807,8 +3877,23 @@ impl EthCoin { .single_addr_or_err() .await .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + sign_and_send_transaction_with_keypair(&coin, key_pair, address, value, action, data, gas).await }, + EthPrivKeyPolicy::WalletConnect { .. } => { + let wc = { + let ctx = MmArc::from_weak(&coin.ctx).expect("No context"); + WalletConnectCtx::from_ctx(&ctx) + .expect("TODO: handle error when enable kdf initialization without key.") + }; + let address = coin + .derivation_method + .single_addr_or_err() + .await + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + + send_transaction_with_walletconnect(coin, &wc, address, value, action, &data, gas).await + }, EthPrivKeyPolicy::Trezor => Err(TransactionErr::Plain(ERRL!("Trezor is not supported for swaps yet!"))), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => { @@ -5442,15 +5527,14 @@ impl EthCoin { } /// Returns `None` if the transaction hasn't appeared on the RPC nodes at the specified time. - #[cfg(target_arch = "wasm32")] async fn wait_for_tx_appears_on_rpc( &self, tx_hash: H256, - wait_rpc_timeout_ms: u64, + wait_rpc_timeout_s: u64, check_every: f64, ) -> Web3RpcResult> { - let wait_until = wait_until_ms(wait_rpc_timeout_ms); - while now_ms() < wait_until { + let wait_until = wait_until_sec(wait_rpc_timeout_s); + while now_sec() < wait_until { let maybe_tx = self.transaction(TransactionId::Hash(tx_hash)).await?; if let Some(tx) = maybe_tx { let signed_tx = signed_tx_from_web3_tx(tx).map_to_mm(Web3RpcError::InvalidResponse)?; @@ -5460,10 +5544,10 @@ impl EthCoin { Timer::sleep(check_every).await; } - let timeout_s = wait_rpc_timeout_ms / 1000; warn!( - "Couldn't fetch the '{tx_hash:02x}' transaction hex as it hasn't appeared on the RPC node in {timeout_s}s" + "Couldn't fetch the '{tx_hash:02x}' transaction hex as it hasn't appeared on the RPC node in {wait_rpc_timeout_s}s" ); + Ok(None) } @@ -7382,6 +7466,15 @@ impl CommonSwapOpsV2 for EthCoin { .expect("slice with incorrect length"); Public::from_slice(&pubkey_bytes) }, + EthPrivKeyPolicy::WalletConnect { + public_key_uncompressed, + .. + } => { + let pubkey_bytes: [u8; 64] = public_key_uncompressed[1..65] + .try_into() + .expect("slice with incorrect length"); + Public::from_slice(&pubkey_bytes) + }, } } diff --git a/mm2src/coins/eth/eth_withdraw.rs b/mm2src/coins/eth/eth_withdraw.rs index 1f61734b55..c9822619d6 100644 --- a/mm2src/coins/eth/eth_withdraw.rs +++ b/mm2src/coins/eth/eth_withdraw.rs @@ -1,5 +1,6 @@ use super::{checksum_address, u256_to_big_decimal, wei_from_big_decimal, ChainSpec, EthCoinType, EthDerivationMethod, EthPrivKeyPolicy, Public, WithdrawError, WithdrawRequest, WithdrawResult, ERC20_CONTRACT, H160, H256}; +use crate::eth::wallet_connect::WcEthTxParams; use crate::eth::{calc_total_fee, get_eth_gas_details_from_withdraw_fee, tx_builder_with_pay_for_gas_option, tx_type_from_pay_for_gas_option, Action, Address, EthTxFeeDetails, KeyPair, PayForGasOption, SignedEthTx, TransactionWrapper, UnSignedEthTxBuilder}; @@ -16,6 +17,7 @@ use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProces use crypto::{CryptoCtx, HwRpcError}; use ethabi::Token; use futures::compat::Future01CompatExt; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::map_mm_error::MapMmError; use mm2_err_handle::mm_error::MmResult; @@ -150,6 +152,9 @@ where let bytes = rlp::encode(&signed); Ok((signed.tx_hash(), BytesJson::from(bytes.to_vec()))) }, + EthPrivKeyPolicy::WalletConnect { .. } => { + MmError::err(WithdrawError::InternalError("invalid policy".to_owned())) + }, #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => MmError::err(WithdrawError::InternalError("invalid policy".to_owned())), } @@ -173,7 +178,7 @@ where } // Wait for 10 seconds for the transaction to appear on the RPC node. - let wait_rpc_timeout = 10_000; + let wait_rpc_timeout = 10; let check_every = 1.; // Please note that this method may take a long time @@ -189,7 +194,10 @@ where .unwrap_or_default(); Ok((tx_hash, tx_hex)) }, - EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } | EthPrivKeyPolicy::Trezor => { + EthPrivKeyPolicy::Iguana(_) + | EthPrivKeyPolicy::HDWallet { .. } + | EthPrivKeyPolicy::Trezor + | EthPrivKeyPolicy::WalletConnect { .. } => { MmError::err(WithdrawError::InternalError("invalid policy".to_owned())) }, } @@ -303,6 +311,51 @@ where }; self.send_withdraw_tx(&req, tx_to_send).await? }, + EthPrivKeyPolicy::WalletConnect { .. } => { + let ctx = MmArc::from_weak(&coin.ctx).expect("No context"); + let wc = WalletConnectCtx::from_ctx(&ctx) + .expect("TODO: handle error when enable kdf initialization without key."); + // Todo: Tron will have to be set with `ChainSpec::Evm` to work with walletconnect. + // This means setting the protocol as `ETH` in coin config and having a different coin for this mode. + let chain_id = coin.chain_spec.chain_id().ok_or(WithdrawError::UnsupportedError( + "WalletConnect needs chain_id to be set".to_owned(), + ))?; + let gas_price = pay_for_gas_option.get_gas_price(); + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); + let (nonce, _) = coin + .clone() + .get_addr_nonce(my_address) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; + let params = WcEthTxParams { + gas, + nonce, + data: &data, + my_address, + action: Action::Call(call_addr), + value: eth_value, + gas_price, + chain_id, + max_fee_per_gas, + max_priority_fee_per_gas, + }; + + let (tx, bytes) = if req.broadcast { + self.coin() + .wc_send_tx(&wc, params) + .await + .mm_err(|err| WithdrawError::SigningError(err.to_string()))? + } else { + self.coin() + .wc_sign_tx(&wc, params) + .await + .mm_err(|err| WithdrawError::SigningError(err.to_string()))? + }; + + (tx.tx_hash(), bytes) + }, }; self.on_finishing()?; diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index a3317ad809..9bcd2d6c57 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -7,10 +7,13 @@ use crate::nft::get_nfts_for_activation; use crate::nft::nft_errors::{GetNftInfoError, ParseChainTypeError}; use crate::nft::nft_structs::Chain; #[cfg(target_arch = "wasm32")] use crate::EthMetamaskPolicy; + use common::executor::AbortedError; use compatible_time::Instant; use crypto::{trezor::TrezorError, Bip32Error, CryptoCtxError, HwError}; use enum_derives::EnumFromTrait; +use ethereum_types::H264; +use kdf_walletconnect::error::WalletConnectError; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; @@ -21,7 +24,9 @@ use std::sync::atomic::Ordering; use url::Url; use web3_transport::websocket_transport::WebsocketTransport; -#[derive(Clone, Debug, Deserialize, Display, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] +#[derive( + Clone, Debug, Deserialize, Display, EnumFromTrait, EnumFromStringify, PartialEq, Serialize, SerializeErrorType, +)] #[serde(tag = "error_type", content = "error_data")] pub enum EthActivationV2Error { InvalidPayload(String), @@ -70,6 +75,9 @@ pub enum EthActivationV2Error { InvalidHardwareWalletCall, #[display(fmt = "Custom token error: {}", _0)] CustomTokenError(CustomTokenError), + // TODO: Map WalletConnectError to distinct error categories (transport, invalid payload) after refactoring. + #[from_stringify("WalletConnectError")] + WalletConnectError(String), } impl From for EthActivationV2Error { @@ -160,12 +168,16 @@ impl From for EthActivationV2Error { /// An alternative to `crate::PrivKeyActivationPolicy`, typical only for ETH coin. #[derive(Clone, Deserialize, Default)] +#[serde(tag = "type", content = "params")] pub enum EthPrivKeyActivationPolicy { #[default] ContextPrivKey, Trezor, #[cfg(target_arch = "wasm32")] Metamask, + WalletConnect { + session_topic: String, + }, } impl EthPrivKeyActivationPolicy { @@ -628,7 +640,8 @@ pub async fn eth_coin_from_conf_and_request_v2( let web3_instances = match (req.rpc_mode, &priv_key_policy) { (EthRpcMode::Default, EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. }) - | (EthRpcMode::Default, EthPrivKeyPolicy::Trezor) => { + | (EthRpcMode::Default, EthPrivKeyPolicy::Trezor) + | (EthRpcMode::Default, EthPrivKeyPolicy::WalletConnect { .. }) => { build_web3_instances(ctx, ticker.to_string(), req.nodes.clone()).await? }, #[cfg(target_arch = "wasm32")] @@ -812,6 +825,21 @@ pub(crate) async fn build_address_and_priv_key_policy( DerivationMethod::SingleAddress(address), )) }, + EthPrivKeyBuildPolicy::WalletConnect { + address, + public_key_uncompressed, + session_topic, + } => { + let public_key = compress_public_key(public_key_uncompressed)?; + Ok(( + EthPrivKeyPolicy::WalletConnect { + public_key, + public_key_uncompressed, + session_topic, + }, + DerivationMethod::SingleAddress(address), + )) + }, } } @@ -828,7 +856,7 @@ async fn build_web3_instances( eth_nodes.as_mut_slice().shuffle(&mut rng); drop_mutability!(eth_nodes); - let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); + let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker); let mut web3_instances = Vec::with_capacity(eth_nodes.len()); for eth_node in eth_nodes { @@ -983,10 +1011,10 @@ async fn check_metamask_supports_chain_id( } } -#[cfg(target_arch = "wasm32")] -fn compress_public_key(uncompressed: EthH520) -> MmResult { +fn compress_public_key(uncompressed: H520) -> MmResult { let public_key = PublicKey::from_slice(uncompressed.as_bytes()) .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; let compressed = public_key.serialize(); - Ok(EthH264::from(compressed)) + + Ok(H264::from(compressed)) } diff --git a/mm2src/coins/eth/wallet_connect.rs b/mm2src/coins/eth/wallet_connect.rs new file mode 100644 index 0000000000..2bc241342e --- /dev/null +++ b/mm2src/coins/eth/wallet_connect.rs @@ -0,0 +1,298 @@ +/// https://docs.reown.com/advanced/multichain/rpc-reference/ethereum-rpc +use super::{ChainSpec, EthCoin, EthPrivKeyPolicy}; + +use crate::common::Future01CompatExt; +use crate::hd_wallet::AddrToString; +use crate::Eip1559Ops; +use crate::{BytesJson, MarketCoinOps, TransactionErr}; + +use common::log::info; +use common::u256_to_hex; +use derive_more::Display; +use enum_derives::EnumFromStringify; +use ethcore_transaction::{Action, SignedTransaction}; +use ethereum_types::H256; +use ethereum_types::{Address, Public, H160, H520, U256}; +use ethkey::{public_to_address, Message, Signature}; +use kdf_walletconnect::chain::{WcChainId, WcRequestMethods}; +use kdf_walletconnect::error::WalletConnectError; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; +use mm2_err_handle::prelude::*; +use secp256k1::recovery::{RecoverableSignature, RecoveryId}; +use secp256k1::{PublicKey, Secp256k1}; +use std::iter::FromIterator; +use std::str::FromStr; +use web3::signing::hash_message; + +// Wait for 60 seconds for the transaction to appear on the RPC node. +const WAIT_RPC_TIMEOUT_SECS: u64 = 60; + +#[derive(Display, Debug, EnumFromStringify)] +pub enum EthWalletConnectError { + UnsupportedChainId(WcChainId), + InvalidSignature(String), + AccountMisMatch(String), + #[from_stringify("rlp::DecoderError", "hex::FromHexError")] + TxDecodingFailed(String), + #[from_stringify("ethkey::Error")] + InternalError(String), + InvalidTxData(String), + SessionError(String), + WalletConnectError(WalletConnectError), +} + +impl From for EthWalletConnectError { + fn from(value: WalletConnectError) -> Self { Self::WalletConnectError(value) } +} + +/// Eth Params required for constructing WalletConnect transaction. +pub struct WcEthTxParams<'a> { + pub(crate) gas: U256, + pub(crate) nonce: U256, + pub(crate) data: &'a [u8], + pub(crate) my_address: H160, + pub(crate) action: Action, + pub(crate) value: U256, + pub(crate) gas_price: Option, + pub(crate) chain_id: u64, + pub(crate) max_fee_per_gas: Option, + pub(crate) max_priority_fee_per_gas: Option, +} + +impl<'a> WcEthTxParams<'a> { + /// Construct WalletConnect transaction json from from `WcEthTxParams` + fn prepare_wc_tx_format(&self) -> MmResult { + let mut tx_object = serde_json::Map::from_iter([ + ("chainId".to_string(), json!(self.chain_id)), + ("nonce".to_string(), json!(u256_to_hex(self.nonce))), + ("from".to_string(), json!(self.my_address.addr_to_string())), + ("gasLimit".to_string(), json!(u256_to_hex(self.gas))), + ("value".to_string(), json!(u256_to_hex(self.value))), + ("data".to_string(), json!(format!("0x{}", hex::encode(self.data)))), + ]); + + if let Some(gas_price) = self.gas_price { + tx_object.insert("gasPrice".to_string(), json!(u256_to_hex(gas_price))); + } + + if let Some(max_fee_per_gas) = self.max_fee_per_gas { + tx_object.insert("maxFeePerGas".to_string(), json!(u256_to_hex(max_fee_per_gas))); + } + + if let Some(max_priority_fee_per_gas) = self.max_priority_fee_per_gas { + tx_object.insert( + "maxPriorityFeePerGas".to_string(), + json!(u256_to_hex(max_priority_fee_per_gas)), + ); + } + + if let Action::Call(addr) = self.action { + tx_object.insert("to".to_string(), json!(format!("0x{}", hex::encode(addr.as_bytes())))); + } + + Ok(json!(vec![serde_json::Value::Object(tx_object)])) + } +} + +#[async_trait::async_trait] +impl WalletConnectOps for EthCoin { + type Error = MmError; + type Params<'a> = WcEthTxParams<'a>; + type SignTxData = (SignedTransaction, BytesJson); + type SendTxData = (SignedTransaction, BytesJson); + + async fn wc_chain_id(&self, wc: &WalletConnectCtx) -> Result { + let session_topic = self.session_topic()?; + let chain_id = match self.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Add Tron signing logic + ChainSpec::Tron { .. } => { + return Err(MmError::new(EthWalletConnectError::InternalError( + "Tron is not supported for this action yet".into(), + ))) + }, + }; + let chain_id = WcChainId::new_eip155(chain_id.to_string()); + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + + Ok(chain_id) + } + + async fn wc_sign_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result { + let bytes = { + let chain_id = self.wc_chain_id(wc).await?; + let tx_json = params.prepare_wc_tx_format()?; + let session_topic = self.session_topic()?; + let tx_hex: String = wc + .send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::EthSignTransaction, tx_json) + .await?; + // if tx_hex.len() < 4 { + // return MmError::err(EthWalletConnectError::TxDecodingFailed( + // "invalid transaction hex returned from wallet".to_string(), + // )); + // } + // // First 4 bytes from WalletConnect represents Protoc info + let normalized_tx_hex = tx_hex.strip_prefix("0x").unwrap_or(&tx_hex); + hex::decode(normalized_tx_hex)? + }; + + let unverified = rlp::decode(&bytes)?; + let signed = SignedTransaction::new(unverified)?; + let bytes = rlp::encode(&signed); + + Ok((signed, BytesJson::from(bytes.to_vec()))) + } + + async fn wc_send_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result { + let tx_hash: String = { + let chain_id = self.wc_chain_id(wc).await?; + let tx_json = params.prepare_wc_tx_format()?; + let session_topic = self.session_topic()?; + wc.send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::EthSendTransaction, tx_json) + .await? + }; + + let tx_hash = tx_hash.strip_prefix("0x").unwrap_or(&tx_hash); + let maybe_signed_tx = { + self.wait_for_tx_appears_on_rpc(H256::from_slice(&hex::decode(tx_hash)?), WAIT_RPC_TIMEOUT_SECS, 1.) + .await + .mm_err(|err| EthWalletConnectError::InternalError(err.to_string()))? + }; + let signed_tx = maybe_signed_tx.ok_or(MmError::new(EthWalletConnectError::InternalError(format!( + "Waited too long until the transaction {tx_hash:?} appear on the RPC node" + ))))?; + let tx_hex = BytesJson::from(rlp::encode(&signed_tx).to_vec()); + + Ok((signed_tx, tx_hex)) + } + + fn session_topic(&self) -> Result<&str, Self::Error> { + if let EthPrivKeyPolicy::WalletConnect { ref session_topic, .. } = &self.priv_key_policy { + return Ok(session_topic); + } + + MmError::err(EthWalletConnectError::SessionError(format!( + "{} is not activated via WalletConnect", + self.ticker() + ))) + } +} + +pub async fn eth_request_wc_personal_sign( + wc: &WalletConnectCtx, + session_topic: &str, + chain_id: u64, +) -> MmResult<(H520, Address), EthWalletConnectError> { + let chain_id = WcChainId::new_eip155(chain_id.to_string()); + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + + let (account_str, _) = wc.get_account_and_properties_for_chain_id(session_topic, &chain_id)?; + let message = "Authenticate with KDF"; + let params = { + let message_hex = format!("0x{}", hex::encode(message)); + json!(&[&message_hex, &account_str]) + }; + let data = wc + .send_session_request_and_wait::(session_topic, &chain_id, WcRequestMethods::PersonalSign, params) + .await?; + + Ok(extract_pubkey_from_signature(&data, message, &account_str) + .mm_err(|err| WalletConnectError::SessionError(err.to_string()))?) +} + +fn extract_pubkey_from_signature( + signature_str: &str, + message: &str, + account: &str, +) -> MmResult<(H520, Address), EthWalletConnectError> { + let account = + H160::from_str(&account[2..]).map_to_mm(|err| EthWalletConnectError::InternalError(err.to_string()))?; + let uncompressed: H520 = { + let message_hash = hash_message(message); + let signature = Signature::from_str(&signature_str[2..]) + .map_to_mm(|err| EthWalletConnectError::InvalidSignature(err.to_string()))?; + let pubkey = recover(&signature, &message_hash).map_to_mm(|err| { + EthWalletConnectError::InvalidSignature(format!( + "Couldn't recover public key from the signature: '{signature:?}, error: {err:?}'" + )) + })?; + pubkey.serialize_uncompressed().into() + }; + + let mut public = Public::default(); + public.as_mut().copy_from_slice(&uncompressed[1..65]); + + let recovered_address = public_to_address(&public); + if account != recovered_address { + return MmError::err(EthWalletConnectError::AccountMisMatch(format!( + "Recovered address '{recovered_address:?}' should be the same as '{account:?}'" + ))); + } + + Ok((uncompressed, recovered_address)) +} + +pub(crate) fn recover(signature: &Signature, message: &Message) -> Result { + let recovery_id = { + let recovery_id = signature[64] + .checked_sub(27) + .ok_or_else(|| ethkey::Error::InvalidSignature)?; + RecoveryId::from_i32(recovery_id as i32)? + }; + let sig = RecoverableSignature::from_compact(&signature[0..64], recovery_id)?; + let pubkey = Secp256k1::new().recover(&secp256k1::Message::from_slice(&message[..])?, &sig)?; + + Ok(pubkey) +} + +/// Sign and send eth transaction with WalletConnect, +/// This fn is primarily for swap transactions so it uses swap tx fee policy +pub(crate) async fn send_transaction_with_walletconnect( + coin: EthCoin, + wc: &WalletConnectCtx, + my_address: Address, + value: U256, + action: Action, + data: &[u8], + gas: U256, +) -> Result { + info!("target: WalletConnect: sign-and-send, get_gas_price…"); + // Todo: Tron will have to use ETH protocol for walletconnect, it will be a different coin than the native one in coins config. + let chain_id = coin + .chain_spec + .chain_id() + .ok_or(TransactionErr::Plain("Tron is not supported for this action!".into()))?; + let pay_for_gas_option = try_tx_s!( + coin.get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) + .await + ); + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); + let (nonce, _) = try_tx_s!(coin.clone().get_addr_nonce(my_address).compat().await); + + let params = WcEthTxParams { + gas, + nonce, + data, + my_address, + action, + value, + gas_price: pay_for_gas_option.get_gas_price(), + chain_id, + max_fee_per_gas, + max_priority_fee_per_gas, + }; + // Please note that this method may take a long time + // due to `eth_sendTransaction` requests. + info!("target: WalletConnect: sign-and-send, signing and sending tx"); + let (signed_tx, _) = try_tx_s!(coin.wc_send_tx(wc, params).await); + + Ok(signed_tx) +} diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index abae7a4c85..c0414335f5 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -53,7 +53,7 @@ use crypto::{derive_secp256k1_secret, Bip32Error, Bip44Chain, CryptoCtx, CryptoC Secp256k1ExtendedPublicKey, Secp256k1Secret, WithHwRpcError}; use derive_more::Display; use enum_derives::{EnumFromStringify, EnumFromTrait}; -use ethereum_types::{H256, U256}; +use ethereum_types::{H256, H264, H520, U256}; use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures::{FutureExt, TryFutureExt}; @@ -102,7 +102,7 @@ cfg_native! { } cfg_wasm32! { - use ethereum_types::{H264 as EthH264, H520 as EthH520}; + use ethereum_types::{H264 as EthH264}; use hd_wallet::HDWalletDb; use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; use tx_history_storage::wasm::{clear_tx_history, load_tx_history, save_tx_history, TxHistoryDb}; @@ -2220,8 +2220,7 @@ pub struct WithdrawRequest { memo: Option, /// Tendermint specific field used for manually providing the IBC channel IDs. ibc_source_channel: Option, - /// Currently, this flag is used by ETH/ERC20 coins activated with MetaMask **only**. - #[cfg(target_arch = "wasm32")] + /// Currently, this flag is used by ETH/ERC20 coins activated with MetaMask/WalletConnect(Some wallets e.g Metamask) **only**. #[serde(default)] broadcast: bool, } @@ -4210,13 +4209,26 @@ pub enum PrivKeyPolicy { /// with the Metamask extension, especially within web-based contexts. #[cfg(target_arch = "wasm32")] Metamask(EthMetamaskPolicy), + /// WalletConnect private key policy. + /// + /// This variant represents the key management details for connections + /// established via WalletConnect. It includes both compressed and uncompressed + /// public keys. + /// - `public_key`: Compressed public key, represented as [H264]. + /// - `public_key_uncompressed`: Uncompressed public key, represented as [H520]. + /// - `session_topic`: WalletConnect session that was used to activate this coin. + WalletConnect { + public_key: H264, + public_key_uncompressed: H520, + session_topic: String, + }, } #[cfg(target_arch = "wasm32")] #[derive(Clone, Debug)] pub struct EthMetamaskPolicy { pub(crate) public_key: EthH264, - pub(crate) public_key_uncompressed: EthH520, + pub(crate) public_key_uncompressed: H520, } impl From for PrivKeyPolicy { @@ -4231,7 +4243,7 @@ impl PrivKeyPolicy { activated_key: activated_key_pair, .. } => Some(activated_key_pair), - PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::WalletConnect { .. } | PrivKeyPolicy::Trezor => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } @@ -4251,7 +4263,7 @@ impl PrivKeyPolicy { PrivKeyPolicy::HDWallet { bip39_secp_priv_key, .. } => Some(bip39_secp_priv_key), - PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor | PrivKeyPolicy::WalletConnect { .. } => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } @@ -4273,8 +4285,7 @@ impl PrivKeyPolicy { path_to_coin: derivation_path, .. } => Some(derivation_path), - PrivKeyPolicy::Trezor => None, - PrivKeyPolicy::Iguana(_) => None, + PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor | PrivKeyPolicy::WalletConnect { .. } => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index a6cfb6ad7a..c0b82f313b 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -310,6 +310,12 @@ impl MarketCoinOps for SiaCoin { ) .into()); }, + PrivKeyPolicy::WalletConnect { .. } => { + return Err(MyAddressError::UnexpectedDerivationMethod( + "WalletConnect not yet supported. Must use iguana seed.".to_string(), + ) + .into()) + }, }; let address = SpendPolicy::PublicKey(key_pair.public).address(); Ok(address.to_string()) diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index ac88f1bac9..a1b2cc1edd 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -10,11 +10,13 @@ pub mod tendermint_balance_events; mod tendermint_coin; mod tendermint_token; pub mod tendermint_tx_history_v2; +pub mod wallet_connect; pub use cosmrs::tendermint::PublicKey as TendermintPublicKey; pub use cosmrs::AccountId; pub use tendermint_coin::*; pub use tendermint_token::*; +pub use wallet_connect::*; pub(crate) const BCH_COIN_PROTOCOL_TYPE: &str = "BCH"; pub(crate) const BCH_TOKEN_PROTOCOL_TYPE: &str = "SLPTOKEN"; diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index cefcd18ec2..5be8983a63 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -68,6 +68,7 @@ use futures::{FutureExt, TryFutureExt}; use futures01::Future; use hex::FromHexError; use itertools::Itertools; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; use keys::{KeyPair, Public}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; @@ -195,7 +196,7 @@ pub struct TendermintProtocolInfo { pub decimals: u8, denom: String, pub account_prefix: String, - chain_id: String, + pub chain_id: String, gas_price: Option, #[serde(default)] ibc_channels: HashMap, @@ -286,14 +287,17 @@ impl TendermintActivationPolicy { PublicKey::from_raw_secp256k1(&activated_key.public_key.to_bytes()) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Couldn't generate public key")) }, - PrivKeyPolicy::Trezor => Err(io::Error::new( io::ErrorKind::Unsupported, "Trezor is not supported yet!", )), - + PrivKeyPolicy::WalletConnect { public_key, .. } => PublicKey::from_raw_secp256k1(public_key.as_bytes()) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Couldn't generate public key")), #[cfg(target_arch = "wasm32")] - PrivKeyPolicy::Metamask(_) => unreachable!(), + PrivKeyPolicy::Metamask(_) => Err(io::Error::new( + io::ErrorKind::Unsupported, + "Metamask is not supported yet!", + )), }, Self::PublicKey(account_public_key) => Ok(*account_public_key), } @@ -372,6 +376,19 @@ impl RpcCommonOps for TendermintCoin { } } +#[derive(PartialEq)] +pub enum TendermintWalletConnectionType { + Wc(String), + WcLedger(String), + KeplrLedger, + Keplr, + Native, +} + +impl Default for TendermintWalletConnectionType { + fn default() -> Self { Self::Native } +} + pub struct TendermintCoinImpl { ticker: String, /// As seconds @@ -382,7 +399,7 @@ pub struct TendermintCoinImpl { pub activation_policy: TendermintActivationPolicy, pub(crate) decimals: u8, pub(super) denom: Denom, - chain_id: ChainId, + pub(crate) chain_id: ChainId, gas_price: Option, pub tokens_info: PaMutex>, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation @@ -390,11 +407,9 @@ pub struct TendermintCoinImpl { pub(super) abortable_system: AbortableQueue, pub(crate) history_sync_state: Mutex, client: TendermintRpcClient, - 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, + pub ctx: MmWeak, + pub(crate) wallet_type: TendermintWalletConnectionType, } #[derive(Clone)] @@ -440,6 +455,10 @@ pub enum TendermintInitErrorKind { #[display(fmt = "avg_blocktime must be in-between '0' and '255'.")] AvgBlockTimeInvalid, BalanceStreamInitError(String), + #[display(fmt = "Watcher features can not be used with pubkey-only activation policy.")] + CantUseWatchersWithPubkeyPolicy, + #[display(fmt = "Unable to fetch account for chain: {_0}")] + UnableToFetchChainAccount(String), } /// TODO: Rename this into `ClientRpcError` because this is very @@ -705,7 +724,7 @@ impl TendermintCoin { nodes: Vec, tx_history: bool, activation_policy: TendermintActivationPolicy, - is_keplr_from_ledger: bool, + wallet_type: TendermintWalletConnectionType, ) -> MmResult { if nodes.is_empty() { return MmError::err(TendermintInitError { @@ -770,7 +789,7 @@ impl TendermintCoin { client: TendermintRpcClient(AsyncMutex::new(client_impl)), ibc_channels: protocol_info.ibc_channels, ctx: ctx.weak(), - is_keplr_from_ledger, + wallet_type, }))) } @@ -946,6 +965,14 @@ impl TendermintCoin { ) }, TendermintActivationPolicy::PublicKey(_) => { + if self.is_wallet_connect() { + return try_tx_s!( + self.seq_safe_send_raw_tx_bytes(tx_payload, fee, timeout_height, memo) + .timeout(expiration) + .await + ); + }; + try_tx_s!( self.send_unsigned_tx_externally(tx_payload, fee, timeout_height, memo, expiration) .timeout(expiration) @@ -955,6 +982,38 @@ impl TendermintCoin { } } + async fn get_tx_raw( + &self, + account_info: &BaseAccount, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: &str, + ) -> Result { + if self.is_wallet_connect() { + let ctx = try_tx_s!(MmArc::from_weak(&self.ctx).ok_or(ERRL!("ctx must be initialized already"))); + let wc = try_tx_s!(WalletConnectCtx::from_ctx(&ctx).map_err(|e| e.to_string())); + let SerializedUnsignedTx { tx_json, .. } = if self.is_ledger_connection() { + try_tx_s!(self.any_to_legacy_amino_json(account_info, tx_payload, fee, timeout_height, memo)) + } else { + try_tx_s!(self.any_to_serialized_sign_doc(account_info, tx_payload, fee, timeout_height, memo)) + }; + + return Ok(try_tx_s!(self.wc_sign_tx(&wc, tx_json).await.map_err(|err| err.to_string())).into()); + } + + let tx_raw = try_tx_s!(self.any_to_signed_raw_tx( + try_tx_s!(self.activation_policy.activated_key_or_err()), + account_info, + tx_payload, + fee, + timeout_height, + memo, + )); + + Ok(tx_raw) + } + async fn seq_safe_send_raw_tx_bytes( &self, tx_payload: Any, @@ -963,31 +1022,29 @@ impl TendermintCoin { memo: &str, ) -> Result<(String, Raw), TransactionErr> { let mut account_info = try_tx_s!(self.account_info(&self.account_id).await); - let (tx_id, tx_raw) = loop { - let tx_raw = try_tx_s!(self.any_to_signed_raw_tx( - try_tx_s!(self.activation_policy.activated_key_or_err()), - &account_info, - tx_payload.clone(), - fee.clone(), - timeout_height, - memo, - )); + loop { + let tx_raw = try_tx_s!( + self.get_tx_raw(&account_info, tx_payload.clone(), fee.clone(), timeout_height, memo,) + .await + ); - match self.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await { - Ok(tx_id) => break (tx_id, tx_raw), + // Attempt to send the transaction bytes + match self.send_raw_tx_bytes(try_tx_s!(&tx_raw.to_bytes())).compat().await { + Ok(tx_id) => { + return Ok((tx_id, tx_raw)); + }, Err(e) => { + // Handle sequence number mismatch and retry if e.contains(ACCOUNT_SEQUENCE_ERR) { account_info.sequence = try_tx_s!(parse_expected_sequence_number(&e)); - debug!("Got wrong account sequence, trying again."); + debug!("Account sequence mismatch, retrying..."); continue; } - return Err(crate::TransactionErr::Plain(ERRL!("{}", e))); + return Err(TransactionErr::Plain(ERRL!("Transaction failed: {}", e))); }, - }; - }; - - Ok((tx_id, tx_raw)) + } + } } async fn send_unsigned_tx_externally( @@ -1006,32 +1063,32 @@ impl TendermintCoin { let ctx = try_tx_s!(MmArc::from_weak(&self.ctx).ok_or(ERRL!("ctx must be initialized already"))); let account_info = try_tx_s!(self.account_info(&self.account_id).await); - let SerializedUnsignedTx { tx_json, body_bytes } = if self.is_keplr_from_ledger { + let SerializedUnsignedTx { tx_json, body_bytes } = if self.is_ledger_connection() { try_tx_s!(self.any_to_legacy_amino_json(&account_info, tx_payload, fee, timeout_height, memo)) } else { try_tx_s!(self.any_to_serialized_sign_doc(&account_info, tx_payload, fee, timeout_height, memo)) }; let data: TxHashData = try_tx_s!(ctx - .ask_for_data(&format!("TX_HASH:{}", self.ticker()), tx_json, timeout) + .ask_for_data(&format!("TX_HASH:{}", self.ticker()), tx_json.clone(), timeout) .await .map_err(|e| ERRL!("{}", e))); let tx = try_tx_s!(self.request_tx(data.hash.clone()).await.map_err(|e| ERRL!("{}", e))); - let tx_raw_inner = TxRaw { + let tx_raw = TxRaw { body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), signatures: tx.signatures, }; - if body_bytes != tx_raw_inner.body_bytes { + if body_bytes != tx_raw.body_bytes { return Err(crate::TransactionErr::Plain(ERRL!( "Unsigned transaction don't match with the externally provided transaction." ))); } - Ok((data.hash, Raw::from(tx_raw_inner))) + Ok((data.hash, Raw::from(tx_raw))) } #[allow(deprecated)] @@ -1282,7 +1339,7 @@ impl TendermintCoin { } } - pub(super) fn any_to_transaction_data( + pub(super) async fn any_to_transaction_data( &self, maybe_priv_key: Option, message: Any, @@ -1296,19 +1353,34 @@ impl TendermintCoin { let tx_bytes = tx_raw.to_bytes()?; let hash = sha256(&tx_bytes); - Ok(TransactionData::new_signed( + return Ok(TransactionData::new_signed( tx_bytes.into(), hex::encode_upper(hash.as_slice()), - )) + )); + }; + + let SerializedUnsignedTx { tx_json, .. } = if self.is_ledger_connection() { + self.any_to_legacy_amino_json(account_info, message, fee, timeout_height, memo) } else { - let SerializedUnsignedTx { tx_json, .. } = if self.is_keplr_from_ledger { - self.any_to_legacy_amino_json(account_info, message, fee, timeout_height, memo) - } else { - self.any_to_serialized_sign_doc(account_info, message, fee, timeout_height, memo) - }?; + self.any_to_serialized_sign_doc(account_info, message, fee, timeout_height, memo) + }?; - Ok(TransactionData::Unsigned(tx_json)) - } + if self.is_wallet_connect() { + let ctx = MmArc::from_weak(&self.ctx) + .ok_or(MyAddressError::InternalError(ERRL!("ctx must be initialized already")))?; + let wallet_connect = WalletConnectCtx::from_ctx(&ctx)?; + + let tx_raw: Raw = self.wc_sign_tx(&wallet_connect, tx_json).await?.into(); + let tx_bytes = tx_raw.to_bytes()?; + let hash = sha256(&tx_bytes); + + return Ok(TransactionData::new_signed( + tx_bytes.into(), + hex::encode_upper(hash.as_slice()), + )); + }; + + Ok(TransactionData::Unsigned(tx_json)) } fn gen_create_htlc_tx( @@ -1396,14 +1468,33 @@ impl TendermintCoin { let auth_info = SignerInfo::single_direct(Some(pubkey), account_info.sequence).auth_info(fee); let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; - let tx_json = json!({ - "sign_doc": { - "body_bytes": sign_doc.body_bytes, - "auth_info_bytes": sign_doc.auth_info_bytes, - "chain_id": sign_doc.chain_id, - "account_number": sign_doc.account_number, - } - }); + let tx_json = if self.is_wallet_connect() { + let ctx = MmArc::from_weak(&self.ctx).expect("No context"); + let wc = WalletConnectCtx::from_ctx(&ctx).expect("should never fail in this block"); + let session_topic = self + .session_topic() + .expect("session_topic can't be None inside this block"); + let encode = |data| wc.encode(session_topic, data); + + json!({ + "signerAddress": self.my_address()?, + "signDoc": { + "accountNumber": sign_doc.account_number.to_string(), + "chainId": sign_doc.chain_id, + "bodyBytes": encode(&sign_doc.body_bytes), + "authInfoBytes": encode(&sign_doc.auth_info_bytes) + } + }) + } else { + json!({ + "sign_doc": { + "body_bytes": &sign_doc.body_bytes, + "auth_info_bytes": sign_doc.auth_info_bytes, + "chain_id": sign_doc.chain_id, + "account_number": sign_doc.account_number, + } + }) + }; Ok(SerializedUnsignedTx { tx_json, @@ -1411,7 +1502,7 @@ impl TendermintCoin { }) } - /// This should only be used for Keplr from Ledger! + /// This should only be used for Keplr/WalletConnect from Ledger! /// When using Keplr from Ledger, they don't accept `SING_MODE_DIRECT` transactions. /// /// Visit https://docs.cosmos.network/main/build/architecture/adr-050-sign-mode-textual#context for more context. @@ -1439,8 +1530,6 @@ impl TendermintCoin { let msg_send = MsgSend::from_any(&tx_payload)?; let timeout_height = u32::try_from(timeout_height)?; - let original_tx_type_url = tx_payload.type_url.clone(); - let body_bytes = tx::Body::new(vec![tx_payload], memo, timeout_height).into_bytes()?; let amount: Vec = msg_send .amount @@ -1477,20 +1566,45 @@ impl TendermintCoin { }) .collect(); - let tx_json = serde_json::json!({ - "legacy_amino_json": { - "account_number": account_info.account_number.to_string(), - "chain_id": self.chain_id.to_string(), - "fee": { - "amount": fee_amount, - "gas": fee.gas_limit.to_string() + let sign_doc = json!({ + "account_number": account_info.account_number.to_string(), + "chain_id": self.chain_id.to_string(), + "fee": { + "amount": fee_amount, + "gas": fee.gas_limit.to_string() }, - "memo": memo, - "msgs": [msg], - "sequence": account_info.sequence.to_string(), - }, - "original_tx_type_url": original_tx_type_url, + "memo": memo, + "msgs": [msg], + "sequence": account_info.sequence.to_string() }); + let (tx_json, body_bytes) = match self.wallet_type { + TendermintWalletConnectionType::WcLedger(_) => { + let signer_address = self + .my_address() + .map_err(|e| ErrorReport::new(io::Error::new(io::ErrorKind::Other, e.to_string())))?; + let body_bytes = tx::Body::new(vec![tx_payload], memo, timeout_height).into_bytes()?; + let json = serde_json::json!({ + "signerAddress": signer_address, + "signDoc": sign_doc, + }); + (json, body_bytes) + }, + TendermintWalletConnectionType::KeplrLedger => { + let original_tx_type_url = tx_payload.type_url.clone(); + let body_bytes = tx::Body::new(vec![tx_payload], memo, timeout_height).into_bytes()?; + let json = serde_json::json!({ + "legacy_amino_json": sign_doc, + "original_tx_type_url": original_tx_type_url, + }); + (json, body_bytes) + }, + _ => { + return Err(ErrorReport::new(io::Error::new( + io::ErrorKind::InvalidInput, + "Only WalletConnect activated with Ledger can call this function", + ))) + }, + }; Ok(SerializedUnsignedTx { tx_json, body_bytes }) } @@ -2330,6 +2444,22 @@ impl TendermintCoin { None } + #[inline] + pub fn is_ledger_connection(&self) -> bool { + matches!( + self.wallet_type, + TendermintWalletConnectionType::WcLedger(_) | TendermintWalletConnectionType::KeplrLedger + ) + } + + #[inline] + pub fn is_wallet_connect(&self) -> bool { + matches!( + self.wallet_type, + TendermintWalletConnectionType::WcLedger(_) | TendermintWalletConnectionType::Wc(_) + ) + } + pub(crate) async fn validators_list( &self, filter_status: ValidatorStatus, @@ -2506,6 +2636,7 @@ impl TendermintCoin { timeout_height, &req.memo, ) + .await .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; let internal_id = { @@ -2636,6 +2767,7 @@ impl TendermintCoin { timeout_height, &req.memo, ) + .await .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; let internal_id = { @@ -2835,6 +2967,7 @@ impl TendermintCoin { let tx = self .any_to_transaction_data(maybe_priv_key, msg, &account_info, fee, timeout_height, &req.memo) + .await .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; let internal_id = { @@ -3029,7 +3162,7 @@ impl MmCoin for TendermintCoin { // treat any Tendermint asset as wallet-only. // // TODO: Once `SIGN_MODE_DIRECT` is supported, we can remove this. - if self.is_keplr_from_ledger { + if self.is_ledger_connection() { common::log::info!("Using Keplr with Ledger: operating in wallet only mode."); return true; } @@ -3057,7 +3190,7 @@ impl MmCoin for TendermintCoin { } let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); - wallet_only_conf || self.is_keplr_from_ledger + wallet_only_conf || self.is_ledger_connection() } fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } @@ -3143,7 +3276,7 @@ impl MmCoin for TendermintCoin { ) .await?; - let fee_amount_u64 = if coin.is_keplr_from_ledger { + let fee_amount_u64 = if coin.is_ledger_connection() { // When using `SIGN_MODE_LEGACY_AMINO_JSON`, Keplr ignores the fee we calculated // and calculates another one which is usually double what we calculate. // To make sure the transaction doesn't fail on the Keplr side (because if Keplr @@ -3201,6 +3334,7 @@ impl MmCoin for TendermintCoin { let tx = coin .any_to_transaction_data(maybe_priv_key, msg_payload, &account_info, fee, timeout_height, &memo) + .await .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let internal_id = { @@ -4073,7 +4207,7 @@ fn parse_expected_sequence_number(e: &str) -> MmResult, + #[serde(deserialize_with = "deserialize_vec_field")] + pub(crate) body_bytes: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum CosmosAccountAlgo { + #[serde(rename = "secp256k1")] + Secp256k1, + #[serde(rename = "tendermint/PubKeySecp256k1")] + TendermintSecp256k1, +} + +impl FromStr for CosmosAccountAlgo { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "secp256k1" => Ok(Self::Secp256k1), + "tendermint/PubKeySecp256k1" => Ok(Self::TendermintSecp256k1), + _ => Err(format!("Unknown pubkey type: {s}")), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CosmosAccount { + pub address: String, + #[serde(deserialize_with = "deserialize_vec_field")] + pub pubkey: Vec, + pub algo: CosmosAccountAlgo, + #[serde(default)] + pub is_ledger: Option, +} + +#[async_trait::async_trait] +impl WalletConnectOps for TendermintCoin { + type Error = MmError; + type Params<'a> = serde_json::Value; + type SignTxData = TxRaw; + type SendTxData = CosmosTransaction; + + async fn wc_chain_id(&self, wc: &WalletConnectCtx) -> Result { + let chain_id = WcChainId::new_cosmos(self.chain_id.to_string()); + let session_topic = self.session_topic()?; + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + Ok(chain_id) + } + + async fn wc_sign_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result { + let chain_id = self.wc_chain_id(wc).await?; + let session_topic = self.session_topic()?; + let method = if wc.is_ledger_connection(session_topic) { + WcRequestMethods::CosmosSignAmino + } else { + WcRequestMethods::CosmosSignDirect + }; + let data: CosmosTxSignedData = wc + .send_session_request_and_wait(session_topic, &chain_id, method, params) + .await?; + let signature = general_purpose::STANDARD + .decode(data.signature.signature) + .map_to_mm(|err| WalletConnectError::PayloadError(err.to_string()))?; + + Ok(TxRaw { + body_bytes: data.signed.body_bytes, + auth_info_bytes: data.signed.auth_info_bytes, + signatures: vec![signature], + }) + } + + async fn wc_send_tx<'a>( + &self, + _ctx: &WalletConnectCtx, + _params: Self::Params<'a>, + ) -> Result { + todo!() + } + + fn session_topic(&self) -> Result<&str, Self::Error> { + match self.wallet_type { + TendermintWalletConnectionType::WcLedger(ref session_topic) + | TendermintWalletConnectionType::Wc(ref session_topic) => Ok(session_topic), + _ => MmError::err(WalletConnectError::SessionError(format!( + "{} is not activated via WalletConnect", + self.ticker() + ))), + } + } +} + +pub async fn cosmos_get_accounts_impl( + wc: &WalletConnectCtx, + session_topic: &str, + chain_id: &str, +) -> MmResult { + let chain_id = WcChainId::new_cosmos(chain_id.to_string()); + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + + let (account, properties) = wc.get_account_and_properties_for_chain_id(session_topic, &chain_id)?; + + // Check if session has session_properties and return wallet account; + if let Some(props) = properties { + if let Some(keys) = &props.keys { + if let Some(key) = keys.iter().next() { + let pubkey = decode_data(&key.pub_key).map_to_mm(|err| { + WalletConnectError::PayloadError(format!("error decoding pubkey payload: {err:?}")) + })?; + let address = decode_data(&key.address).map_to_mm(|err| { + WalletConnectError::PayloadError(format!("error decoding address payload: {err:?}")) + })?; + let address = hex::encode(address); + let algo = CosmosAccountAlgo::from_str(&key.algo).map_to_mm(|err| { + WalletConnectError::PayloadError(format!("error decoding algo payload: {err:?}")) + })?; + + return Ok(CosmosAccount { + address, + pubkey, + algo, + is_ledger: Some(key.is_nano_ledger), + }); + } + } + } + + let params = serde_json::to_value(&account).unwrap(); + let accounts: Vec = wc + .send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::CosmosGetAccounts, params) + .await?; + + accounts.first().cloned().or_mm_err(|| { + WalletConnectError::NoAccountFound("Expected atleast an account from connected wallet".to_string()) + }) +} + +fn deserialize_vec_field<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + + match value { + Value::Object(map) => map + .iter() + .map(|(_, value)| { + value + .as_u64() + .ok_or_else(|| serde::de::Error::custom("Invalid byte value")) + .and_then(|n| { + if n <= 255 { + Ok(n as u8) + } else { + Err(serde::de::Error::custom("Invalid byte value")) + } + }) + }) + .collect(), + Value::Array(arr) => arr + .into_iter() + .map(|v| { + v.as_u64() + .ok_or_else(|| serde::de::Error::custom("Invalid byte value")) + .map(|n| n as u8) + }) + .collect(), + Value::String(data) => { + let data = decode_data(&data).map_err(|err| serde::de::Error::custom(err.to_string()))?; + Ok(data) + }, + _ => Err(serde::de::Error::custom("Pubkey must be an string, object or array")), + } +} + +fn decode_data(encoded: &str) -> Result, &'static str> { + if encoded.chars().all(|c| c.is_ascii_hexdigit()) && encoded.len() % 2 == 0 { + hex::decode(encoded).map_err(|_| "Invalid hex encoding") + } else if encoded.contains('=') || encoded.contains('/') || encoded.contains('+') || encoded.len() % 4 == 0 { + general_purpose::STANDARD + .decode(encoded) + .map_err(|_| "Invalid base64 encoding") + } else { + Err("Unknown encoding format") + } +} + +#[cfg(test)] +mod test_cosmos_walletconnect { + use serde_json::json; + + use super::{decode_data, CosmosSignData, CosmosTxPublicKey, CosmosTxSignature, CosmosTxSignedData}; + + #[test] + fn test_decode_base64() { + // "Hello world" in base64 + let base64_data = "SGVsbG8gd29ybGQ="; + let expected = b"Hello world".to_vec(); + let result = decode_data(base64_data); + assert_eq!(result.unwrap(), expected, "Base64 decoding failed"); + } + + #[test] + fn test_decode_hex() { + // "Hello world" in hex + let hex_data = "48656c6c6f20776f726c64"; + let expected = b"Hello world".to_vec(); + let result = decode_data(hex_data); + assert_eq!(result.unwrap(), expected, "Hex decoding failed"); + } + + #[test] + fn test_deserialize_sign_message_response() { + let json = json!({ + "signature": { + "signature": "eGrmDGKTmycxJO56yTQORDzTFjBEBgyBmHc8ey6FbHh9WytzgsJilYBywz5uludhyKePZdRwznamg841fXw50Q==", + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "AjqZ1rq/EsPAb4SA6l0qjpVMHzqXotYXz23D5kOceYYu" + } + }, + "signed": { + "chainId": "cosmoshub-4", + "authInfoBytes": "0a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21023a99d6babf12c3c06f8480ea5d2a8e954c1f3a97a2d617cf6dc3e6439c79862e12040a020801180212140a0e0a057561746f6d1205313837353010c8d007", + "bodyBytes": "0a8e010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126e0a2d636f736d6f7331376c386432737973646e3667683636786d366664666b6575333634703836326a68396c6e6667122d636f736d6f7331376c386432737973646e3667683636786d366664666b6575333634703836326a68396c6e66671a0e0a057561746f6d12053430303030189780e00a", + "accountNumber": "2934714" + } + }); + let expected_tx = CosmosTxSignedData { + signature: CosmosTxSignature { + pub_key: CosmosTxPublicKey { + key_type: "tendermint/PubKeySecp256k1".to_owned(), + value: "AjqZ1rq/EsPAb4SA6l0qjpVMHzqXotYXz23D5kOceYYu".to_owned(), + }, + signature: "eGrmDGKTmycxJO56yTQORDzTFjBEBgyBmHc8ey6FbHh9WytzgsJilYBywz5uludhyKePZdRwznamg841fXw50Q==" + .to_owned(), + }, + signed: CosmosSignData { + chain_id: "cosmoshub-4".to_owned(), + account_number: "2934714".to_owned(), + auth_info_bytes: vec![ + 10, 80, 10, 70, 10, 31, 47, 99, 111, 115, 109, 111, 115, 46, 99, 114, 121, 112, 116, 111, 46, 115, + 101, 99, 112, 50, 53, 54, 107, 49, 46, 80, 117, 98, 75, 101, 121, 18, 35, 10, 33, 2, 58, 153, 214, + 186, 191, 18, 195, 192, 111, 132, 128, 234, 93, 42, 142, 149, 76, 31, 58, 151, 162, 214, 23, 207, + 109, 195, 230, 67, 156, 121, 134, 46, 18, 4, 10, 2, 8, 1, 24, 2, 18, 20, 10, 14, 10, 5, 117, 97, + 116, 111, 109, 18, 5, 49, 56, 55, 53, 48, 16, 200, 208, 7, + ], + body_bytes: vec![ + 10, 142, 1, 10, 28, 47, 99, 111, 115, 109, 111, 115, 46, 98, 97, 110, 107, 46, 118, 49, 98, 101, + 116, 97, 49, 46, 77, 115, 103, 83, 101, 110, 100, 18, 110, 10, 45, 99, 111, 115, 109, 111, 115, 49, + 55, 108, 56, 100, 50, 115, 121, 115, 100, 110, 54, 103, 104, 54, 54, 120, 109, 54, 102, 100, 102, + 107, 101, 117, 51, 54, 52, 112, 56, 54, 50, 106, 104, 57, 108, 110, 102, 103, 18, 45, 99, 111, 115, + 109, 111, 115, 49, 55, 108, 56, 100, 50, 115, 121, 115, 100, 110, 54, 103, 104, 54, 54, 120, 109, + 54, 102, 100, 102, 107, 101, 117, 51, 54, 52, 112, 56, 54, 50, 106, 104, 57, 108, 110, 102, 103, + 26, 14, 10, 5, 117, 97, 116, 111, 109, 18, 5, 52, 48, 48, 48, 48, 24, 151, 128, 224, 10, + ], + }, + }; + + let actual_tx = serde_json::from_value::(json).unwrap(); + assert_eq!(expected_tx, actual_tx); + } +} diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 760731d28b..b5af6899ac 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -369,6 +369,9 @@ pub fn my_public_key(coin: &UtxoCoinFields) -> Result<&Public, MmError MmError::err(UnexpectedDerivationMethod::UnsupportedError( "`PrivKeyPolicy::Metamask` is not supported in this context".to_string(), )), + PrivKeyPolicy::WalletConnect { .. } => MmError::err(UnexpectedDerivationMethod::UnsupportedError( + "`PrivKeyPolicy::WalletConnect` is not supported in this context".to_string(), + )), } } @@ -3240,6 +3243,7 @@ pub fn display_priv_key(coin: &UtxoCoinFields) -> Result { PrivKeyPolicy::Trezor => ERR!("'display_priv_key' is not supported for Hardware Wallets"), #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support Metamask"), + PrivKeyPolicy::WalletConnect { .. } => ERR!("'display_priv_key' doesn't support WalletConnect"), } } @@ -5031,9 +5035,10 @@ pub fn derive_htlc_key_pair(coin: &UtxoCoinFields, _swap_unique_data: &[u8]) -> activated_key: activated_key_pair, .. } => activated_key_pair, - PrivKeyPolicy::Trezor => todo!(), + PrivKeyPolicy::Trezor => panic!("`PrivKeyPolicy::Trezor` is not supported for UTXO coins"), #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => panic!("`PrivKeyPolicy::Metamask` is not supported for UTXO coins"), + PrivKeyPolicy::WalletConnect { .. } => panic!("`PrivKeyPolicy::WalletConnect` is not supported for UTXO coins"), } } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 0c8d9a53f8..c53d81045d 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -1076,7 +1076,7 @@ fn test_electrum_rpc_client_error() { // use the static string instead because the actual error message cannot be obtain // by serde_json serialization - let expected = r#"method: "blockchain.transaction.get", params: [String("0000000000000000000000000000000000000000000000000000000000000000"), Bool(true)] }, error: Response(electrum1.cipig.net:10060, Object({"code": Number(2), "message": String("daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})")})) }"#; + let expected = r#"method: "blockchain.transaction.get", params: [String("0000000000000000000000000000000000000000000000000000000000000000"), Bool(true)] }, error: Response(electrum1.cipig.net:10060, Object {"code": Number(2), "message": String("daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})")}) }"#; let actual = format!("{}", err); assert!(actual.contains(expected)); diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 353ac230d7..8dd491babe 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -371,6 +371,11 @@ where "`PrivKeyPolicy::Metamask` is not supported for UTXO coins!".to_string(), )) }, + PrivKeyPolicy::WalletConnect { .. } => { + return MmError::err(WithdrawError::UnsupportedError( + "`PrivKeyPolicy::WalletConnect` is not supported for UTXO coins!".to_string(), + )) + }, }; Ok(signed) diff --git a/mm2src/coins_activation/Cargo.toml b/mm2src/coins_activation/Cargo.toml index 215a4067c6..69f020c809 100644 --- a/mm2src/coins_activation/Cargo.toml +++ b/mm2src/coins_activation/Cargo.toml @@ -20,6 +20,7 @@ derive_more.workspace = true ethereum-types.workspace = true futures = { workspace = true, features = ["compat", "async-await"] } hex.workspace = true +kdf_walletconnect = { path = "../kdf_walletconnect" } mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 6f8dcccb65..95b74f71a4 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -10,14 +10,16 @@ use crate::prelude::*; use async_trait::async_trait; use coins::coin_balance::{CoinBalanceReport, EnableCoinBalanceOps}; use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationRequest, - EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy}; -use coins::eth::v2_activation::{EthTokenActivationError, NftActivationRequest, NftProviderEnum}; + EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy, + EthTokenActivationError, NftActivationRequest, NftProviderEnum}; +use coins::eth::wallet_connect::eth_request_wc_personal_sign; use coins::eth::{ChainSpec, Erc20TokenDetails, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; use coins::hd_wallet::{DisplayAddress, RpcTaskXPubExtractor}; use coins::my_tx_history_v2::TxHistoryStorage; use coins::nft::nft_structs::NftInfo; use coins::{CoinBalance, CoinBalanceMap, CoinProtocol, CoinWithDerivationMethod, DerivationMethod, MarketCoinOps, MmCoin, MmCoinEnum}; +use kdf_walletconnect::WalletConnectCtx; use crate::platform_coin_with_tokens::InitPlatformCoinWithTokensTask; use common::Future01CompatExt; @@ -65,6 +67,7 @@ impl From for EnablePlatformCoinWithTokensError { EnablePlatformCoinWithTokensError::FailedSpawningBalanceEvents(e) }, EthActivationV2Error::HDWalletStorageError(e) => EnablePlatformCoinWithTokensError::Internal(e), + EthActivationV2Error::WalletConnectError(e) => EnablePlatformCoinWithTokensError::WalletConnectError(e), #[cfg(target_arch = "wasm32")] EthActivationV2Error::MetamaskError(metamask) => { EnablePlatformCoinWithTokensError::Transport(metamask.to_string()) @@ -282,7 +285,8 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { activation_request: Self::ActivationRequest, protocol: Self::PlatformProtocolInfo, ) -> Result> { - let priv_key_policy = eth_priv_key_build_policy(&ctx, &activation_request.platform_request.priv_key_policy)?; + let priv_key_policy = + eth_priv_key_build_policy(&ctx, &activation_request.platform_request.priv_key_policy, &protocol).await?; let platform_coin = eth_coin_from_conf_and_request_v2( &ctx, @@ -464,9 +468,10 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { } } -fn eth_priv_key_build_policy( +async fn eth_priv_key_build_policy( ctx: &MmArc, activation_policy: &EthPrivKeyActivationPolicy, + protocol: &ChainSpec, ) -> MmResult { match activation_policy { EthPrivKeyActivationPolicy::ContextPrivKey => Ok(EthPrivKeyBuildPolicy::detect_priv_key_policy(ctx)?), @@ -478,5 +483,20 @@ fn eth_priv_key_build_policy( Ok(EthPrivKeyBuildPolicy::Metamask(metamask_ctx)) }, EthPrivKeyActivationPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), + EthPrivKeyActivationPolicy::WalletConnect { session_topic } => { + let wc = WalletConnectCtx::from_ctx(ctx) + .expect("TODO: handle error when enable kdf initialization without key."); + let chain_id = protocol.chain_id().ok_or(EthActivationV2Error::ChainIdNotSet)?; + let (public_key_uncompressed, address) = + eth_request_wc_personal_sign(&wc, session_topic, chain_id) + .await + .mm_err(|err| EthActivationV2Error::WalletConnectError(err.to_string()))?; + + Ok(EthPrivKeyBuildPolicy::WalletConnect { + address, + public_key_uncompressed, + session_topic: session_topic.clone(), + }) + }, } } diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index 5c1677865e..24a1aca710 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -83,7 +83,7 @@ pub trait TokenAsMmCoinInitializer: Send + Sync { async fn enable_tokens_as_mm_coins( &self, - ctx: MmArc, + ctx: &MmArc, request: &Self::ActivationRequest, ) -> Result, MmError>; } @@ -134,15 +134,14 @@ where async fn enable_tokens_as_mm_coins( &self, - ctx: MmArc, + ctx: &MmArc, request: &Self::ActivationRequest, ) -> Result, MmError> { let tokens_requests = T::tokens_requests_from_platform_request(request); let token_params = tokens_requests .into_iter() .map(|req| -> Result<_, MmError> { - let (token_conf, protocol): (_, T::TokenProtocol) = - coin_conf_with_protocol(&ctx, &req.ticker, req.protocol.clone())?; + let (token_conf, protocol) = coin_conf_with_protocol(ctx, &req.ticker, req.protocol.clone())?; Ok(TokenActivationParams { ticker: req.ticker, conf: token_conf, @@ -285,6 +284,8 @@ pub enum EnablePlatformCoinWithTokensError { UnexpectedDeviceActivationPolicy, #[display(fmt = "Custom token error: {}", _0)] CustomTokenError(CustomTokenError), + #[display(fmt = "WalletConnect Error: {}", _0)] + WalletConnectError(String), } impl From for EnablePlatformCoinWithTokensError { @@ -374,7 +375,8 @@ impl HttpStatusCode for EnablePlatformCoinWithTokensError { | EnablePlatformCoinWithTokensError::NoSuchTask(_) | EnablePlatformCoinWithTokensError::UnexpectedDeviceActivationPolicy | EnablePlatformCoinWithTokensError::FailedSpawningBalanceEvents(_) - | EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { .. } => StatusCode::BAD_REQUEST, + | EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { .. } + | EnablePlatformCoinWithTokensError::WalletConnectError(_) => StatusCode::BAD_REQUEST, EnablePlatformCoinWithTokensError::Transport(_) => StatusCode::BAD_GATEWAY, } } @@ -393,7 +395,7 @@ where { let mut mm_tokens = Vec::new(); for initializer in platform_coin.token_initializers() { - let tokens = initializer.enable_tokens_as_mm_coins(ctx.clone(), &req.request).await?; + let tokens = initializer.enable_tokens_as_mm_coins(&ctx, &req.request).await?; mm_tokens.extend(tokens); } @@ -463,7 +465,7 @@ where let mut mm_tokens = Vec::new(); for initializer in platform_coin.token_initializers() { - let tokens = initializer.enable_tokens_as_mm_coins(ctx.clone(), &req.request).await?; + let tokens = initializer.enable_tokens_as_mm_coins(&ctx, &req.request).await?; mm_tokens.extend(tokens); } diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 18cf645110..c371ec9564 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -11,13 +11,15 @@ use async_trait::async_trait; use coins::hd_wallet::HDPathAccountToAddressId; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tendermint::tendermint_tx_history_v2::tendermint_history_loop; -use coins::tendermint::{tendermint_priv_key_policy, RpcNode, TendermintActivationPolicy, TendermintCoin, - TendermintCommons, TendermintConf, TendermintInitError, TendermintInitErrorKind, - TendermintProtocolInfo, TendermintPublicKey, TendermintToken, TendermintTokenActivationParams, - TendermintTokenInitError, TendermintTokenProtocolInfo}; +use coins::tendermint::{cosmos_get_accounts_impl, tendermint_priv_key_policy, CosmosAccountAlgo, RpcNode, + TendermintActivationPolicy, TendermintCoin, TendermintCommons, TendermintConf, + TendermintInitError, TendermintInitErrorKind, TendermintProtocolInfo, TendermintPublicKey, + TendermintToken, TendermintTokenActivationParams, TendermintTokenInitError, + TendermintTokenProtocolInfo, TendermintWalletConnectionType}; use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum, PrivKeyBuildPolicy}; use common::executor::{AbortSettings, SpawnAbortable}; use common::{true_f, Future01CompatExt}; +use kdf_walletconnect::WalletConnectCtx; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; @@ -38,6 +40,19 @@ impl RegisterTokenInfo for TendermintCoin { } } +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "type", content = "params")] +pub enum TendermintPubkeyActivationParams { + /// Activate with public key + WithPubkey { + #[serde(deserialize_with = "deserialize_account_public_key")] + pubkey: TendermintPublicKey, + is_ledger_connection: bool, + }, + /// Activate with WalletConnect + WalletConnect { session_topic: String }, +} + #[derive(Clone, Deserialize)] pub struct TendermintActivationParams { nodes: Vec, @@ -50,13 +65,10 @@ pub struct TendermintActivationParams { #[serde(default)] pub path_to_address: HDPathAccountToAddressId, #[serde(default)] - #[serde(deserialize_with = "deserialize_account_public_key")] - with_pubkey: Option, - #[serde(default)] - is_keplr_from_ledger: bool, + pub activation_params: Option, } -fn deserialize_account_public_key<'de, D>(deserializer: D) -> Result, D::Error> +fn deserialize_account_public_key<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { @@ -74,7 +86,7 @@ where .iter() .map(|i| i.as_u64().unwrap() as u8) .collect(); - Ok(Some(TendermintPublicKey::from_raw_ed25519(&value).unwrap())) + Ok(TendermintPublicKey::from_raw_ed25519(&value).unwrap()) }, Some("secp256k1") => { let value: Vec = value @@ -83,7 +95,7 @@ where .iter() .map(|i| i.as_u64().unwrap() as u8) .collect(); - Ok(Some(TendermintPublicKey::from_raw_secp256k1(&value).unwrap())) + Ok(TendermintPublicKey::from_raw_secp256k1(&value).unwrap()) }, _ => Err(serde::de::Error::custom( "Unsupported pubkey algorithm. Use one of ['ed25519', 'secp256k1']", @@ -217,6 +229,37 @@ impl From for EnablePlatformCoinWithTokensError { } } +async fn activate_with_walletconnect( + ctx: &MmArc, + session_topic: String, + chain_id: &str, + ticker: &str, +) -> MmResult<(TendermintActivationPolicy, TendermintWalletConnectionType), TendermintInitError> { + let wc = WalletConnectCtx::from_ctx(ctx).expect("TODO: handle error when enable kdf initialization without key."); + let account = cosmos_get_accounts_impl(&wc, &session_topic, chain_id) + .await + .mm_err(|err| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::UnableToFetchChainAccount(err.to_string()), + })?; + let wallet_type = if wc.is_ledger_connection(&session_topic) { + TendermintWalletConnectionType::WcLedger(session_topic) + } else { + TendermintWalletConnectionType::Wc(session_topic) + }; + + let pubkey = match account.algo { + CosmosAccountAlgo::Secp256k1 | CosmosAccountAlgo::TendermintSecp256k1 => { + TendermintPublicKey::from_raw_secp256k1(&account.pubkey).ok_or(TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::Internal("Invalid secp256k1 pubkey".to_owned()), + })? + }, + }; + + Ok((TendermintActivationPolicy::with_public_key(pubkey), wallet_type)) +} + #[async_trait] impl PlatformCoinWithTokensActivationOps for TendermintCoin { type ActivationRequest = TendermintActivationParams; @@ -246,10 +289,28 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { } let conf = TendermintConf::try_from_json(&ticker, coin_conf)?; - let is_keplr_from_ledger = activation_request.is_keplr_from_ledger && activation_request.with_pubkey.is_some(); - let activation_policy = if let Some(pubkey) = activation_request.with_pubkey { - TendermintActivationPolicy::with_public_key(pubkey) + let (activation_policy, wallet_connection_type) = if let Some(params) = activation_request.activation_params { + match params { + TendermintPubkeyActivationParams::WithPubkey { + pubkey, + is_ledger_connection, + } => { + let wallet_connection_type = if is_ledger_connection { + TendermintWalletConnectionType::KeplrLedger + } else { + TendermintWalletConnectionType::Keplr + }; + + ( + TendermintActivationPolicy::with_public_key(pubkey), + wallet_connection_type, + ) + }, + TendermintPubkeyActivationParams::WalletConnect { session_topic } => { + activate_with_walletconnect(&ctx, session_topic, protocol_conf.chain_id.as_ref(), &ticker).await? + }, + } } else { let private_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(&ctx).mm_err(|e| TendermintInitError { @@ -260,22 +321,23 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { let tendermint_private_key_policy = tendermint_priv_key_policy(&conf, &ticker, private_key_policy, activation_request.path_to_address)?; - TendermintActivationPolicy::with_private_key_policy(tendermint_private_key_policy) + ( + TendermintActivationPolicy::with_private_key_policy(tendermint_private_key_policy), + TendermintWalletConnectionType::Native, + ) }; - let coin = TendermintCoin::init( + TendermintCoin::init( &ctx, - ticker.clone(), + ticker, conf, protocol_conf, activation_request.nodes, activation_request.tx_history, activation_policy, - is_keplr_from_ledger, + wallet_connection_type, ) - .await?; - - Ok(coin) + .await } async fn enable_global_nft( diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 078f601f66..b2e00d5e57 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -158,6 +158,7 @@ use http::header::CONTENT_TYPE; use http::Response; use parking_lot::{Mutex as PaMutex, MutexGuard as PaMutexGuard}; pub use paste::paste; +use primitive_types::U256; use rand::RngCore; use rand::{rngs::SmallRng, SeedableRng}; use serde::{de, ser}; @@ -1187,6 +1188,10 @@ pub fn http_uri_to_ws_address(uri: http::Uri) -> String { format!("{}{}{}{}", address_prefix, host_address, port, path) } +/// Converts a U256 value to a lowercase hexadecimal string with "0x" prefix +#[inline] +pub fn u256_to_hex(value: U256) -> String { format!("0x{:x}", value) } + /// If 0x prefix exists in an str strip it or return the str as-is #[macro_export] macro_rules! str_strip_0x { diff --git a/mm2src/db_common/src/async_sql_conn.rs b/mm2src/db_common/src/async_sql_conn.rs index 78357405a1..f9e8747fd7 100644 --- a/mm2src/db_common/src/async_sql_conn.rs +++ b/mm2src/db_common/src/async_sql_conn.rs @@ -43,6 +43,10 @@ impl std::error::Error for AsyncConnError { } } +impl From for AsyncConnError { + fn from(err: String) -> Self { Self::Internal(InternalError(err)) } +} + #[derive(Debug)] pub struct InternalError(pub String); diff --git a/mm2src/kdf_walletconnect/Cargo.toml b/mm2src/kdf_walletconnect/Cargo.toml new file mode 100644 index 0000000000..88978fd0d5 --- /dev/null +++ b/mm2src/kdf_walletconnect/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "kdf_walletconnect" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +base64.workspace = true +chrono = { workspace = true, "features" = ["serde"] } +common = { path = "../common" } +hex.workspace = true +cfg-if.workspace = true +db_common = { path = "../db_common" } +derive_more.workspace = true +enum_derives = { path = "../derives/enum_derives" } +futures = { workspace = true, features = ["compat", "async-await"] } +hkdf.workspace = true +mm2_db = { path = "../mm2_db" } +mm2_core = { path = "../mm2_core" } +mm2_err_handle = { path = "../mm2_err_handle" } +parking_lot = { workspace = true, features = ["nightly"] } +pairing_api.workspace = true +rand = "0.8" +relay_client.workspace = true +relay_rpc.workspace = true +secp256k1.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +sha2.workspace = true +timed-map = { workspace = true, features = ["rustc-hash"] } +thiserror.workspace = true +tokio.workspace = true +x25519-dalek.workspace = true +wc_common.workspace = true + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys.workspace = true +mm2_db = { path = "../mm2_db" } +timed-map = { workspace = true, features = ["rustc-hash", "wasm"] } +wasm-bindgen.workspace = true +wasm-bindgen-test.workspace = true +wasm-bindgen-futures.workspace = true +web-sys = { workspace = true, features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", + "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", + "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", + "IdbVersionChangeEvent", "MessageEvent", "MessagePort", "ReadableStreamDefaultReader", "ReadableStream"]} + +[dev-dependencies] +mm2_test_helpers = { path = "../mm2_test_helpers" } diff --git a/mm2src/kdf_walletconnect/src/chain.rs b/mm2src/kdf_walletconnect/src/chain.rs new file mode 100644 index 0000000000..20e1acd6a8 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/chain.rs @@ -0,0 +1,107 @@ +use mm2_err_handle::prelude::{MmError, MmResult}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +use crate::error::WalletConnectError; + +pub(crate) const SUPPORTED_PROTOCOL: &str = "irn"; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum WcChain { + Eip155, + Cosmos, +} + +impl FromStr for WcChain { + type Err = MmError; + fn from_str(s: &str) -> Result { + match s { + "eip155" => Ok(WcChain::Eip155), + "cosmos" => Ok(WcChain::Cosmos), + _ => MmError::err(WalletConnectError::InvalidChainId(format!( + "chain_id not supported: {s}" + ))), + } + } +} + +impl AsRef for WcChain { + fn as_ref(&self) -> &str { + match self { + Self::Eip155 => "eip155", + Self::Cosmos => "cosmos", + } + } +} + +impl WcChain { + pub(crate) fn derive_chain_id(&self, id: String) -> WcChainId { + WcChainId { + chain: self.clone(), + id, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WcChainId { + pub chain: WcChain, + pub id: String, +} + +impl std::fmt::Display for WcChainId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.chain.as_ref(), self.id) + } +} + +impl WcChainId { + pub fn new_eip155(id: String) -> Self { + Self { + chain: WcChain::Eip155, + id, + } + } + + pub fn new_cosmos(id: String) -> Self { + Self { + chain: WcChain::Cosmos, + id, + } + } + + pub fn try_from_str(chain_id: &str) -> MmResult { + let sp = chain_id.split(':').collect::>(); + if sp.len() != 2 { + return MmError::err(WalletConnectError::InvalidChainId(chain_id.to_string())); + }; + + Ok(Self { + chain: WcChain::from_str(sp[0])?, + id: sp[1].to_owned(), + }) + } +} + +#[derive(Debug, Clone)] +pub enum WcRequestMethods { + CosmosSignDirect, + CosmosSignAmino, + CosmosGetAccounts, + EthSignTransaction, + EthSendTransaction, + PersonalSign, +} + +impl AsRef for WcRequestMethods { + fn as_ref(&self) -> &str { + match self { + Self::CosmosSignDirect => "cosmos_signDirect", + Self::CosmosSignAmino => "cosmos_signAmino", + Self::CosmosGetAccounts => "cosmos_getAccounts", + Self::EthSignTransaction => "eth_signTransaction", + Self::EthSendTransaction => "eth_sendTransaction", + Self::PersonalSign => "personal_sign", + } + } +} diff --git a/mm2src/kdf_walletconnect/src/connection_handler.rs b/mm2src/kdf_walletconnect/src/connection_handler.rs new file mode 100644 index 0000000000..2ac7d7b7c3 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/connection_handler.rs @@ -0,0 +1,98 @@ +use crate::WalletConnectCtxImpl; + +use common::executor::Timer; +use common::log::{debug, error, info}; +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use futures::StreamExt; +use relay_client::error::ClientError; +use relay_client::websocket::{CloseFrame, ConnectionHandler, PublishedMessage}; + +pub(crate) const MAX_BACKOFF: u64 = 60; + +pub struct Handler { + name: &'static str, + msg_sender: UnboundedSender, + conn_live_sender: UnboundedSender>, +} + +impl Handler { + pub fn new( + name: &'static str, + msg_sender: UnboundedSender, + conn_live_sender: UnboundedSender>, + ) -> Self { + Self { + name, + msg_sender, + conn_live_sender, + } + } +} + +impl ConnectionHandler for Handler { + fn connected(&mut self) { + debug!("[{}] connection to WalletConnect relay server successful", self.name); + } + + fn disconnected(&mut self, frame: Option>) { + debug!("[{}] connection closed: frame={frame:?}", self.name); + + if let Err(e) = self.conn_live_sender.unbounded_send(frame.map(|f| f.to_string())) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } + + fn message_received(&mut self, message: PublishedMessage) { + debug!( + "[{}] inbound message: message_id={} topic={} tag={} message={}", + self.name, message.message_id, message.topic, message.tag, message.message, + ); + + if let Err(e) = self.msg_sender.unbounded_send(message) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } + + fn inbound_error(&mut self, error: ClientError) { + debug!("[{}] inbound error: {error}", self.name); + if let Err(e) = self.conn_live_sender.unbounded_send(Some(error.to_string())) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } + + fn outbound_error(&mut self, error: ClientError) { + debug!("[{}] outbound error: {error}", self.name); + if let Err(e) = self.conn_live_sender.unbounded_send(Some(error.to_string())) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } +} + +/// Handles unexpected disconnections from WalletConnect relay server. +/// Implements exponential backoff retry mechanism for reconnection attempts. +/// After successful reconnection, resubscribes to previous topics to restore full functionality. +pub(crate) async fn handle_disconnections( + this: &WalletConnectCtxImpl, + mut connection_live_rx: UnboundedReceiver>, +) { + let mut backoff = 1; + + while let Some(msg) = connection_live_rx.next().await { + info!("WalletConnect disconnected with message: {msg:?}. Attempting to reconnect..."); + loop { + match this.reconnect_and_subscribe().await { + Ok(_) => { + info!("Reconnection process complete."); + backoff = 1; + break; + }, + Err(e) => { + error!("Reconnection attempt failed: {:?}. Retrying in {:?}...", e, backoff); + Timer::sleep(backoff as f64).await; + // Exponentially increase backoff, but cap it at MAX_BACKOFF + backoff = std::cmp::min(backoff * 2, MAX_BACKOFF); + }, + } + } + } +} diff --git a/mm2src/kdf_walletconnect/src/error.rs b/mm2src/kdf_walletconnect/src/error.rs new file mode 100644 index 0000000000..1d8b6bcf93 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/error.rs @@ -0,0 +1,186 @@ +use enum_derives::EnumFromStringify; +#[cfg(target_arch = "wasm32")] +use mm2_db::indexed_db::cursor_prelude::*; +#[cfg(target_arch = "wasm32")] +use mm2_db::indexed_db::{DbTransactionError, InitDbError}; +use pairing_api::PairingClientError; +use relay_client::error::{ClientError, Error}; +use relay_rpc::rpc::{PublishError, SubscriptionError}; +use serde::{Deserialize, Serialize}; + +// Error codes for various cases +pub(crate) const INVALID_METHOD: i32 = 1001; +pub(crate) const INVALID_EVENT: i32 = 1002; +pub(crate) const INVALID_UPDATE_REQUEST: i32 = 1003; +pub(crate) const INVALID_EXTEND_REQUEST: i32 = 1004; +pub(crate) const INVALID_SESSION_SETTLE_REQUEST: i32 = 1005; + +// Unauthorized error codes +pub(crate) const UNAUTHORIZED_METHOD: i32 = 3001; +pub(crate) const UNAUTHORIZED_EVENT: i32 = 3002; +pub(crate) const UNAUTHORIZED_UPDATE_REQUEST: i32 = 3003; +pub(crate) const UNAUTHORIZED_EXTEND_REQUEST: i32 = 3004; +pub(crate) const UNAUTHORIZED_CHAIN: i32 = 3005; + +// EIP-1193 error code +pub(crate) const USER_REJECTED_REQUEST: i32 = 4001; + +// Rejected (CAIP-25) error codes +pub(crate) const USER_REJECTED: i32 = 5000; +pub(crate) const USER_REJECTED_CHAINS: i32 = 5001; +pub(crate) const USER_REJECTED_METHODS: i32 = 5002; +pub(crate) const USER_REJECTED_EVENTS: i32 = 5003; + +// Unsupported error codes +pub(crate) const UNSUPPORTED_CHAINS: i32 = 5100; +pub(crate) const UNSUPPORTED_METHODS: i32 = 5101; +pub(crate) const UNSUPPORTED_EVENTS: i32 = 5102; +pub(crate) const UNSUPPORTED_ACCOUNTS: i32 = 5103; +pub(crate) const UNSUPPORTED_NAMESPACE_KEY: i32 = 5104; + +pub(crate) const USER_REQUESTED: i64 = 6000; + +#[derive(Debug, Serialize, Deserialize, EnumFromStringify, thiserror::Error)] +pub enum WalletConnectError { + #[error("Pairing Error: {0}")] + #[from_stringify("PairingClientError")] + PairingError(String), + #[error("Publish Error: {0}")] + PublishError(String), + #[error("Client Error: {0}")] + #[from_stringify("ClientError")] + ClientError(String), + #[error("Subscription Error: {0}")] + SubscriptionError(String), + #[error("Internal Error: {0}")] + InternalError(String), + #[error("Serde Error: {0}")] + #[from_stringify("serde_json::Error")] + SerdeError(String), + #[error("UnSuccessfulResponse Error: {0}")] + UnSuccessfulResponse(String), + #[error("Session Error: {0}")] + #[from_stringify("SessionError")] + SessionError(String), + #[error("Unknown params")] + InvalidRequest, + #[error("Request is not yet implemented")] + NotImplemented, + #[error("Hex Error: {0}")] + #[from_stringify("hex::FromHexError")] + HexError(String), + #[error("Payload Error: {0}")] + #[from_stringify("wc_common::PayloadError")] + PayloadError(String), + #[error("Account not found for chain_id: {0}")] + NoAccountFound(String), + #[error("Account not found for index: {0}")] + NoAccountFoundForIndex(usize), + #[error("Empty account approved for chain_id: {0}")] + EmptyAccount(String), + #[error("WalletConnect is not initaliazed yet!")] + NotInitialized, + #[error("Storage Error: {0}")] + StorageError(String), + #[error("ChainId mismatch")] + ChainIdMismatch, + #[error("No feedback from wallet")] + NoWalletFeedback, + #[error("Invalid ChainId Error: {0}")] + InvalidChainId(String), + #[error("ChainId not supported: {0}")] + ChainIdNotSupported(String), + #[error("Request timeout error")] + TimeoutError, +} + +impl From> for WalletConnectError { + fn from(error: Error) -> Self { WalletConnectError::PublishError(format!("{error:?}")) } +} + +impl From> for WalletConnectError { + fn from(error: Error) -> Self { WalletConnectError::SubscriptionError(format!("{error:?}")) } +} + +/// Session key and topic derivation errors. +#[derive(Debug, Clone, thiserror::Error)] +pub enum SessionError { + #[error("Failed to generate symmetric session key: {0}")] + SymKeyGeneration(String), +} + +#[cfg(target_arch = "wasm32")] +#[derive(Debug, Clone, thiserror::Error)] +pub enum WcIndexedDbError { + #[error("Internal Error: {0}")] + InternalError(String), + #[error("Not supported: {0}")] + NotSupported(String), + #[error("Delete Error: {0}")] + DeletionError(String), + #[error("Insert Error: {0}")] + AddToStorageErr(String), + #[error("GetFromStorage Error: {0}")] + GetFromStorageError(String), + #[error("Decoding Error: {0}")] + DecodingError(String), +} + +#[cfg(target_arch = "wasm32")] +impl From for WcIndexedDbError { + fn from(e: InitDbError) -> Self { + match &e { + InitDbError::NotSupported(_) => WcIndexedDbError::NotSupported(e.to_string()), + InitDbError::EmptyTableList + | InitDbError::DbIsOpenAlready { .. } + | InitDbError::InvalidVersion(_) + | InitDbError::OpeningError(_) + | InitDbError::TypeMismatch { .. } + | InitDbError::UnexpectedState(_) + | InitDbError::UpgradingError { .. } => WcIndexedDbError::InternalError(e.to_string()), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl From for WcIndexedDbError { + fn from(e: DbTransactionError) -> Self { + match e { + DbTransactionError::ErrorSerializingItem(_) | DbTransactionError::ErrorDeserializingItem(_) => { + WcIndexedDbError::DecodingError(e.to_string()) + }, + DbTransactionError::ErrorUploadingItem(_) => WcIndexedDbError::AddToStorageErr(e.to_string()), + DbTransactionError::ErrorGettingItems(_) | DbTransactionError::ErrorCountingItems(_) => { + WcIndexedDbError::GetFromStorageError(e.to_string()) + }, + DbTransactionError::ErrorDeletingItems(_) => WcIndexedDbError::DeletionError(e.to_string()), + DbTransactionError::NoSuchTable { .. } + | DbTransactionError::ErrorCreatingTransaction(_) + | DbTransactionError::ErrorOpeningTable { .. } + | DbTransactionError::ErrorSerializingIndex { .. } + | DbTransactionError::UnexpectedState(_) + | DbTransactionError::TransactionAborted + | DbTransactionError::MultipleItemsByUniqueIndex { .. } + | DbTransactionError::NoSuchIndex { .. } + | DbTransactionError::InvalidIndex { .. } => WcIndexedDbError::InternalError(e.to_string()), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl From for WcIndexedDbError { + fn from(value: CursorError) -> Self { + match value { + CursorError::ErrorSerializingIndexFieldValue { .. } + | CursorError::ErrorDeserializingIndexValue { .. } + | CursorError::ErrorDeserializingItem(_) => Self::DecodingError(value.to_string()), + CursorError::ErrorOpeningCursor { .. } + | CursorError::AdvanceError { .. } + | CursorError::InvalidKeyRange { .. } + | CursorError::IncorrectNumberOfKeysPerIndex { .. } + | CursorError::UnexpectedState(_) + | CursorError::IncorrectUsage { .. } + | CursorError::TypeMismatch { .. } => Self::InternalError(value.to_string()), + } + } +} diff --git a/mm2src/kdf_walletconnect/src/inbound_message.rs b/mm2src/kdf_walletconnect/src/inbound_message.rs new file mode 100644 index 0000000000..f2ac60036a --- /dev/null +++ b/mm2src/kdf_walletconnect/src/inbound_message.rs @@ -0,0 +1,98 @@ +use crate::{error::WalletConnectError, + pairing::{reply_pairing_delete_response, reply_pairing_extend_response, reply_pairing_ping_response}, + session::rpc::{delete::reply_session_delete_request, + event::handle_session_event, + extend::reply_session_extend_request, + ping::reply_session_ping_request, + propose::{process_session_propose_response, reply_session_proposal_request}, + settle::reply_session_settle_request, + update::reply_session_update_request}, + WalletConnectCtxImpl}; + +use common::log::{info, LogOnError}; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::{params::ResponseParamsSuccess, Params, Request, Response}; + +pub(crate) type SessionMessageType = MmResult; + +#[derive(Debug)] +pub struct SessionMessage { + pub message_id: MessageId, + pub topic: Topic, + pub data: ResponseParamsSuccess, +} + +/// Processes an inbound WalletConnect request and performs the appropriate action based on the request type. +/// +/// Handles various session and pairing requests, routing them to their corresponding handlers. +pub(crate) async fn process_inbound_request( + ctx: &WalletConnectCtxImpl, + request: Request, + topic: &Topic, +) -> MmResult<(), WalletConnectError> { + let message_id = request.id; + match request.params { + Params::SessionPropose(proposal) => reply_session_proposal_request(ctx, proposal, topic, &message_id).await?, + Params::SessionExtend(param) => reply_session_extend_request(ctx, topic, &message_id, param).await?, + Params::SessionDelete(param) => reply_session_delete_request(ctx, topic, &message_id, param).await?, + Params::SessionPing(()) => reply_session_ping_request(ctx, topic, &message_id).await?, + Params::SessionSettle(param) => reply_session_settle_request(ctx, topic, param).await?, + Params::SessionUpdate(param) => reply_session_update_request(ctx, topic, &message_id, param).await?, + Params::SessionEvent(param) => handle_session_event(ctx, topic, &message_id, param).await?, + Params::SessionRequest(_param) => { + // TODO: Implement when integrating KDF as a Dapp. + return MmError::err(WalletConnectError::NotImplemented); + }, + + Params::PairingPing(_param) => reply_pairing_ping_response(ctx, topic, &message_id).await?, + Params::PairingDelete(param) => reply_pairing_delete_response(ctx, topic, &message_id, param).await?, + Params::PairingExtend(param) => reply_pairing_extend_response(ctx, topic, &message_id, param).await?, + _ => { + info!("Unknown request params received."); + return MmError::err(WalletConnectError::InvalidRequest); + }, + }; + + Ok(()) +} + +/// Processes an inbound WalletConnect response and sends the result to the provided message channel. +/// +/// Handles successful responses, errors, and specific session proposal processing. +pub(crate) async fn process_inbound_response(ctx: &WalletConnectCtxImpl, response: Response, topic: &Topic) { + let message_id = response.id(); + let result = match &response { + Response::Success(value) => match serde_json::from_value::(value.result.clone()) { + Ok(ResponseParamsSuccess::SessionPropose(propose)) => { + // If this is a session propose response, process it right away and return. + // Session proposal responses are not waited for since it might take a long time + // for the proposal to be accepted (user interaction). So they are handled in async fashion. + ctx.pending_requests + .lock() + .expect("pending request lock shouldn't fail!") + .remove(&message_id); + return process_session_propose_response(ctx, topic, &propose) + .await + .error_log_with_msg("Failed to process session propose response"); + }, + Ok(data) => Ok(SessionMessage { + message_id, + topic: topic.clone(), + data, + }), + Err(err) => MmError::err(WalletConnectError::SerdeError(err.to_string())), + }, + Response::Error(err) => MmError::err(WalletConnectError::UnSuccessfulResponse(format!("{err:?}"))), + }; + + let mut pending_requests = ctx + .pending_requests + .lock() + .expect("pending request lock shouldn't fail!"); + if let Some(tx) = pending_requests.remove(&message_id) { + tx.send(result).ok(); + } else { + common::log::error!("[{topic}] unrecognized inbound response/message: {response:?}"); + }; +} diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs new file mode 100644 index 0000000000..8f8740e081 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -0,0 +1,682 @@ +pub mod chain; +mod connection_handler; +#[allow(unused)] pub mod error; +pub mod inbound_message; +mod metadata; +#[allow(unused)] mod pairing; +pub mod session; +mod storage; + +use crate::connection_handler::{handle_disconnections, MAX_BACKOFF}; +use crate::session::rpc::propose::send_proposal_request; +use chain::{WcChainId, WcRequestMethods, SUPPORTED_PROTOCOL}; +use common::custom_futures::timeout::FutureTimerExt; +use common::executor::abortable_queue::AbortableQueue; +use common::executor::{AbortableSystem, Timer}; +use common::log::{debug, info, LogOnError}; +use common::{executor::SpawnFuture, log::error}; +use connection_handler::Handler; +use error::WalletConnectError; +use futures::channel::mpsc::{unbounded, UnboundedReceiver}; +use futures::StreamExt; +use inbound_message::{process_inbound_request, process_inbound_response, SessionMessageType}; +use metadata::{generate_metadata, AUTH_TOKEN_DURATION, AUTH_TOKEN_SUB, PROJECT_ID, RELAY_ADDRESS}; +use mm2_core::mm_ctx::{from_ctx, MmArc}; +use mm2_err_handle::prelude::*; +use pairing_api::PairingClient; +use relay_client::websocket::{connection_event_loop as client_event_loop, Client, PublishedMessage}; +use relay_client::{ConnectionOptions, MessageIdGenerator}; +use relay_rpc::auth::{ed25519_dalek::SigningKey, AuthToken}; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::params::session::{Namespace, ProposeNamespaces}; +use relay_rpc::rpc::params::session_request::SessionRequestRequest; +use relay_rpc::rpc::params::{session_request::Request as SessionRequest, IrnMetadata, Metadata, Relay, + RelayProtocolMetadata, RequestParams, ResponseParamsError, ResponseParamsSuccess}; +use relay_rpc::rpc::{ErrorResponse, Payload, Request, Response, SuccessfulResponse}; +use serde::de::DeserializeOwned; +use session::rpc::delete::send_session_delete_request; +use session::{key::SymKeyPair, SessionManager}; +use session::{EncodingAlgo, Session, SessionProperties, FIVE_MINUTES}; +use std::collections::BTreeSet; +use std::ops::Deref; +use std::{sync::{Arc, Mutex}, + time::Duration}; +use storage::SessionStorageDb; +use storage::WalletConnectStorageOps; +use timed_map::TimedMap; +use tokio::sync::oneshot; +use wc_common::{decode_and_decrypt_type0, encrypt_and_encode, EnvelopeType, SymKey}; + +const PUBLISH_TIMEOUT_SECS: f64 = 6.; +const MAX_RETRIES: usize = 5; + +#[async_trait::async_trait] +pub trait WalletConnectOps { + type Error; + type Params<'a>; + type SignTxData; + type SendTxData; + + /// Unique chain_id associated with an activated/supported coin. + async fn wc_chain_id(&self, ctx: &WalletConnectCtx) -> Result; + + /// Send sign transaction request to WalletConnect Wallet. + async fn wc_sign_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result; + + /// Send sign and send/broadcast transaction request to WalletConnect Wallet. + async fn wc_send_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result; + + /// Session topic used to activate this. + fn session_topic(&self) -> Result<&str, Self::Error>; +} + +/// Implements the WalletConnect context, providing functionality for +/// establishing and managing wallet connections. +/// This struct contains the necessary state and methods to handle +/// wallet connection sessions, signing requests, and connection events. +pub struct WalletConnectCtxImpl { + pub(crate) client: Client, + pub(crate) pairing: PairingClient, + pub(crate) key_pair: SymKeyPair, + pub session_manager: SessionManager, + relay: Relay, + metadata: Metadata, + message_id_generator: MessageIdGenerator, + pending_requests: Mutex>>, + abortable_system: AbortableQueue, +} + +/// A newtype wrapper around a thread-safe reference to `WalletConnectCtxImpl`. +/// Provides shared access to wallet connection functionality through an Arc pointer. +pub struct WalletConnectCtx(pub Arc); +impl Deref for WalletConnectCtx { + type Target = WalletConnectCtxImpl; + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl WalletConnectCtx { + /// Attempt to initialize a new WalletConnect context. + pub fn try_init(ctx: &MmArc) -> MmResult { + let abortable_system = ctx + .abortable_system + .create_subsystem::() + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))?; + let storage = SessionStorageDb::new(ctx)?; + let pairing = PairingClient::new(); + let relay = Relay { + protocol: SUPPORTED_PROTOCOL.to_string(), + data: None, + }; + let (inbound_message_tx, inbound_message_rx) = unbounded(); + let (conn_live_sender, conn_live_receiver) = unbounded(); + let (client, _) = Client::new_with_callback( + Handler::new("KDF", inbound_message_tx, conn_live_sender), + |receiver, handler| { + abortable_system + .weak_spawner() + .spawn(client_event_loop(receiver, handler)) + }, + ); + + let message_id_generator = MessageIdGenerator::new(); + let context = Arc::new(WalletConnectCtxImpl { + client, + pairing, + relay, + metadata: generate_metadata(), + key_pair: SymKeyPair::new(), + session_manager: SessionManager::new(storage), + pending_requests: Default::default(), + message_id_generator, + abortable_system, + }); + + // Connect to relayer client and spawn a watcher loop for disconnection. + context + .abortable_system + .weak_spawner() + .spawn(context.clone().spawn_connection_initialization_fut(conn_live_receiver)); + + // spawn message handler event loop + context + .abortable_system + .weak_spawner() + .spawn(context.clone().spawn_published_message_fut(inbound_message_rx)); + + Ok(Self(context)) + } + + pub fn from_ctx(ctx: &MmArc) -> MmResult, WalletConnectError> { + from_ctx(&ctx.wallet_connect, move || { + Self::try_init(ctx).map_err(|err| err.to_string()) + }) + .map_to_mm(WalletConnectError::InternalError) + } +} + +impl WalletConnectCtxImpl { + /// Establishes initial connection to WalletConnect relay server with linear retry mechanism. + /// Uses increasing delay between retry attempts starting from 1sec and increase exponentially. + /// After successful connection, attempts to restore previous session state from storage. + pub(crate) async fn spawn_connection_initialization_fut( + self: Arc, + connection_live_rx: UnboundedReceiver>, + ) { + info!("Initializing WalletConnect connection"); + let mut retry_count = 0; + let mut retry_secs = 1; + + // Connect to WalletConnect relay client(retry until successful) before proceeeding with other initializations. + while let Err(err) = self.connect_client().await { + retry_count += 1; + error!( + "Error during initial connection attempt {}: {:?}. Retrying in {retry_secs} seconds...", + retry_count, err + ); + Timer::sleep(retry_secs as f64).await; + retry_secs = std::cmp::min(retry_secs * 2, MAX_BACKOFF); + } + + // Initialize storage + if let Err(err) = self.session_manager.storage().init().await { + error!("Failed to initialize WalletConnect storage, shutting down: {err:?}"); + self.abortable_system.abort_all().error_log(); + }; + + // load session from storage + if let Err(err) = self.load_session_from_storage().await { + error!("Failed to load sessions from storage, shutting down: {err:?}"); + self.abortable_system.abort_all().error_log(); + }; + + // Spawn session disconnection watcher. + handle_disconnections(&self, connection_live_rx).await; + } + + /// Attempt to connect to a wallet connection relay server. + pub async fn connect_client(&self) -> MmResult<(), WalletConnectError> { + let auth = { + let key = SigningKey::generate(&mut rand::thread_rng()); + AuthToken::new(AUTH_TOKEN_SUB) + .aud(RELAY_ADDRESS) + .ttl(AUTH_TOKEN_DURATION) + .as_jwt(&key) + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))? + }; + let opts = ConnectionOptions::new(PROJECT_ID, auth).with_address(RELAY_ADDRESS); + self.client.connect(&opts).await?; + + Ok(()) + } + + /// Re-connect to WalletConnect relayer and re-subscribes to previously active session topics after reconnection. + pub(crate) async fn reconnect_and_subscribe(&self) -> MmResult<(), WalletConnectError> { + self.connect_client().await?; + let sessions = self + .session_manager + .get_sessions() + .flat_map(|s| vec![s.topic, s.pairing_topic]) + .collect::>(); + + if !sessions.is_empty() { + self.client.batch_subscribe(sessions).await?; + } + + Ok(()) + } + + /// Create a WalletConnect pairing connection url. + pub async fn new_connection( + &self, + required_namespaces: serde_json::Value, + optional_namespaces: Option, + ) -> MmResult { + let required_namespaces = serde_json::from_value(required_namespaces)?; + let optional_namespaces = match optional_namespaces { + Some(value) => serde_json::from_value(value)?, + None => ProposeNamespaces::default(), + }; + let (topic, url) = self.pairing.create(self.metadata.clone(), None)?; + + info!("[{topic}] Subscribing to topic"); + + for attempt in 0..MAX_RETRIES { + match self + .client + .subscribe(topic.clone()) + .timeout_secs(PUBLISH_TIMEOUT_SECS) + .await + { + Ok(res) => { + res.map_to_mm(|err| err.into())?; + info!("[{topic}] Subscribed to topic"); + send_proposal_request(self, &topic, required_namespaces, optional_namespaces).await?; + return Ok(url); + }, + Err(_) => self.wait_until_client_is_online_loop(attempt).await, + } + } + + MmError::err(WalletConnectError::InternalError( + "client connection timeout".to_string(), + )) + } + + /// Get symmetric key associated with a for `topic`. + fn sym_key(&self, topic: &Topic) -> MmResult { + self.session_manager + .sym_key(topic) + .or_else(|| self.pairing.sym_key(topic).ok()) + .ok_or_else(|| { + error!("Failed to find sym_key for topic: {topic}"); + MmError::new(WalletConnectError::InternalError(format!( + "topic sym_key not found: {topic}" + ))) + }) + } + + /// Handles an inbound published message by decrypting, decoding, and processing it. + async fn handle_published_message(&self, msg: PublishedMessage) -> MmResult<(), WalletConnectError> { + let message = { + let key = self.sym_key(&msg.topic)?; + decode_and_decrypt_type0(msg.message.as_bytes(), &key)? + }; + + info!("[{}] Inbound message payload={message}", msg.topic); + + match serde_json::from_str(&message)? { + Payload::Request(request) => process_inbound_request(self, request, &msg.topic).await?, + Payload::Response(response) => process_inbound_response(self, response, &msg.topic).await, + } + + debug!("[{}] Inbound message was handled successfully", msg.topic); + + Ok(()) + } + + /// Spawns a task that continuously processes published messages from inbound message channel. + async fn spawn_published_message_fut(self: Arc, mut recv: UnboundedReceiver) { + while let Some(msg) = recv.next().await { + self.handle_published_message(msg) + .await + .error_log_with_msg("Error processing message"); + } + } + + /// Loads sessions from storage, activates valid ones, and deletes expired. + async fn load_session_from_storage(&self) -> MmResult<(), WalletConnectError> { + info!("Loading WalletConnect session from storage"); + let now = chrono::Utc::now().timestamp() as u64; + let sessions = self + .session_manager + .storage() + .get_all_sessions() + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + let mut valid_topics = Vec::with_capacity(sessions.len()); + let mut pairing_topics = Vec::with_capacity(sessions.len()); + + // bring most recent active session to the back. + for session in sessions.into_iter().rev() { + // delete expired session + if now > session.expiry { + debug!("Session {} expired, trying to delete from storage", session.topic); + self.session_manager + .storage() + .delete_session(&session.topic) + .await + .error_log_with_msg(&format!("[{}] Unable to delete session from storage", session.topic)); + continue; + }; + + let topic = session.topic.clone(); + let pairing_topic = session.pairing_topic.clone(); + debug!("[{topic}] Session found! activating"); + self.session_manager.add_session(session); + + valid_topics.push(topic); + pairing_topics.push(pairing_topic); + } + + let all_topics = valid_topics.into_iter().chain(pairing_topics).collect::>(); + + if !all_topics.is_empty() { + self.client.batch_subscribe(all_topics).await?; + } + + info!("Loaded WalletConnect session from storage"); + + Ok(()) + } + + pub fn encode>(&self, session_topic: &str, data: T) -> String { + let session_topic = session_topic.into(); + let algo = self + .session_manager + .get_session(&session_topic) + .map(|session| session.encoding_algo.unwrap_or(EncodingAlgo::Hex)) + .unwrap_or(EncodingAlgo::Hex); + + algo.encode(data) + } + + /// Private function to publish a WC request. + pub(crate) async fn publish_request( + &self, + topic: &Topic, + param: RequestParams, + ) -> MmResult<(oneshot::Receiver, Duration), WalletConnectError> { + let irn_metadata = param.irn_metadata(); + let ttl = irn_metadata.ttl; + let message_id = self.message_id_generator.next(); + let request = Request::new(message_id, param.into()); + + self.publish_payload(topic, irn_metadata, Payload::Request(request)) + .await?; + + let (tx, rx) = oneshot::channel(); + // insert request to map with a reasonable expiration time of 5 minutes + self.pending_requests + .lock() + .unwrap() + .insert_expirable(message_id, tx, Duration::from_secs(FIVE_MINUTES)); + + Ok((rx, Duration::from_secs(ttl))) + } + + /// Private function to publish a success WC request response. + pub(crate) async fn publish_response_ok( + &self, + topic: &Topic, + result: ResponseParamsSuccess, + message_id: &MessageId, + ) -> MmResult<(), WalletConnectError> { + let irn_metadata = result.irn_metadata(); + let value = serde_json::to_value(result)?; + let response = Response::Success(SuccessfulResponse::new(*message_id, value)); + + self.publish_payload(topic, irn_metadata, Payload::Response(response)) + .await + } + + /// Private function to publish an error WC request response. + pub(crate) async fn publish_response_err( + &self, + topic: &Topic, + error_data: ResponseParamsError, + message_id: &MessageId, + ) -> MmResult<(), WalletConnectError> { + let error = error_data.error(); + let irn_metadata = error_data.irn_metadata(); + let response = Response::Error(ErrorResponse::new(*message_id, error)); + + self.publish_payload(topic, irn_metadata, Payload::Response(response)) + .await + } + + /// Private function to publish a WC payload. + pub(crate) async fn publish_payload( + &self, + topic: &Topic, + irn_metadata: IrnMetadata, + payload: Payload, + ) -> MmResult<(), WalletConnectError> { + info!("[{topic}] Publishing message={payload:?}"); + let message = { + let sym_key = self.sym_key(topic)?; + let payload = serde_json::to_string(&payload)?; + encrypt_and_encode(EnvelopeType::Type0, payload, &sym_key)? + }; + + for attempt in 0..MAX_RETRIES { + match self + .client + .publish( + topic.clone(), + &*message, + None, + irn_metadata.tag, + Duration::from_secs(irn_metadata.ttl), + irn_metadata.prompt, + ) + .timeout_secs(PUBLISH_TIMEOUT_SECS) + .await + { + Ok(Ok(_)) => { + info!("[{topic}] Message published successfully"); + return Ok(()); + }, + Ok(Err(err)) => return MmError::err(err.into()), + Err(_) => self.wait_until_client_is_online_loop(attempt).await, + } + } + + MmError::err(WalletConnectError::InternalError( + "[{topic}] client connection timeout".to_string(), + )) + } + + /// Persistent reconnection and retry strategy keeps the WebSocket connection active, + /// allowing the client to automatically resume operations after network interruptions or disconnections. + /// Since TCP handles connection timeouts (which can be lengthy and it's determined by the OS), we're using a shorter timeout here + /// to detect issues quickly and reconnect as needed. + async fn wait_until_client_is_online_loop(&self, attempt: usize) { + debug!("Attempt {} failed due to timeout. Reconnecting...", attempt + 1); + loop { + match self.reconnect_and_subscribe().await { + Ok(_) => { + info!("Reconnected and subscribed successfully."); + break; + }, + Err(reconnect_err) => { + error!("Reconnection attempt failed: {reconnect_err:?}. Retrying..."); + Timer::sleep(1.5).await; + }, + } + } + } + + /// Checks if the current session is connected to a Ledger device. + /// NOTE: for COSMOS chains only. + pub fn is_ledger_connection(&self, session_topic: &str) -> bool { + let session_topic = session_topic.into(); + self.session_manager + .get_session(&session_topic) + .and_then(|session| session.session_properties) + .and_then(|props| props.keys.as_ref().cloned()) + .and_then(|keys| keys.first().cloned()) + .map(|key| key.is_nano_ledger) + .unwrap_or(false) + } + + /// Checks if the current session is connected via Keplr wallet. + /// NOTE: for COSMOS chains only. + pub fn is_keplr_connection(&self, session_topic: &str) -> bool { + let session_topic = session_topic.into(); + self.session_manager + .get_session(&session_topic) + .map(|session| session.controller.metadata.name == "Keplr") + .unwrap_or_default() + } + + /// Checks if a given chain ID is supported. + pub(crate) fn validate_chain_id( + &self, + session: &Session, + chain_id: &WcChainId, + ) -> MmResult<(), WalletConnectError> { + if let Some(Namespace { chains, .. }) = session.namespaces.get(chain_id.chain.as_ref()) { + match chains { + Some(chains) => { + if chains.contains(&chain_id.to_string()) { + return Ok(()); + } + }, + None => { + // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#13-chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index + if let Some(SessionProperties { keys: Some(keys) }) = &session.session_properties { + if keys.iter().any(|k| k.chain_id == chain_id.id) { + return Ok(()); + } + } + }, + }; + } + + // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#13-chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index + if session.namespaces.contains_key(&chain_id.to_string()) { + return Ok(()); + } + + MmError::err(WalletConnectError::ChainIdNotSupported(chain_id.to_string())) + } + + /// Validate and send update active chain to WC if needed. + pub async fn validate_update_active_chain_id( + &self, + session_topic: &str, + chain_id: &WcChainId, + ) -> MmResult<(), WalletConnectError> { + let session_topic = session_topic.into(); + let session = + self.session_manager + .get_session(&session_topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))?; + + self.validate_chain_id(&session, chain_id)?; + + // TODO: uncomment when WalletConnect wallets start listening to chainChanged event + // if WcChain::Eip155 != chain_id.chain { + // return Ok(()); + // }; + // + // if let Some(active_chain_id) = session.get_active_chain_id().await { + // if chain_id == active_chain_id { + // return Ok(()); + // } + // }; + // + // let event = SessionEventRequest { + // event: Event { + // name: "chainChanged".to_string(), + // data: serde_json::to_value(&chain_id.id)?, + // }, + // chain_id: chain_id.to_string(), + // }; + // self.publish_request(&session.topic, RequestParams::SessionEvent(event)) + // .await?; + // + // let wait_duration = Duration::from_secs(60); + // if let Ok(Some(resp)) = self.message_rx.lock().await.next().timeout(wait_duration).await { + // let result = resp.mm_err(WalletConnectError::InternalError)?; + // if let ResponseParamsSuccess::SessionEvent(data) = result.data { + // if !data { + // return MmError::err(WalletConnectError::PayloadError( + // "Please approve chain id change".to_owned(), + // )); + // } + // + // self.session + // .get_session_mut(&session.topic) + // .ok_or(MmError::new(WalletConnectError::SessionError( + // "No active WalletConnect session found".to_string(), + // )))? + // .set_active_chain_id(chain_id.clone()) + // .await; + // } + // } + + Ok(()) + } + + /// Get available account for a given chain ID. + pub fn get_account_and_properties_for_chain_id( + &self, + session_topic: &str, + chain_id: &WcChainId, + ) -> MmResult<(String, Option), WalletConnectError> { + let session_topic = session_topic.into(); + let session = + self.session_manager + .get_session(&session_topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))?; + + if let Some(Namespace { + accounts: Some(accounts), + .. + }) = &session.namespaces.get(chain_id.chain.as_ref()) + { + if let Some(account) = find_account_in_namespace(accounts, &chain_id.id) { + return Ok((account, session.session_properties)); + } + }; + + MmError::err(WalletConnectError::NoAccountFound(chain_id.to_string())) + } + + /// Waits for and handles a WalletConnect session response with arbitrary data. + /// https://specs.walletconnect.com/2.0/specs/clients/sign/session-events#session_request + pub async fn send_session_request_and_wait( + &self, + session_topic: &str, + chain_id: &WcChainId, + method: WcRequestMethods, + params: serde_json::Value, + ) -> MmResult + where + R: DeserializeOwned, + { + let session_topic = session_topic.into(); + self.session_manager.validate_session_exists(&session_topic)?; + + let request = SessionRequestRequest { + chain_id: chain_id.to_string(), + request: SessionRequest { + method: method.as_ref().to_string(), + expiry: None, + params, + }, + }; + let (rx, ttl) = self + .publish_request(&session_topic, RequestParams::SessionRequest(request)) + .await?; + + let response = rx + .timeout(ttl) + .await + .map_to_mm(|_| WalletConnectError::TimeoutError)? + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))??; + match response.data { + ResponseParamsSuccess::Arbitrary(data) => Ok(serde_json::from_value::(data)?), + _ => MmError::err(WalletConnectError::PayloadError("Unexpected response type".to_string())), + } + } + + // Destroy WC session. + pub async fn drop_session(&self, topic: &Topic) -> MmResult<(), WalletConnectError> { + send_session_delete_request(self, topic).await + } +} + +fn find_account_in_namespace<'a>(accounts: &'a BTreeSet, chain_id: &'a str) -> Option { + accounts.iter().find_map(move |account_name| { + let parts: Vec<&str> = account_name.split(':').collect(); + if parts.len() >= 3 && parts[1] == chain_id { + Some(parts[2].to_string()) + } else { + None + } + }) +} diff --git a/mm2src/kdf_walletconnect/src/metadata.rs b/mm2src/kdf_walletconnect/src/metadata.rs new file mode 100644 index 0000000000..600695d9a9 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/metadata.rs @@ -0,0 +1,20 @@ +use std::time::Duration; + +use relay_rpc::rpc::params::Metadata; + +pub(crate) const RELAY_ADDRESS: &str = "wss://relay.walletconnect.com"; +pub(crate) const PROJECT_ID: &str = "86e916bcbacee7f98225dde86b697f5b"; +pub(crate) const AUTH_TOKEN_SUB: &str = "http://127.0.0.1:3000"; +pub(crate) const AUTH_TOKEN_DURATION: Duration = Duration::from_secs(5 * 60 * 60); +pub(crate) const APP_NAME: &str = "Komodefi Framework"; +pub(crate) const APP_DESCRIPTION: &str = "WallectConnect Komodefi Framework Playground"; + +#[inline] +pub(crate) fn generate_metadata() -> Metadata { + Metadata { + description: APP_DESCRIPTION.to_owned(), + url: AUTH_TOKEN_SUB.to_owned(), + icons: vec!["https://avatars.githubusercontent.com/u/21276113?s=200&v=4".to_owned()], + name: APP_NAME.to_owned(), + } +} diff --git a/mm2src/kdf_walletconnect/src/pairing.rs b/mm2src/kdf_walletconnect/src/pairing.rs new file mode 100644 index 0000000000..4990dd197a --- /dev/null +++ b/mm2src/kdf_walletconnect/src/pairing.rs @@ -0,0 +1,48 @@ +use crate::session::{WcRequestResponseResult, THIRTY_DAYS}; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use chrono::Utc; +use mm2_err_handle::prelude::MmResult; +use relay_rpc::domain::MessageId; +use relay_rpc::rpc::params::pairing_ping::PairingPingRequest; +use relay_rpc::rpc::params::{RelayProtocolMetadata, RequestParams}; +use relay_rpc::{domain::Topic, + rpc::params::{pairing_delete::PairingDeleteRequest, pairing_extend::PairingExtendRequest, + ResponseParamsSuccess}}; + +pub(crate) async fn reply_pairing_ping_response( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, +) -> MmResult<(), WalletConnectError> { + let param = ResponseParamsSuccess::PairingPing(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +pub(crate) async fn reply_pairing_extend_response( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + extend: PairingExtendRequest, +) -> MmResult<(), WalletConnectError> { + ctx.pairing.activate(topic)?; + let param = ResponseParamsSuccess::PairingExtend(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +pub(crate) async fn reply_pairing_delete_response( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + _delete: PairingDeleteRequest, +) -> MmResult<(), WalletConnectError> { + ctx.pairing.disconnect_rpc(topic, &ctx.client).await?; + let param = ResponseParamsSuccess::PairingDelete(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/key.rs b/mm2src/kdf_walletconnect/src/session/key.rs new file mode 100644 index 0000000000..7ac299cae6 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/key.rs @@ -0,0 +1,197 @@ +use crate::error::SessionError; + +use serde::{Deserialize, Serialize}; +use wc_common::SymKey; +use x25519_dalek::{PublicKey, SharedSecret, StaticSecret}; +use {hkdf::Hkdf, + rand::{rngs::OsRng, CryptoRng, RngCore}, + sha2::{Digest, Sha256}}; + +pub(crate) struct SymKeyPair { + pub(crate) secret: StaticSecret, + pub(crate) public_key: PublicKey, +} + +impl SymKeyPair { + pub(crate) fn new() -> Self { + let static_secret = StaticSecret::random_from_rng(OsRng); + let public_key = PublicKey::from(&static_secret); + Self { + secret: static_secret, + public_key, + } + } +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SessionKey { + pub(crate) sym_key: SymKey, + pub(crate) public_key: SymKey, +} + +impl std::fmt::Debug for SessionKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SessionKey") + .field("sym_key", &"*******") + .field("public_key", &self.public_key) + .finish() + } +} + +impl SessionKey { + /// Creates a new `SessionKey` with the given public key and an empty symmetric key. + pub fn new(public_key: PublicKey) -> Self { + Self { + sym_key: [0u8; 32], + public_key: public_key.to_bytes(), + } + } + + /// Creates a new `SessionKey` using a random number generator and a peer's public key. + pub fn from_osrng(other_public_key: &SymKey) -> Result { + SessionKey::diffie_hellman(OsRng, other_public_key) + } + + /// Performs Diffie-Hellman key exchange to derive a symmetric key. + pub fn diffie_hellman(csprng: T, other_public_key: &SymKey) -> Result + where + T: RngCore + CryptoRng, + { + let static_private_key = StaticSecret::random_from_rng(csprng); + let public_key = PublicKey::from(&static_private_key); + let shared_secret = static_private_key.diffie_hellman(&PublicKey::from(*other_public_key)); + + let mut session_key = Self { + sym_key: [0u8; 32], + public_key: public_key.to_bytes(), + }; + session_key.derive_symmetric_key(&shared_secret)?; + + Ok(session_key) + } + + /// Generates the symmetric key using the static secret and the peer's public key. + pub fn generate_symmetric_key( + &mut self, + static_secret: &StaticSecret, + peer_public_key: &SymKey, + ) -> Result<(), SessionError> { + let shared_secret = static_secret.diffie_hellman(&PublicKey::from(*peer_public_key)); + self.derive_symmetric_key(&shared_secret) + } + + /// Derives the symmetric key from a shared secret. + fn derive_symmetric_key(&mut self, shared_secret: &SharedSecret) -> Result<(), SessionError> { + let hk = Hkdf::::new(None, shared_secret.as_bytes()); + hk.expand(&[], &mut self.sym_key) + .map_err(|e| SessionError::SymKeyGeneration(e.to_string())) + } + + /// Gets symmetic key reference. + pub fn symmetric_key(&self) -> SymKey { self.sym_key } + + /// Gets "our" public key used in symmetric key derivation. + pub fn diffie_public_key(&self) -> SymKey { self.public_key } + + /// Generates new session topic. + pub fn generate_topic(&self) -> String { + let mut hasher = Sha256::new(); + hasher.update(self.sym_key); + hex::encode(hasher.finalize()) + } +} + +#[cfg(test)] +mod session_key_tests { + use super::*; + use anyhow::Result; + use rand::rngs::OsRng; + use x25519_dalek::{PublicKey, StaticSecret}; + + #[test] + fn test_diffie_hellman_key_exchange() -> Result<()> { + // Alice's key pair + let alice_static_secret = StaticSecret::random_from_rng(OsRng); + let alice_public_key = PublicKey::from(&alice_static_secret); + + // Bob's key pair + let bob_static_secret = StaticSecret::random_from_rng(OsRng); + let bob_public_key = PublicKey::from(&bob_static_secret); + + // Alice computes shared secret and session key + let alice_shared_secret = alice_static_secret.diffie_hellman(&bob_public_key); + let mut alice_session_key = SessionKey::new(alice_public_key); + alice_session_key.derive_symmetric_key(&alice_shared_secret)?; + + // Bob computes shared secret and session key + let bob_shared_secret = bob_static_secret.diffie_hellman(&alice_public_key); + let mut bob_session_key = SessionKey::new(bob_public_key); + bob_session_key.derive_symmetric_key(&bob_shared_secret)?; + + // Both symmetric keys should be the same + assert_eq!(alice_session_key.symmetric_key(), bob_session_key.symmetric_key()); + + // Ensure public keys are different + assert_ne!(alice_session_key.public_key, bob_session_key.public_key); + + Ok(()) + } + + #[test] + fn test_generate_symmetric_key() -> Result<()> { + // Alice's key pair + let alice_static_secret = StaticSecret::random_from_rng(OsRng); + let alice_public_key = PublicKey::from(&alice_static_secret); + + // Bob's public key + let bob_static_secret = StaticSecret::random_from_rng(OsRng); + let bob_public_key = PublicKey::from(&bob_static_secret); + + // Alice initializes session key + let mut alice_session_key = SessionKey::new(alice_public_key); + + // Alice generates symmetric key using Bob's public key + alice_session_key.generate_symmetric_key(&alice_static_secret, &bob_public_key.to_bytes())?; + + // Bob computes shared secret and session key + let bob_shared_secret = bob_static_secret.diffie_hellman(&alice_public_key); + let mut bob_session_key = SessionKey::new(bob_public_key); + bob_session_key.derive_symmetric_key(&bob_shared_secret)?; + + // Both symmetric keys should be the same + assert_eq!(alice_session_key.symmetric_key(), bob_session_key.symmetric_key()); + + Ok(()) + } + + #[test] + fn test_from_osrng() -> Result<()> { + // Bob's public key + let bob_static_secret = StaticSecret::random_from_rng(OsRng); + let bob_public_key = PublicKey::from(&bob_static_secret); + + // Alice creates session key using from_osrng + let alice_session_key = SessionKey::from_osrng(&bob_public_key.to_bytes())?; + + // Bob computes shared secret and session key + let bob_shared_secret = bob_static_secret.diffie_hellman(&PublicKey::from(alice_session_key.public_key)); + let mut bob_session_key = SessionKey::new(bob_public_key); + bob_session_key.derive_symmetric_key(&bob_shared_secret)?; + + // Both symmetric keys should be the same + assert_eq!(alice_session_key.symmetric_key(), bob_session_key.symmetric_key()); + + Ok(()) + } + + #[test] + fn test_debug_trait() { + let static_secret = StaticSecret::random_from_rng(OsRng); + let public_key = PublicKey::from(&static_secret); + let session_key = SessionKey::new(public_key); + + let debug_str = format!("{:?}", session_key); + assert!(debug_str.contains("SessionKey")); + assert!(debug_str.contains("sym_key: \"*******\"")); + } +} diff --git a/mm2src/kdf_walletconnect/src/session/mod.rs b/mm2src/kdf_walletconnect/src/session/mod.rs new file mode 100644 index 0000000000..de43ea967b --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/mod.rs @@ -0,0 +1,376 @@ +pub(crate) mod key; +pub mod rpc; + +use crate::chain::WcChainId; +use crate::storage::SessionStorageDb; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use chrono::Utc; +use common::log::info; +use derive_more::Display; +use key::SessionKey; +use mm2_err_handle::prelude::{MmError, MmResult}; +use relay_rpc::domain::Topic; +use relay_rpc::rpc::params::session::Namespace; +use relay_rpc::rpc::params::session_propose::Proposer; +use relay_rpc::rpc::params::IrnMetadata; +use relay_rpc::{domain::SubscriptionId, + rpc::params::{session::ProposeNamespaces, session_settle::Controller, Metadata, Relay}}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::Debug; +use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use wc_common::SymKey; + +pub(crate) const FIVE_MINUTES: u64 = 5 * 60; +pub(crate) const THIRTY_DAYS: u64 = 30 * 24 * 60 * 60; + +pub(crate) type WcRequestResponseResult = MmResult<(Value, IrnMetadata), WalletConnectError>; + +/// In the WalletConnect protocol, a session involves two parties: a controller +/// (typically a wallet) and a proposer (typically a dApp). This enum is used +/// to distinguish between these two roles. +#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum SessionType { + /// Represents the controlling party in a session, typically a wallet. + Controller, + /// Represents the proposing party in a session, typically a dApp. + Proposer, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct SessionRpcInfo { + pub topic: Topic, + pub metadata: Metadata, + pub pairing_topic: Topic, + pub namespaces: BTreeMap, + pub expiry: u64, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct KeyInfo { + pub chain_id: String, + pub name: String, + pub algo: String, + pub pub_key: String, + pub address: String, + pub bech32_address: String, + pub ethereum_hex_address: String, + pub is_nano_ledger: bool, + pub is_keystone: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SessionProperties { + #[serde(default, deserialize_with = "deserialize_keys_from_string")] + pub keys: Option>, +} + +fn deserialize_keys_from_string<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum KeysField { + String(String), + Vec(Vec), + None, + } + + match KeysField::deserialize(deserializer)? { + KeysField::String(key_string) => serde_json::from_str(&key_string) + .map(Some) + .map_err(serde::de::Error::custom), + KeysField::Vec(keys) => Ok(Some(keys)), + KeysField::None => Ok(None), + } +} + +/// Encoding Algorithm for encoding data sent over to external wallets. +/// Most wallets relies on hex. However, Keplr uses base64. +#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] +pub enum EncodingAlgo { + /// HEX encoding format + #[default] + Hex, + /// BASE64 encoding format + Base64, +} + +impl EncodingAlgo { + fn new(name: &str) -> Self { + match name { + "Keplr" => Self::Base64, + _ => Self::Hex, + } + } + + pub fn encode>(&self, data: T) -> String { + match self { + Self::Hex => hex::encode(data), + Self::Base64 => STANDARD.encode(data), + } + } +} + +/// This struct is typically used in the core session management logic of a WalletConnect +/// implementation. It's used to store, retrieve, and update session information throughout +/// the lifecycle of a WalletConnect connection. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct Session { + /// Session topic + pub topic: Topic, + /// Pairing subscription id. + pub subscription_id: SubscriptionId, + /// Session symmetric key + pub session_key: SessionKey, + /// Information about the controlling party (typically a wallet). + pub controller: Controller, + /// Information about the proposing party (typically a dApp). + pub proposer: Proposer, + /// Details about the relay used for communication. + pub relay: Relay, + /// Agreed-upon namespaces for the session, mapping namespace strings to their definitions. + pub namespaces: BTreeMap, + /// Namespaces proposed for the session, may differ from agreed namespaces. + pub propose_namespaces: ProposeNamespaces, + /// Unix timestamp (in seconds) when the session expires. + pub expiry: u64, + /// Topic used for the initial pairing process. + pub pairing_topic: Topic, + /// Indicates whether this session info represents a Controller or Proposer perspective. + pub session_type: SessionType, + pub session_properties: Option, + /// Session active chain_id + pub active_chain_id: Option, + /// Encoding algorithm. + pub encoding_algo: Option, +} + +impl Session { + pub fn new( + ctx: &WalletConnectCtxImpl, + session_topic: Topic, + subscription_id: SubscriptionId, + session_key: SessionKey, + pairing_topic: Topic, + metadata: Metadata, + session_type: SessionType, + ) -> Self { + let (proposer, controller) = match session_type { + SessionType::Proposer => ( + Proposer { + public_key: hex::encode(session_key.diffie_public_key()), + metadata, + }, + Controller::default(), + ), + SessionType::Controller => (Proposer::default(), Controller { + public_key: hex::encode(session_key.diffie_public_key()), + metadata, + }), + }; + + Self { + subscription_id, + session_key, + encoding_algo: Some(EncodingAlgo::new(&controller.metadata.name)), + controller, + namespaces: BTreeMap::new(), + proposer, + propose_namespaces: ProposeNamespaces::default(), + relay: ctx.relay.clone(), + expiry: Utc::now().timestamp() as u64 + FIVE_MINUTES, + pairing_topic, + session_type, + topic: session_topic, + session_properties: None, + active_chain_id: Default::default(), + } + } + + pub(crate) fn extend(&mut self, till: u64) { self.expiry = till; } + + /// Get the active chain ID for the current session. + pub fn get_active_chain_id(&self) -> &Option { &self.active_chain_id } + + /// Sets the active chain ID for the current session. + pub fn set_active_chain_id(&mut self, chain_id: WcChainId) { self.active_chain_id = Some(chain_id); } +} + +/// Internal implementation of session management. +struct SessionManagerImpl { + /// A thread-safe map of sessions indexed by topic. + sessions: Arc>>, + pub(crate) storage: SessionStorageDb, +} + +pub struct SessionManager(Arc); + +impl From for SessionRpcInfo { + fn from(value: Session) -> Self { + Self { + topic: value.topic, + metadata: value.controller.metadata, + pairing_topic: value.pairing_topic, + namespaces: value.namespaces, + expiry: value.expiry, + } + } +} + +#[allow(unused)] +impl SessionManager { + pub(crate) fn new(storage: SessionStorageDb) -> Self { + Self( + SessionManagerImpl { + sessions: Default::default(), + storage, + } + .into(), + ) + } + + pub(crate) fn read(&self) -> RwLockReadGuard> { + self.0.sessions.read().expect("read shouldn't fail") + } + + pub(crate) fn write(&self) -> RwLockWriteGuard> { + self.0.sessions.write().expect("read shouldn't fail") + } + + pub(crate) fn storage(&self) -> &SessionStorageDb { &self.0.storage } + + /// Inserts `Session` into the session store, associated with the specified topic. + /// If a session with the same topic already exists, it will be overwritten. + pub(crate) fn add_session(&self, session: Session) { + // insert session + self.write().insert(session.topic.clone(), session); + } + + /// Removes session corresponding to the specified topic from the session store. + /// If the session does not exist, this method does nothing. + pub(crate) fn delete_session(&self, topic: &Topic) -> Option { + info!("[{topic}] Deleting session with topic"); + // Remove the session and return the removed session (if any) + self.write().remove(topic) + } + + /// Retrieves a cloned session associated with a given topic. + pub fn get_session(&self, topic: &Topic) -> Option { self.read().get(topic).cloned() } + + /// Retrieves a cloned session associated with a given sessionn or pairing topic. + pub fn get_session_with_any_topic(&self, topic: &Topic, with_pairing_topic: bool) -> Option { + if with_pairing_topic { + return self.read().values().find(|s| &s.pairing_topic == topic).cloned(); + } + + self.read().get(topic).cloned() + } + + /// Retrieves all sessions(active and inactive) + pub fn get_sessions(&self) -> impl Iterator { + self.read().clone().into_values().map(|session| session.into()) + } + + /// Retrieves all active session topic with their controller. + pub(crate) fn get_sessions_topic_and_controller(&self) -> Vec<(Topic, Controller)> { + self.read() + .iter() + .map(|(topic, session)| (topic.clone(), session.controller.clone())) + .collect::>() + } + + /// Updates the expiry time of the session associated with the given topic to the specified timestamp. + /// If the session does not exist, this method does nothing. + pub(crate) fn extend_session(&self, topic: &Topic, till: u64) { + info!("[{topic}] Extending session with topic"); + if let Some(mut session) = self.write().get_mut(topic) { + session.extend(till); + } + } + + /// Retrieves the symmetric key associated with a given topic. + pub(crate) fn sym_key(&self, topic: &Topic) -> Option { + self.get_session(topic).map(|sess| sess.session_key.symmetric_key()) + } + + /// Check if a session exists. + pub(crate) fn validate_session_exists(&self, topic: &Topic) -> Result<(), MmError> { + if self.read().contains_key(topic) { + return Ok(()); + }; + + MmError::err(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_sample_key_info() -> KeyInfo { + KeyInfo { + chain_id: "test-chain".to_string(), + name: "Test Key".to_string(), + algo: "secp256k1".to_string(), + pub_key: "0123456789ABCDEF".to_string(), + address: "test_address".to_string(), + bech32_address: "bech32_test_address".to_string(), + ethereum_hex_address: "0xtest_eth_address".to_string(), + is_nano_ledger: false, + is_keystone: false, + } + } + + #[test] + fn test_deserialize_keys_from_string() { + let key_info = create_sample_key_info(); + let key_json = serde_json::to_string(&vec![key_info.clone()]).unwrap(); + let json = format!(r#"{{"keys": "{}"}}"#, key_json.replace('\"', "\\\"")); + let session: SessionProperties = serde_json::from_str(&json).unwrap(); + assert!(session.keys.is_some()); + assert_eq!(session.keys.unwrap(), vec![key_info]); + } + + #[test] + fn test_deserialize_keys_from_vec() { + let key_info = create_sample_key_info(); + let json = format!(r#"{{"keys": [{}]}}"#, serde_json::to_string(&key_info).unwrap()); + let session: SessionProperties = serde_json::from_str(&json).unwrap(); + assert!(session.keys.is_some()); + assert_eq!(session.keys.unwrap(), vec![key_info]); + } + + #[test] + fn test_deserialize_empty_keys() { + let json = r#"{"keys": []}"#; + let session: SessionProperties = serde_json::from_str(json).unwrap(); + assert_eq!(session.keys, Some(vec![])); + } + + #[test] + fn test_deserialize_no_keys() { + let json = r#"{}"#; + let session: SessionProperties = serde_json::from_str(json).unwrap(); + assert_eq!(session.keys, None); + } + + #[test] + fn test_serialize_deserialize_roundtrip() { + let key_info = create_sample_key_info(); + let original = SessionProperties { + keys: Some(vec![key_info]), + }; + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: SessionProperties = serde_json::from_str(&serialized).unwrap(); + assert_eq!(original, deserialized); + } +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/delete.rs b/mm2src/kdf_walletconnect/src/session/rpc/delete.rs new file mode 100644 index 0000000000..c88922575a --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/delete.rs @@ -0,0 +1,58 @@ +use crate::{error::{WalletConnectError, USER_REQUESTED}, + storage::WalletConnectStorageOps, + WalletConnectCtxImpl}; + +use common::log::debug; +use mm2_err_handle::prelude::{MapMmError, MmResult}; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::params::{session_delete::SessionDeleteRequest, RequestParams, ResponseParamsSuccess}; + +pub(crate) async fn reply_session_delete_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + _delete_params: SessionDeleteRequest, +) -> MmResult<(), WalletConnectError> { + let param = ResponseParamsSuccess::SessionDelete(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + session_delete_cleanup(ctx, topic).await +} + +pub(crate) async fn send_session_delete_request( + ctx: &WalletConnectCtxImpl, + session_topic: &Topic, +) -> MmResult<(), WalletConnectError> { + let delete_request = SessionDeleteRequest { + code: USER_REQUESTED, + message: "User Disconnected".to_owned(), + }; + let param = RequestParams::SessionDelete(delete_request); + + ctx.publish_request(session_topic, param).await?; + + session_delete_cleanup(ctx, session_topic).await +} + +async fn session_delete_cleanup(ctx: &WalletConnectCtxImpl, topic: &Topic) -> MmResult<(), WalletConnectError> { + ctx.client.unsubscribe(topic.clone()).await?; + + if let Some(session) = ctx.session_manager.delete_session(topic) { + debug!( + "[{}] No active sessions for pairing disconnecting", + session.pairing_topic + ); + //Attempt to unsubscribe from topic + ctx.client.unsubscribe(session.pairing_topic.clone()).await?; + // Attempt to delete/disconnect the pairing + ctx.pairing.delete(&session.pairing_topic); + // delete session from storage as well. + ctx.session_manager + .storage() + .delete_session(topic) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + }; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/event.rs b/mm2src/kdf_walletconnect/src/session/rpc/event.rs new file mode 100644 index 0000000000..62159e6b91 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/event.rs @@ -0,0 +1,73 @@ +use crate::{chain::{WcChain, WcChainId}, + error::{WalletConnectError, UNSUPPORTED_CHAINS}, + WalletConnectCtxImpl}; + +use common::log::{error, info}; +use mm2_err_handle::prelude::*; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::{params::{session_event::SessionEventRequest, ResponseParamsError}, + ErrorData}}; + +pub async fn handle_session_event( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + event: SessionEventRequest, +) -> MmResult<(), WalletConnectError> { + let chain_id = WcChainId::try_from_str(&event.chain_id)?; + let event_name = event.event.name.as_str(); + + match event_name { + "chainChanged" => { + let session = + ctx.session_manager + .get_session(topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))?; + + if WcChain::Eip155 != chain_id.chain { + return Ok(()); + }; + + ctx.validate_chain_id(&session, &chain_id)?; + + if session.get_active_chain_id().as_ref().map_or(false, |c| c == &chain_id) { + return Ok(()); + }; + + // check if if new chain_id is supported. + let new_id = serde_json::from_value::(event.event.data)?; + let new_chain = chain_id.chain.derive_chain_id(new_id.to_string()); + if let Err(err) = ctx.validate_chain_id(&session, &new_chain) { + error!("[{topic}] {err:?}"); + let error_data = ErrorData { + code: UNSUPPORTED_CHAINS, + message: "Unsupported chain id".to_string(), + data: None, + }; + let params = ResponseParamsError::SessionEvent(error_data); + ctx.publish_response_err(topic, params, message_id).await?; + } else { + { + ctx.session_manager + .write() + .get_mut(topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))? + .set_active_chain_id(chain_id); + } + }; + }, + "accountsChanged" => { + // TODO: Handle accountsChanged event logic. + }, + _ => { + // TODO: Handle other event logic., + }, + }; + + info!("[{topic}] {event_name} event handled successfully"); + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/extend.rs b/mm2src/kdf_walletconnect/src/session/rpc/extend.rs new file mode 100644 index 0000000000..0574277af1 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/extend.rs @@ -0,0 +1,20 @@ +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use mm2_err_handle::prelude::MmResult; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::params::{session_extend::SessionExtendRequest, ResponseParamsSuccess}}; + +/// Process session extend request. +pub(crate) async fn reply_session_extend_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + extend: SessionExtendRequest, +) -> MmResult<(), WalletConnectError> { + ctx.session_manager.extend_session(topic, extend.expiry); + + let param = ResponseParamsSuccess::SessionExtend(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/mod.rs b/mm2src/kdf_walletconnect/src/session/rpc/mod.rs new file mode 100644 index 0000000000..b94443c191 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/mod.rs @@ -0,0 +1,9 @@ +pub mod delete; +pub(crate) mod event; +pub(crate) mod extend; +pub mod ping; +pub(crate) mod propose; +pub(crate) mod settle; +pub(crate) mod update; + +pub use ping::*; diff --git a/mm2src/kdf_walletconnect/src/session/rpc/ping.rs b/mm2src/kdf_walletconnect/src/session/rpc/ping.rs new file mode 100644 index 0000000000..ad4423c7d5 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/ping.rs @@ -0,0 +1,30 @@ +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use common::custom_futures::timeout::FutureTimerExt; +use mm2_err_handle::prelude::*; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::params::{RequestParams, ResponseParamsSuccess}}; + +pub(crate) async fn reply_session_ping_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, +) -> MmResult<(), WalletConnectError> { + let param = ResponseParamsSuccess::SessionPing(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +pub async fn send_session_ping_request(ctx: &WalletConnectCtxImpl, topic: &Topic) -> MmResult<(), WalletConnectError> { + let param = RequestParams::SessionPing(()); + let (rx, ttl) = ctx.publish_request(topic, param).await?; + println!("ping sent successfuly"); + rx.timeout(ttl) + .await + .map_to_mm(|_| WalletConnectError::TimeoutError)? + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))??; + println!("ping sent successfuly"); + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/propose.rs b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs new file mode 100644 index 0000000000..d3626430ce --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs @@ -0,0 +1,152 @@ +use super::settle::send_session_settle_request; +use crate::storage::WalletConnectStorageOps; +use crate::{error::WalletConnectError, + metadata::generate_metadata, + session::{Session, SessionKey, SessionType, THIRTY_DAYS}, + WalletConnectCtxImpl}; + +use chrono::Utc; +use mm2_err_handle::map_to_mm::MapToMmResult; +use mm2_err_handle::prelude::*; +use relay_rpc::rpc::params::session::ProposeNamespaces; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::params::{session_propose::{Proposer, SessionProposeRequest, SessionProposeResponse}, + RequestParams, ResponseParamsSuccess}}; + +/// Creates a new session proposal from topic and metadata. +pub(crate) async fn send_proposal_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + required_namespaces: ProposeNamespaces, + optional_namespaces: ProposeNamespaces, +) -> MmResult<(), WalletConnectError> { + let proposer = Proposer { + metadata: ctx.metadata.clone(), + public_key: hex::encode(ctx.key_pair.public_key.as_bytes()), + }; + let session_proposal = RequestParams::SessionPropose(SessionProposeRequest { + relays: vec![ctx.relay.clone()], + proposer, + required_namespaces, + optional_namespaces: Some(optional_namespaces), + }); + let _ = ctx.publish_request(topic, session_proposal).await?; + + Ok(()) +} + +/// Process session proposal request +/// https://specs.walletconnect.com/2.0/specs/clients/sign/session-proposal +pub async fn reply_session_proposal_request( + ctx: &WalletConnectCtxImpl, + proposal: SessionProposeRequest, + topic: &Topic, + message_id: &MessageId, +) -> MmResult<(), WalletConnectError> { + let session = { + let sender_public_key = hex::decode(&proposal.proposer.public_key)? + .as_slice() + .try_into() + .map_to_mm(|_| WalletConnectError::InternalError("Invalid sender_public_key".to_owned()))?; + let session_key = SessionKey::from_osrng(&sender_public_key)?; + let session_topic: Topic = session_key.generate_topic().into(); + let subscription_id = ctx + .client + .subscribe(session_topic.clone()) + .await + .map_to_mm(|err| WalletConnectError::SubscriptionError(err.to_string()))?; + + Session::new( + ctx, + session_topic.clone(), + subscription_id, + session_key, + topic.clone(), + proposal.proposer.metadata, + SessionType::Controller, + ) + }; + session + .propose_namespaces + .supported(&proposal.required_namespaces) + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))?; + + { + // save session to storage + ctx.session_manager + .storage() + .save_session(&session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + // Add session to session lists + ctx.session_manager.add_session(session.clone()); + } + + send_session_settle_request(ctx, &session).await?; + + // Respond to incoming session propose. + let param = ResponseParamsSuccess::SessionPropose(SessionProposeResponse { + relay: ctx.relay.clone(), + responder_public_key: proposal.proposer.public_key, + }); + + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +/// Process session propose reponse. +pub(crate) async fn process_session_propose_response( + ctx: &WalletConnectCtxImpl, + pairing_topic: &Topic, + response: &SessionProposeResponse, +) -> MmResult<(), WalletConnectError> { + let session_key = { + let other_public_key = hex::decode(&response.responder_public_key)? + .as_slice() + .try_into() + .unwrap(); + let mut session_key = SessionKey::new(ctx.key_pair.public_key); + session_key.generate_symmetric_key(&ctx.key_pair.secret, &other_public_key)?; + session_key + }; + + let session = { + let session_topic: Topic = session_key.generate_topic().into(); + let subscription_id = ctx + .client + .subscribe(session_topic.clone()) + .await + .map_to_mm(|err| WalletConnectError::SubscriptionError(err.to_string()))?; + + let mut session = Session::new( + ctx, + session_topic.clone(), + subscription_id, + session_key, + pairing_topic.clone(), + generate_metadata(), + SessionType::Proposer, + ); + session.relay = response.relay.clone(); + session.expiry = Utc::now().timestamp() as u64 + THIRTY_DAYS; + session.controller.public_key = response.responder_public_key.clone(); + session + }; + + // save session to storage + ctx.session_manager + .storage() + .save_session(&session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + // Add session to session lists + ctx.session_manager.add_session(session.clone()); + + // Activate pairing_topic + ctx.pairing.activate(pairing_topic)?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/settle.rs b/mm2src/kdf_walletconnect/src/session/rpc/settle.rs new file mode 100644 index 0000000000..2c03131e74 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/settle.rs @@ -0,0 +1,83 @@ +use crate::session::{EncodingAlgo, Session, SessionProperties}; +use crate::storage::WalletConnectStorageOps; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use common::log::{debug, info}; +use mm2_err_handle::prelude::{MapMmError, MmError, MmResult}; +use relay_rpc::domain::Topic; +use relay_rpc::rpc::params::session_settle::SessionSettleRequest; + +/// TODO: Finish when implementing KDF as a Wallet. +pub(crate) async fn send_session_settle_request( + _ctx: &WalletConnectCtxImpl, + _session_info: &Session, +) -> MmResult<(), WalletConnectError> { + // let mut settled_namespaces = BTreeMap::::new(); + // let nam + // settled_namespaces.insert("eip155".to_string(), Namespace { + // chains: Some(SUPPORTED_CHAINS.iter().map(|c| c.to_string()).collect()), + // methods: SUPPORTED_METHODS.iter().map(|m| m.to_string()).collect(), + // events: SUPPORTED_EVENTS.iter().map(|e| e.to_string()).collect(), + // accounts: None, + // }); + // + // let request = RequestParams::SessionSettle(SessionSettleRequest { + // relay: session_info.relay.clone(), + // controller: session_info.controller.clone(), + // namespaces: SettleNamespaces(settled_namespaces), + // expiry: Utc::now().timestamp() as u64 + THIRTY_DAYS, + // session_properties: None, + // }); + // + // ctx.publish_request(&session_info.topic, request).await?; + + Ok(()) +} + +/// Process session settle request. +pub(crate) async fn reply_session_settle_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + settle: SessionSettleRequest, +) -> MmResult<(), WalletConnectError> { + let current_session = { + let mut sessions = ctx.session_manager.write(); + let Some(session) = sessions.get_mut(topic) else { + return MmError::err(WalletConnectError::SessionError(format!( + "No session found for topic: {topic}" + ))); + }; + if let Some(value) = settle.session_properties { + let session_properties = serde_json::from_value::(value)?; + session.session_properties = Some(session_properties); + }; + session.encoding_algo = Some(EncodingAlgo::new(&settle.controller.metadata.name)); + session.namespaces = settle.namespaces.0; + session.controller = settle.controller; + session.relay = settle.relay; + session.expiry = settle.expiry; + + session.clone() + }; + + // Update storage session. + ctx.session_manager + .storage() + .update_session(¤t_session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + // Delete other sessions with same controller + let sessions = ctx.session_manager.get_sessions_topic_and_controller(); + for (topic, _) in sessions + .into_iter() + .filter(|(topic, controller)| controller == ¤t_session.controller && topic != ¤t_session.topic) + { + ctx.drop_session(&topic).await?; + debug!("[{topic}] session deleted"); + } + + info!("[{topic}] Session successfully settled for topic"); + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/update.rs b/mm2src/kdf_walletconnect/src/session/rpc/update.rs new file mode 100644 index 0000000000..e15793fe76 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/update.rs @@ -0,0 +1,48 @@ +use crate::storage::WalletConnectStorageOps; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use common::log::info; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::params::{session_update::SessionUpdateRequest, ResponseParamsSuccess}; + +pub(crate) async fn reply_session_update_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + update: SessionUpdateRequest, +) -> MmResult<(), WalletConnectError> { + { + let mut session = ctx.session_manager.write(); + let Some(session) = session.get_mut(topic) else { + return MmError::err(WalletConnectError::SessionError(format!( + "No session found for topic: {topic}" + ))); + }; + update + .namespaces + .caip2_validate() + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))?; + session.namespaces = update.namespaces.0; + let session = session; + info!("Updated extended, info: {:?}", session.topic); + } + + // Update storage session. + let session = ctx + .session_manager + .get_session(topic) + .ok_or(MmError::new(WalletConnectError::SessionError(format!( + "session not foun topic: {topic}" + ))))?; + ctx.session_manager + .storage() + .update_session(&session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + let param = ResponseParamsSuccess::SessionUpdate(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/storage/indexed_db.rs b/mm2src/kdf_walletconnect/src/storage/indexed_db.rs new file mode 100644 index 0000000000..ac9205600f --- /dev/null +++ b/mm2src/kdf_walletconnect/src/storage/indexed_db.rs @@ -0,0 +1,129 @@ +use super::WalletConnectStorageOps; +use crate::error::WcIndexedDbError; +use crate::session::Session; +use async_trait::async_trait; +use common::log::debug; +use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::{ConstructibleDb, DbIdentifier, DbInstance, DbLocked, DbUpgrader, IndexedDb, IndexedDbBuilder, + InitDbResult, OnUpgradeResult, SharedDb, TableSignature}; +use mm2_err_handle::prelude::MmResult; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::Topic; + +const DB_VERSION: u32 = 1; + +pub type IDBSessionStorageLocked<'a> = DbLocked<'a, IDBSessionStorageInner>; + +impl TableSignature for Session { + const TABLE_NAME: &'static str = "sessions"; + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_index("topic", false)?; + } + Ok(()) + } +} + +pub struct IDBSessionStorageInner(IndexedDb); + +#[async_trait] +impl DbInstance for IDBSessionStorageInner { + const DB_NAME: &'static str = "wc_session_storage"; + + async fn init(db_id: DbIdentifier) -> InitDbResult { + let inner = IndexedDbBuilder::new(db_id) + .with_version(DB_VERSION) + .with_table::() + .build() + .await?; + + Ok(Self(inner)) + } +} + +impl IDBSessionStorageInner { + pub(crate) fn get_inner(&self) -> &IndexedDb { &self.0 } +} + +#[derive(Clone)] +pub struct IDBSessionStorage(SharedDb); + +impl IDBSessionStorage { + pub(crate) fn new(ctx: &MmArc) -> MmResult { + Ok(Self(ConstructibleDb::new(ctx).into_shared())) + } + + async fn lock_db(&self) -> MmResult, WcIndexedDbError> { + self.0 + .get_or_initialize() + .await + .mm_err(|err| WcIndexedDbError::InternalError(err.to_string())) + } +} + +#[async_trait::async_trait] +impl WalletConnectStorageOps for IDBSessionStorage { + type Error = WcIndexedDbError; + + async fn init(&self) -> MmResult<(), Self::Error> { + debug!("Initializing WalletConnect session storage"); + Ok(()) + } + + async fn is_initialized(&self) -> MmResult { Ok(true) } + + async fn save_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Saving WalletConnect session to storage", session.topic); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + session_table + .replace_item_by_unique_index("topic", session.topic.clone(), session) + .await?; + + Ok(()) + } + + async fn get_session(&self, topic: &Topic) -> MmResult, Self::Error> { + debug!("[{topic}] Retrieving WalletConnect session from storage"); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + + Ok(session_table + .get_item_by_unique_index("topic", topic) + .await? + .map(|s| s.1)) + } + + async fn get_all_sessions(&self) -> MmResult, Self::Error> { + debug!("Loading WalletConnect sessions from storage"); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + + Ok(session_table + .get_all_items() + .await? + .into_iter() + .map(|s| s.1) + .collect::>()) + } + + async fn delete_session(&self, topic: &Topic) -> MmResult<(), Self::Error> { + debug!("[{topic}] Deleting WalletConnect session from storage"); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + + session_table.delete_item_by_unique_index("topic", topic).await?; + Ok(()) + } + + async fn update_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Updating WalletConnect session in storage", session.topic); + self.save_session(session).await + } +} diff --git a/mm2src/kdf_walletconnect/src/storage/mod.rs b/mm2src/kdf_walletconnect/src/storage/mod.rs new file mode 100644 index 0000000000..088e052e2f --- /dev/null +++ b/mm2src/kdf_walletconnect/src/storage/mod.rs @@ -0,0 +1,202 @@ +use std::ops::Deref; + +use async_trait::async_trait; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmResult; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::Topic; + +use crate::{error::WalletConnectError, session::Session}; + +#[cfg(target_arch = "wasm32")] pub(crate) mod indexed_db; +#[cfg(not(target_arch = "wasm32"))] pub(crate) mod sqlite; + +#[async_trait] +pub(crate) trait WalletConnectStorageOps { + type Error: std::fmt::Debug + NotMmError + NotEqual + Send; + + async fn init(&self) -> MmResult<(), Self::Error>; + async fn is_initialized(&self) -> MmResult; + async fn save_session(&self, session: &Session) -> MmResult<(), Self::Error>; + async fn get_session(&self, topic: &Topic) -> MmResult, Self::Error>; + async fn get_all_sessions(&self) -> MmResult, Self::Error>; + async fn delete_session(&self, topic: &Topic) -> MmResult<(), Self::Error>; + async fn update_session(&self, session: &Session) -> MmResult<(), Self::Error>; +} + +#[cfg(target_arch = "wasm32")] +type DB = indexed_db::IDBSessionStorage; +#[cfg(not(target_arch = "wasm32"))] +type DB = sqlite::SqliteSessionStorage; + +pub(crate) struct SessionStorageDb(DB); + +impl Deref for SessionStorageDb { + type Target = DB; + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl SessionStorageDb { + pub(crate) fn new(ctx: &MmArc) -> MmResult { + let db = DB::new(ctx).mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + Ok(SessionStorageDb(db)) + } +} + +#[cfg(test)] +pub(crate) mod session_storage_tests { + common::cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + use common::cross_test; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_async_db; + use relay_rpc::{domain::SubscriptionId, rpc::params::Metadata}; + + use crate::{session::key::SessionKey, + session::{Session, SessionType}, + WalletConnectCtx}; + + use super::WalletConnectStorageOps; + + fn sample_test_session(wc_ctx: &WalletConnectCtx) -> Session { + let session_key = SessionKey { + sym_key: [ + 115, 159, 247, 31, 199, 84, 88, 59, 158, 252, 98, 225, 51, 125, 201, 239, 142, 34, 9, 201, 128, 114, + 144, 166, 102, 131, 87, 191, 33, 24, 153, 7, + ], + public_key: [ + 115, 159, 247, 31, 199, 84, 88, 59, 158, 252, 98, 225, 51, 125, 201, 239, 142, 34, 9, 201, 128, 114, + 144, 166, 102, 131, 87, 191, 33, 24, 153, 7, + ], + }; + + Session::new( + wc_ctx, + "bb89e3bae8cb89e5549f4d9bcc5a1ac2aae6dd90ef37eb2f59d80c5773f36343".into(), + SubscriptionId::generate(), + session_key, + "5af44bdf8d6b11f4635c964a15e9e2d50942534824791757b2c26528e8feef39".into(), + Metadata::default(), + SessionType::Controller, + ) + } + + cross_test!(save_and_get_session_test, { + let mm_ctx = mm_ctx_with_custom_async_db().await; + let wc_ctx = WalletConnectCtx::try_init(&mm_ctx).unwrap(); + wc_ctx.session_manager.storage().init().await.unwrap(); + + let sample_session = sample_test_session(&wc_ctx); + + // try save session + wc_ctx + .session_manager + .storage() + .save_session(&sample_session) + .await + .unwrap(); + + // try get session + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap(); + assert_eq!(sample_session, db_session.unwrap()); + }); + + cross_test!(delete_session_test, { + let mm_ctx = mm_ctx_with_custom_async_db().await; + let wc_ctx = WalletConnectCtx::try_init(&mm_ctx).unwrap(); + wc_ctx.session_manager.storage().init().await.unwrap(); + + let sample_session = sample_test_session(&wc_ctx); + + // try save session + wc_ctx + .session_manager + .storage() + .save_session(&sample_session) + .await + .unwrap(); + + // try get session + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap() + .unwrap(); + assert_eq!(sample_session, db_session); + + // try delete session + wc_ctx + .session_manager + .storage() + .delete_session(&db_session.topic) + .await + .unwrap(); + + // try get_session deleted again + let db_session = wc_ctx.session_manager.storage().get_session(&db_session.topic).await; + assert!(db_session.is_err()); + }); + + cross_test!(update_session_test, { + let mm_ctx = mm_ctx_with_custom_async_db().await; + let wc_ctx = WalletConnectCtx::try_init(&mm_ctx).unwrap(); + wc_ctx.session_manager.storage().init().await.unwrap(); + + let sample_session = sample_test_session(&wc_ctx); + + // try save session + wc_ctx + .session_manager + .storage() + .save_session(&sample_session) + .await + .unwrap(); + + // try get session + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap() + .unwrap(); + assert_eq!(sample_session, db_session); + + // modify sample_session + let mut modified_sample_session = sample_session.clone(); + modified_sample_session.expiry = 100; + + // assert that original session expiry isn't the same as our new expiry. + assert_ne!(sample_session.expiry, modified_sample_session.expiry); + + // try update session + wc_ctx + .session_manager + .storage() + .update_session(&modified_sample_session) + .await + .unwrap(); + + // try get_session again with new updated expiry + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap() + .unwrap(); + assert_ne!(sample_session.expiry, db_session.expiry); + + assert_eq!(modified_sample_session, db_session); + assert_eq!(100, db_session.expiry); + }); +} diff --git a/mm2src/kdf_walletconnect/src/storage/sqlite.rs b/mm2src/kdf_walletconnect/src/storage/sqlite.rs new file mode 100644 index 0000000000..40bfcf9431 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/storage/sqlite.rs @@ -0,0 +1,176 @@ +use async_trait::async_trait; +use common::log::debug; +use db_common::async_sql_conn::InternalError; +use db_common::sqlite::rusqlite::Result as SqlResult; +use db_common::sqlite::{query_single_row, string_from_row, CHECK_TABLE_EXISTS_SQL}; +use db_common::{async_sql_conn::{AsyncConnError, AsyncConnection}, + sqlite::validate_table_name}; +use futures::lock::{Mutex, MutexGuard}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::Topic; +use std::sync::Arc; + +use super::WalletConnectStorageOps; +use crate::session::Session; + +const SESSION_TABLE_NAME: &str = "wc_session"; + +/// Sessions table +fn create_sessions_table() -> SqlResult { + validate_table_name(SESSION_TABLE_NAME)?; + Ok(format!( + "CREATE TABLE IF NOT EXISTS {SESSION_TABLE_NAME} ( + topic char(32) PRIMARY KEY, + data TEXT NOT NULL, + expiry BIGINT NOT NULL + );" + )) +} + +#[derive(Clone, Debug)] +pub(crate) struct SqliteSessionStorage { + pub conn: Arc>, +} + +impl SqliteSessionStorage { + pub(crate) fn new(ctx: &MmArc) -> MmResult { + let conn = ctx + .async_sqlite_connection + .get() + .ok_or(AsyncConnError::Internal(InternalError( + "async_sqlite_connection is not initialized".to_owned(), + )))?; + + Ok(Self { conn: conn.clone() }) + } + + pub(crate) async fn lock_db(&self) -> MutexGuard<'_, AsyncConnection> { self.conn.lock().await } +} + +#[async_trait] +impl WalletConnectStorageOps for SqliteSessionStorage { + type Error = AsyncConnError; + + async fn init(&self) -> MmResult<(), Self::Error> { + debug!("Initializing WalletConnect session storage"); + let lock = self.lock_db().await; + lock.call(move |conn| { + conn.execute(&create_sessions_table()?, []).map(|_| ())?; + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn is_initialized(&self) -> MmResult { + let lock = self.lock_db().await; + validate_table_name(SESSION_TABLE_NAME).map_err(AsyncConnError::from)?; + lock.call(move |conn| { + let initialized = query_single_row(conn, CHECK_TABLE_EXISTS_SQL, [SESSION_TABLE_NAME], string_from_row)?; + Ok(initialized.is_some()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn save_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Saving WalletConnect session to storage", session.topic); + let lock = self.lock_db().await; + + let session = session.clone(); + lock.call(move |conn| { + let sql = format!( + "INSERT INTO {} (topic, data, expiry) VALUES (?1, ?2, ?3);", + SESSION_TABLE_NAME + ); + let transaction = conn.transaction()?; + + let session_data = serde_json::to_string(&session).map_err(|err| AsyncConnError::from(err.to_string()))?; + + let params = [session.topic.to_string(), session_data, session.expiry.to_string()]; + + transaction.execute(&sql, params)?; + transaction.commit()?; + + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn get_session(&self, topic: &Topic) -> MmResult, Self::Error> { + debug!("[{topic}] Retrieving WalletConnect session from storage"); + let lock = self.lock_db().await; + let topic = topic.clone(); + let session_str = lock + .call(move |conn| { + let sql = format!("SELECT topic, data, expiry FROM {} WHERE topic=?1;", SESSION_TABLE_NAME); + let mut stmt = conn.prepare(&sql)?; + let session: String = stmt.query_row([topic.to_string()], |row| row.get::<_, String>(1))?; + Ok(session) + }) + .await + .map_to_mm(AsyncConnError::from)?; + + let session = serde_json::from_str(&session_str).map_to_mm(|err| AsyncConnError::from(err.to_string()))?; + Ok(session) + } + + async fn get_all_sessions(&self) -> MmResult, Self::Error> { + debug!("Loading WalletConnect sessions from storage"); + let lock = self.lock_db().await; + let sessions_str = lock + .call(move |conn| { + let sql = format!("SELECT topic, data, expiry FROM {};", SESSION_TABLE_NAME); + let mut stmt = conn.prepare(&sql)?; + let sessions = stmt.query_map([], |row| row.get::<_, String>(1))?.collect::>(); + Ok(sessions) + }) + .await + .map_to_mm(AsyncConnError::from)?; + + let mut sessions = Vec::with_capacity(sessions_str.len()); + for session in sessions_str { + let session = serde_json::from_str(&session.map_to_mm(AsyncConnError::from)?) + .map_to_mm(|err| AsyncConnError::from(err.to_string()))?; + sessions.push(session); + } + + Ok(sessions) + } + + async fn delete_session(&self, topic: &Topic) -> MmResult<(), Self::Error> { + debug!("[{topic}] Deleting WalletConnect session from storage"); + let topic = topic.clone(); + let lock = self.lock_db().await; + lock.call(move |conn| { + let sql = format!("DELETE FROM {} WHERE topic = ?1", SESSION_TABLE_NAME); + let mut stmt = conn.prepare(&sql)?; + let _ = stmt.execute([topic.to_string()])?; + + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn update_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Updating WalletConnect session in storage", session.topic); + let session = session.clone(); + let lock = self.lock_db().await; + lock.call(move |conn| { + let sql = format!( + "UPDATE {} SET data = ?1, expiry = ?2 WHERE topic = ?3", + SESSION_TABLE_NAME + ); + let session_data = serde_json::to_string(&session).map_err(|err| AsyncConnError::from(err.to_string()))?; + let params = [session_data, session.expiry.to_string(), session.topic.to_string()]; + let _row = conn.prepare(&sql)?.execute(params)?; + + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } +} diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index d0d6bfb87c..79da897fc4 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -162,6 +162,7 @@ pub struct MmCtx { pub async_sqlite_connection: OnceLock>>, /// Links the RPC context to the P2P context to handle health check responses. pub healthcheck_response_handler: AsyncMutex>>, + pub wallet_connect: Mutex>>, } impl MmCtx { @@ -220,6 +221,7 @@ impl MmCtx { healthcheck_response_handler: AsyncMutex::new( TimedMap::new_with_map_kind(MapKind::FxHashMap).expiration_tick_cap(3), ), + wallet_connect: Mutex::new(None), } } @@ -325,7 +327,7 @@ impl MmCtx { #[cfg(not(target_arch = "wasm32"))] pub fn db_root(&self) -> PathBuf { path_to_db_root(self.conf["dbdir"].as_str()) } - /// MM database path. + /// MM database path. /// Defaults to a relative "DB". /// /// Can be changed via the "dbdir" configuration field, for example: @@ -744,7 +746,7 @@ impl MmArc { } } - /// Tries getting access to the MM context. + /// Tries getting access to the MM context. /// Fails if an invalid MM context handler is passed (no such context or dropped context). #[track_caller] pub fn from_ffi_handle(ffi_handle: u32) -> Result { diff --git a/mm2src/mm2_io/src/fs.rs b/mm2src/mm2_io/src/fs.rs index 739e1c950d..e24ee04284 100644 --- a/mm2src/mm2_io/src/fs.rs +++ b/mm2src/mm2_io/src/fs.rs @@ -275,11 +275,10 @@ where pub fn create_parents(path: &impl AsRef) -> IoResult<()> { let parent_dir = path.as_ref().parent(); let Some(parent_dir) = parent_dir else { - return MmError::err( - io::Error::new( - io::ErrorKind::InvalidInput, - format!("{} has no parent directory", path.as_ref().display()), - )) + return MmError::err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("{} has no parent directory", path.as_ref().display()), + )); }; match fs::metadata(parent_dir) { // Path exists, make sure it's a directory (and not a file for example). @@ -303,11 +302,10 @@ pub fn create_parents(path: &impl AsRef) -> IoResult<()> { pub async fn create_parents_async(path: &Path) -> IoResult<()> { let parent_dir = path.parent(); let Some(parent_dir) = parent_dir else { - return MmError::err( - io::Error::new( - io::ErrorKind::InvalidInput, - format!("{} has no parent directory", path.display()), - )) + return MmError::err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("{} has no parent directory", path.display()), + )); }; match async_fs::metadata(parent_dir).await { // Path exists, make sure it's a directory (and not a file, for instance). diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index ad0109793c..560156e148 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -58,6 +58,7 @@ hex.workspace = true http.workspace = true hw_common = { path = "../hw_common" } itertools.workspace = true +kdf_walletconnect = { path = "../kdf_walletconnect" } keys = { path = "../mm2_bitcoin/keys" } lazy_static.workspace = true # ledger = { path = "../ledger" } @@ -71,7 +72,7 @@ mm2_libp2p = { path = "../mm2_p2p", package = "mm2_p2p" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net"} mm2_number = { path = "../mm2_number" } -mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"]} +mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"] } mm2_state_machine = { path = "../mm2_state_machine" } trading_api = { path = "../trading_api" } num-traits.workspace = true diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index a24a75d8ce..65f13ca732 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -35,6 +35,7 @@ use common::log::{info, warn}; use crypto::{from_hw_error, CryptoCtx, HwError, HwProcessingError, HwRpcError, WithHwRpcError}; use derive_more::Display; use enum_derives::EnumFromTrait; +use kdf_walletconnect::WalletConnectCtx; use mm2_core::mm_ctx::{MmArc, MmCtx}; use mm2_err_handle::common_errors::InternalError; use mm2_err_handle::prelude::*; @@ -418,6 +419,10 @@ pub async fn lp_init_continue(ctx: MmArc) -> MmInitResult<()> { #[cfg(target_arch = "wasm32")] init_wasm_event_streaming(&ctx); + // This function spawns related WalletConnect related tasks needed for initialization before + // WalletConnect can be usable in KDF. + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| MmInitError::WalletInitError(err.to_string()))?; + ctx.spawner().spawn(clean_memory_loop(ctx.weak())); Ok(()) diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index d3df2042db..4e6a30402c 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -47,6 +47,7 @@ mod dispatcher_legacy; pub mod lp_commands; mod rate_limiter; mod streaming_activations; +pub mod wc_commands; /// Lists the RPC method not requiring the "userpass" authentication. /// None is also public to skip auth and display proper error in case of method is missing diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index bc5fb414ee..924b87f387 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -1,4 +1,5 @@ use super::streaming_activations; +use super::wc_commands::{disconnect_session, get_all_sessions, get_session}; use super::{DispatcherError, DispatcherResult, PUBLIC_METHODS}; use crate::lp_healthcheck::peer_connection_healthcheck_rpc; use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor_status, init_trezor_user_action}; @@ -22,6 +23,8 @@ use crate::rpc::lp_commands::tokens::get_token_info; use crate::rpc::lp_commands::tokens::{approve_token_rpc, get_token_allowance_rpc}; use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; +use crate::rpc::wc_commands::{new_connection, ping_session}; + use coins::eth::fee_estimation::rpc::get_eth_estimated_fee_per_gas; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; @@ -257,6 +260,11 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_tokens_rpc).await, + "wc_new_connection" => handle_mmrpc(ctx, request, new_connection).await, + "wc_get_session" => handle_mmrpc(ctx, request, get_session).await, + "wc_get_sessions" => handle_mmrpc(ctx, request, get_all_sessions).await, + "wc_delete_session" => handle_mmrpc(ctx, request, disconnect_session).await, + "wc_ping_session" => handle_mmrpc(ctx, request, ping_session).await, _ => MmError::err(DispatcherError::NoSuchMethod), } } diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs index 3a2773c8c1..978a1d80b5 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs @@ -232,7 +232,7 @@ mod tests { ], "swap_contract_address": "0xeA6D65434A15377081495a9E7C5893543E7c32cB", "erc20_tokens_requests": [{"ticker": ticker_token}], - "priv_key_policy": "ContextPrivKey" + "priv_key_policy": { "type": "ContextPrivKey" } })) .unwrap(), )) diff --git a/mm2src/mm2_main/src/rpc/wc_commands/mod.rs b/mm2src/mm2_main/src/rpc/wc_commands/mod.rs new file mode 100644 index 0000000000..8e39dac169 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/wc_commands/mod.rs @@ -0,0 +1,34 @@ +mod new_connection; +mod sessions; + +use common::HttpStatusCode; +use derive_more::Display; +use http::StatusCode; +pub use new_connection::new_connection; +use serde::Deserialize; +pub use sessions::*; + +#[derive(Deserialize)] +pub struct EmptyRpcRequest {} + +#[derive(Debug, Serialize)] +pub struct EmptyRpcResponse {} + +#[derive(Serialize, Display, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum WalletConnectRpcError { + InternalError(String), + InitializationError(String), + SessionRequestError(String), +} + +impl HttpStatusCode for WalletConnectRpcError { + fn status_code(&self) -> StatusCode { + match self { + WalletConnectRpcError::InitializationError(_) => StatusCode::BAD_REQUEST, + WalletConnectRpcError::SessionRequestError(_) | WalletConnectRpcError::InternalError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + }, + } + } +} diff --git a/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs b/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs new file mode 100644 index 0000000000..7b95d705fc --- /dev/null +++ b/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs @@ -0,0 +1,32 @@ +use kdf_walletconnect::WalletConnectCtx; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use serde::{Deserialize, Serialize}; + +use super::WalletConnectRpcError; + +#[derive(Debug, PartialEq, Serialize)] +pub struct CreateConnectionResponse { + pub url: String, +} + +#[derive(Deserialize)] +pub struct NewConnectionRequest { + required_namespaces: serde_json::Value, + optional_namespaces: Option, +} + +/// `new_connection` RPC command implementation. +pub async fn new_connection( + ctx: MmArc, + req: NewConnectionRequest, +) -> MmResult { + let wc_ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + let url = wc_ctx + .new_connection(req.required_namespaces, req.optional_namespaces) + .await + .mm_err(|err| WalletConnectRpcError::SessionRequestError(err.to_string()))?; + + Ok(CreateConnectionResponse { url }) +} diff --git a/mm2src/mm2_main/src/rpc/wc_commands/sessions.rs b/mm2src/mm2_main/src/rpc/wc_commands/sessions.rs new file mode 100644 index 0000000000..3f8c457d63 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/wc_commands/sessions.rs @@ -0,0 +1,86 @@ +use kdf_walletconnect::session::rpc::send_session_ping_request; +use kdf_walletconnect::session::SessionRpcInfo; +use kdf_walletconnect::WalletConnectCtx; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use serde::Serialize; + +use super::{EmptyRpcRequest, EmptyRpcResponse, WalletConnectRpcError}; + +#[derive(Debug, PartialEq, Serialize)] +pub struct SessionResponse { + pub result: String, +} + +#[derive(Debug, PartialEq, Serialize)] +pub struct GetSessionsResponse { + pub sessions: Vec, +} + +/// `Get all sessions connection` RPC command implementation. +pub async fn get_all_sessions( + ctx: MmArc, + _req: EmptyRpcRequest, +) -> MmResult { + let wc_ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + let sessions = wc_ctx + .session_manager + .get_sessions() + .map(SessionRpcInfo::from) + .collect::>(); + + Ok(GetSessionsResponse { sessions }) +} + +#[derive(Debug, Serialize)] +pub struct GetSessionResponse { + pub session: Option, +} + +#[derive(Deserialize)] +pub struct GetSessionRequest { + topic: String, + #[serde(default)] + with_pairing_topic: bool, +} + +/// `Get session connection` RPC command implementation. +pub async fn get_session(ctx: MmArc, req: GetSessionRequest) -> MmResult { + let wc_ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + let session = wc_ctx + .session_manager + .get_session_with_any_topic(&req.topic.into(), req.with_pairing_topic) + .map(SessionRpcInfo::from); + + Ok(GetSessionResponse { session }) +} + +/// `Delete session connection` RPC command implementation. +pub async fn disconnect_session( + ctx: MmArc, + req: GetSessionRequest, +) -> MmResult { + let wc_ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + wc_ctx + .drop_session(&req.topic.into()) + .await + .mm_err(|err| WalletConnectRpcError::SessionRequestError(err.to_string()))?; + + Ok(EmptyRpcResponse {}) +} + +/// `ping session` RPC command implementation. +pub async fn ping_session(ctx: MmArc, req: GetSessionRequest) -> MmResult { + let wc_ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + send_session_ping_request(&wc_ctx, &req.topic.into()) + .await + .mm_err(|err| WalletConnectRpcError::SessionRequestError(err.to_string()))?; + + Ok(SessionResponse { + result: "Ping successful".to_owned(), + }) +} diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 276ab5f9f7..7dfba3395f 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -5536,7 +5536,7 @@ fn test_sign_verify_message_eth_with_derivation_path() { "mmrpc": "2.0", "params": { "ticker": "ETH", - "priv_key_policy": "ContextPrivKey", + "priv_key_policy": { "type": "ContextPrivKey" }, "mm2": 1, "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, "nodes": ETH_SEPOLIA_NODES.iter().map(|node| json!({ "url": node})).collect::>(), @@ -6655,7 +6655,7 @@ mod trezor_tests { "coin": "ETH", "urls": ETH_SEPOLIA_NODES, "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, - "priv_key_policy": "Trezor", + "priv_key_policy": { "type": "Trezor" }, }); let mut eth_conf = eth_sepolia_trezor_firmware_compat_conf(); @@ -6704,7 +6704,7 @@ mod trezor_tests { "method": "electrum", "coin": ticker, "servers": tbtc_electrums(), - "priv_key_policy": "Trezor", + "priv_key_policy": { "type": "Trezor" }, }); let activation_params = UtxoActivationParams::from_legacy_req(&enable_req).unwrap(); let request: InitStandaloneCoinReq = json::from_value(json!({ @@ -6908,7 +6908,7 @@ mod trezor_tests { ], "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, "erc20_tokens_requests": [{"ticker": ticker_token}], - "priv_key_policy": "Trezor" + "priv_key_policy": { "type": "Trezor" } })) .unwrap(), )) @@ -7026,7 +7026,7 @@ mod trezor_tests { ], "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, "erc20_tokens_requests": [], - "priv_key_policy": "Trezor" + "priv_key_policy": { "type": "Trezor" } })) .unwrap(), )) diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index f47fcde661..5d7c1dad6b 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -1191,6 +1191,9 @@ pub async fn mm_ctx_with_custom_async_db() -> MmArc { ctx } +#[cfg(target_arch = "wasm32")] +pub async fn mm_ctx_with_custom_async_db() -> MmArc { MmCtxBuilder::new().with_test_db_namespace().into_mm_arc() } + /// Automatically kill a wrapped process. pub struct RaiiKill { pub handle: Child, From 00704989616ab12dad0ab74878953bc5c4574282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 9 Jun 2025 11:30:28 +0300 Subject: [PATCH 21/36] improvement(tendermint): `tendermint_tx_internal_id` helper (#2438) * remove tendermint logics from `my_tx_history_v2` and create common function Signed-off-by: onur-ozkan * use tx hash instead of hex Signed-off-by: onur-ozkan --------- Signed-off-by: onur-ozkan --- mm2src/coins/my_tx_history_v2.rs | 14 ++++------ mm2src/coins/tendermint/tendermint_coin.rs | 29 +++++++++------------ mm2src/coins/tendermint/tendermint_token.rs | 6 ++--- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index d35de25ddc..9bd5877ff5 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -187,6 +187,9 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T self.from_addresses.insert(address); } + /// TODO: This implementation is messy. We should do all the calculations before storing them + /// to the database. We shouldn’t need these on-demand calculations in this module; it's better + /// to remove this function entirely but some coins like UTXOs still depend on it. pub fn build(self) -> TransactionDetails { let (block_height, timestamp) = match self.block_height_and_time { Some(height_with_time) => (height_with_time.height, height_with_time.timestamp), @@ -210,15 +213,8 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T bytes_for_hash.extend_from_slice(&token_id.0); sha256(&bytes_for_hash).to_vec().into() }, - TransactionType::TendermintIBCTransfer { token_id } - | TransactionType::CustomTendermintMsg { token_id, .. } => { - if let Some(token_id) = token_id { - let mut bytes_for_hash = tx_hash.0.clone(); - bytes_for_hash.extend_from_slice(&token_id.0); - sha256(&bytes_for_hash).to_vec().into() - } else { - tx_hash.clone() - } + TransactionType::TendermintIBCTransfer { .. } | TransactionType::CustomTendermintMsg { .. } => { + unreachable!("Tendermint never invokes this function.") }, TransactionType::StakingDelegation | TransactionType::RemoveDelegation diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 5be8983a63..8e4934201b 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2639,10 +2639,7 @@ impl TendermintCoin { .await .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; - let internal_id = { - let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); - sha256(&hex_vec).to_vec().into() - }; + let internal_id = tendermint_tx_internal_id(tx.tx_hash().unwrap_or_default().as_bytes(), None); Ok(TransactionDetails { tx, @@ -2770,10 +2767,7 @@ impl TendermintCoin { .await .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; - let internal_id = { - let hex_vec = tx.tx_hex().map_or_else(Vec::new, |h| h.to_vec()); - sha256(&hex_vec).to_vec().into() - }; + let internal_id = tendermint_tx_internal_id(tx.tx_hash().unwrap_or_default().as_bytes(), None); Ok(TransactionDetails { tx, @@ -2970,10 +2964,7 @@ impl TendermintCoin { .await .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; - let internal_id = { - let hex_vec = tx.tx_hex().map_or_else(Vec::new, |h| h.to_vec()); - sha256(&hex_vec).to_vec().into() - }; + let internal_id = tendermint_tx_internal_id(tx.tx_hash().unwrap_or_default().as_bytes(), None); Ok(TransactionDetails { tx, @@ -3337,10 +3328,7 @@ impl MmCoin for TendermintCoin { .await .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let internal_id = { - let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); - sha256(&hex_vec).to_vec().into() - }; + let internal_id = tendermint_tx_internal_id(tx.tx_hash().unwrap_or_default().as_bytes(), None); Ok(TransactionDetails { tx, @@ -4206,6 +4194,15 @@ fn parse_expected_sequence_number(e: &str) -> MmResult) -> BytesJson { + let mut bytes = bytes.to_vec(); + + if let Some(token_id) = token_id { + bytes.extend_from_slice(&token_id); + } + sha256(&bytes).to_vec().into() +} + #[cfg(test)] pub mod tendermint_falsecoin_tests { use super::*; diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 359e2c8e61..4183473d6d 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -497,10 +497,8 @@ impl MmCoin for TendermintToken { .await .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let internal_id = { - let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); - sha256(&hex_vec).to_vec().into() - }; + let internal_id = + super::tendermint_tx_internal_id(tx.tx_hash().unwrap_or_default().as_bytes(), Some(token.token_id())); Ok(TransactionDetails { tx, From b52d244a8f07f7ef6800fc1f0f2c602b14e92a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 9 Jun 2025 11:31:20 +0300 Subject: [PATCH 22/36] improvement(RPC): unified interface for legacy and current RPC interfaces (#2450) * re-write legacy `process_single_request` Signed-off-by: onur-ozkan * provide unified RPC interface for clients Signed-off-by: onur-ozkan * remove `mmrpc` from various tests to have a coverage on recent change Signed-off-by: onur-ozkan * use `ERR` macro instead of `ERRL` Signed-off-by: onur-ozkan * remove unknown method test for legacy dispatcher Signed-off-by: onur-ozkan * add info log Signed-off-by: onur-ozkan * nit Signed-off-by: onur-ozkan --------- Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/rpc.rs | 21 +++++++++--- .../src/rpc/dispatcher/dispatcher_legacy.rs | 34 +++++++++++++++---- .../tests/mm2_tests/lightning_tests.rs | 10 ------ .../tests/mm2_tests/mm2_tests_inner.rs | 10 ------ 4 files changed, 45 insertions(+), 30 deletions(-) diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index 4e6a30402c..7a338bd552 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -176,12 +176,25 @@ fn response_from_dispatcher_error( response.serialize_http_response() } -async fn process_single_request(ctx: MmArc, req: Json, client: SocketAddr) -> Result>, String> { +async fn process_single_request(ctx: MmArc, mut req: Json, client: SocketAddr) -> Result>, String> { let local_only = ctx.conf["rpc_local_only"].as_bool().unwrap_or(true); if req["mmrpc"].is_null() { - return dispatcher_legacy::process_single_request(ctx, req, client, local_only) - .await - .map_err(|e| ERRL!("{}", e)); + match dispatcher_legacy::process_single_request(ctx.clone(), req.clone(), client, local_only).await { + Ok(t) => return Ok(t), + + Err(dispatcher_legacy::LegacyRequestProcessError::NoMatch) => { + // Try the v2 implementation + req["mmrpc"] = json!("2.0"); + info!( + "Couldn't resolve '{}' RPC using the legacy API, trying v2 (mmrpc: 2.0) instead.", + req["method"] + ); + }, + + Err(e) => { + return ERR!("{}", e); + }, + }; } let id = req["id"].as_u64().map(|id| id as usize); diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs index 5f4b14f8b4..8d1a71db85 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs @@ -117,27 +117,49 @@ pub fn dispatcher(req: Json, ctx: MmArc) -> DispatcherRes { }) } +#[derive(Debug, Display)] +pub enum LegacyRequestProcessError { + #[display(fmt = "Selected method is not allowed: {reason}")] + NotAllowed { reason: String }, + #[display(fmt = "No such method")] + NoMatch, + #[display(fmt = "RPC call failed: {reason}")] + Failed { reason: String }, +} + pub async fn process_single_request( ctx: MmArc, req: Json, client: SocketAddr, local_only: bool, -) -> Result>, String> { +) -> Result>, LegacyRequestProcessError> { // https://github.com/artemii235/SuperNET/issues/368 if local_only && !client.ip().is_loopback() && !PUBLIC_METHODS.contains(&req["method"].as_str()) { - return ERR!("Selected method can be called from localhost only!"); + return Err(LegacyRequestProcessError::NotAllowed { + reason: "Selected method can only be called from localhost.".to_owned(), + }); } let rate_limit_ctx = RateLimitContext::from_ctx(&ctx).unwrap(); if rate_limit_ctx.is_banned(client.ip()).await { - return ERR!("Your ip is banned."); + return Err(LegacyRequestProcessError::NotAllowed { + reason: "Your IP is banned.".to_owned(), + }); } - try_s!(auth(&req, &ctx, &client).await); + auth(&req, &ctx, &client) + .await + .map_err(|reason| LegacyRequestProcessError::Failed { reason })?; let handler = match dispatcher(req, ctx.clone()) { DispatcherRes::Match(handler) => handler, - DispatcherRes::NoMatch(_) => return ERR!("No such method."), + DispatcherRes::NoMatch(_) => { + return Err(LegacyRequestProcessError::NoMatch); + }, }; - Ok(try_s!(handler.compat().await)) + + handler + .compat() + .await + .map_err(|reason| LegacyRequestProcessError::Failed { reason }) } /// The set of functions that convert the result of the updated handlers into the legacy format. diff --git a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs index f4317f4d26..7b7c15688f 100644 --- a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs @@ -164,7 +164,6 @@ async fn open_channel( let request = mm .rpc(&json!({ "userpass": mm.userpass, - "mmrpc": "2.0", "method": "lightning::channels::open_channel", "params": { "coin": coin, @@ -198,7 +197,6 @@ async fn close_channel(mm: &MarketMakerIt, uuid: &str, force_close: bool) -> Jso let request = mm .rpc(&json!({ "userpass": mm.userpass, - "mmrpc": "2.0", "method": "lightning::channels::close_channel", "params": { "coin": "tBTC-TEST-lightning", @@ -222,7 +220,6 @@ async fn add_trusted_node(mm: &MarketMakerIt, node_id: &str) -> Json { let request = mm .rpc(&json!({ "userpass": mm.userpass, - "mmrpc": "2.0", "method": "lightning::nodes::add_trusted_node", "params": { "coin": "tBTC-TEST-lightning", @@ -244,7 +241,6 @@ async fn generate_invoice(mm: &MarketMakerIt, amount_in_msat: u64) -> Json { let request = mm .rpc(&json!({ "userpass": mm.userpass, - "mmrpc": "2.0", "method": "lightning::payments::generate_invoice", "params": { "coin": "tBTC-TEST-lightning", @@ -268,7 +264,6 @@ async fn pay_invoice(mm: &MarketMakerIt, invoice: &str) -> Json { let request = mm .rpc(&json!({ "userpass": mm.userpass, - "mmrpc": "2.0", "method": "lightning::payments::send_payment", "params": { "coin": "tBTC-TEST-lightning", @@ -294,7 +289,6 @@ async fn get_payment_details(mm: &MarketMakerIt, payment_hash: &str) -> Json { let request = mm .rpc(&json!({ "userpass": mm.userpass, - "mmrpc": "2.0", "method": "lightning::payments::get_payment_details", "params": { "coin": "tBTC-TEST-lightning", @@ -430,7 +424,6 @@ fn test_connect_to_node() { let connect = block_on(mm_node_2.rpc(&json!({ "userpass": mm_node_2.userpass, - "mmrpc": "2.0", "method": "lightning::nodes::connect_to_node", "params": { "coin": "tBTC-TEST-lightning", @@ -470,7 +463,6 @@ fn test_open_channel() { let list_channels_node_1 = block_on(mm_node_1.rpc(&json!({ "userpass": mm_node_1.userpass, - "mmrpc": "2.0", "method": "lightning::channels::list_open_channels_by_filter", "params": { "coin": "tBTC-TEST-lightning", @@ -499,7 +491,6 @@ fn test_open_channel() { let list_channels_node_2 = block_on(mm_node_2.rpc(&json!({ "userpass": mm_node_2.userpass, - "mmrpc": "2.0", "method": "lightning::channels::list_open_channels_by_filter", "params": { "coin": "tBTC-TEST-lightning", @@ -549,7 +540,6 @@ fn test_send_payment() { let send_payment = block_on(mm_node_2.rpc(&json!({ "userpass": mm_node_2.userpass, - "mmrpc": "2.0", "method": "lightning::payments::send_payment", "params": { "coin": "tBTC-TEST-lightning", diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 7dfba3395f..117fff3558 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -53,16 +53,6 @@ cfg_wasm32! { fn test_rpc() { let (_, mm, _dump_log, _dump_dashboard) = mm_spat(); - let no_method = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "coin": "RICK", - "ipaddr": "electrum1.cipig.net", - "port": 10017 - }))) - .unwrap(); - assert!(no_method.0.is_server_error()); - assert_eq!((no_method.2)[ACCESS_CONTROL_ALLOW_ORIGIN], "http://localhost:4000"); - let not_json = mm.rpc_str("It's just a string").unwrap(); assert!(not_json.0.is_server_error()); assert_eq!((not_json.2)[ACCESS_CONTROL_ALLOW_ORIGIN], "http://localhost:4000"); From 3f0ae7f13da25fa531aa7e8932750753f3193f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 9 Jun 2025 11:53:47 +0300 Subject: [PATCH 23/36] bump timed-map to `1.4.1` (#2481) Signed-off-by: onur-ozkan --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbe2d395a0..50e3b5b41c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7106,9 +7106,9 @@ dependencies = [ [[package]] name = "timed-map" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74a5331850dc3b08de854b57674af757b6e286e7ef930baf71e0a196f53790" +checksum = "6f664a6b916d03d3e32c312c3b6ce31c24697c0f7ea6d87e20eb6372053ddf29" dependencies = [ "rustc-hash", "serde", diff --git a/Cargo.toml b/Cargo.toml index 6fdc0564bb..eac8eac5fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -197,7 +197,7 @@ testcontainers = "0.15.0" tiny-bip39 = "0.8.0" thiserror = "1.0.40" time = "0.3.20" -timed-map = { version = "1.4", features = ["rustc-hash", "wasm"] } +timed-map = { version = "1.4", features = ["rustc-hash", "serde", "wasm"] } tokio = { version = "1.20", default-features = false } tokio-rustls = { version = "0.24", default-features = false } tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "8fc7e2f", defautl-features = false, features = ["rustls-tls-native-roots"]} From 37bf06a76aa3b0f36f0ec68b6155de34115bab67 Mon Sep 17 00:00:00 2001 From: Ahmed Mohamed Ibrahim <63132227+BigFish2086@users.noreply.github.com> Date: Mon, 9 Jun 2025 16:13:27 +0300 Subject: [PATCH 24/36] improvement(event-streaming): strong type streamer IDs (#2441) * refactor(event-streaming): having a strong type for streamer id #2207 used enumeration to represent the IDs of the different types of streamers that are currently supported. * refactor(event-streaming): wrap StreamerId into an Arc #2207 * Revert "refactor(event-streaming): wrap StreamerId into an Arc #2207" This reverts commit 702befeb56814c3c50d22fb9443275c6e5d33796. * fix(event-streaming): having derive_streamer_id returns only StreamerId `derive_streamer_id` was returning `StreamerId` in some places and `&'static StreamerId` in others, so it's now fixed to return `StreamerId` in every place. * fix(event-streaming): reorder derived traits for StreamerId #2207 * refactor(event-streaming): move StreamerId from streamer.rs to streamer_ids.rs - move StreamerId from streamer.rs to streamer_ids.rs - use the default Debug implementation for StreamerId - use custom serialization and deserialization for StreamerId e.g. StreamerId::Balance(String::from("ETH")) will look like BALANCE:ETH instead of {"Balance":"ETH"} * refactor(event-streaming): centralize StreamerId string constants and fix OrderbookUpdate format - Moved all StreamerId string constants to module-level scope for reuse across Display and Deserialize implementations. - Fixed inconsistency in OrderbookUpdate variant string format (was using "/" instead of ":"). - Improved test variant handling using FOR_TESTING_PREFIX under #[cfg(test)]. * refactor(event-streaming): use struct-like enums for StreamerId variants for better clarity * chore(event-streaming): change XXX to TODO for clarity * fix(event-streaming): enable StreamerId::ForTesting for wasm32 builds Replaces `#[cfg(test)]` with `#[cfg(any(test, target_arch = "wasm32"))]` so that the ForTesting variant is compiled when targeting WebAssembly. --- mm2src/coins/eth/eth_balance_events.rs | 10 +- .../eth/fee_estimation/eth_fee_events.rs | 8 +- .../tendermint/tendermint_balance_events.rs | 8 +- mm2src/coins/utxo/tx_history_events.rs | 6 +- mm2src/coins/utxo/utxo_balance_events.rs | 10 +- mm2src/coins/z_coin/tx_history_events.rs | 6 +- .../coins/z_coin/tx_streaming_tests/native.rs | 8 +- mm2src/coins/z_coin/z_balance_streaming.rs | 10 +- mm2src/mm2_core/src/data_asker.rs | 12 +- mm2src/mm2_event_stream/src/event.rs | 22 +-- mm2src/mm2_event_stream/src/lib.rs | 2 + mm2src/mm2_event_stream/src/manager.rs | 33 +++-- mm2src/mm2_event_stream/src/streamer.rs | 22 ++- mm2src/mm2_event_stream/src/streamer_ids.rs | 129 ++++++++++++++++++ mm2src/mm2_main/src/heartbeat_event.rs | 4 +- mm2src/mm2_main/src/lp_ordermatch.rs | 8 +- .../src/lp_ordermatch/order_events.rs | 6 +- .../src/lp_ordermatch/orderbook_events.rs | 10 +- mm2src/mm2_main/src/lp_swap/maker_swap.rs | 2 +- mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs | 2 +- mm2src/mm2_main/src/lp_swap/swap_events.rs | 6 +- mm2src/mm2_main/src/lp_swap/taker_swap.rs | 2 +- mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs | 2 +- .../src/rpc/streaming_activations/disable.rs | 3 +- .../src/rpc/streaming_activations/mod.rs | 6 +- .../mm2_p2p/src/application/network_event.rs | 4 +- mm2src/rpc_task/src/manager.rs | 4 +- 27 files changed, 262 insertions(+), 83 deletions(-) create mode 100644 mm2src/mm2_event_stream/src/streamer_ids.rs diff --git a/mm2src/coins/eth/eth_balance_events.rs b/mm2src/coins/eth/eth_balance_events.rs index dd3d125832..a991dab101 100644 --- a/mm2src/coins/eth/eth_balance_events.rs +++ b/mm2src/coins/eth/eth_balance_events.rs @@ -4,7 +4,7 @@ use crate::{eth::{u256_to_big_decimal, Erc20TokenDetails}, BalanceError, CoinWithDerivationMethod}; use common::{executor::Timer, log, Future01CompatExt}; use mm2_err_handle::prelude::MmError; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput, StreamerId}; use mm2_number::BigDecimal; use async_trait::async_trait; @@ -147,7 +147,11 @@ async fn fetch_balance( impl EventStreamer for EthBalanceEventStreamer { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker) } + fn streamer_id(&self) -> StreamerId { + StreamerId::Balance { + coin: self.coin.ticker.to_string(), + } + } async fn handle( self, @@ -155,7 +159,7 @@ impl EventStreamer for EthBalanceEventStreamer { ready_tx: oneshot::Sender>, _: impl StreamHandlerInput, ) { - async fn start_polling(streamer_id: String, broadcaster: Broadcaster, coin: EthCoin, interval: f64) { + async fn start_polling(streamer_id: StreamerId, broadcaster: Broadcaster, coin: EthCoin, interval: f64) { async fn sleep_remaining_time(interval: f64, now: Instant) { // If the interval is x seconds, // our goal is to broadcast changed balances every x seconds. diff --git a/mm2src/coins/eth/fee_estimation/eth_fee_events.rs b/mm2src/coins/eth/fee_estimation/eth_fee_events.rs index 0af1f13579..1c79e7da40 100644 --- a/mm2src/coins/eth/fee_estimation/eth_fee_events.rs +++ b/mm2src/coins/eth/fee_estimation/eth_fee_events.rs @@ -1,7 +1,7 @@ use super::ser::FeePerGasEstimated; use crate::eth::EthCoin; use common::executor::Timer; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput, StreamerId}; use async_trait::async_trait; use compatible_time::Instant; @@ -52,7 +52,11 @@ impl EthFeeEventStreamer { impl EventStreamer for EthFeeEventStreamer { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { format!("FEE_ESTIMATION:{}", self.coin.ticker) } + fn streamer_id(&self) -> StreamerId { + StreamerId::FeeEstimation { + coin: self.coin.ticker.to_string(), + } + } async fn handle( self, diff --git a/mm2src/coins/tendermint/tendermint_balance_events.rs b/mm2src/coins/tendermint/tendermint_balance_events.rs index eed451b5dd..3304e35e97 100644 --- a/mm2src/coins/tendermint/tendermint_balance_events.rs +++ b/mm2src/coins/tendermint/tendermint_balance_events.rs @@ -3,7 +3,7 @@ use common::{http_uri_to_ws_address, log, PROXY_REQUEST_EXPIRATION_SEC}; use futures::channel::oneshot; use futures_util::{SinkExt, StreamExt}; use jsonrpc_core::{Id as RpcId, Params as RpcParams, Value as RpcValue, Version as RpcVersion}; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput, StreamerId}; use mm2_number::BigDecimal; use proxy_signature::RawMessage; use std::collections::{HashMap, HashSet}; @@ -23,7 +23,11 @@ impl TendermintBalanceEventStreamer { impl EventStreamer for TendermintBalanceEventStreamer { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker()) } + fn streamer_id(&self) -> StreamerId { + StreamerId::Balance { + coin: self.coin.ticker().to_string(), + } + } async fn handle( self, diff --git a/mm2src/coins/utxo/tx_history_events.rs b/mm2src/coins/utxo/tx_history_events.rs index c336e6fbb0..e0404228a9 100644 --- a/mm2src/coins/utxo/tx_history_events.rs +++ b/mm2src/coins/utxo/tx_history_events.rs @@ -1,5 +1,5 @@ use crate::TransactionDetails; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; use async_trait::async_trait; use futures::channel::oneshot; @@ -14,14 +14,14 @@ impl TxHistoryEventStreamer { pub fn new(coin: String) -> Self { Self { coin } } #[inline(always)] - pub fn derive_streamer_id(coin: &str) -> String { format!("TX_HISTORY:{coin}") } + pub fn derive_streamer_id(coin: &str) -> StreamerId { StreamerId::TxHistory { coin: coin.to_string() } } } #[async_trait] impl EventStreamer for TxHistoryEventStreamer { type DataInType = Vec; - fn streamer_id(&self) -> String { Self::derive_streamer_id(&self.coin) } + fn streamer_id(&self) -> StreamerId { Self::derive_streamer_id(&self.coin) } async fn handle( self, diff --git a/mm2src/coins/utxo/utxo_balance_events.rs b/mm2src/coins/utxo/utxo_balance_events.rs index 8fdde86ab7..9451675cdb 100644 --- a/mm2src/coins/utxo/utxo_balance_events.rs +++ b/mm2src/coins/utxo/utxo_balance_events.rs @@ -12,7 +12,7 @@ use common::log; use futures::channel::oneshot; use futures::StreamExt; use keys::Address; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; use std::collections::{HashMap, HashSet}; macro_rules! try_or_continue { @@ -40,14 +40,18 @@ impl UtxoBalanceEventStreamer { } } - pub fn derive_streamer_id(coin: &str) -> String { format!("BALANCE:{coin}") } + pub fn derive_streamer_id(coin: &str) -> StreamerId { StreamerId::Balance { coin: coin.to_string() } } } #[async_trait] impl EventStreamer for UtxoBalanceEventStreamer { type DataInType = ScripthashNotification; - fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker()) } + fn streamer_id(&self) -> StreamerId { + StreamerId::Balance { + coin: self.coin.ticker().to_string(), + } + } async fn handle( self, diff --git a/mm2src/coins/z_coin/tx_history_events.rs b/mm2src/coins/z_coin/tx_history_events.rs index f374bc22b1..c09da5d732 100644 --- a/mm2src/coins/z_coin/tx_history_events.rs +++ b/mm2src/coins/z_coin/tx_history_events.rs @@ -4,7 +4,7 @@ use crate::utxo::rpc_clients::UtxoRpcError; use crate::MarketCoinOps; use common::log; use mm2_err_handle::prelude::MmError; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; use rpc::v1::types::H256 as H256Json; use async_trait::async_trait; @@ -23,14 +23,14 @@ impl ZCoinTxHistoryEventStreamer { pub fn new(coin: ZCoin) -> Self { Self { coin } } #[inline(always)] - pub fn derive_streamer_id(coin: &str) -> String { format!("TX_HISTORY:{coin}") } + pub fn derive_streamer_id(coin: &str) -> StreamerId { StreamerId::TxHistory { coin: coin.to_string() } } } #[async_trait] impl EventStreamer for ZCoinTxHistoryEventStreamer { type DataInType = Vec>; - fn streamer_id(&self) -> String { Self::derive_streamer_id(self.coin.ticker()) } + fn streamer_id(&self) -> StreamerId { Self::derive_streamer_id(self.coin.ticker()) } async fn handle( self, diff --git a/mm2src/coins/z_coin/tx_streaming_tests/native.rs b/mm2src/coins/z_coin/tx_streaming_tests/native.rs index f4bc2849dc..6cb7f9a97c 100644 --- a/mm2src/coins/z_coin/tx_streaming_tests/native.rs +++ b/mm2src/coins/z_coin/tx_streaming_tests/native.rs @@ -60,13 +60,13 @@ fn test_zcoin_tx_streaming() { .expect("tx history sender shutdown"); log!("{:?}", event.get()); - let (event_type, event_data) = event.get(); + let (streamer_id, event_data) = event.get(); // Make sure this is not an error event, - assert!(!event_type.starts_with("ERROR_")); + assert!(!streamer_id.starts_with("ERROR:")); // from the expected streamer, assert_eq!( - event_type, - ZCoinTxHistoryEventStreamer::derive_streamer_id(coin.ticker()) + streamer_id, + ZCoinTxHistoryEventStreamer::derive_streamer_id(coin.ticker()).to_string() ); // and has the expected data. assert_eq!(event_data["tx_hash"].as_str().unwrap(), tx.txid().to_string()); diff --git a/mm2src/coins/z_coin/z_balance_streaming.rs b/mm2src/coins/z_coin/z_balance_streaming.rs index 0760bfc929..c5b012fb3b 100644 --- a/mm2src/coins/z_coin/z_balance_streaming.rs +++ b/mm2src/coins/z_coin/z_balance_streaming.rs @@ -6,7 +6,7 @@ use async_trait::async_trait; use common::log::error; use futures::channel::oneshot; use futures_util::StreamExt; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; pub struct ZCoinBalanceEventStreamer { coin: ZCoin, @@ -17,14 +17,18 @@ impl ZCoinBalanceEventStreamer { pub fn new(coin: ZCoin) -> Self { Self { coin } } #[inline(always)] - pub fn derive_streamer_id(coin: &str) -> String { format!("BALANCE:{coin}") } + pub fn derive_streamer_id(coin: &str) -> StreamerId { StreamerId::Balance { coin: coin.to_string() } } } #[async_trait] impl EventStreamer for ZCoinBalanceEventStreamer { type DataInType = (); - fn streamer_id(&self) -> String { Self::derive_streamer_id(self.coin.ticker()) } + fn streamer_id(&self) -> StreamerId { + StreamerId::Balance { + coin: self.coin.ticker().to_string(), + } + } async fn handle( self, diff --git a/mm2src/mm2_core/src/data_asker.rs b/mm2src/mm2_core/src/data_asker.rs index 2e9a125d56..eba0158b24 100644 --- a/mm2src/mm2_core/src/data_asker.rs +++ b/mm2src/mm2_core/src/data_asker.rs @@ -5,7 +5,7 @@ use derive_more::Display; use futures::channel::oneshot; use futures::lock::Mutex as AsyncMutex; use mm2_err_handle::prelude::*; -use mm2_event_stream::Event; +use mm2_event_stream::{Event, StreamerId}; use ser_error_derive::SerializeErrorType; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -16,8 +16,6 @@ use timed_map::{MapKind, TimedMap}; use crate::mm_ctx::{MmArc, MmCtx}; -const EVENT_NAME: &str = "DATA_NEEDED"; - #[derive(Clone, Debug)] pub struct DataAsker { data_id: Arc, @@ -81,8 +79,12 @@ impl MmCtx { "data": data }); - self.event_stream_manager - .broadcast_all(Event::new(format!("{EVENT_NAME}:{data_type}"), input)); + self.event_stream_manager.broadcast_all(Event::new( + StreamerId::DataNeeded { + data_type: data_type.to_string(), + }, + input, + )); match receiver.timeout(timeout).await { Ok(Ok(response)) => match serde_json::from_value::(response) { diff --git a/mm2src/mm2_event_stream/src/event.rs b/mm2src/mm2_event_stream/src/event.rs index 306bbc9e49..d652b75f09 100644 --- a/mm2src/mm2_event_stream/src/event.rs +++ b/mm2src/mm2_event_stream/src/event.rs @@ -1,13 +1,13 @@ +use crate::StreamerId; use serde_json::Value as Json; // Note `Event` shouldn't be `Clone`able, but rather Arc/Rc wrapped and then shared. // This is only for testing. /// Multi-purpose/generic event type that can easily be used over the event streaming #[cfg_attr(any(test, target_arch = "wasm32"), derive(Clone, Debug, PartialEq))] -#[derive(Default)] pub struct Event { /// The type of the event (balance, network, swap, etc...). - event_type: String, + streamer_id: StreamerId, /// The message to be sent to the client. message: Json, /// Indicating whether this event is an error event or a normal one. @@ -17,9 +17,9 @@ pub struct Event { impl Event { /// Creates a new `Event` instance with the specified event type and message. #[inline(always)] - pub fn new(streamer_id: String, message: Json) -> Self { + pub fn new(streamer_id: StreamerId, message: Json) -> Self { Self { - event_type: streamer_id, + streamer_id, message, error: false, } @@ -27,21 +27,25 @@ impl Event { /// Create a new error `Event` instance with the specified error event type and message. #[inline(always)] - pub fn err(streamer_id: String, message: Json) -> Self { + pub fn err(streamer_id: StreamerId, message: Json) -> Self { Self { - event_type: streamer_id, + streamer_id, message, error: true, } } - /// Returns the `event_type` (the ID of the streamer firing this event). + /// Returns whether this event is an error or not #[inline(always)] - pub fn origin(&self) -> &str { &self.event_type } + pub fn is_error(&self) -> bool { self.error } + + /// Returns the `streamer_id` (the ID of the streamer firing this event). + #[inline(always)] + pub fn origin(&self) -> &StreamerId { &self.streamer_id } /// Returns the event type and message as a pair. pub fn get(&self) -> (String, &Json) { let prefix = if self.error { "ERROR:" } else { "" }; - (format!("{prefix}{}", self.event_type), &self.message) + (format!("{prefix}{}", self.streamer_id), &self.message) } } diff --git a/mm2src/mm2_event_stream/src/lib.rs b/mm2src/mm2_event_stream/src/lib.rs index db4587a77a..25a5758fff 100644 --- a/mm2src/mm2_event_stream/src/lib.rs +++ b/mm2src/mm2_event_stream/src/lib.rs @@ -2,9 +2,11 @@ pub mod configuration; pub mod event; pub mod manager; pub mod streamer; +pub mod streamer_ids; // Re-export important types. pub use configuration::EventStreamingConfiguration; pub use event::Event; pub use manager::{StreamingManager, StreamingManagerError}; pub use streamer::{Broadcaster, EventStreamer, NoDataIn, StreamHandlerInput}; +pub use streamer_ids::StreamerId; diff --git a/mm2src/mm2_event_stream/src/manager.rs b/mm2src/mm2_event_stream/src/manager.rs index b480ddd070..1bef846ef5 100644 --- a/mm2src/mm2_event_stream/src/manager.rs +++ b/mm2src/mm2_event_stream/src/manager.rs @@ -4,7 +4,7 @@ use std::ops::{Deref, DerefMut}; use std::sync::Arc; use crate::streamer::spawn; -use crate::{Event, EventStreamer}; +use crate::{Event, EventStreamer, StreamerId}; use common::executor::abortable_queue::WeakSpawner; use common::log::{error, LogOnError}; @@ -62,7 +62,7 @@ impl StreamerInfo { #[derive(Debug)] struct ClientInfo { /// The streamers the client is listening to. - listening_to: HashSet, + listening_to: HashSet, /// The communication/stream-out channel to the client. // NOTE: Here we are using `tokio`'s `mpsc` because the one in `futures` have some extra feature // (ref: https://users.rust-lang.org/t/why-does-try-send-from-crate-futures-require-mut-self/100389). @@ -80,11 +80,11 @@ impl ClientInfo { } } - fn add_streamer(&mut self, streamer_id: String) { self.listening_to.insert(streamer_id); } + fn add_streamer(&mut self, streamer_id: StreamerId) { self.listening_to.insert(streamer_id); } - fn remove_streamer(&mut self, streamer_id: &str) { self.listening_to.remove(streamer_id); } + fn remove_streamer(&mut self, streamer_id: &StreamerId) { self.listening_to.remove(streamer_id); } - fn listens_to(&self, streamer_id: &str) -> bool { self.listening_to.contains(streamer_id) } + fn listens_to(&self, streamer_id: &StreamerId) -> bool { self.listening_to.contains(streamer_id) } fn send_event(&self, event: Arc) { // Only `try_send` here. If the channel is full (client is slow), the message @@ -97,7 +97,7 @@ impl ClientInfo { #[derive(Default, Debug)] struct StreamingManagerInner { /// A map from streamer IDs to their communication channels (if present) and shutdown handles. - streamers: HashMap, + streamers: HashMap, /// An inverse map from client IDs to the streamers they are listening to and the communication channel with the client. clients: HashMap, } @@ -118,7 +118,7 @@ impl StreamingManager { client_id: u64, streamer: impl EventStreamer, spawner: WeakSpawner, - ) -> Result { + ) -> Result { let streamer_id = streamer.streamer_id(); // Remove the streamer if it died for some reason. self.remove_streamer_if_down(&streamer_id); @@ -173,7 +173,7 @@ impl StreamingManager { } /// Sends data to a streamer with `streamer_id`. - pub fn send(&self, streamer_id: &str, data: T) -> Result<(), StreamingManagerError> { + pub fn send(&self, streamer_id: &StreamerId, data: T) -> Result<(), StreamingManagerError> { let this = self.read(); let streamer_info = this .streamers @@ -192,7 +192,7 @@ impl StreamingManager { /// `data_fn` will only be evaluated if the streamer is found and accepts an input. pub fn send_fn( &self, - streamer_id: &str, + streamer_id: &StreamerId, data_fn: impl FnOnce() -> T, ) -> Result<(), StreamingManagerError> { let this = self.read(); @@ -207,7 +207,7 @@ impl StreamingManager { } /// Stops streaming from the streamer with `streamer_id` to the client with `client_id`. - pub fn stop(&self, client_id: u64, streamer_id: &str) -> Result<(), StreamingManagerError> { + pub fn stop(&self, client_id: u64, streamer_id: &StreamerId) -> Result<(), StreamingManagerError> { let mut this = self.write(); let client_info = this .clients @@ -312,7 +312,7 @@ impl StreamingManager { /// Aside from us shutting down a streamer when all its clients are disconnected, /// the streamer might die by itself (e.g. the spawner it was spawned with aborted). /// In this case, we need to remove the streamer and de-list it from all clients. - fn remove_streamer_if_down(&self, streamer_id: &str) { + fn remove_streamer_if_down(&self, streamer_id: &StreamerId) { let mut this = self.write(); let Some(streamer_info) = this.streamers.get(streamer_id) else { return; @@ -400,7 +400,12 @@ mod tests { let manager = StreamingManager::default(); let mut client1 = manager.new_client(1).unwrap(); let mut client2 = manager.new_client(2).unwrap(); - let event = Event::new("test".to_string(), json!("test")); + let event = Event::new( + StreamerId::ForTesting { + test_streamer: "test".to_string(), + }, + json!("test"), + ); // Broadcast the event to all clients. manager.broadcast_all(event.clone()); @@ -440,7 +445,7 @@ mod tests { // The streamer should send an event every 0.1s. Wait for 0.15s for safety. Timer::sleep(0.15).await; let event = client1.try_recv().unwrap(); - assert_eq!(event.origin(), streamer_id); + assert_eq!(event.origin(), &streamer_id); } // The other client shouldn't have received any events. @@ -472,7 +477,7 @@ mod tests { Timer::sleep(0.1).await; // The streamer should broadcast some event to the subscribed clients. let event = client1.try_recv().unwrap(); - assert_eq!(event.origin(), streamer_id); + assert_eq!(event.origin(), &streamer_id); // It's an echo streamer, so the message should be the same. assert_eq!(event.get().1, &json!(msg)); } diff --git a/mm2src/mm2_event_stream/src/streamer.rs b/mm2src/mm2_event_stream/src/streamer.rs index 6c319cb89c..dee79af8d1 100644 --- a/mm2src/mm2_event_stream/src/streamer.rs +++ b/mm2src/mm2_event_stream/src/streamer.rs @@ -1,6 +1,6 @@ use std::any::{self, Any}; -use crate::{Event, StreamingManager}; +use crate::{Event, StreamerId, StreamingManager}; use common::executor::{abortable_queue::WeakSpawner, AbortSettings, SpawnAbortable}; use common::log::{error, info}; @@ -25,7 +25,7 @@ where /// Returns a human readable unique identifier for the event streamer. /// No other event streamer should have the same identifier. - fn streamer_id(&self) -> String; + fn streamer_id(&self) -> StreamerId; /// Event handler that is responsible for broadcasting event data to the streaming channels. /// @@ -129,7 +129,11 @@ pub mod test_utils { impl EventStreamer for PeriodicStreamer { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { "periodic_streamer".to_string() } + fn streamer_id(&self) -> StreamerId { + StreamerId::ForTesting { + test_streamer: "periodic_streamer".to_string(), + } + } async fn handle( self, @@ -152,7 +156,11 @@ pub mod test_utils { impl EventStreamer for ReactiveStreamer { type DataInType = String; - fn streamer_id(&self) -> String { "reactive_streamer".to_string() } + fn streamer_id(&self) -> StreamerId { + StreamerId::ForTesting { + test_streamer: "reactive_streamer".to_string(), + } + } async fn handle( self, @@ -175,7 +183,11 @@ pub mod test_utils { impl EventStreamer for InitErrorStreamer { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { "init_error_streamer".to_string() } + fn streamer_id(&self) -> StreamerId { + StreamerId::ForTesting { + test_streamer: "init_error_streamer".to_string(), + } + } async fn handle( self, diff --git a/mm2src/mm2_event_stream/src/streamer_ids.rs b/mm2src/mm2_event_stream/src/streamer_ids.rs new file mode 100644 index 0000000000..d019b9a9e0 --- /dev/null +++ b/mm2src/mm2_event_stream/src/streamer_ids.rs @@ -0,0 +1,129 @@ +use serde::de::{self, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; + +const NETWORK: &str = "NETWORK"; +const HEARTBEAT: &str = "HEARTBEAT"; +const SWAP_STATUS: &str = "SWAP_STATUS"; +const ORDER_STATUS: &str = "ORDER_STATUS"; + +const TASK_PREFIX: &str = "TASK:"; +const BALANCE_PREFIX: &str = "BALANCE:"; +const TX_HISTORY_PREFIX: &str = "TX_HISTORY:"; +const FEE_ESTIMATION_PREFIX: &str = "FEE_ESTIMATION:"; +const DATA_NEEDED_PREFIX: &str = "DATA_NEEDED:"; +const ORDERBOOK_UPDATE_PREFIX: &str = "ORDERBOOK_UPDATE:"; +#[cfg(any(test, target_arch = "wasm32"))] +const FOR_TESTING_PREFIX: &str = "TEST_STREAMER:"; + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum StreamerId { + Network, + Heartbeat, + SwapStatus, + OrderStatus, + Task { + task_id: u64, // TODO: should be TaskId (from rpc_task) + }, + Balance { + coin: String, + }, + DataNeeded { + data_type: String, + }, + TxHistory { + coin: String, + }, + FeeEstimation { + coin: String, + }, + OrderbookUpdate { + topic: String, + }, + #[cfg(any(test, target_arch = "wasm32"))] + ForTesting { + test_streamer: String, + }, +} + +impl fmt::Display for StreamerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StreamerId::Network => write!(f, "{}", NETWORK), + StreamerId::Heartbeat => write!(f, "{}", HEARTBEAT), + StreamerId::SwapStatus => write!(f, "{}", SWAP_STATUS), + StreamerId::OrderStatus => write!(f, "{}", ORDER_STATUS), + StreamerId::Task { task_id } => write!(f, "{}{}", TASK_PREFIX, task_id), + StreamerId::Balance { coin } => write!(f, "{}{}", BALANCE_PREFIX, coin), + StreamerId::TxHistory { coin } => write!(f, "{}{}", TX_HISTORY_PREFIX, coin), + StreamerId::FeeEstimation { coin } => write!(f, "{}{}", FEE_ESTIMATION_PREFIX, coin), + StreamerId::DataNeeded { data_type } => write!(f, "{}{}", DATA_NEEDED_PREFIX, data_type), + StreamerId::OrderbookUpdate { topic } => write!(f, "{}{}", ORDERBOOK_UPDATE_PREFIX, topic), + #[cfg(any(test, target_arch = "wasm32"))] + StreamerId::ForTesting { test_streamer } => write!(f, "{}{}", FOR_TESTING_PREFIX, test_streamer), + } + } +} + +impl Serialize for StreamerId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for StreamerId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct StreamerIdVisitor; + + impl<'de> Visitor<'de> for StreamerIdVisitor { + type Value = StreamerId; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string representing a StreamerId") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + NETWORK => Ok(StreamerId::Network), + HEARTBEAT => Ok(StreamerId::Heartbeat), + SWAP_STATUS => Ok(StreamerId::SwapStatus), + ORDER_STATUS => Ok(StreamerId::OrderStatus), + v if v.starts_with(TASK_PREFIX) => Ok(StreamerId::Task { + task_id: v[TASK_PREFIX.len()..].parse().map_err(de::Error::custom)?, + }), + v if v.starts_with(BALANCE_PREFIX) => Ok(StreamerId::Balance { + coin: v[BALANCE_PREFIX.len()..].to_string(), + }), + v if v.starts_with(TX_HISTORY_PREFIX) => Ok(StreamerId::TxHistory { + coin: v[TX_HISTORY_PREFIX.len()..].to_string(), + }), + v if v.starts_with(FEE_ESTIMATION_PREFIX) => Ok(StreamerId::FeeEstimation { + coin: v[FEE_ESTIMATION_PREFIX.len()..].to_string(), + }), + v if v.starts_with(DATA_NEEDED_PREFIX) => Ok(StreamerId::DataNeeded { + data_type: v[DATA_NEEDED_PREFIX.len()..].to_string(), + }), + v if v.starts_with(ORDERBOOK_UPDATE_PREFIX) => Ok(StreamerId::OrderbookUpdate { + topic: v[ORDERBOOK_UPDATE_PREFIX.len()..].to_string(), + }), + #[cfg(any(test, target_arch = "wasm32"))] + v if v.starts_with(FOR_TESTING_PREFIX) => Ok(StreamerId::ForTesting { + test_streamer: v[FOR_TESTING_PREFIX.len()..].to_string(), + }), + _ => Err(de::Error::custom(format!("Invalid StreamerId: {}", value))), + } + } + } + + deserializer.deserialize_str(StreamerIdVisitor) + } +} diff --git a/mm2src/mm2_main/src/heartbeat_event.rs b/mm2src/mm2_main/src/heartbeat_event.rs index a2c46f2fb6..6645f407fe 100644 --- a/mm2src/mm2_main/src/heartbeat_event.rs +++ b/mm2src/mm2_main/src/heartbeat_event.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use common::executor::Timer; use futures::channel::oneshot; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput, StreamerId}; use serde::Deserialize; #[derive(Deserialize)] @@ -31,7 +31,7 @@ impl HeartbeatEvent { impl EventStreamer for HeartbeatEvent { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { "HEARTBEAT".to_string() } + fn streamer_id(&self) -> StreamerId { StreamerId::Heartbeat } async fn handle( self, diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 91b728c2a2..ab0928595b 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -3835,7 +3835,7 @@ async fn process_maker_reserved(ctx: MmArc, from_pubkey: H256Json, reserved_msg: }; ctx.event_stream_manager - .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + .send_fn(&OrderStatusStreamer::derive_streamer_id(), || { OrderStatusEvent::TakerMatch(taker_match.clone()) }) .ok(); @@ -3890,7 +3890,7 @@ async fn process_maker_connected(ctx: MmArc, from_pubkey: PublicKey, connected: } ctx.event_stream_manager - .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + .send_fn(&OrderStatusStreamer::derive_streamer_id(), || { OrderStatusEvent::TakerConnected(order_match.clone()) }) .ok(); @@ -4004,7 +4004,7 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: }; ctx.event_stream_manager - .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + .send_fn(&OrderStatusStreamer::derive_streamer_id(), || { OrderStatusEvent::MakerMatch(maker_match.clone()) }) .ok(); @@ -4075,7 +4075,7 @@ async fn process_taker_connect(ctx: MmArc, sender_pubkey: PublicKey, connect_msg let order_match = order_match.clone(); ctx.event_stream_manager - .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + .send_fn(&OrderStatusStreamer::derive_streamer_id(), || { OrderStatusEvent::MakerConnected(order_match.clone()) }) .ok(); diff --git a/mm2src/mm2_main/src/lp_ordermatch/order_events.rs b/mm2src/mm2_main/src/lp_ordermatch/order_events.rs index 547ee7df4e..9e0fa97593 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/order_events.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/order_events.rs @@ -1,5 +1,5 @@ use super::{MakerMatch, TakerMatch}; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; use async_trait::async_trait; use futures::channel::oneshot; @@ -12,7 +12,7 @@ impl OrderStatusStreamer { pub fn new() -> Self { Self } #[inline(always)] - pub const fn derive_streamer_id() -> &'static str { "ORDER_STATUS" } + pub const fn derive_streamer_id() -> StreamerId { StreamerId::OrderStatus } } #[derive(Serialize)] @@ -28,7 +28,7 @@ pub enum OrderStatusEvent { impl EventStreamer for OrderStatusStreamer { type DataInType = OrderStatusEvent; - fn streamer_id(&self) -> String { Self::derive_streamer_id().to_string() } + fn streamer_id(&self) -> StreamerId { Self::derive_streamer_id() } async fn handle( self, diff --git a/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs b/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs index f7149bd05e..852da7fb1f 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs @@ -1,7 +1,7 @@ use super::{orderbook_topic_from_base_rel, subscribe_to_orderbook_topic, OrderbookP2PItem}; use coins::{is_wallet_only_ticker, lp_coinfind}; use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; use async_trait::async_trait; use futures::channel::oneshot; @@ -17,8 +17,10 @@ pub struct OrderbookStreamer { impl OrderbookStreamer { pub fn new(ctx: MmArc, base: String, rel: String) -> Self { Self { ctx, base, rel } } - pub fn derive_streamer_id(base: &str, rel: &str) -> String { - format!("ORDERBOOK_UPDATE/{}", orderbook_topic_from_base_rel(base, rel)) + pub fn derive_streamer_id(base: &str, rel: &str) -> StreamerId { + StreamerId::OrderbookUpdate { + topic: orderbook_topic_from_base_rel(base, rel), + } } } @@ -36,7 +38,7 @@ pub enum OrderbookItemChangeEvent { impl EventStreamer for OrderbookStreamer { type DataInType = OrderbookItemChangeEvent; - fn streamer_id(&self) -> String { Self::derive_streamer_id(&self.base, &self.rel) } + fn streamer_id(&self) -> StreamerId { Self::derive_streamer_id(&self.base, &self.rel) } async fn handle( self, diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 7ec3a86245..aa2977a81d 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -2190,7 +2190,7 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { drop(dispatcher); // Send a notification to the swap status streamer about a new event. ctx.event_stream_manager - .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::MakerV1 { + .send_fn(&SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::MakerV1 { uuid: running_swap.uuid, event: to_save.clone(), }) diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs index c4592fd494..3783e43950 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -792,7 +792,7 @@ impl Self { Self } #[inline(always)] - pub const fn derive_streamer_id() -> &'static str { "SWAP_STATUS" } + pub const fn derive_streamer_id() -> StreamerId { StreamerId::SwapStatus } } #[derive(Serialize)] @@ -32,7 +32,7 @@ pub enum SwapStatusEvent { impl EventStreamer for SwapStatusStreamer { type DataInType = SwapStatusEvent; - fn streamer_id(&self) -> String { Self::derive_streamer_id().to_string() } + fn streamer_id(&self) -> StreamerId { Self::derive_streamer_id() } async fn handle( self, diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 08657cc572..fa4e396d79 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -491,7 +491,7 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { // Send a notification to the swap status streamer about a new event. ctx.event_stream_manager - .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::TakerV1 { + .send_fn(&SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::TakerV1 { uuid: running_swap.uuid, event: to_save.clone(), }) diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs index eea040316d..7f8270f139 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs @@ -909,7 +909,7 @@ impl { /// The success/ok response for any event streaming activation request. #[derive(Serialize)] pub struct EnableStreamingResponse { - pub streamer_id: String, + pub streamer_id: StreamerId, // TODO: If the the streamer was already running, it is probably running with different configuration. // We might want to inform the client that the configuration they asked for wasn't applied and return // the active configuration instead? @@ -41,5 +43,5 @@ pub struct EnableStreamingResponse { } impl EnableStreamingResponse { - fn new(streamer_id: String) -> Self { Self { streamer_id } } + fn new(streamer_id: StreamerId) -> Self { Self { streamer_id } } } diff --git a/mm2src/mm2_p2p/src/application/network_event.rs b/mm2src/mm2_p2p/src/application/network_event.rs index fa152469d1..bdab12f1dc 100644 --- a/mm2src/mm2_p2p/src/application/network_event.rs +++ b/mm2src/mm2_p2p/src/application/network_event.rs @@ -1,6 +1,6 @@ use common::executor::Timer; use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput, StreamerId}; use async_trait::async_trait; use futures::channel::oneshot; @@ -38,7 +38,7 @@ impl NetworkEvent { impl EventStreamer for NetworkEvent { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { "NETWORK".to_string() } + fn streamer_id(&self) -> StreamerId { StreamerId::Network } async fn handle( self, diff --git a/mm2src/rpc_task/src/manager.rs b/mm2src/rpc_task/src/manager.rs index b5c43a04b6..232c8efaef 100644 --- a/mm2src/rpc_task/src/manager.rs +++ b/mm2src/rpc_task/src/manager.rs @@ -6,7 +6,7 @@ use common::log::{debug, info, trace, warn}; use futures::channel::oneshot; use futures::future::{select, Either}; use mm2_err_handle::prelude::*; -use mm2_event_stream::{Event, StreamingManager, StreamingManagerError}; +use mm2_event_stream::{Event, StreamerId, StreamingManager, StreamingManagerError}; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::sync::atomic::Ordering; @@ -192,7 +192,7 @@ impl RpcTaskManager { // Note that this should really always be `Some`, since we updated the status *successfully*. if let Some(new_status) = self.task_status(task_id, false) { let event = Event::new( - format!("TASK:{task_id}"), + StreamerId::Task { task_id }, serde_json::to_value(new_status).expect("Serialization shouldn't fail."), ); if let Err(e) = self.streaming_manager.broadcast_to(event, client_id) { From 116840b07e2e383f98ee56842e73e2d010bf7d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 12 Jun 2025 06:21:47 +0300 Subject: [PATCH 25/36] fix(dns): better ip resolution (#2487) * resolve hostnames better Signed-off-by: onur-ozkan * re-org errors Signed-off-by: onur-ozkan --------- Signed-off-by: onur-ozkan --- mm2src/mm2_net/src/ip_addr.rs | 64 +++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/mm2src/mm2_net/src/ip_addr.rs b/mm2src/mm2_net/src/ip_addr.rs index 4c4c77f448..ec3d66f54e 100644 --- a/mm2src/mm2_net/src/ip_addr.rs +++ b/mm2src/mm2_net/src/ip_addr.rs @@ -11,7 +11,7 @@ use std::io::Read; use std::net::{IpAddr, Ipv4Addr}; use std::path::Path; -use mm2_err_handle::prelude::MmError; +use mm2_err_handle::prelude::{MapToMmResult, MmError}; use std::net::ToSocketAddrs; const IP_PROVIDERS: [&str; 2] = ["http://checkip.amazonaws.com/", "http://api.ipify.org"]; @@ -172,12 +172,10 @@ pub async fn myipaddr(ctx: MmArc) -> Result { #[derive(Debug, Display)] pub enum ParseAddressError { - #[display(fmt = "Address/Seed {} resolved to IPv6 which is not supported", _0)] - UnsupportedIPv6Address(String), - #[display(fmt = "Address/Seed {} to_socket_addrs empty iter", _0)] - EmptyIterator(String), - #[display(fmt = "Couldn't resolve '{}' Address/Seed: {}", _0, _1)] - UnresolvedAddress(String, String), + #[display(fmt = "Address '{address}' cannot be resolved to IPv4.")] + CannotResolveIPv4 { address: String }, + #[display(fmt = "Couldn't resolve any IP on '{}' address. {}", address, reason)] + UnresolvedAddress { address: String, reason: String }, } pub fn addr_to_ipv4_string(address: &str) -> Result> { @@ -189,28 +187,34 @@ pub fn addr_to_ipv4_string(address: &str) -> Result match iter.next() { - Some(addr) => { - if addr.is_ipv4() { - Ok(addr.ip().to_string()) - } else { - log::warn!( - "Address/Seed {} resolved to IPv6 {} which is not supported", - address, - addr - ); - MmError::err(ParseAddressError::UnsupportedIPv6Address(address.into())) - } - }, - None => { - log::warn!("Address/Seed {} to_socket_addrs empty iter", address); - MmError::err(ParseAddressError::EmptyIterator(address.into())) - }, - }, - Err(e) => { - log::error!("Couldn't resolve '{}' seed: {}", address, e); - MmError::err(ParseAddressError::UnresolvedAddress(address.into(), e.to_string())) - }, + let iter = address_with_port.as_str().to_socket_addrs().map_to_mm(|e| { + log::error!("Couldn't resolve '{}' seed: {}", address, e); + ParseAddressError::UnresolvedAddress { + address: address.to_owned(), + reason: e.to_string(), + } + })?; + + if iter.len() == 0 { + return MmError::err(ParseAddressError::UnresolvedAddress { + address: address.to_owned(), + reason: "Empty DNS result.".to_owned(), + }); } + + for resolved in iter { + if resolved.is_ipv4() { + return Ok(resolved.ip().to_string()); + } else { + log::warn!( + "Address/Seed {} resolved to IPv6 {} which is not supported", + address, + resolved + ); + } + } + + MmError::err(ParseAddressError::CannotResolveIPv4 { + address: address.to_owned(), + }) } From 158cdf9ffbd821a24d5062eda8c9bac6fba629b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 12 Jun 2025 06:35:53 +0300 Subject: [PATCH 26/36] fix(startup): don't initialize WalletConnect during startup (#2485) WalletConnectCtx is now initialized lazily in the `new_connection` RPC handler instead of during startup, preventing WalletConnect-related errors from affecting node initialization and avoiding unnecessary setup for users not using WalletConnect; this may slightly delay the first WalletConnect RPC call but has no functional impact. --- mm2src/mm2_main/src/lp_native_dex.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index 65f13ca732..a24a75d8ce 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -35,7 +35,6 @@ use common::log::{info, warn}; use crypto::{from_hw_error, CryptoCtx, HwError, HwProcessingError, HwRpcError, WithHwRpcError}; use derive_more::Display; use enum_derives::EnumFromTrait; -use kdf_walletconnect::WalletConnectCtx; use mm2_core::mm_ctx::{MmArc, MmCtx}; use mm2_err_handle::common_errors::InternalError; use mm2_err_handle::prelude::*; @@ -419,10 +418,6 @@ pub async fn lp_init_continue(ctx: MmArc) -> MmInitResult<()> { #[cfg(target_arch = "wasm32")] init_wasm_event_streaming(&ctx); - // This function spawns related WalletConnect related tasks needed for initialization before - // WalletConnect can be usable in KDF. - WalletConnectCtx::from_ctx(&ctx).mm_err(|err| MmInitError::WalletInitError(err.to_string()))?; - ctx.spawner().spawn(clean_memory_loop(ctx.weak())); Ok(()) From f714ff648ff36509df3edbbb694166ea699316e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 12 Jun 2025 09:57:52 +0300 Subject: [PATCH 27/36] feat(ibc-routing-part-2): supporting entire Cosmos network for swaps (#2476) ### Adds pre-swap checks and order metadata This change is the second part of implementing IBC-routed swaps for the Cosmos ecosystem. It introduces pre-swap validation mechanisms to prevent users from entering swaps that are likely to fail due to misconfigured or unhealthy IBC channels. The key additions include: * **Pre-Swap Validation:** A new `pre_check_for_order_creation` method has been integrated into the `buy` and `sell` workflows. For non-HTLC Tendermint coins, this check verifies: * The existence of a healthy IBC channel to an HTLC-enabled chain (e.g., IRIS, NUCLEUS). * A sufficient balance on the HTLC chain to cover potential routing fees, configured via `min_balance_for_ibc_routing`. * **Order Metadata:** A new `order_metadata` field has been added to `MakerOrder` and `TakerRequest`. For non-HTLC swaps, this field carries the `channel_id`, allowing the counterparty to validate the proposed swap route before proceeding. * **Tendermint Refactoring:** To support these features, the Tendermint coin implementation has been significantly refactored: * Protocol-specific fields (`denom`, `chain_id`, `account_prefix`, etc.) have been consolidated into a `TendermintProtocolInfo` struct for better organization. * Type safety has been improved by using `cosmrs::Denom` and `cosmrs::ChainId` instead of generic `String` types. * Hardcoded prefixes like "iaa" and "nuc" have been replaced with shared constants (`IRIS_PREFIX`, `NUCLEUS_PREFIX`). All changes are gated behind the `ibc-routing-for-swaps` feature flag and do not affect existing functionality in default builds. --- mm2src/coins/tendermint/htlc/mod.rs | 4 +- mm2src/coins/tendermint/tendermint_coin.rs | 365 +++++++++++------- mm2src/coins/tendermint/tendermint_token.rs | 15 +- mm2src/coins/tendermint/wallet_connect.rs | 2 +- .../src/tendermint_token_activation.rs | 1 - .../src/tendermint_with_assets_activation.rs | 14 +- mm2src/mm2_main/src/lp_ordermatch.rs | 117 +++++- .../src/lp_ordermatch/my_orders_storage.rs | 4 + mm2src/mm2_main/src/lp_swap/maker_swap.rs | 1 + .../src/lp_swap/recreate_swap_data.rs | 1 + mm2src/mm2_main/src/lp_swap/taker_swap.rs | 3 + mm2src/mm2_main/src/ordermatch_tests.rs | 104 +++++ .../tests/docker_tests/eth_docker_tests.rs | 4 +- mm2src/mm2_test_helpers/src/electrums.rs | 6 +- mm2src/mm2_test_helpers/src/for_tests.rs | 2 + 15 files changed, 454 insertions(+), 189 deletions(-) diff --git a/mm2src/coins/tendermint/htlc/mod.rs b/mm2src/coins/tendermint/htlc/mod.rs index 5675c915f2..77b38b8208 100644 --- a/mm2src/coins/tendermint/htlc/mod.rs +++ b/mm2src/coins/tendermint/htlc/mod.rs @@ -33,8 +33,8 @@ impl FromStr for HtlcType { fn from_str(s: &str) -> Result { match s { - "iaa" => Ok(HtlcType::Iris), - "nuc" => Ok(HtlcType::Nucleus), + super::IRIS_PREFIX => Ok(HtlcType::Iris), + super::NUCLEUS_PREFIX => Ok(HtlcType::Nucleus), unsupported => Err(io::Error::new( io::ErrorKind::Unsupported, format!("Account type '{unsupported}' is not supported for HTLCs"), diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 8e4934201b..b442f94ab2 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -106,6 +106,9 @@ const ABCI_DELEGATOR_UNDELEGATIONS_PATH: &str = "/cosmos.staking.v1beta1.Query/D const ABCI_DELEGATION_REWARDS_PATH: &str = "/cosmos.distribution.v1beta1.Query/DelegationRewards"; const ABCI_IBC_CHANNEL_QUERY_PATH: &str = "/ibc.core.channel.v1.Query/Channel"; +#[cfg(feature = "ibc-routing-for-swaps")] +const DEFAULT_MIN_BALANCE_FOR_IBC_ROUTING: f32 = 2.0; + pub(crate) const MIN_TX_SATOSHIS: i64 = 1; // ABCI Request Defaults @@ -125,6 +128,9 @@ const MIN_TIME_LOCK: i64 = 50; const ACCOUNT_SEQUENCE_ERR: &str = "account sequence mismatch"; +pub(crate) const IRIS_PREFIX: &str = "iaa"; +pub(crate) const NUCLEUS_PREFIX: &str = "nuc"; + lazy_static! { static ref SEQUENCE_PARSER_REGEX: Regex = Regex::new(r"expected (\d+)").unwrap(); } @@ -194,10 +200,13 @@ pub struct TendermintFeeDetails { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct TendermintProtocolInfo { pub decimals: u8, - denom: String, + pub(crate) denom: Denom, + min_balance_for_ibc_routing: Option, pub account_prefix: String, - pub chain_id: String, + pub chain_id: ChainId, gas_price: Option, + /// Key represents the account prefix of the target chain and + /// the value is the channel ID used for sending transactions. #[serde(default)] ibc_channels: HashMap, } @@ -395,21 +404,16 @@ pub struct TendermintCoinImpl { avg_blocktime: u8, /// My address pub account_id: AccountId, - pub(super) account_prefix: String, pub activation_policy: TendermintActivationPolicy, - pub(crate) decimals: u8, - pub(super) denom: Denom, - pub(crate) chain_id: ChainId, - gas_price: Option, pub tokens_info: PaMutex>, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation /// or on [`MmArc::stop`]. pub(super) abortable_system: AbortableQueue, pub(crate) history_sync_state: Mutex, client: TendermintRpcClient, - ibc_channels: HashMap, pub ctx: MmWeak, pub(crate) wallet_type: TendermintWalletConnectionType, + pub(crate) protocol_info: TendermintProtocolInfo, } #[derive(Clone)] @@ -435,7 +439,6 @@ pub enum TendermintInitErrorKind { EmptyRpcUrls, RpcClientInitError(String), InvalidChainId(String), - InvalidDenom(String), InvalidProtocolData(String), InvalidPathToAddress(String), #[display(fmt = "'derivation_path' field is not found in config")] @@ -651,7 +654,7 @@ impl From for SearchForSwapTxSpendErr { #[async_trait] impl TendermintCommons for TendermintCoin { - fn platform_denom(&self) -> &Denom { &self.denom } + fn platform_denom(&self) -> &Denom { &self.protocol_info.denom } fn set_history_sync_state(&self, new_state: HistorySyncState) { *self.history_sync_state.lock().unwrap() = new_state; @@ -666,7 +669,7 @@ impl TendermintCommons for TendermintCoin { } fn denom_to_ticker(&self, denom: &str) -> Option { - if self.denom.as_ref() == denom { + if self.protocol_info.denom.as_ref() == denom { return Some(self.ticker.clone()); } @@ -682,9 +685,9 @@ impl TendermintCommons for TendermintCoin { async fn get_all_balances(&self) -> MmResult { let platform_balance_denom = self - .account_balance_for_denom(&self.account_id, self.denom.to_string()) + .account_balance_for_denom(&self.account_id, self.protocol_info.denom.to_string()) .await?; - let platform_balance = big_decimal_from_sat_unsigned(platform_balance_denom, self.decimals); + let platform_balance = big_decimal_from_sat_unsigned(platform_balance_denom, self.protocol_info.decimals); let ibc_assets_info = self.tokens_info.lock().clone(); let mut requests = Vec::with_capacity(ibc_assets_info.len()); @@ -747,16 +750,6 @@ impl TendermintCoin { let client_impl = TendermintRpcClientImpl { rpc_clients }; - let chain_id = ChainId::try_from(protocol_info.chain_id).map_to_mm(|e| TendermintInitError { - ticker: ticker.clone(), - kind: TendermintInitErrorKind::InvalidChainId(e.to_string()), - })?; - - let denom = Denom::from_str(&protocol_info.denom).map_to_mm(|e| TendermintInitError { - ticker: ticker.clone(), - kind: TendermintInitErrorKind::InvalidDenom(e.to_string()), - })?; - let history_sync_state = if tx_history { HistorySyncState::NotStarted } else { @@ -776,18 +769,13 @@ impl TendermintCoin { Ok(TendermintCoin(Arc::new(TendermintCoinImpl { ticker, account_id, - account_prefix: protocol_info.account_prefix, activation_policy, - decimals: protocol_info.decimals, - denom, - chain_id, - gas_price: protocol_info.gas_price, avg_blocktime: conf.avg_blocktime, tokens_info: PaMutex::new(HashMap::new()), abortable_system, history_sync_state: Mutex::new(history_sync_state), client: TendermintRpcClient(AsyncMutex::new(client_impl)), - ibc_channels: protocol_info.ibc_channels, + protocol_info, ctx: ctx.weak(), wallet_type, }))) @@ -827,20 +815,32 @@ impl TendermintCoin { response.channel.ok_or(IBCError::IBCChannelMissingOnNode { channel_id }) } + /// Looks for a healthy IBC channel on a network that supports HTLC transactions. + /// Right now it first tries to find a channel on IRIS network, if none is found, then falls + /// back to NUCLEUS network. + pub async fn get_healthy_ibc_channel_to_htlc_chain(&self) -> Result> { + let channel_id = if let Ok(channel_id) = self.get_healthy_ibc_channel_for_address_prefix(IRIS_PREFIX).await { + channel_id + } else { + self.get_healthy_ibc_channel_for_address_prefix(NUCLEUS_PREFIX).await? + }; + + Ok(channel_id) + } + /// Returns a **healthy** IBC channel ID for the given target address. - pub(crate) async fn get_healthy_ibc_channel_for_address( + pub async fn get_healthy_ibc_channel_for_address_prefix( &self, address_prefix: &str, ) -> 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_id = *self - .ibc_channels - .get(address_prefix) - .ok_or_else(|| IBCError::IBCChannelCouldNotBeFound { + let channel_id = *self.protocol_info.ibc_channels.get(address_prefix).ok_or_else(|| { + IBCError::IBCChannelCouldNotBeFound { address_prefix: address_prefix.to_owned(), - })?; + } + })?; let channel = self.query_ibc_channel(channel_id, "transfer").await?; @@ -856,8 +856,12 @@ impl TendermintCoin { Ok(channel_id) } + pub fn supports_htlc(&self) -> bool { + matches!(self.protocol_info.account_prefix.as_str(), NUCLEUS_PREFIX | IRIS_PREFIX) + } + #[inline(always)] - fn gas_price(&self) -> f64 { self.gas_price.unwrap_or(DEFAULT_GAS_PRICE) } + fn gas_price(&self) -> f64 { self.protocol_info.gas_price.unwrap_or(DEFAULT_GAS_PRICE) } #[allow(unused)] async fn get_latest_block(&self) -> MmResult { @@ -901,7 +905,7 @@ impl TendermintCoin { memo: &str, ) -> cosmrs::Result> { let fee_amount = Coin { - denom: self.denom.clone(), + denom: self.protocol_info.denom.clone(), amount: 0_u64.into(), }; @@ -910,7 +914,12 @@ impl TendermintCoin { let signkey = SigningKey::from_slice(priv_key.as_slice())?; let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); let auth_info = SignerInfo::single_direct(Some(signkey.public_key()), account_info.sequence).auth_info(fee); - let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; + let sign_doc = SignDoc::new( + &tx_body, + &auth_info, + &self.protocol_info.chain_id, + account_info.account_number, + )?; sign_doc.sign(&signkey)?.to_bytes() } @@ -1254,10 +1263,10 @@ impl TendermintCoin { .account .or_mm_err(|| TendermintCoinRpcError::InvalidResponse("Account is None".into()))?; - let account_prefix = self.account_prefix.clone(); + let account_prefix = self.protocol_info.account_prefix.clone(); let base_account = match BaseAccount::decode(account.value.as_slice()) { Ok(account) => account, - Err(err) if account_prefix.as_str() == "iaa" => { + Err(err) if account_prefix.as_str() == IRIS_PREFIX => { let ethermint_account = EthermintAccount::decode(account.value.as_slice())?; ethermint_account @@ -1324,7 +1333,7 @@ impl TendermintCoin { .hd_wallet_derived_priv_key_or_err(&path_to_address) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &self.account_prefix) + let account_id = account_id_from_privkey(priv_key.as_slice(), &self.protocol_info.account_prefix) .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?; Ok((account_id, Some(priv_key))) }, @@ -1394,10 +1403,10 @@ impl TendermintCoin { let amount = vec![Coin { denom, amount }]; let timestamp = 0_u64; - let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + let htlc_type = HtlcType::from_str(&self.protocol_info.account_prefix).map_err(|_| { TxMarshalingErr::NotSupported(format!( "Account type '{}' is not supported for HTLCs", - self.account_prefix + self.protocol_info.account_prefix )) })?; @@ -1422,10 +1431,10 @@ impl TendermintCoin { } fn gen_claim_htlc_tx(&self, htlc_id: String, secret: &[u8]) -> MmResult { - let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + let htlc_type = HtlcType::from_str(&self.protocol_info.account_prefix).map_err(|_| { TxMarshalingErr::NotSupported(format!( "Account type '{}' is not supported for HTLCs", - self.account_prefix + self.protocol_info.account_prefix )) })?; @@ -1451,7 +1460,12 @@ impl TendermintCoin { let signkey = SigningKey::from_slice(priv_key.as_slice())?; let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); let auth_info = SignerInfo::single_direct(Some(signkey.public_key()), account_info.sequence).auth_info(fee); - let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; + let sign_doc = SignDoc::new( + &tx_body, + &auth_info, + &self.protocol_info.chain_id, + account_info.account_number, + )?; sign_doc.sign(&signkey) } @@ -1466,7 +1480,12 @@ impl TendermintCoin { let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); let pubkey = self.activation_policy.public_key()?.into(); let auth_info = SignerInfo::single_direct(Some(pubkey), account_info.sequence).auth_info(fee); - let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; + let sign_doc = SignDoc::new( + &tx_body, + &auth_info, + &self.protocol_info.chain_id, + account_info.account_number, + )?; let tx_json = if self.is_wallet_connect() { let ctx = MmArc::from_weak(&self.ctx).expect("No context"); @@ -1568,7 +1587,7 @@ impl TendermintCoin { let sign_doc = json!({ "account_number": account_info.account_number.to_string(), - "chain_id": self.chain_id.to_string(), + "chain_id": self.protocol_info.chain_id.to_string(), "fee": { "amount": fee_amount, "gas": fee.gas_limit.to_string() @@ -1637,7 +1656,10 @@ impl TendermintCoin { }]; let pubkey_hash = dhash160(other_pub); - let to_address = try_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); + let to_address = try_fus!(AccountId::new( + &self.protocol_info.account_prefix, + pubkey_hash.as_slice() + )); let htlc_id = self.calculate_htlc_id(&self.account_id, &to_address, &amount, secret_hash); @@ -1681,7 +1703,7 @@ 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!(CreateHtlcProto::decode( - try_s!(HtlcType::from_str(&coin.account_prefix)), + try_s!(HtlcType::from_str(&coin.protocol_info.account_prefix)), msg.value.as_slice() )); @@ -1713,7 +1735,10 @@ impl TendermintCoin { decimals: u8, ) -> TransactionFut { let pubkey_hash = dhash160(other_pub); - let to = try_tx_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); + let to = try_tx_fus!(AccountId::new( + &self.protocol_info.account_prefix, + pubkey_hash.as_slice() + )); let amount_as_u64 = try_tx_fus!(sat_from_big_decimal(&amount, decimals)); let amount = cosmrs::Amount::from(amount_as_u64); @@ -1769,8 +1794,14 @@ impl TendermintCoin { let from_address = self.account_id.clone(); let dex_pubkey_hash = dhash160(self.dex_pubkey()); let burn_pubkey_hash = dhash160(self.burn_pubkey()); - let dex_address = try_tx_fus!(AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice())); - let burn_address = try_tx_fus!(AccountId::new(&self.account_prefix, burn_pubkey_hash.as_slice())); + let dex_address = try_tx_fus!(AccountId::new( + &self.protocol_info.account_prefix, + dex_pubkey_hash.as_slice() + )); + let burn_address = try_tx_fus!(AccountId::new( + &self.protocol_info.account_prefix, + burn_pubkey_hash.as_slice() + )); let fee_amount_as_u64 = try_tx_fus!(dex_fee.fee_amount_as_u64(decimals)); let fee_amount = vec![Coin { @@ -1871,8 +1902,11 @@ impl TendermintCoin { .to_string(); let sender_pubkey_hash = dhash160(expected_sender); - let expected_sender_address = try_f!(AccountId::new(&self.account_prefix, sender_pubkey_hash.as_slice()) - .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))); + let expected_sender_address = try_f!(AccountId::new( + &self.protocol_info.account_prefix, + sender_pubkey_hash.as_slice() + ) + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))); let coin = self.clone(); let dex_fee = dex_fee.clone(); @@ -1940,10 +1974,10 @@ impl TendermintCoin { "Payment tx must have exactly one message".into(), )); } - let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + let htlc_type = HtlcType::from_str(&self.protocol_info.account_prefix).map_err(|_| { ValidatePaymentError::InvalidParameter(format!( "Account type '{}' is not supported for HTLCs", - self.account_prefix + self.protocol_info.account_prefix )) })?; @@ -1953,7 +1987,7 @@ impl TendermintCoin { .map_to_mm(|e| ValidatePaymentError::WrongPaymentTx(e.to_string()))?; let sender_pubkey_hash = dhash160(&input.other_pub); - let sender = AccountId::new(&self.account_prefix, sender_pubkey_hash.as_slice()) + let sender = AccountId::new(&self.protocol_info.account_prefix, sender_pubkey_hash.as_slice()) .map_to_mm(|e| ValidatePaymentError::InvalidParameter(e.to_string()))?; let amount = sat_from_big_decimal(&input.amount, decimals)?; @@ -2020,7 +2054,7 @@ impl TendermintCoin { } let dex_pubkey_hash = dhash160(self.dex_pubkey()); - let expected_dex_address = AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice()) + let expected_dex_address = AccountId::new(&self.protocol_info.account_prefix, dex_pubkey_hash.as_slice()) .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; let fee_amount_as_u64 = dex_fee.fee_amount_as_u64(decimals)?; @@ -2072,11 +2106,11 @@ impl TendermintCoin { } let dex_pubkey_hash = dhash160(self.dex_pubkey()); - let expected_dex_address = AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice()) + let expected_dex_address = AccountId::new(&self.protocol_info.account_prefix, dex_pubkey_hash.as_slice()) .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; let burn_pubkey_hash = dhash160(self.burn_pubkey()); - let expected_burn_address = AccountId::new(&self.account_prefix, burn_pubkey_hash.as_slice()) + let expected_burn_address = AccountId::new(&self.protocol_info.account_prefix, burn_pubkey_hash.as_slice()) .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; let fee_amount_as_u64 = dex_fee.fee_amount_as_u64(decimals)?; @@ -2164,7 +2198,7 @@ impl TendermintCoin { common::os_rng(&mut sec).map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; drop_mutability!(sec); - let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) + let to_address = account_id_from_pubkey_hex(&self.protocol_info.account_prefix, DEX_FEE_ADDR_PUBKEY) .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let amount = sat_from_big_decimal(&amount, decimals)?; @@ -2198,7 +2232,7 @@ impl TendermintCoin { ) .await?; - let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, self.decimals); + let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, self.protocol_info.decimals); Ok(TradeFee { coin: ticker, @@ -2214,7 +2248,7 @@ impl TendermintCoin { decimals: u8, dex_fee_amount: DexFee, ) -> TradePreimageResult { - let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) + let to_address = account_id_from_pubkey_hex(&self.protocol_info.account_prefix, DEX_FEE_ADDR_PUBKEY) .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let amount = sat_from_big_decimal(&dex_fee_amount.fee_amount().into(), decimals)?; @@ -2323,10 +2357,11 @@ impl TendermintCoin { } 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 htlc_type = HtlcType::from_str(&self.protocol_info.account_prefix).map_err(|_| { + TendermintCoinRpcError::UnexpectedAccountType { + prefix: self.protocol_info.account_prefix.clone(), + } + })?; let request = QueryHtlcRequestProto { id }; let response = self @@ -2360,10 +2395,11 @@ impl TendermintCoin { .first() .or_mm_err(|| SearchForSwapTxSpendErr::TxMessagesEmpty)?; - let htlc_type = - HtlcType::from_str(&self.account_prefix).map_err(|_| SearchForSwapTxSpendErr::UnexpectedAccountType { - prefix: self.account_prefix.clone(), - })?; + let htlc_type = HtlcType::from_str(&self.protocol_info.account_prefix).map_err(|_| { + SearchForSwapTxSpendErr::UnexpectedAccountType { + prefix: self.protocol_info.account_prefix.clone(), + } + })?; let htlc_proto = CreateHtlcProto::decode(htlc_type, first_message.value.as_slice())?; let htlc = CreateHtlcMsg::try_from(htlc_proto)?; @@ -2431,8 +2467,8 @@ impl TendermintCoin { } pub(crate) fn active_ticker_and_decimals_from_denom(&self, denom: &str) -> Option<(String, u8)> { - if self.denom.as_ref() == denom { - return Some((self.ticker.clone(), self.decimals)); + if self.protocol_info.denom.as_ref() == denom { + return Some((self.ticker.clone(), self.protocol_info.decimals)); } let tokens = self.tokens_info.lock(); @@ -2542,7 +2578,7 @@ impl TendermintCoin { return Err(not_sufficient(total)); } - let amount_u64 = sat_from_big_decimal(&request_amount, coin.decimals) + let amount_u64 = sat_from_big_decimal(&request_amount, coin.protocol_info.decimals) .map_err(|e| DelegationError::InternalError(e.to_string()))?; Ok((amount_u64, total)) @@ -2556,13 +2592,13 @@ impl TendermintCoin { .map_err(|e| DelegationError::InternalError(e.to_string()))?; let (balance_u64, balance_dec) = self - .get_balance_as_unsigned_and_decimal(&delegator_address, &self.denom, self.decimals()) + .get_balance_as_unsigned_and_decimal(&delegator_address, &self.protocol_info.denom, self.decimals()) .await?; let amount_u64 = if req.max { balance_u64 } else { - sat_from_big_decimal(&req.amount, self.decimals) + sat_from_big_decimal(&req.amount, self.protocol_info.decimals) .map_err(|e| DelegationError::InternalError(e.to_string()))? }; @@ -2570,7 +2606,7 @@ impl TendermintCoin { let msg_for_fee_prediction = generate_message( delegator_address.clone(), validator_address.clone(), - self.denom.clone(), + self.protocol_info.denom.clone(), amount_u64.into(), ) .map_err(|e| DelegationError::InternalError(e.to_string()))?; @@ -2601,7 +2637,7 @@ impl TendermintCoin { let fee = Fee::from_amount_and_gas( Coin { - denom: self.denom.clone(), + denom: self.protocol_info.denom.clone(), amount: fee_amount_u64.into(), }, gas_limit, @@ -2620,7 +2656,7 @@ impl TendermintCoin { let msg_for_actual_tx = generate_message( delegator_address.clone(), validator_address.clone(), - self.denom.clone(), + self.protocol_info.denom.clone(), amount_u64.into(), ) .map_err(|e| DelegationError::InternalError(e.to_string()))?; @@ -2699,14 +2735,14 @@ impl TendermintCoin { }); }; - sat_from_big_decimal(&req.amount, self.decimals) + sat_from_big_decimal(&req.amount, self.protocol_info.decimals) .map_err(|e| DelegationError::InternalError(e.to_string()))? }; let undelegate_msg = generate_message( delegator_address.clone(), validator_address.clone(), - self.denom.clone(), + self.protocol_info.denom.clone(), uamount_to_undelegate.into(), ) .map_err(|e| DelegationError::InternalError(e.to_string()))?; @@ -2747,7 +2783,7 @@ impl TendermintCoin { let fee = Fee::from_amount_and_gas( Coin { - denom: self.denom.clone(), + denom: self.protocol_info.denom.clone(), amount: fee_amount_u64.into(), }, gas_limit, @@ -2872,9 +2908,9 @@ impl TendermintCoin { match decoded_response .rewards .iter() - .find(|t| t.denom == self.denom.to_string()) + .find(|t| t.denom == self.protocol_info.denom.to_string()) { - Some(dec_coin) => extract_big_decimal_from_dec_coin(dec_coin, self.decimals as u32) + Some(dec_coin) => extract_big_decimal_from_dec_coin(dec_coin, self.protocol_info.decimals as u32) .map_to_mm(|e| DelegationError::InternalError(e.to_string())), None => MmError::err(DelegationError::NothingToClaim { coin: self.ticker.clone(), @@ -2951,7 +2987,7 @@ impl TendermintCoin { let fee = Fee::from_amount_and_gas( Coin { - denom: self.denom.clone(), + denom: self.protocol_info.denom.clone(), amount: fee_amount_u64.into(), }, gas_limit, @@ -3192,24 +3228,28 @@ impl MmCoin for TendermintCoin { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - let is_ibc_transfer = to_address.prefix() != coin.account_prefix || req.ibc_source_channel.is_some(); + let is_ibc_transfer = + to_address.prefix() != coin.protocol_info.account_prefix || req.ibc_source_channel.is_some(); let (account_id, maybe_priv_key) = coin .extract_account_id_and_private_key(req.from) .map_err(|e| WithdrawError::InternalError(e.to_string()))?; let (balance_denom, balance_dec) = coin - .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) + .get_balance_as_unsigned_and_decimal(&account_id, &coin.protocol_info.denom, coin.decimals()) .await?; let (amount_denom, amount_dec) = if req.max { let amount_denom = balance_denom; - (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) + ( + amount_denom, + big_decimal_from_sat_unsigned(amount_denom, coin.decimals()), + ) } else { - (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) + (sat_from_big_decimal(&req.amount, coin.decimals())?, req.amount.clone()) }; - if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { + if !coin.is_tx_amount_enough(coin.decimals(), &amount_dec) { return MmError::err(WithdrawError::AmountTooLow { amount: amount_dec, threshold: coin.min_tx_amount(), @@ -3225,7 +3265,10 @@ 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_healthy_ibc_channel_for_address(to_address.prefix()).await?), + None => Some( + coin.get_healthy_ibc_channel_for_address_prefix(to_address.prefix()) + .await?, + ), } } else { None @@ -3234,7 +3277,7 @@ impl MmCoin for TendermintCoin { let msg_payload = create_withdraw_msg_as_any( account_id.clone(), to_address.clone(), - &coin.denom, + &coin.protocol_info.denom, amount_denom, channel_id, ) @@ -3283,7 +3326,7 @@ impl MmCoin for TendermintCoin { let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); let fee_amount = Coin { - denom: coin.denom.clone(), + denom: coin.protocol_info.denom.clone(), amount: fee_amount_u64.into(), }; @@ -3309,13 +3352,13 @@ impl MmCoin for TendermintCoin { }); } - (sat_from_big_decimal(&req.amount, coin.decimals)?, total) + (sat_from_big_decimal(&req.amount, coin.decimals())?, total) }; let msg_payload = create_withdraw_msg_as_any( account_id.clone(), to_address.clone(), - &coin.denom, + &coin.protocol_info.denom, amount_denom, channel_id, ) @@ -3388,7 +3431,7 @@ impl MmCoin for TendermintCoin { Box::new(fut.boxed().compat()) } - fn decimals(&self) -> u8 { self.decimals } + fn decimals(&self) -> u8 { self.protocol_info.decimals } fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { // TODO @@ -3428,8 +3471,13 @@ impl MmCoin for TendermintCoin { let amount = match value { TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => decimal, }; - self.get_sender_trade_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, amount) - .await + self.get_sender_trade_fee_for_denom( + self.ticker.clone(), + self.protocol_info.denom.clone(), + self.protocol_info.decimals, + amount, + ) + .await } /// Overrides the default `pre_check_for_order_creation` implementation with @@ -3471,17 +3519,22 @@ impl MmCoin for TendermintCoin { coin: &TendermintCoin, ctx: &MmArc, ) -> Result, MmError> { - const IRIS_PREFIX: &str = "iaa"; const IRIS_TICKER: &str = "IRIS"; - - const NUCLEUS_PREFIX: &str = "nuc"; const NUCLEUS_TICKER: &str = "NUCLEUS"; - if coin.get_healthy_ibc_channel_for_address(IRIS_PREFIX).await.is_ok() { + if coin + .get_healthy_ibc_channel_for_address_prefix(IRIS_PREFIX) + .await + .is_ok() + { return find_tendermint_platform_coin(ctx, IRIS_TICKER).await; } - if coin.get_healthy_ibc_channel_for_address(NUCLEUS_PREFIX).await.is_ok() { + if coin + .get_healthy_ibc_channel_for_address_prefix(NUCLEUS_PREFIX) + .await + .is_ok() + { return find_tendermint_platform_coin(ctx, NUCLEUS_TICKER).await; } @@ -3502,9 +3555,7 @@ impl MmCoin for TendermintCoin { }); } - let supports_htlc = matches!(self.account_prefix.as_str(), "nuc" | "iaa"); - - if supports_htlc { + if self.supports_htlc() { return Ok(()); } @@ -3526,14 +3577,18 @@ impl MmCoin for TendermintCoin { .map_err(|e| OrderCreationPreCheckError::InternalError { reason: e.to_string() })? .spendable; - // TODO: Take this value from the coins file. - let min = BigDecimal::from(2); + let min_balance_for_ibc_routing = htlc_coin + .protocol_info + .min_balance_for_ibc_routing + .unwrap_or(DEFAULT_MIN_BALANCE_FOR_IBC_ROUTING); + let min_balance_for_ibc_routing = BigDecimal::try_from(min_balance_for_ibc_routing) + .map_err(|e| OrderCreationPreCheckError::InternalError { reason: e.to_string() })?; - if min > my_balance { + if min_balance_for_ibc_routing > my_balance { let htlc_ticker = htlc_coin.ticker(); let self_ticker = self.ticker(); let reason = format!( - "Insufficient balance on HTLC coin ({htlc_ticker}) for making orders with {self_ticker}. Minimum required expected balance {min}, current balance {my_balance}.", + "Insufficient balance on HTLC coin ({htlc_ticker}) for making orders with {self_ticker}. Minimum required expected balance {min_balance_for_ibc_routing}, current balance {my_balance}.", ); return MmError::err(OrderCreationPreCheckError::PreCheckFailed { reason }); } @@ -3548,8 +3603,8 @@ impl MmCoin for TendermintCoin { // Since create and claim htlc fees are almost same, we can simply simulate create htlc tx. coin.get_sender_trade_fee_for_denom( coin.ticker.clone(), - coin.denom.clone(), - coin.decimals, + coin.protocol_info.denom.clone(), + coin.decimals(), coin.min_tx_amount(), ) .await @@ -3562,8 +3617,13 @@ impl MmCoin for TendermintCoin { dex_fee_amount: DexFee, _stage: FeeApproxStage, ) -> TradePreimageResult { - self.get_fee_to_send_taker_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, dex_fee_amount) - .await + self.get_fee_to_send_taker_fee_for_denom( + self.ticker.clone(), + self.protocol_info.denom.clone(), + self.protocol_info.decimals, + dex_fee_amount, + ) + .await } fn required_confirmations(&self) -> u64 { 0 } @@ -3606,7 +3666,7 @@ impl MarketCoinOps for TendermintCoin { fn my_address(&self) -> MmResult { Ok(self.account_id.to_string()) } fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { - let address = account_id_from_raw_pubkey(&self.account_prefix, &pubkey.0) + let address = account_id_from_raw_pubkey(&self.protocol_info.account_prefix, &pubkey.0) .map_err(|e| AddressFromPubkeyError::InternalError(e.to_string()))?; Ok(address.to_string()) } @@ -3636,10 +3696,10 @@ impl MarketCoinOps for TendermintCoin { let coin = self.clone(); let fut = async move { let balance_denom = coin - .account_balance_for_denom(&coin.account_id, coin.denom.to_string()) + .account_balance_for_denom(&coin.account_id, coin.protocol_info.denom.to_string()) .await?; Ok(CoinBalance { - spendable: big_decimal_from_sat_unsigned(balance_denom, coin.decimals), + spendable: big_decimal_from_sat_unsigned(balance_denom, coin.decimals()), unspendable: BigDecimal::default(), }) }; @@ -3738,7 +3798,7 @@ impl MarketCoinOps for TendermintCoin { let tx = try_tx_s!(cosmrs::Tx::from_bytes(args.tx_bytes)); let first_message = try_tx_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc_proto = try_tx_s!(CreateHtlcProto::decode( - try_tx_s!(HtlcType::from_str(&self.account_prefix)), + try_tx_s!(HtlcType::from_str(&self.protocol_info.account_prefix)), first_message.value.as_slice() )); let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); @@ -3797,7 +3857,7 @@ impl MarketCoinOps for TendermintCoin { } #[inline] - fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat(MIN_TX_SATOSHIS, self.decimals) } + fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat(MIN_TX_SATOSHIS, self.protocol_info.decimals) } #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } @@ -3817,9 +3877,15 @@ impl MarketCoinOps for TendermintCoin { #[allow(unused_variables)] impl SwapOps for TendermintCoin { async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { - self.send_taker_fee_for_denom(&dex_fee, self.denom.clone(), self.decimals, uuid, expire_at) - .compat() - .await + self.send_taker_fee_for_denom( + &dex_fee, + self.protocol_info.denom.clone(), + self.protocol_info.decimals, + uuid, + expire_at, + ) + .compat() + .await } async fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionResult { @@ -3828,8 +3894,8 @@ impl SwapOps for TendermintCoin { maker_payment_args.other_pubkey, maker_payment_args.secret_hash, maker_payment_args.amount, - self.denom.clone(), - self.decimals, + self.protocol_info.denom.clone(), + self.protocol_info.decimals, ) .compat() .await @@ -3841,8 +3907,8 @@ impl SwapOps for TendermintCoin { taker_payment_args.other_pubkey, taker_payment_args.secret_hash, taker_payment_args.amount, - self.denom.clone(), - self.decimals, + self.protocol_info.denom.clone(), + self.protocol_info.decimals, ) .compat() .await @@ -3861,7 +3927,7 @@ impl SwapOps for TendermintCoin { let msg = try_tx_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc_proto = try_tx_s!(CreateHtlcProto::decode( - try_tx_s!(HtlcType::from_str(&self.account_prefix)), + try_tx_s!(HtlcType::from_str(&self.protocol_info.account_prefix)), msg.value.as_slice() )); let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); @@ -3917,7 +3983,7 @@ impl SwapOps for TendermintCoin { let msg = try_tx_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc_proto = try_tx_s!(CreateHtlcProto::decode( - try_tx_s!(HtlcType::from_str(&self.account_prefix)), + try_tx_s!(HtlcType::from_str(&self.protocol_info.account_prefix)), msg.value.as_slice() )); let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); @@ -3982,21 +4048,21 @@ impl SwapOps for TendermintCoin { validate_fee_args.fee_tx, validate_fee_args.expected_sender, validate_fee_args.dex_fee, - self.decimals, + self.protocol_info.decimals, validate_fee_args.uuid, - self.denom.to_string(), + self.protocol_info.denom.to_string(), ) .compat() .await } async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { - self.validate_payment_for_denom(input, self.denom.clone(), self.decimals) + self.validate_payment_for_denom(input, self.protocol_info.denom.clone(), self.protocol_info.decimals) .await } async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { - self.validate_payment_for_denom(input, self.denom.clone(), self.decimals) + self.validate_payment_for_denom(input, self.protocol_info.denom.clone(), self.protocol_info.decimals) .await } @@ -4005,8 +4071,8 @@ impl SwapOps for TendermintCoin { if_my_payment_sent_args: CheckIfMyPaymentSentArgs<'_>, ) -> Result, String> { self.check_if_my_payment_sent_for_denom( - self.decimals, - self.denom.clone(), + self.protocol_info.decimals, + self.protocol_info.denom.clone(), if_my_payment_sent_args.other_pub, if_my_payment_sent_args.secret_hash, if_my_payment_sent_args.amount, @@ -4039,7 +4105,7 @@ impl SwapOps for TendermintCoin { let msg = try_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc_proto = try_s!(ClaimHtlcProto::decode( - try_s!(HtlcType::from_str(&self.account_prefix)), + try_s!(HtlcType::from_str(&self.protocol_info.account_prefix)), msg.value.as_slice() )); let htlc = try_s!(ClaimHtlcMsg::try_from(htlc_proto)); @@ -4254,9 +4320,10 @@ pub mod tendermint_falsecoin_tests { fn get_iris_usdc_ibc_protocol() -> TendermintProtocolInfo { TendermintProtocolInfo { decimals: 6, - denom: String::from("ibc/5C465997B4F582F602CD64E12031C6A6E18CAF1E6EDC9B5D808822DC0B5F850C"), - account_prefix: String::from("iaa"), - chain_id: String::from("nyancat-9"), + denom: Denom::from_str("ibc/5C465997B4F582F602CD64E12031C6A6E18CAF1E6EDC9B5D808822DC0B5F850C").unwrap(), + min_balance_for_ibc_routing: None, + account_prefix: String::from(IRIS_PREFIX), + chain_id: ChainId::from_str("nyancat-9").unwrap(), gas_price: None, ibc_channels: HashMap::new(), } @@ -4268,9 +4335,10 @@ pub mod tendermint_falsecoin_tests { TendermintProtocolInfo { decimals: 6, - denom: String::from("unyan"), - account_prefix: String::from("iaa"), - chain_id: String::from("nyancat-9"), + denom: Denom::from_str("unyan").unwrap(), + min_balance_for_ibc_routing: None, + account_prefix: String::from(IRIS_PREFIX), + chain_id: ChainId::from_str("nyancat-9").unwrap(), gas_price: None, ibc_channels, } @@ -4279,9 +4347,10 @@ pub mod tendermint_falsecoin_tests { fn get_iris_ibc_nucleus_protocol() -> TendermintProtocolInfo { TendermintProtocolInfo { decimals: 6, - denom: String::from("ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C"), - account_prefix: String::from("nuc"), - chain_id: String::from("nucleus-testnet"), + denom: Denom::from_str("ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C").unwrap(), + min_balance_for_ibc_routing: None, + account_prefix: String::from(NUCLEUS_PREFIX), + chain_id: ChainId::from_str("nucleus-testnet").unwrap(), gas_price: None, ibc_channels: HashMap::new(), } @@ -4339,7 +4408,7 @@ pub mod tendermint_falsecoin_tests { // << BEGIN HTLC CREATION let to: AccountId = IRIS_TESTNET_HTLC_PAIR2_ADDRESS.parse().unwrap(); let amount = 1; - let amount_dec = big_decimal_from_sat_unsigned(amount, coin.decimals); + let amount_dec = big_decimal_from_sat_unsigned(amount, coin.decimals()); let mut sec = [0u8; 32]; common::os_rng(&mut sec).unwrap(); @@ -4349,7 +4418,7 @@ pub mod tendermint_falsecoin_tests { let create_htlc_tx = coin .gen_create_htlc_tx( - coin.denom.clone(), + coin.protocol_info.denom.clone(), &to, amount.into(), sha256(&sec).as_slice(), @@ -5345,7 +5414,7 @@ pub mod tendermint_falsecoin_tests { let expected_channel = ChannelId::new(0); let expected_channel_str = "channel-0"; - let actual_channel = block_on(coin.get_healthy_ibc_channel_for_address("cosmos")).unwrap(); + let actual_channel = block_on(coin.get_healthy_ibc_channel_for_address_prefix("cosmos")).unwrap(); let actual_channel_str = actual_channel.to_string(); assert_eq!(expected_channel, actual_channel); diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 4183473d6d..728541e9c3 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -59,7 +59,7 @@ impl Deref for TendermintToken { pub struct TendermintTokenProtocolInfo { pub platform: String, pub decimals: u8, - pub denom: String, + pub denom: Denom, } #[derive(Clone, Deserialize)] @@ -67,7 +67,6 @@ pub struct TendermintTokenActivationParams {} pub enum TendermintTokenInitError { Internal(String), - InvalidDenom(String), MyAddressError(String), CouldNotFetchBalance(String), } @@ -85,9 +84,8 @@ impl TendermintToken { ticker: String, platform_coin: TendermintCoin, decimals: u8, - denom: String, + denom: Denom, ) -> MmResult { - let denom = Denom::from_str(&denom).map_to_mm(|e| TendermintTokenInitError::InvalidDenom(e.to_string()))?; let token_impl = TendermintTokenImpl { abortable_system: platform_coin.abortable_system.create_subsystem()?, ticker, @@ -376,14 +374,15 @@ impl MmCoin for TendermintToken { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - let is_ibc_transfer = to_address.prefix() != platform.account_prefix || req.ibc_source_channel.is_some(); + let is_ibc_transfer = + to_address.prefix() != platform.protocol_info.account_prefix || req.ibc_source_channel.is_some(); let (account_id, maybe_priv_key) = platform .extract_account_id_and_private_key(req.from) .map_err(|e| WithdrawError::InternalError(e.to_string()))?; let (base_denom_balance, base_denom_balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) + .get_balance_as_unsigned_and_decimal(&account_id, &platform.protocol_info.denom, token.decimals()) .await?; let (balance_denom, balance_dec) = platform @@ -430,7 +429,7 @@ impl MmCoin for TendermintToken { Some(_) => req.ibc_source_channel, None => Some( platform - .get_healthy_ibc_channel_for_address(to_address.prefix()) + .get_healthy_ibc_channel_for_address_prefix(to_address.prefix()) .await?, ), } @@ -484,7 +483,7 @@ impl MmCoin for TendermintToken { } let fee_amount = Coin { - denom: platform.denom.clone(), + denom: platform.protocol_info.denom.clone(), amount: fee_amount_u64.into(), }; diff --git a/mm2src/coins/tendermint/wallet_connect.rs b/mm2src/coins/tendermint/wallet_connect.rs index 86229499ee..d3242d4f77 100644 --- a/mm2src/coins/tendermint/wallet_connect.rs +++ b/mm2src/coins/tendermint/wallet_connect.rs @@ -81,7 +81,7 @@ impl WalletConnectOps for TendermintCoin { type SendTxData = CosmosTransaction; async fn wc_chain_id(&self, wc: &WalletConnectCtx) -> Result { - let chain_id = WcChainId::new_cosmos(self.chain_id.to_string()); + let chain_id = WcChainId::new_cosmos(self.protocol_info.chain_id.to_string()); let session_topic = self.session_topic()?; wc.validate_update_active_chain_id(session_topic, &chain_id).await?; Ok(chain_id) diff --git a/mm2src/coins_activation/src/tendermint_token_activation.rs b/mm2src/coins_activation/src/tendermint_token_activation.rs index 12808505a9..8f31ef75b2 100644 --- a/mm2src/coins_activation/src/tendermint_token_activation.rs +++ b/mm2src/coins_activation/src/tendermint_token_activation.rs @@ -13,7 +13,6 @@ use std::collections::HashMap; impl From for EnableTokenError { fn from(err: TendermintTokenInitError) -> Self { match err { - TendermintTokenInitError::InvalidDenom(e) => EnableTokenError::InvalidConfig(e), TendermintTokenInitError::MyAddressError(e) | TendermintTokenInitError::Internal(e) => { EnableTokenError::Internal(e) }, diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index c371ec9564..349fce564a 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -124,10 +124,7 @@ struct TendermintTokenInitializer { platform_coin: TendermintCoin, } -struct TendermintTokenInitializerErr { - ticker: String, - inner: TendermintTokenInitError, -} +struct TendermintTokenInitializerErr(TendermintTokenInitError); #[async_trait] impl TokenInitializer for TendermintTokenInitializer { @@ -149,14 +146,13 @@ impl TokenInitializer for TendermintTokenInitializer { params .into_iter() .map(|param| { - let ticker = param.ticker.clone(); TendermintToken::new( param.ticker, self.platform_coin.clone(), param.protocol.decimals, param.protocol.denom, ) - .mm_err(|inner| TendermintTokenInitializerErr { ticker, inner }) + .mm_err(TendermintTokenInitializerErr) }) .collect() } @@ -184,11 +180,7 @@ impl TryFromCoinProtocol for TendermintTokenProtocolInfo { impl From for InitTokensAsMmCoinsError { fn from(err: TendermintTokenInitializerErr) -> Self { - match err.inner { - TendermintTokenInitError::InvalidDenom(error) => InitTokensAsMmCoinsError::TokenProtocolParseError { - ticker: err.ticker, - error, - }, + match err.0 { TendermintTokenInitError::MyAddressError(error) | TendermintTokenInitError::Internal(error) => { InitTokensAsMmCoinsError::Internal(error) }, diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index ab0928595b..9f57101d42 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -89,6 +89,9 @@ use crate::swap_versioning::{legacy_swap_version, SwapVersion}; #[cfg(any(test, feature = "run-docker-tests"))] use crate::lp_swap::taker_swap::FailAt; +#[cfg(feature = "ibc-routing-for-swaps")] +use coins::rpc_command::tendermint::ibc::ChannelId; + pub use best_orders::{best_orders_rpc, best_orders_rpc_v2}; use crypto::secret_hash_algo::SecretHashAlgo; pub use orderbook_depth::orderbook_depth_rpc; @@ -1195,6 +1198,8 @@ pub struct TakerRequest { pub rel_protocol_info: Option>, #[serde(default, skip_serializing_if = "SwapVersion::is_legacy")] pub swap_version: SwapVersion, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata, } impl TakerRequest { @@ -1216,6 +1221,9 @@ impl TakerRequest { base_protocol_info: message.base_protocol_info, rel_protocol_info: message.rel_protocol_info, swap_version: message.swap_version, + /// TODO: Support the new protocol types. + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), } } @@ -1288,6 +1296,8 @@ pub struct TakerOrderBuilder<'a> { timeout: u64, save_in_history: bool, swap_version: u8, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata, } pub enum TakerOrderBuildError { @@ -1368,6 +1378,8 @@ impl<'a> TakerOrderBuilder<'a> { timeout: TAKER_ORDER_TIMEOUT, save_in_history: true, swap_version: SWAP_VERSION_DEFAULT, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), } } @@ -1526,6 +1538,8 @@ impl<'a> TakerOrderBuilder<'a> { base_protocol_info: Some(base_protocol_info), rel_protocol_info: Some(rel_protocol_info), swap_version: SwapVersion::from(self.swap_version), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: self.order_metadata, }, matches: Default::default(), min_volume, @@ -1567,6 +1581,8 @@ impl<'a> TakerOrderBuilder<'a> { base_protocol_info: Some(base_protocol_info), rel_protocol_info: Some(rel_protocol_info), swap_version: SwapVersion::from(self.swap_version), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: self.order_metadata, }, matches: HashMap::new(), min_volume: Default::default(), @@ -1728,8 +1744,12 @@ pub struct MakerOrder { /// A custom priv key for more privacy to prevent linking orders of the same node between each other /// Commonly used with privacy coins (ARRR, ZCash, etc.) p2p_privkey: Option, + /// TODO: Move this into the `OrderMetadata` type when we are doing BC + /// on orders already. #[serde(default, skip_serializing_if = "SwapVersion::is_legacy")] pub swap_version: SwapVersion, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata, } pub struct MakerOrderBuilder<'a> { @@ -1743,6 +1763,18 @@ pub struct MakerOrderBuilder<'a> { conf_settings: Option, save_in_history: bool, swap_version: u8, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata, +} + +/// Contains extra and/or optional metadata (e.g., protocol-specific information) that can +/// be used for both taker and maker orders. +/// +/// TODO: `swap_version` should likely be moved into this type. +#[cfg(feature = "ibc-routing-for-swaps")] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +struct OrderMetadata { + channel_id_if_ibc_routing: Option, } pub enum MakerOrderBuildError { @@ -1893,6 +1925,8 @@ impl<'a> MakerOrderBuilder<'a> { conf_settings: None, save_in_history: true, swap_version: SWAP_VERSION_DEFAULT, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), } } @@ -1994,6 +2028,8 @@ impl<'a> MakerOrderBuilder<'a> { rel_orderbook_ticker: self.rel_orderbook_ticker, p2p_privkey, swap_version: SwapVersion::from(self.swap_version), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: self.order_metadata, }) } @@ -2019,6 +2055,8 @@ impl<'a> MakerOrderBuilder<'a> { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::from(self.swap_version), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: self.order_metadata, } } } @@ -2150,6 +2188,9 @@ impl From for MakerOrder { rel_orderbook_ticker: taker_order.rel_orderbook_ticker, p2p_privkey: taker_order.p2p_privkey, swap_version: taker_order.request.swap_version, + // TODO: Add test coverage for this once we have an integration test for this feature. + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: taker_order.request.order_metadata, }, // The "buy" taker order is recreated with reversed pair as Maker order is always considered as "sell" TakerAction::Buy => { @@ -2173,6 +2214,9 @@ impl From for MakerOrder { rel_orderbook_ticker: taker_order.base_orderbook_ticker, p2p_privkey: taker_order.p2p_privkey, swap_version: taker_order.request.swap_version, + // TODO: Add test coverage for this once we have an integration test for this feature. + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: taker_order.request.order_metadata, } }, } @@ -2225,6 +2269,8 @@ pub struct MakerReserved { pub rel_protocol_info: Option>, #[serde(default, skip_serializing_if = "SwapVersion::is_legacy")] pub swap_version: SwapVersion, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata, } impl MakerReserved { @@ -2253,6 +2299,9 @@ impl MakerReserved { base_protocol_info: message.base_protocol_info, rel_protocol_info: message.rel_protocol_info, swap_version: message.swap_version, + /// TODO: Support the new protocol types. + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), } } } @@ -3042,6 +3091,18 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO let maker_amount = maker_match.reserved.get_base_amount().clone(); let taker_amount = maker_match.reserved.get_rel_amount().clone(); + #[cfg(feature = "ibc-routing-for-swaps")] + { + let _taker_order_metadata = &maker_match.request.order_metadata; + let _maker_order_metadata = &maker_order.order_metadata; + + // TODO + // - If this is non-HTLC tendermint swap, cross-check IBC channels for routing before start. + // - Could malformed orders trick us by intentionally modfying channel IDs? + // - Unify this logic with `lp_connected_alice`. + unreachable!(); + } + // lp_connect_start_bob is called only from process_taker_connect, which returns if CryptoCtx is not initialized let crypto_ctx = CryptoCtx::from_ctx(&ctx).expect("'CryptoCtx' must be initialized already"); let raw_priv = crypto_ctx.mm2_internal_privkey_secret(); @@ -3276,6 +3337,18 @@ fn lp_connected_alice(ctx: MmArc, taker_order: TakerOrder, taker_match: TakerMat let raw_priv = crypto_ctx.mm2_internal_privkey_secret(); let my_persistent_pub = compressed_pub_key_from_priv_raw(&raw_priv.take(), ChecksumType::DSHA256).unwrap(); + #[cfg(feature = "ibc-routing-for-swaps")] + { + let _taker_order_metadata = &taker_order.request.order_metadata; + let _maker_order_metadata = &taker_match.reserved.order_metadata; + + // TODO + // - If this is non-HTLC tendermint swap, cross-check IBC channels for routing before start. + // - Could malformed orders trick us by intentionally modfying channel IDs? + // - Unify this logic with `lp_connect_start_bob`. + unreachable!(); + } + let maker_amount = taker_match.reserved.get_base_amount().clone(); let taker_amount = taker_match.reserved.get_rel_amount().clone(); @@ -3991,6 +4064,8 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: base_protocol_info: Some(base_coin.coin_protocol_info(None)), rel_protocol_info: Some(rel_coin.coin_protocol_info(Some(rel_amount.clone()))), swap_version: order.swap_version, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: order.order_metadata.clone(), }; let topic = order.orderbook_topic(); log::debug!("Request matched sending reserved {:?}", reserved); @@ -4107,12 +4182,9 @@ pub async fn buy(ctx: MmArc, req: Json) -> Result>, String> { let rel_coin = try_s!(rel_coin.ok_or("Rel coin is not found or inactive")); let base_coin = try_s!(lp_coinfind(&ctx, &input.base).await); let base_coin: MmCoinEnum = try_s!(base_coin.ok_or("Base coin is not found or inactive")); - if base_coin.wallet_only(&ctx) { - return ERR!("Base coin {} is wallet only", input.base); - } - if rel_coin.wallet_only(&ctx) { - return ERR!("Rel coin {} is wallet only", input.rel); - } + + try_s!(base_coin.pre_check_for_order_creation(&ctx, &rel_coin).await); + let my_amount = &input.volume * &input.price; try_s!( check_balance_for_taker_swap( @@ -4139,12 +4211,9 @@ pub async fn sell(ctx: MmArc, req: Json) -> Result>, String> { let base_coin = try_s!(base_coin.ok_or("Base coin is not found or inactive")); let rel_coin = try_s!(lp_coinfind(&ctx, &input.rel).await); let rel_coin = try_s!(rel_coin.ok_or("Rel coin is not found or inactive")); - if base_coin.wallet_only(&ctx) { - return ERR!("Base coin {} is wallet only", input.base); - } - if rel_coin.wallet_only(&ctx) { - return ERR!("Rel coin {} is wallet only", input.rel); - } + + try_s!(base_coin.pre_check_for_order_creation(&ctx, &rel_coin).await); + try_s!( check_balance_for_taker_swap( &ctx, @@ -4157,6 +4226,7 @@ pub async fn sell(ctx: MmArc, req: Json) -> Result>, String> { ) .await ); + let res = try_s!(lp_auto_buy(&ctx, &base_coin, &rel_coin, input).await); Ok(try_s!(Response::builder().body(res))) } @@ -4226,6 +4296,7 @@ pub async fn lp_auto_buy( rel_confs: input.rel_confs.unwrap_or_else(|| rel_coin.required_confirmations()), rel_nota: input.rel_nota.unwrap_or_else(|| rel_coin.requires_notarization()), }; + let mut order_builder = TakerOrderBuilder::new(base_coin, rel_coin) .with_base_amount(input.volume) .with_rel_amount(rel_volume) @@ -4238,10 +4309,21 @@ pub async fn lp_auto_buy( .with_save_in_history(input.save_in_history) .with_base_orderbook_ticker(ordermatch_ctx.orderbook_ticker(base_coin.ticker())) .with_rel_orderbook_ticker(ordermatch_ctx.orderbook_ticker(rel_coin.ticker())); + if !ctx.use_trading_proto_v2() { order_builder.set_legacy_swap_v(); } + // For non-HTLC Tendermint orders, include the channel information which will be used + // later from the other pair. + #[cfg(feature = "ibc-routing-for-swaps")] + if let MmCoinEnum::Tendermint(tendermint_coin) = &base_coin { + if !tendermint_coin.supports_htlc() { + let channel_id = try_s!(tendermint_coin.get_healthy_ibc_channel_to_htlc_chain().await); + order_builder.order_metadata.channel_id_if_ibc_routing = Some(channel_id); + } + } + if let Some(timeout) = input.timeout { order_builder = order_builder.with_timeout(timeout); } @@ -4974,6 +5056,7 @@ pub async fn create_maker_order(ctx: &MmArc, req: SetPriceReq) -> Result Result Recreate let mut taker_p2p_pubkey = [0; 32]; taker_p2p_pubkey.copy_from_slice(&started_event.my_persistent_pub.0[1..33]); + let maker_started_event = MakerSwapEvent::Started(MakerSwapData { taker_coin: started_event.taker_coin, maker_coin: started_event.maker_coin.clone(), diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index fa4e396d79..1fdb9e728f 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -2658,6 +2658,7 @@ pub async fn taker_swap_trade_preimage( rel_nota: rel_coin.requires_notarization(), }; let our_public_id = CryptoCtx::from_ctx(ctx)?.mm2_internal_public_id(); + let order_builder = TakerOrderBuilder::new(&base_coin, &rel_coin) .with_base_amount(base_amount) .with_rel_amount(rel_amount) @@ -2665,6 +2666,8 @@ pub async fn taker_swap_trade_preimage( .with_match_by(MatchBy::Any) .with_conf_settings(conf_settings) .with_sender_pubkey(H256Json::from(our_public_id.bytes)); + + // perform an additional validation let _ = order_builder .build() .map_to_mm(|e| TradePreimageRpcError::from_taker_order_build_error(e, &req.base, &req.rel))?; diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 7f88f129b3..c1788e2c2c 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -40,6 +40,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -56,6 +58,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -80,6 +84,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -96,6 +102,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -120,6 +128,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -136,6 +146,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -160,6 +172,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -176,6 +190,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -200,6 +216,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -216,6 +234,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -240,6 +260,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -256,6 +278,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -282,6 +306,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { base: "KMD".to_owned(), @@ -297,6 +323,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); assert_eq!(actual, OrderMatchResult::NotMatched); @@ -324,6 +352,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { base: "REL".to_owned(), @@ -339,6 +369,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); let expected_base_amount = MmNumber::from(3); @@ -397,6 +429,8 @@ fn test_maker_order_available_amount() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; maker.matches.insert(new_uuid(), MakerMatch { request: TakerRequest { @@ -413,6 +447,8 @@ fn test_maker_order_available_amount() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, reserved: MakerReserved { base: "BASE".into(), @@ -427,6 +463,8 @@ fn test_maker_order_available_amount() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, connect: None, connected: None, @@ -447,6 +485,8 @@ fn test_maker_order_available_amount() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, reserved: MakerReserved { base: "BASE".into(), @@ -461,6 +501,8 @@ fn test_maker_order_available_amount() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, connect: None, connected: None, @@ -490,6 +532,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -518,6 +562,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -536,6 +582,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -564,6 +612,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -582,6 +632,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -610,6 +662,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -628,6 +682,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -656,6 +712,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::NotMatched, order.match_reserved(&reserved)); @@ -674,6 +732,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -702,6 +762,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -720,6 +782,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -748,6 +812,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -766,6 +832,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -794,6 +862,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -812,6 +882,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -840,6 +912,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::NotMatched, order.match_reserved(&reserved)); @@ -862,6 +936,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, matches: HashMap::new(), order_type: OrderType::GoodTillCancelled, @@ -886,6 +962,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -907,6 +985,8 @@ fn test_taker_order_cancellable() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -938,6 +1018,8 @@ fn test_taker_order_cancellable() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let mut order = TakerOrder { @@ -968,6 +1050,8 @@ fn test_taker_order_cancellable() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, connect: TakerConnect { sender_pubkey: H256Json::default(), @@ -1017,6 +1101,8 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, None, ); @@ -1040,6 +1126,8 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, None, ); @@ -1063,6 +1151,8 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, None, ); @@ -1083,6 +1173,8 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), @@ -1186,6 +1278,8 @@ fn test_taker_order_match_by() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let mut order = TakerOrder { @@ -1214,6 +1308,8 @@ fn test_taker_order_match_by() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::NotMatched, order.match_reserved(&reserved)); @@ -1255,6 +1351,8 @@ fn test_maker_order_was_updated() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let mut update_msg = MakerOrderUpdated::new(maker_order.uuid); update_msg.with_new_price(BigRational::from_integer(2.into())); @@ -3265,6 +3363,8 @@ fn test_maker_order_balance_loops() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let morty_order = MakerOrder { @@ -3285,6 +3385,8 @@ fn test_maker_order_balance_loops() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert!(!maker_orders_ctx.balance_loop_exists(rick_ticker)); @@ -3318,6 +3420,8 @@ fn test_maker_order_balance_loops() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; maker_orders_ctx.add_order(ctx.weak(), rick_order_2.clone(), None); diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index 3b126c39c4..76b7f773c8 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -2672,7 +2672,9 @@ fn test_enable_custom_erc20() { .unwrap(); assert!(!buy.0.is_success(), "buy success, but should fail: {}", buy.1); assert!( - buy.1.contains(&format!("Rel coin {} is wallet only", ticker)), + buy.1.contains(&format!( + "'{ticker}' is a wallet only asset and can't be used in orders." + )), "Expected error message indicating that the token is wallet only, but got: {}", buy.1 ); diff --git a/mm2src/mm2_test_helpers/src/electrums.rs b/mm2src/mm2_test_helpers/src/electrums.rs index 03eaac51e6..a410daf1c2 100644 --- a/mm2src/mm2_test_helpers/src/electrums.rs +++ b/mm2src/mm2_test_helpers/src/electrums.rs @@ -73,11 +73,7 @@ pub fn tbtc_electrums() -> Vec { } #[cfg(target_arch = "wasm32")] -pub fn tqtum_electrums() -> Vec { - vec![ - json!({ "url": "electrum3.cipig.net:30071", "protocol": "WSS" }), - ] -} +pub fn tqtum_electrums() -> Vec { vec![json!({ "url": "electrum3.cipig.net:30071", "protocol": "WSS" })] } #[cfg(not(target_arch = "wasm32"))] pub fn tqtum_electrums() -> Vec { diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 5d7c1dad6b..86697593a5 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -186,6 +186,8 @@ pub const DOC_ELECTRUM_ADDRS: &[&str] = &[ "electrum2.cipig.net:10020", "electrum3.cipig.net:10020", ]; + +/// NOTE: These are websocket servers. #[cfg(target_arch = "wasm32")] pub const DOC_ELECTRUM_ADDRS: &[&str] = &[ "electrum1.cipig.net:30020", From 585e8b5aa359397c90785b529edfac3f473091b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Fri, 13 Jun 2025 05:10:13 +0300 Subject: [PATCH 28/36] improvement(orders): remove BTC specific volume from min_trading_vol logic (#2483) The removed logic is quite unnecessary today. With this patch, the minimum trading volume becomes 0.0001, which is around the equivalent of 10 USD and is roughly 10 times the average transaction fee on the BTC network. --- mm2src/coins/utxo/utxo_common.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index b5af6899ac..73f7043784 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -76,7 +76,6 @@ pub const DEFAULT_FEE_VOUT: usize = 0; pub const DEFAULT_SWAP_TX_SPEND_SIZE: u64 = 496; // TODO: checking with komodo-like tx size, included the burn output pub const DEFAULT_SWAP_VOUT: usize = 0; pub const DEFAULT_SWAP_VIN: usize = 0; -const MIN_BTC_TRADING_VOL: &str = "0.00777"; macro_rules! return_err_if { ($cond: expr, $etype: expr) => { @@ -3252,9 +3251,6 @@ pub fn min_tx_amount(coin: &UtxoCoinFields) -> BigDecimal { } pub fn min_trading_vol(coin: &UtxoCoinFields) -> MmNumber { - if coin.conf.ticker == "BTC" { - return MmNumber::from(MIN_BTC_TRADING_VOL); - } let dust_multiplier = MmNumber::from(10); dust_multiplier * min_tx_amount(coin).into() } From 0d2d1f8bc24b79fc2d9b0fbce6a4308513417b17 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 13 Jun 2025 03:17:59 +0100 Subject: [PATCH 29/36] fix(zcoin): correctly track unconfirmed z-coin notes (#2331) Previously, when a Z-coin transaction with a change output was created, the change note was not properly accounted for before the transaction was confirmed. This resulted in an inaccurate spendable balance, often leading to subsequent transactions failing due to a perceived lack of funds. This commit introduces `LockedNotesStorage`, a new persistence layer to track notes involved in pending (unconfirmed) transactions. - When a transaction is created, its change output is now recorded as a `Change` note in this storage. - The `my_balance` function has been updated to subtract the value of these locked notes, ensuring the spendable balance is accurate. - Notes used as inputs are also locked as `Spent` to prevent double-spending. - Once the transaction is confirmed, `scan_cached_block` removes the corresponding entries from `LockedNotesStorage`, making the change notes available for use. This ensures that the wallet's state is consistent even with pending transactions, preventing incorrect balance calculations and transaction failures. --- Cargo.lock | 1 + Cargo.toml | 1 + mm2src/coins/Cargo.toml | 1 + mm2src/coins/eth/eth_tests.rs | 2 +- mm2src/coins/utxo/utxo_tests.rs | 7 +- mm2src/coins/z_coin.rs | 324 ++++++++++++++---- mm2src/coins/z_coin/storage.rs | 25 +- .../storage/blockdb/blockdb_idb_storage.rs | 8 +- .../storage/blockdb/blockdb_sql_storage.rs | 11 +- mm2src/coins/z_coin/storage/blockdb/mod.rs | 13 +- .../coins/z_coin/storage/walletdb/wasm/mod.rs | 67 +++- .../z_coin/storage/walletdb/wasm/storage.rs | 14 +- .../z_coin/storage/walletdb/wasm/tables.rs | 5 - .../z_coin/storage/z_locked_notes/mod.rs | 155 +++++++++ .../z_coin/storage/z_locked_notes/sqlite.rs | 158 +++++++++ .../z_coin/storage/z_locked_notes/wasm.rs | 158 +++++++++ mm2src/coins/z_coin/z_coin_errors.rs | 12 +- mm2src/coins/z_coin/z_rpc.rs | 36 +- mm2src/mm2_main/Cargo.toml | 1 + .../tests/docker_tests/docker_tests_common.rs | 2 +- .../tests/docker_tests/z_coin_docker_tests.rs | 149 +++++--- mm2src/mm2_test_helpers/src/for_tests.rs | 8 +- 22 files changed, 966 insertions(+), 192 deletions(-) create mode 100644 mm2src/coins/z_coin/storage/z_locked_notes/mod.rs create mode 100644 mm2src/coins/z_coin/storage/z_locked_notes/sqlite.rs create mode 100644 mm2src/coins/z_coin/storage/z_locked_notes/wasm.rs diff --git a/Cargo.lock b/Cargo.lock index 50e3b5b41c..9748b76b83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4143,6 +4143,7 @@ dependencies = [ "sp-runtime-interface", "sp-trie", "spv_validation", + "tempfile", "testcontainers", "timed-map", "tokio", diff --git a/Cargo.toml b/Cargo.toml index eac8eac5fd..1ce0e81764 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -191,6 +191,7 @@ sp-trie = { version = "6.0", default-features = false } sql-builder = "3.1.1" syn = "1.0" sysinfo = "0.28" +tempfile = "3.4.0" # using the same version as cosmrs tendermint-rpc = { version = "0.35", default-features = false } testcontainers = "0.15.0" diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 1042df4966..c099395605 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -61,6 +61,7 @@ hex.workspace = true http.workspace = true itertools = { workspace = true, features = ["use_std"] } jsonrpc-core.workspace = true +jubjub.workspace = true keys = { path = "../mm2_bitcoin/keys" } lazy_static.workspace = true libc.workspace = true diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index ce5d58f58a..7cde52e740 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1,7 +1,6 @@ use super::*; use crate::IguanaPrivKey; use common::block_on; -use futures_util::future; use mm2_core::mm_ctx::MmCtxBuilder; cfg_native!( @@ -10,6 +9,7 @@ cfg_native!( use common::{now_sec, block_on_f01}; use ethkey::{Generator, Random}; + use futures_util::future; use mm2_test_helpers::for_tests::{ETH_MAINNET_CHAIN_ID, ETH_MAINNET_NODES, ETH_SEPOLIA_CHAIN_ID, ETH_SEPOLIA_NODES, ETH_SEPOLIA_TOKEN_CONTRACT}; use mocktopus::mocking::*; diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index c53d81045d..7d8d760452 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -12,10 +12,10 @@ use crate::rpc_command::init_scan_for_new_addresses::{InitScanAddressesRpcOps, S ScanAddressesResponse}; use crate::utxo::qtum::{qtum_coin_with_priv_key, QtumCoin, QtumDelegationOps, QtumDelegationRequest}; #[cfg(not(target_arch = "wasm32"))] -use crate::utxo::rpc_clients::{BlockHashOrHeight, NativeUnspent}; +use crate::utxo::rpc_clients::{BlockHashOrHeight, ElectrumClientSettings, NativeUnspent}; use crate::utxo::rpc_clients::{ElectrumBalance, ElectrumBlockHeader, ElectrumClient, ElectrumClientImpl, - ElectrumClientSettings, GetAddressInfoRes, ListSinceBlockRes, NativeClient, - NativeClientImpl, NetworkInfo, UtxoRpcClientOps, ValidateAddressRes, VerboseBlock}; + GetAddressInfoRes, ListSinceBlockRes, NativeClient, NativeClientImpl, NetworkInfo, + UtxoRpcClientOps, ValidateAddressRes, VerboseBlock}; use crate::utxo::spv::SimplePaymentVerification; #[cfg(not(target_arch = "wasm32"))] use crate::utxo::utxo_block_header_storage::{BlockHeaderStorage, SqliteBlockHeadersStorage}; @@ -43,6 +43,7 @@ use futures::future::{join_all, Either, FutureExt, TryFutureExt}; use hex::FromHex; use keys::prefixes::*; use mm2_core::mm_ctx::MmCtxBuilder; +#[cfg(not(target_arch = "wasm32"))] use mm2_event_stream::StreamingManager; use mm2_number::bigdecimal::{BigDecimal, Signed}; use mm2_number::MmNumber; diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index e31a8f8fa0..243d6f89e6 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -14,18 +14,17 @@ use crate::my_tx_history_v2::{MyTxHistoryErrorV2, MyTxHistoryRequestV2, MyTxHist use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::utxo::rpc_clients::{ElectrumConnectionSettings, UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; -use crate::utxo::utxo_builder::UtxoCoinBuildError; -use crate::utxo::utxo_builder::{UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, - UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; -use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat}; -use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, payment_script}; +use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, + UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, + UtxoFieldsWithIguanaSecretBuilder}; +use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat, big_decimal_from_sat_unsigned, + payment_script}; use crate::utxo::{sat_from_big_decimal, utxo_common, ActualFeeRate, AdditionalTxData, AddrFromStrError, Address, BroadcastTxErr, FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, - RecentlySpentOutPointsGuard, UtxoActivationParams, UtxoAddressFormat, UtxoArc, UtxoCoinFields, - UtxoCommonOps, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; -use crate::utxo::{UnsupportedAddr, UtxoFeeDetails}; -use crate::z_coin::storage::{BlockDbImpl, WalletDbShared}; - + RecentlySpentOutPointsGuard, UnsupportedAddr, UtxoActivationParams, UtxoAddressFormat, UtxoArc, + UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, + VerboseTransactionFrom}; +use crate::z_coin::storage::{BlockDbImpl, LockedNotesStorage, WalletDbShared}; use crate::z_coin::z_tx_history::{fetch_tx_history_from_db, ZCoinTxHistoryItem}; use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, @@ -38,15 +37,18 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, Con ValidatePaymentError, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawError, WithdrawFut, WithdrawRequest}; +use crate::z_coin::storage::z_locked_notes::LockedNote; use async_trait::async_trait; use bitcrypto::dhash256; use chain::constants::SEQUENCE_FINAL; use chain::{Transaction as UtxoTx, TransactionOutput}; -use common::executor::{AbortableSystem, AbortedError}; +use common::executor::{AbortableSystem, AbortedError, SpawnFuture}; +use common::log::info; use common::{calc_total_pages, log}; use crypto::privkey::{key_pair_from_secret, secp_privkey_from_hash}; use crypto::HDPathToCoin; use crypto::{Bip32DerPathOps, GlobalHDAccountArc}; +use futures::channel::oneshot; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; @@ -62,11 +64,10 @@ use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; use serialization::CoinVariant; use std::collections::{HashMap, HashSet}; -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; use std::iter; use std::num::NonZeroU32; use std::num::TryFromIntError; -use std::path::PathBuf; use std::sync::Arc; pub use z_coin_errors::*; pub use z_htlc::z_send_dex_fee; @@ -79,8 +80,10 @@ use zcash_client_backend::wallet::{AccountId, SpendableNote}; use zcash_extras::WalletRead; use zcash_primitives::consensus::{BlockHeight, BranchId, NetworkUpgrade, Parameters, H0}; use zcash_primitives::memo::MemoBytes; +use zcash_primitives::sapling::keys::prf_expand; use zcash_primitives::sapling::keys::OutgoingViewingKey; use zcash_primitives::sapling::note_encryption::try_sapling_output_recovery; +use zcash_primitives::sapling::Rseed; use zcash_primitives::transaction::builder::Builder as ZTxBuilder; use zcash_primitives::transaction::components::{Amount, OutputDescription, TxOut}; use zcash_primitives::transaction::Transaction as ZTransaction; @@ -91,8 +94,7 @@ use zcash_proofs::prover::LocalTxProver; cfg_native!( use common::{async_blocking, sha256_digest}; - use zcash_client_sqlite::error::SqliteClientError as ZcashClientError; - use zcash_client_sqlite::wallet::get_balance; + use std::path::PathBuf; use zcash_proofs::default_params_folder; use z_rpc::init_native_client; ); @@ -100,7 +102,6 @@ cfg_native!( cfg_wasm32!( use crate::z_coin::storage::ZcashParamsWasmImpl; use common::executor::AbortOnDropHandle; - use futures::channel::oneshot; use rand::rngs::OsRng; use zcash_primitives::transaction::builder::TransactionMetadata; pub use z_coin_errors::ZCoinBalanceError; @@ -207,6 +208,7 @@ pub struct ZCoinFields { light_wallet_db: WalletDbShared, consensus_params: ZcoinConsensusParams, sync_state_connector: AsyncMutex, + locked_notes_db: LockedNotesStorage, } impl Transaction for ZTransaction { @@ -262,6 +264,13 @@ pub struct ZcoinTxDetails { internal_id: i64, } +struct GenTxData<'a> { + tx: ZTransaction, + data: AdditionalTxData, + sync_guard: SaplingSyncGuard<'a>, + rseeds: Vec, +} + impl ZCoin { #[inline] pub fn utxo_rpc_client(&self) -> &UtxoRpcClientEnum { &self.utxo_arc.rpc_client } @@ -325,25 +334,7 @@ impl ZCoin { }) } - #[cfg(not(target_arch = "wasm32"))] - async fn my_balance_sat(&self) -> Result> { - let wallet_db = self.z_fields.light_wallet_db.clone(); - async_blocking(move || { - let db_guard = wallet_db.db.inner(); - let db_guard = db_guard.lock().unwrap(); - let balance = get_balance(&db_guard, AccountId::default())?.into(); - Ok(balance) - }) - .await - } - - #[cfg(target_arch = "wasm32")] - async fn my_balance_sat(&self) -> Result> { - let wallet_db = self.z_fields.light_wallet_db.clone(); - Ok(wallet_db.db.get_balance(AccountId::default()).await?.into()) - } - - async fn get_spendable_notes(&self) -> Result, MmError> { + async fn get_wallet_notes(&self) -> Result, MmError> { let wallet_db = self.z_fields.light_wallet_db.clone(); let db_guard = wallet_db.db; let latest_db_block = match db_guard @@ -362,8 +353,8 @@ impl ZCoin { } /// Returns spendable notes - async fn spendable_notes_ordered(&self) -> Result, MmError> { - let mut unspents = self.get_spendable_notes().await?; + async fn wallet_notes_ordered(&self) -> Result, MmError> { + let mut unspents = self.get_wallet_notes().await?; unspents.sort_unstable_by(|a, b| a.note_value.cmp(&b.note_value)); Ok(unspents) @@ -383,27 +374,28 @@ impl ZCoin { &self, t_outputs: Vec, z_outputs: Vec, - ) -> Result<(ZTransaction, AdditionalTxData, SaplingSyncGuard<'_>), MmError> { + ) -> Result, MmError> { + // Wait for chain to sync before selecting spendable notes or waiting for locked_notes to become + // available. let sync_guard = self.wait_for_gen_tx_blockchain_sync().await?; - + drop(sync_guard); let tx_fee = self.get_one_kbyte_tx_fee().await?; let t_output_sat: u64 = t_outputs.iter().fold(0, |cur, out| cur + u64::from(out.value)); let z_output_sat: u64 = z_outputs.iter().fold(0, |cur, out| cur + u64::from(out.amount)); let total_output_sat = t_output_sat + z_output_sat; let total_output = big_decimal_from_sat_unsigned(total_output_sat, self.utxo_arc.decimals); let total_required = &total_output + &tx_fee; + let spendable_notes = wait_for_spendable_balance_spawner(self, &total_required).await?; + + // Recreate sync_guard + let sync_guard = self.wait_for_gen_tx_blockchain_sync().await?; - let spendable_notes = self - .spendable_notes_ordered() - .await - .mm_err(|err| GenTxError::SpendableNotesError(err.to_string()))?; let mut total_input_amount = BigDecimal::from(0); let mut change = BigDecimal::from(0); - let mut received_by_me = 0u64; - let mut tx_builder = ZTxBuilder::new(self.consensus_params(), sync_guard.respawn_guard.current_block()); + let mut rseeds: Vec = vec![]; for spendable_note in spendable_notes { total_input_amount += big_decimal_from_sat_unsigned(spendable_note.note_value.into(), self.decimals()); @@ -422,6 +414,8 @@ impl ZCoin { .or_mm_err(|| GenTxError::FailedToGetMerklePath)?, )?; + rseeds.push(rseed_to_string(&spendable_note.rseed)); + if total_input_amount >= total_required { change = &total_input_amount - &total_required; break; @@ -444,19 +438,21 @@ impl ZCoin { tx_builder.add_sapling_output(z_out.viewing_key, z_out.to_addr, z_out.amount, z_out.memo)?; } + // add change to tx output + let change_sat = sat_from_big_decimal(&change, self.utxo_arc.decimals)?; if change > BigDecimal::from(0u8) { - let change_sat = sat_from_big_decimal(&change, self.utxo_arc.decimals)?; received_by_me += change_sat; + let change_amount = Amount::from_u64(change_sat).map_to_mm(|_| { + GenTxError::NumConversion(NumConversError(format!( + "Failed to get ZCash amount from {}", + change_sat + ))) + })?; tx_builder.add_sapling_output( Some(self.z_fields.evk.fvk.ovk), self.z_fields.my_z_addr.clone(), - Amount::from_u64(change_sat).map_to_mm(|_| { - GenTxError::NumConversion(NumConversError(format!( - "Failed to get ZCash amount from {}", - change_sat - ))) - })?, + change_amount, None, )?; } @@ -478,13 +474,19 @@ impl ZCoin { .await? .tx_result?; - let additional_data = AdditionalTxData { + let data = AdditionalTxData { received_by_me, spent_by_me: sat_from_big_decimal(&total_input_amount, self.decimals())?, fee_amount: sat_from_big_decimal(&tx_fee, self.decimals())?, kmd_rewards: None, }; - Ok((tx, additional_data, sync_guard)) + + Ok(GenTxData { + tx, + data, + sync_guard, + rseeds, + }) } pub async fn send_outputs( @@ -492,7 +494,12 @@ impl ZCoin { t_outputs: Vec, z_outputs: Vec, ) -> Result> { - let (tx, _, mut sync_guard) = self.gen_tx(t_outputs, z_outputs).await?; + let GenTxData { + tx, + data, + rseeds, + mut sync_guard, + } = self.gen_tx(t_outputs, z_outputs).await?; let mut tx_bytes = Vec::with_capacity(1024); tx.write(&mut tx_bytes).expect("Write should not fail"); @@ -501,6 +508,25 @@ impl ZCoin { .compat() .await?; + // TODO: Execute updates to `locked_notes_db` and `wallet_db` in a single transaction. + // This will be possible with a newer librustzcash that supports both spent notes and unconfirmed change tracking. + // See: https://github.com/KomodoPlatform/komodo-defi-framework/pull/2331#pullrequestreview-2883773336 + for rseed in rseeds { + self.z_fields + .locked_notes_db + .insert_spent_note(tx.txid().to_string(), rseed) + .await + .mm_err(|err| SendOutputsErr::InternalError(err.to_string()))?; + } + + if data.received_by_me > 0 { + self.z_fields + .locked_notes_db + .insert_change_note(tx.txid().to_string(), data.received_by_me) + .await + .mm_err(|err| SendOutputsErr::InternalError(err.to_string()))?; + } + sync_guard.respawn_guard.watch_for_tx(tx.txid()); Ok(tx) } @@ -678,7 +704,7 @@ impl ZCoin { else { return Ok(false); }; - if &address == expected_address { + if &address != expected_address { return Ok(false); } if note.value != amount_sat { @@ -918,10 +944,13 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { let z_tx_prover = self.z_tx_prover().await?; let blocks_db = self.init_blocks_db().await?; + let locked_notes_db = LockedNotesStorage::new(self.ctx, self.my_z_addr_encoded.clone()).await?; let (sync_state_connector, light_wallet_db) = match &self.z_coin_params.mode { #[cfg(not(target_arch = "wasm32"))] - ZcoinRpcMode::Native => init_native_client(&self, self.native_client()?, blocks_db).await?, + ZcoinRpcMode::Native => { + init_native_client(&self, self.native_client()?, blocks_db, locked_notes_db.clone()).await? + }, ZcoinRpcMode::Light { light_wallet_d_servers, sync_params, @@ -934,6 +963,7 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { blocks_db, sync_params, skip_sync_params.unwrap_or_default(), + locked_notes_db.clone(), ) .await? }, @@ -950,6 +980,7 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { light_wallet_db, consensus_params: self.protocol_info.consensus_params, sync_state_connector, + locked_notes_db, }); Ok(ZCoin { utxo_arc, z_fields }) @@ -1030,12 +1061,7 @@ impl<'a> ZCoinBuilder<'a> { let ctx = &self.ctx; let ticker = self.ticker.to_string(); - #[cfg(target_arch = "wasm32")] - let cache_db_path = PathBuf::new(); - #[cfg(not(target_arch = "wasm32"))] - let cache_db_path = self.ctx.global_dir().join(format!("{}_cache.db", self.ticker)); - - BlockDbImpl::new(ctx, ticker, cache_db_path) + BlockDbImpl::new(ctx, ticker) .await .mm_err(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string())) } @@ -1150,15 +1176,62 @@ impl MarketCoinOps for ZCoin { )) } + /// Calculates the wallet balance, divided into spendable and unspendable portions. + /// Unspendable balance consists of notes that are locked in the wallet. + /// TODO: Track unconfirmed change outputs in a dedicated DB/table (similar to locked_notes_db). + /// - Include them in the unspendable portion of the balance until confirmed. + /// - This will improve spendable/unspendable accuracy. fn my_balance(&self) -> BalanceFut { let coin = self.clone(); let fut = async move { - let sat = coin - .my_balance_sat() + let locked_notes = coin + .z_fields + .locked_notes_db + .load_all_notes() .await .mm_err(|e| BalanceError::WalletStorageError(e.to_string()))?; - Ok(CoinBalance::new(big_decimal_from_sat_unsigned(sat, coin.decimals()))) + + // Locked (unconfirmed) spent notes are not counted as spendable. + let spent_rseeds: HashSet<_> = locked_notes + .iter() + .filter_map(|n| { + if let LockedNote::Spent { rseed, .. } = n { + Some(rseed.clone()) + } else { + None + } + }) + .collect(); + + // Locked (unconfirmed) change notes are counted as unspendable. + let unspendable_change_sat: u64 = locked_notes + .iter() + .filter_map(|n| { + if let LockedNote::Change { value, .. } = n { + Some(*value) + } else { + None + } + }) + .sum(); + + let wallet_notes = coin + .get_wallet_notes() + .await + .map_err(|err| BalanceError::WalletStorageError(err.to_string()))?; + + let spendable_amount = wallet_notes + .iter() + .filter(|n| !spent_rseeds.contains(&rseed_to_string(&n.rseed))) + .fold(Amount::zero(), |acc, n| acc + n.note_value); + + let spendable_sat = + u64::try_from(spendable_amount).map_to_mm(|err| BalanceError::Internal(err.to_string()))?; + let unspendable = big_decimal_from_sat_unsigned(unspendable_change_sat, coin.decimals()); + let spendable = big_decimal_from_sat_unsigned(spendable_sat, coin.decimals()); + Ok(CoinBalance { spendable, unspendable }) }; + Box::new(fut.boxed().compat()) } @@ -1409,7 +1482,7 @@ impl SwapOps for ZCoin { /// TODO: when all mm2 nodes upgrade to support the burn account then disable validation of the Standard option async fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentResult<()> { let z_tx = match validate_fee_args.fee_tx { - TransactionEnum::ZTransaction(t) => t.clone(), + TransactionEnum::ZTransaction(t) => t, fee_tx => { return MmError::err(ValidatePaymentError::InternalError(format!( "Invalid fee tx type. fee tx: {:?}", @@ -1916,7 +1989,7 @@ impl InitWithdrawCoin for ZCoin { memo, }; - let (tx, data, _sync_guard) = self.gen_tx(vec![], vec![z_output]).await?; + let GenTxData { tx, data, .. } = self.gen_tx(vec![], vec![z_output]).await?; let mut tx_bytes = Vec::with_capacity(1024); tx.write(&mut tx_bytes) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; @@ -1925,9 +1998,10 @@ impl InitWithdrawCoin for ZCoin { let received_by_me = big_decimal_from_sat_unsigned(data.received_by_me, self.decimals()); let spent_by_me = big_decimal_from_sat_unsigned(data.spent_by_me, self.decimals()); + let tx_hash_hex = hex::encode(&tx_hash); Ok(TransactionDetails { - tx: TransactionData::new_signed(tx_bytes.into(), hex::encode(&tx_hash)), + tx: TransactionData::new_signed(tx_bytes.into(), tx_hash_hex), from: vec![self.z_fields.my_z_addr_encoded.clone()], to: vec![req.to], my_balance_change: &received_by_me - &spent_by_me, @@ -1949,6 +2023,108 @@ impl InitWithdrawCoin for ZCoin { } } +/// Waits until there are enough _unlocked_ Sapling notes to cover `total_required`. +/// TODO: Consider adding `wait_until` argument. +/// TODO: Integrate this into `light_wallet_db_sync_loop` instead of having a separate function. +/// Can be addressed when migrating to a newer librustzcash which supports spent note tracking. +/// See: https://github.com/KomodoPlatform/komodo-defi-framework/pull/2331#pullrequestreview-2883773336 +async fn wait_for_spendable_balance_impl( + selfi: ZCoin, + total_required: BigDecimal, +) -> Result, MmError> { + const MAX_RETRIES: usize = 40; + const RETRY_DELAY: f64 = 15.0; + + let mut retries = 0; + + loop { + let wallet_notes = selfi + .wallet_notes_ordered() + .await + .map_err(|e| GenTxError::SpendableNotesError(e.to_string()))?; + let wallet_notes_len = wallet_notes.len(); + + let locked_notes = selfi.z_fields.locked_notes_db.load_all_notes().await?; + + let unlocked_notes: Vec = if locked_notes.is_empty() { + wallet_notes + } else { + let unconfirmed_spent_rseeds: HashSet = locked_notes + .iter() + .filter_map(|n| { + if let LockedNote::Spent { rseed, .. } = n { + Some(rseed.clone()) + } else { + None + } + }) + .collect(); + + wallet_notes + .into_iter() + .filter(|note| !unconfirmed_spent_rseeds.contains(&rseed_to_string(¬e.rseed))) + .collect() + }; + let unlocked_notes_len = unlocked_notes.len(); + + let sum_available = unlocked_notes.iter().map(|n| n.note_value).sum::(); + let sum_available = u64::try_from(sum_available).map_to_mm(|err| GenTxError::Internal(err.to_string()))?; + let sum_available = big_decimal_from_sat_unsigned(sum_available, selfi.decimals()); + + // Reteurn InsufficientBalance error when all notes are unlocked but amount is insufficient. + if sum_available < total_required && unlocked_notes_len == wallet_notes_len { + return MmError::err(GenTxError::InsufficientBalance { + coin: selfi.ticker().to_string(), + available: sum_available, + required: total_required, + }); + } + + // Returns available notes when either sufficient funds exist or all notes are unlocked. + // Otherwise, waits for locked notes to become available up to MAX_RETRIES. + if sum_available >= total_required || unlocked_notes_len == wallet_notes_len { + return Ok(unlocked_notes.into_iter()); + } + + if retries >= MAX_RETRIES { + return MmError::err(GenTxError::Internal(format!( + "Locked notes did not become available after {} retries", + MAX_RETRIES + ))); + } + + info!( + "Locked notes present; retrying in {}s (attempt {}/{})", + RETRY_DELAY, + retries + 1, + MAX_RETRIES + ); + common::executor::Timer::sleep(RETRY_DELAY).await; + retries += 1; + } +} + +async fn wait_for_spendable_balance_spawner( + selfi: &ZCoin, + total_required: &BigDecimal, +) -> Result, MmError> { + let coin = selfi.clone(); + let required = total_required.clone(); + let (tx, rx) = oneshot::channel(); + + selfi.spawner().spawn(async move { + let result = wait_for_spendable_balance_impl(coin, required).await; + let _ = tx.send(result); + }); + + match rx.await { + Ok(res) => res, + Err(_) => MmError::err(GenTxError::Internal( + "wait_for_spendable_balance task was cancelled".into(), + )), + } +} + /// Interpret a string or hex-encoded memo, and return a Memo object. /// Inspired by https://github.com/adityapk00/zecwallet-light-cli/blob/v1.7.20/lib/src/lightwallet/utils.rs#L23 #[allow(clippy::result_large_err)] @@ -2012,6 +2188,16 @@ fn extended_spending_key_from_global_hd_account( Ok(spending_key) } +#[inline] +fn rseed_to_string(rseed: &Rseed) -> String { + const INPUT: [u8; 1] = [0x04]; + + match rseed { + Rseed::BeforeZip212(rcm) => rcm.to_string(), + Rseed::AfterZip212(rseed) => jubjub::Fr::from_bytes_wide(prf_expand(rseed, &INPUT).as_array()).to_string(), + } +} + #[test] fn derive_z_key_from_mm_seed() { use crypto::privkey::key_pair_from_seed; diff --git a/mm2src/coins/z_coin/storage.rs b/mm2src/coins/z_coin/storage.rs index e2534281b7..e8b1b6e971 100644 --- a/mm2src/coins/z_coin/storage.rs +++ b/mm2src/coins/z_coin/storage.rs @@ -1,16 +1,19 @@ use crate::z_coin::{ValidateBlocksError, ZcoinConsensusParams, ZcoinStorageError}; use mm2_event_stream::StreamingManager; -pub mod blockdb; -pub use blockdb::*; +pub(crate) mod blockdb; +pub(crate) use blockdb::*; + +pub(crate) mod walletdb; +pub(crate) use walletdb::*; + +pub(crate) mod z_locked_notes; +pub(crate) use z_locked_notes::{LockedNotesStorage, LockedNotesStorageError}; -pub mod walletdb; #[cfg(target_arch = "wasm32")] mod z_params; #[cfg(target_arch = "wasm32")] pub(crate) use z_params::ZcashParamsWasmImpl; -pub use walletdb::*; - use mm2_err_handle::mm_error::MmResult; #[cfg(target_arch = "wasm32")] use walletdb::wasm::storage::DataConnStmtCacheWasm; @@ -118,6 +121,7 @@ pub async fn scan_cached_block( data: &DataConnStmtCacheWrapper, params: &ZcoinConsensusParams, block: &CompactBlock, + locked_notes_db: &LockedNotesStorage, last_height: &mut BlockHeight, ) -> Result>, ValidateBlocksError> { let mut data_guard = data.inner().clone(); @@ -159,7 +163,6 @@ pub async fn scan_cached_block( // To enforce that all roots match, // see -> https://github.com/KomodoPlatform/librustzcash/blob/e92443a7bbd1c5e92e00e6deb45b5a33af14cea4/zcash_client_backend/src/data_api/chain.rs#L304-L326 - let new_witnesses = data_guard .advance_by_block( &(PrunedBlock { @@ -186,5 +189,15 @@ pub async fn scan_cached_block( witnesses.extend(new_witnesses); *last_height = current_height; + // TODO: Execute updates to `locked_notes_db` and `wallet_db` in a single transaction. + // This will be possible with a newer librustzcash that supports both spent notes and unconfirmed change tracking. + // See: https://github.com/KomodoPlatform/komodo-defi-framework/pull/2331#pullrequestreview-2883773336 + for tx in &txs { + locked_notes_db + .remove_notes_for_txid(tx.txid.to_string()) + .await + .map_err(|err| ValidateBlocksError::DbError(err.to_string()))?; + } + Ok(txs) } diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs index 826ed52bdd..b95af8c4cd 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs @@ -1,5 +1,5 @@ use crate::z_coin::storage::{scan_cached_block, validate_chain, BlockDbImpl, BlockProcessingMode, CompactBlockRow, - ZcoinConsensusParams, ZcoinStorageRes}; + LockedNotesStorage, ZcoinConsensusParams, ZcoinStorageRes}; use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; use crate::z_coin::z_balance_streaming::ZCoinBalanceEventStreamer; use crate::z_coin::z_coin_errors::ZcoinStorageError; @@ -10,7 +10,6 @@ use mm2_db::indexed_db::{BeBigUint, ConstructibleDb, DbIdentifier, DbInstance, D IndexedDbBuilder, InitDbResult, MultiIndex, OnUpgradeResult, TableSignature}; use mm2_err_handle::prelude::*; use protobuf::Message; -use std::path::PathBuf; use zcash_client_backend::proto::compact_formats::CompactBlock; use zcash_extras::WalletRead; use zcash_primitives::block::BlockHash; @@ -68,7 +67,7 @@ impl BlockDbInner { } impl BlockDbImpl { - pub async fn new(ctx: &MmArc, ticker: String, _path: PathBuf) -> ZcoinStorageRes { + pub async fn new(ctx: &MmArc, ticker: String) -> ZcoinStorageRes { Ok(Self { db: ConstructibleDb::new(ctx).into_shared(), ticker, @@ -221,6 +220,7 @@ impl BlockDbImpl { mode: BlockProcessingMode, validate_from: Option<(BlockHeight, BlockHash)>, limit: Option, + locked_notes_db: &LockedNotesStorage, ) -> ZcoinStorageRes<()> { let ticker = self.ticker.to_owned(); let mut from_height = match &mode { @@ -254,7 +254,7 @@ impl BlockDbImpl { validate_chain(block, &mut prev_height, &mut prev_hash).await?; }, BlockProcessingMode::Scan(data, streaming_manager) => { - let txs = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; + let txs = scan_cached_block(data, ¶ms, &block, locked_notes_db, &mut from_height).await?; if !txs.is_empty() { // Stream out the new transactions. streaming_manager diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs index 8dd4dd39f7..6083751df6 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs @@ -1,5 +1,5 @@ use crate::z_coin::storage::{scan_cached_block, validate_chain, BlockDbImpl, BlockProcessingMode, CompactBlockRow, - ZcoinStorageRes}; + LockedNotesStorage, ZcoinStorageRes}; use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; use crate::z_coin::z_balance_streaming::ZCoinBalanceEventStreamer; use crate::z_coin::z_coin_errors::ZcoinStorageError; @@ -12,7 +12,6 @@ use itertools::Itertools; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use protobuf::Message; -use std::path::PathBuf; use std::sync::{Arc, Mutex}; use zcash_client_backend::data_api::error::Error as ChainError; use zcash_client_backend::proto::compact_formats::CompactBlock; @@ -46,7 +45,8 @@ impl From> for ZcoinStorageError { impl BlockDbImpl { #[cfg(not(test))] - pub async fn new(_ctx: &MmArc, ticker: String, path: PathBuf) -> ZcoinStorageRes { + pub async fn new(ctx: &MmArc, ticker: String) -> ZcoinStorageRes { + let path = ctx.global_dir().join(format!("{}_cache.db", ticker)); async_blocking(move || { mm2_io::fs::create_parents(&path).map_err(|err| ZcoinStorageError::IoError(err.to_string()))?; let conn = Connection::open(path).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; @@ -70,7 +70,7 @@ impl BlockDbImpl { } #[cfg(test)] - pub(crate) async fn new(ctx: &MmArc, ticker: String, _path: PathBuf) -> ZcoinStorageRes { + pub(crate) async fn new(ctx: &MmArc, ticker: String) -> ZcoinStorageRes { let ctx = ctx.clone(); async_blocking(move || { let conn = ctx @@ -192,6 +192,7 @@ impl BlockDbImpl { mode: BlockProcessingMode, validate_from: Option<(BlockHeight, BlockHash)>, limit: Option, + locked_notes_db: &LockedNotesStorage, ) -> ZcoinStorageRes<()> { let ticker = self.ticker.to_owned(); let mut from_height = match &mode { @@ -230,7 +231,7 @@ impl BlockDbImpl { validate_chain(block, &mut prev_height, &mut prev_hash).await?; }, BlockProcessingMode::Scan(data, streaming_manager) => { - let txs = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; + let txs = scan_cached_block(data, ¶ms, &block, locked_notes_db, &mut from_height).await?; if !txs.is_empty() { // Stream out the new transactions. streaming_manager diff --git a/mm2src/coins/z_coin/storage/blockdb/mod.rs b/mm2src/coins/z_coin/storage/blockdb/mod.rs index bc41c4de00..e1b9c13d5b 100644 --- a/mm2src/coins/z_coin/storage/blockdb/mod.rs +++ b/mm2src/coins/z_coin/storage/blockdb/mod.rs @@ -25,7 +25,6 @@ pub struct BlockDbImpl { mod block_db_storage_tests { use crate::z_coin::storage::BlockDbImpl; use common::log::info; - use std::path::PathBuf; use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; @@ -37,9 +36,7 @@ mod block_db_storage_tests { pub(crate) async fn test_insert_block_and_get_latest_block_impl() { let ctx = mm_ctx_with_custom_db(); - let db = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) - .await - .unwrap(); + let db = BlockDbImpl::new(&ctx, TICKER.to_string()).await.unwrap(); // insert block for header in HEADERS.iter() { db.insert_block(header.0, hex::decode(header.1).unwrap()).await.unwrap(); @@ -52,9 +49,7 @@ mod block_db_storage_tests { pub(crate) async fn test_rewind_to_height_impl() { let ctx = mm_ctx_with_custom_db(); - let db = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) - .await - .unwrap(); + let db = BlockDbImpl::new(&ctx, TICKER.to_string()).await.unwrap(); // insert block for header in HEADERS.iter() { db.insert_block(header.0, hex::decode(header.1).unwrap()).await.unwrap(); @@ -77,9 +72,7 @@ mod block_db_storage_tests { #[allow(unused)] pub(crate) async fn test_process_blocks_with_mode_impl() { let ctx = mm_ctx_with_custom_db(); - let db = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) - .await - .unwrap(); + let db = BlockDbImpl::new(&ctx, TICKER.to_string()).await.unwrap(); // insert block for header in HEADERS.iter() { let inserted_id = db.insert_block(header.0, hex::decode(header.1).unwrap()).await.unwrap(); diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs index c1ffdfb0a2..5fb428db4f 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs @@ -68,6 +68,7 @@ fn to_spendable_note(note: SpendableNoteConstructor) -> MmResult LocalTxProver { let (spend_buf, output_buf) = wagyu_zcash_parameters::load_sapling_parameters(); @@ -206,6 +208,9 @@ mod wasm_test { async fn test_valid_chain_state() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -226,6 +231,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -247,6 +253,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -259,6 +266,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -271,6 +279,7 @@ mod wasm_test { BlockProcessingMode::Validate, max_height_hash, None, + &locked_notes_db, ) .await .unwrap(); @@ -292,6 +301,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -304,6 +314,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -315,6 +326,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -324,6 +336,9 @@ mod wasm_test { async fn invalid_chain_cache_disconnected() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -363,6 +378,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -374,6 +390,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -403,6 +420,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap_err(); @@ -418,6 +436,9 @@ mod wasm_test { async fn test_invalid_chain_reorg() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -457,6 +478,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -468,6 +490,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -497,6 +520,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap_err(); @@ -512,6 +536,9 @@ mod wasm_test { async fn test_data_db_rewinding() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -546,6 +573,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -576,6 +604,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -588,6 +617,9 @@ mod wasm_test { async fn test_scan_cached_blocks_requires_sequential_blocks() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -615,6 +647,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -633,6 +666,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap_err(); @@ -656,7 +690,8 @@ mod wasm_test { consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, - None + None, + &locked_notes_db ) .await .is_ok()); @@ -671,6 +706,9 @@ mod wasm_test { async fn test_scan_cached_blokcs_finds_received_notes() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -700,7 +738,8 @@ mod wasm_test { consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, - None + None, + &locked_notes_db ) .await .is_ok()); @@ -721,7 +760,8 @@ mod wasm_test { consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, - None + None, + &locked_notes_db ) .await .is_ok()); @@ -734,6 +774,9 @@ mod wasm_test { async fn test_scan_cached_blocks_finds_change_notes() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -763,7 +806,8 @@ mod wasm_test { consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, - None + None, + &locked_notes_db ) .await .is_ok()); @@ -794,6 +838,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await; assert!(scan.is_ok()); @@ -810,6 +855,7 @@ mod wasm_test { // async fn create_to_address_fails_on_unverified_notes() { // // init blocks_db // let ctx = mm_ctx_with_custom_db(); + // let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()).await.unwrap(); // let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()).await.unwrap(); // // // init walletdb. @@ -853,7 +899,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .is_ok()); // @@ -898,7 +944,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .is_ok()); // @@ -929,7 +975,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .is_ok()); // @@ -1079,6 +1125,7 @@ mod wasm_test { // // // init blocks_db // let ctx = mm_ctx_with_custom_db(); + // let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()).await.unwrap(); // let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()).await.unwrap(); // // // init walletdb. @@ -1099,7 +1146,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .unwrap(); // assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value); @@ -1156,7 +1203,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .unwrap(); // @@ -1192,7 +1239,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .unwrap(); // diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs index 06d06c4d32..78d8a6fbaa 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs @@ -1082,7 +1082,7 @@ impl WalletRead for WalletIndexedDb { let matching_tx = maybe_txs.iter().find(|(id_tx, _tx)| id_tx.to_bigint() == note.spent); if let Some((_, tx)) = matching_tx { - if tx.block.is_none() { + if tx.block.is_some() { nullifiers.push(( AccountId( note.account @@ -1095,18 +1095,6 @@ impl WalletRead for WalletIndexedDb { .unwrap(), )); } - } else { - nullifiers.push(( - AccountId( - note.account - .to_u32() - .ok_or_else(|| ZcoinStorageError::GetFromStorageError("Invalid amount".to_string()))?, - ), - Nullifier::from_slice(¬e.nf.clone().ok_or_else(|| { - ZcoinStorageError::GetFromStorageError("Error while putting tx_meta".to_string()) - })?) - .unwrap(), - )); } } diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/tables.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/tables.rs index 53471571d4..92ce152837 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/tables.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/tables.rs @@ -141,11 +141,6 @@ impl WalletDbReceivedNotesTable { pub const TICKER_ACCOUNT_INDEX: &'static str = "ticker_account_index"; /// A **unique** index that consists of the following properties: /// * ticker - /// * note_id - /// * nf - pub const TICKER_NOTES_ID_NF_INDEX: &'static str = "ticker_note_id_nf_index"; - /// A **unique** index that consists of the following properties: - /// * ticker /// * tx /// * output_index pub const TICKER_TX_OUTPUT_INDEX: &'static str = "ticker_tx_output_index"; diff --git a/mm2src/coins/z_coin/storage/z_locked_notes/mod.rs b/mm2src/coins/z_coin/storage/z_locked_notes/mod.rs new file mode 100644 index 0000000000..fdbd9b8b76 --- /dev/null +++ b/mm2src/coins/z_coin/storage/z_locked_notes/mod.rs @@ -0,0 +1,155 @@ +use enum_derives::EnumFromStringify; + +cfg_native!( + pub(crate) mod sqlite; + + use db_common::async_sql_conn::{AsyncConnError, AsyncConnection}; + use futures::lock::Mutex; + use std::sync::Arc; +); + +cfg_wasm32!( + pub(crate) mod wasm; + + use self::wasm::LockedNoteDbInner; + use mm2_db::indexed_db::{DbTransactionError, InitDbError, SharedDb}; +); + +/// Represents a shielded note temporarily locked due to a pending transaction. +/// Locked notes are excluded from the spendable balance until confirmed or cleared. +#[derive(Debug, Clone)] +pub(crate) enum LockedNote { + /// A note being spent by a pending shielded transaction (`rseed` is the note's randomness). + Spent { rseed: String }, + + /// A pending change output from an unconfirmed shielded transaction (`value` is the expected amount). + Change { value: u64 }, +} + +/// A wrapper for the db connection to the change note cache database in native and browser. +#[derive(Clone)] +pub struct LockedNotesStorage { + #[cfg(not(target_arch = "wasm32"))] + pub db: Arc>, + #[cfg(target_arch = "wasm32")] + pub db: SharedDb, + #[allow(unused)] + address: String, +} + +#[derive(Clone, Debug, Display, Eq, PartialEq, EnumFromStringify)] +pub(crate) enum LockedNotesStorageError { + #[cfg(not(target_arch = "wasm32"))] + #[display(fmt = "Sqlite Error: {_0}")] + #[from_stringify("AsyncConnError", "db_common::sqlite::rusqlite::Error")] + SqliteError(String), + #[cfg(target_arch = "wasm32")] + #[display(fmt = "IndexedDb Error: {_0}")] + #[from_stringify("InitDbError", "DbTransactionError")] + IndexedDbError(String), +} + +#[cfg(any(test, target_arch = "wasm32"))] +pub(super) mod locked_notes_test { + use crate::z_coin::storage::z_locked_notes::{LockedNote, LockedNotesStorage}; + use common::cross_test; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + + common::cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + + const MY_ADDRESS: &str = "my_address"; + + cross_test!(test_insert_and_remove_note, { + let ctx = mm_ctx_with_custom_db(); + let db = LockedNotesStorage::new(&ctx, MY_ADDRESS.to_string()).await.unwrap(); + + // Insert a pending spent note + let spent_txid = "0x18b1acd8ceae8d71a2ae8b7e4a3e48ceb39dc237f0aa38c468425b88dc8d5f3e".to_string(); + let spent_rseed = "0xcfec34a81e67e85aa1ce1a6666f92f9bc5606f0795be555bb3c9f9ac089aa4f7".to_string(); + db.insert_spent_note(spent_txid.clone(), spent_rseed.clone()) + .await + .unwrap(); + + // Insert a pending change note + let change_txid = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string(); + let change_value = 123456; + db.insert_change_note(change_txid.clone(), change_value).await.unwrap(); + + // Remove by txid + db.remove_notes_for_txid(spent_txid.clone()).await.unwrap(); + db.remove_notes_for_txid(change_txid.clone()).await.unwrap(); + + let notes = db.load_all_notes().await.unwrap(); + assert!(notes.is_empty()); + + // Insert both again but using same txid + db.insert_spent_note(spent_txid.clone(), spent_rseed.clone()) + .await + .unwrap(); + db.insert_change_note(spent_txid.clone(), change_value).await.unwrap(); + + // Remove by txid (removes both input and output if same txid) + db.remove_notes_for_txid(spent_txid.clone()).await.unwrap(); + + let notes = db.load_all_notes().await.unwrap(); + assert!(notes.is_empty()); + }); + + cross_test!(test_load_all_notes, { + let ctx = mm_ctx_with_custom_db(); + let db = LockedNotesStorage::new(&ctx, MY_ADDRESS.to_string()).await.unwrap(); + + let spent_txid = "0x01".to_string(); + let spent_rseed = "0xcafe000000000000000000000000000000000000000000000000000000000000".to_string(); + let change_txid = "0x02".to_string(); + let change_value = 123456789; + + db.insert_spent_note(spent_txid.clone(), spent_rseed.clone()) + .await + .unwrap(); + db.insert_change_note(change_txid.clone(), change_value).await.unwrap(); + + let notes = db.load_all_notes().await.unwrap(); + + assert_eq!(notes.len(), 2); + + match ¬es[0] { + LockedNote::Spent { rseed } => { + assert_eq!(rseed, &spent_rseed); + }, + _ => panic!("First note should be a Spent note"), + } + match ¬es[1] { + LockedNote::Change { value } => { + assert_eq!(*value, change_value); + }, + _ => panic!("Second note should be a Change note"), + } + }); + + cross_test!(test_sum_changes, { + let ctx = mm_ctx_with_custom_db(); + let db = LockedNotesStorage::new(&ctx, MY_ADDRESS.to_string()).await.unwrap(); + + db.insert_change_note("txid1".to_string(), 1000).await.unwrap(); + db.insert_change_note("txid2".to_string(), 2000).await.unwrap(); + db.insert_spent_note("0xinputrseed".to_string(), "txid3".to_string()) + .await + .unwrap(); + + let notes = db.load_all_notes().await.unwrap(); + + // Only sum Output note values + let sum: u64 = notes + .iter() + .filter_map(|n| match n { + LockedNote::Change { value, .. } => Some(*value), + _ => None, + }) + .sum(); + assert_eq!(sum, 3000); + }); +} diff --git a/mm2src/coins/z_coin/storage/z_locked_notes/sqlite.rs b/mm2src/coins/z_coin/storage/z_locked_notes/sqlite.rs new file mode 100644 index 0000000000..1738c82025 --- /dev/null +++ b/mm2src/coins/z_coin/storage/z_locked_notes/sqlite.rs @@ -0,0 +1,158 @@ +use super::{LockedNote, LockedNotesStorage, LockedNotesStorageError}; +use db_common::async_sql_conn::{AsyncConnError, AsyncConnection}; +use db_common::sqlite::run_optimization_pragmas; +use db_common::sqlite::rusqlite::params; +use futures::lock::Mutex; +use itertools::Itertools; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use std::convert::TryInto; +use std::sync::Arc; + +const TABLE_NAME: &str = "locked_notes_cache"; + +async fn create_table(conn: Arc>) -> Result<(), AsyncConnError> { + let conn = conn.lock().await; + conn.call(move |conn| { + run_optimization_pragmas(conn)?; + conn.execute( + &format!( + "CREATE TABLE IF NOT EXISTS {TABLE_NAME} ( + variant TEXT NOT NULL, -- 'Spent' or 'Change' + txid VARCHAR NOT NULL, + rseed VARCHAR, -- only for Spent + value INTEGER, -- only for Change + UNIQUE (variant, txid, rseed, value) + )" + ), + [], + )?; + Ok(()) + }).await +} + +impl LockedNotesStorage { + #[cfg(not(any(test, feature = "run-docker-tests")))] + pub(crate) async fn new(ctx: &MmArc, address: String) -> MmResult { + let path = ctx.wallet_dir().join(format!("{}_locked_notes_cache.db", address)); + let db = AsyncConnection::open(path) + .await + .map_to_mm(|err| LockedNotesStorageError::SqliteError(err.to_string()))?; + let db = Arc::new(Mutex::new(db)); + + create_table(db.clone()).await?; + + Ok(Self { db, address }) + } + + #[cfg(any(test, feature = "run-docker-tests"))] + pub(crate) async fn new(ctx: &MmArc, address: String) -> MmResult { + #[cfg(feature = "run-docker-tests")] + let db = { + let path = ctx.wallet_dir().join(format!("{}_locked_notes_cache.db", address)); + mm2_io::fs::create_parents_async(&path) + .await + .map_err(|err| LockedNotesStorageError::SqliteError(err.to_string()))?; + Arc::new(Mutex::new( + AsyncConnection::open(path) + .await + .map_to_mm(|err| LockedNotesStorageError::SqliteError(err.to_string()))?, + )) + }; + #[cfg(all(test, not(feature = "run-docker-tests")))] + let db = { + let test_conn = Arc::new(Mutex::new(AsyncConnection::open_in_memory().await.unwrap())); + ctx.async_sqlite_connection.get().cloned().unwrap_or(test_conn) + }; + + create_table(db.clone()).await?; + + Ok(Self { db, address }) + } + + pub(crate) async fn insert_spent_note( + &self, + txid: String, + rseed: String, + ) -> MmResult<(), LockedNotesStorageError> { + let db = self.db.lock().await; + Ok(db.call(move |conn| { + conn.prepare(&format!( + "INSERT OR REPLACE INTO {TABLE_NAME} (variant, txid, rseed, value) VALUES (?, ?, ?, NULL)" + ))? + .execute(params!["Spent", txid, rseed])?; + Ok(()) + }).await?) + } + + pub(crate) async fn insert_change_note( + &self, + txid: String, + value: u64, + ) -> MmResult<(), LockedNotesStorageError> { + let db = self.db.lock().await; + Ok(db.call(move |conn| { + conn.prepare(&format!( + "INSERT OR REPLACE INTO {TABLE_NAME} (variant, txid, rseed, value) VALUES (?, ?, NULL, ?)" + ))? + .execute(params!["Change", txid, value as i64])?; + Ok(()) + }).await?) + } + + pub(crate) async fn remove_notes_for_txid(&self, txid: String) -> MmResult<(), LockedNotesStorageError> { + let db = self.db.lock().await; + Ok(db + .call(move |conn| { + conn.execute( + &format!("DELETE FROM {TABLE_NAME} WHERE txid=?"), + [&txid], + )?; + Ok(()) + }) + .await?) + } + + pub(crate) async fn load_all_notes(&self) -> MmResult, LockedNotesStorageError> { + let db = self.db.lock().await; + Ok(db.call(move |conn| { + let mut stmt = conn.prepare(&format!( + "SELECT variant, txid, rseed, value FROM {TABLE_NAME};" + ))?; + let rows = stmt.query_map(params![], |row| { + let variant: String = row.get(0)?; + let rseed: Option = row.get(2)?; + let value: Option = row.get(3)?; + + match variant.as_str() { + "Spent" => { + let rseed = rseed.ok_or_else(|| db_common::sqlite::rusqlite::Error::FromSqlConversionFailure( + 2, // Column index for "rseed" + db_common::sqlite::rusqlite::types::Type::Text, + "NULL value found for required rseed field".into() + ))?; + Ok(LockedNote::Spent { rseed }) + }, + "Change" => { + let i64_value = value.ok_or_else(|| db_common::sqlite::rusqlite::Error::FromSqlConversionFailure( + 3, // Column index for "value" + db_common::sqlite::rusqlite::types::Type::Integer, + "NULL value found for required value field".into() + ))?; + + let value = i64_value.try_into() + .map_err(|_| db_common::sqlite::rusqlite::Error::IntegralValueOutOfRange(3, i64_value))?; + + Ok(LockedNote::Change { value }) + }, + unexpected => Err(db_common::sqlite::rusqlite::Error::FromSqlConversionFailure( + 0, // Column index for "variant" + db_common::sqlite::rusqlite::types::Type::Text, + format!("Unexpected variant value: {}", unexpected).into() + )), + } + })?; + Ok(rows.flatten().collect_vec()) + }).await?) + } +} diff --git a/mm2src/coins/z_coin/storage/z_locked_notes/wasm.rs b/mm2src/coins/z_coin/storage/z_locked_notes/wasm.rs new file mode 100644 index 0000000000..dc72ca9d4e --- /dev/null +++ b/mm2src/coins/z_coin/storage/z_locked_notes/wasm.rs @@ -0,0 +1,158 @@ +use super::{LockedNote, LockedNotesStorage, LockedNotesStorageError}; + +use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::{ConstructibleDb, DbIdentifier, DbInstance, DbLocked, DbUpgrader, IndexedDb, IndexedDbBuilder, + InitDbResult, OnUpgradeResult, TableSignature, OnUpgradeError}; +use mm2_err_handle::prelude::*; + +const DB_NAME: &str = "z_change_note_storage"; +const DB_VERSION: u32 = 1; + +pub type LockedNotesDbInnerLocked<'a> = DbLocked<'a, LockedNoteDbInner>; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct LockedNoteTable { + address: String, + variant: String, // "Spent" or "Change" + txid: String, + rseed: Option, // Only for Spent + value: Option, // Only for Change +} + +impl TableSignature for LockedNoteTable { + const TABLE_NAME: &'static str = "change_notes"; + + fn on_upgrade_needed(upgrader: &DbUpgrader, mut old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + while old_version < new_version { + match old_version { + 0 => { + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_index("address", false)?; + table.create_index("variant", false)?; + table.create_index("txid", false)?; + table.create_index("rseed", false)?; + table.create_index("value", false)?; + } + unsupported_version => { + return MmError::err(OnUpgradeError::UnsupportedVersion { + unsupported_version, + old_version, + new_version, + }); + } + } + old_version += 1; + } + Ok(()) + } +} + +pub struct LockedNoteDbInner(IndexedDb); + +#[async_trait::async_trait] +impl DbInstance for LockedNoteDbInner { + const DB_NAME: &'static str = DB_NAME; + + async fn init(db_id: DbIdentifier) -> InitDbResult { + let inner = IndexedDbBuilder::new(db_id) + .with_version(DB_VERSION) + .with_table::() + .build() + .await?; + + Ok(Self(inner)) + } +} + +impl LockedNoteDbInner { + pub fn get_inner(&self) -> &IndexedDb { &self.0 } +} + +impl LockedNotesStorage { + async fn lockdb(&self) -> MmResult { + Ok(self.db.get_or_initialize().await?) + } +} + +impl LockedNotesStorage { + pub(crate) async fn new(ctx: &MmArc, address: String) -> Result { + let db = ConstructibleDb::new(ctx).into_shared(); + Ok(Self { address, db }) + } + + pub(crate) async fn insert_spent_note( + &self, + txid: String, + rseed: String, + ) -> MmResult<(), LockedNotesStorageError> { + let db = self.lockdb().await?; + let address = self.address.clone(); + let transaction = db.get_inner().transaction().await?; + let change_note_table = transaction.table::().await?; + + let change_note = LockedNoteTable { + address, + variant: "Spent".to_owned(), + txid, + rseed: Some(rseed), + value: None, + }; + Ok(change_note_table + .add_item(&change_note) + .await + .map(|_| ())?) + } + + pub(crate) async fn insert_change_note( + &self, + txid: String, + value: u64, + ) -> MmResult<(), LockedNotesStorageError> { + let db = self.lockdb().await?; + let address = self.address.clone(); + let transaction = db.get_inner().transaction().await?; + let change_note_table = transaction.table::().await?; + + let change_note = LockedNoteTable { + address, + variant: "Change".to_owned(), + txid, + rseed: None, + value: Some(value), + }; + Ok(change_note_table + .add_item(&change_note) + .await + .map(|_| ())?) + } + + pub(crate) async fn remove_notes_for_txid(&self, txid: String) -> MmResult<(), LockedNotesStorageError> { + let db = self.lockdb().await?; + let transaction = db.get_inner().transaction().await?; + let change_note_table = transaction.table::().await?; + change_note_table.delete_items_by_index("txid", &txid).await?; + + Ok(()) + } + + pub(crate) async fn load_all_notes(&self) -> MmResult, LockedNotesStorageError> { + let db = self.lockdb().await?; + let transaction = db.get_inner().transaction().await?; + let change_note_table = transaction.table::().await?; + let records = change_note_table.get_items("address", &self.address).await?; + Ok(records + .into_iter() + .filter_map(|(_, n)| { + match n.variant.as_str() { + "Spent" => n.rseed.clone().map(|rseed| LockedNote::Spent { + rseed, + }), + "Change" => n.value.map(|value| LockedNote::Change { + value, + }), + _ => None, + } + }) + .collect()) + } +} diff --git a/mm2src/coins/z_coin/z_coin_errors.rs b/mm2src/coins/z_coin/z_coin_errors.rs index 3c6e44a32a..23b1664a5b 100644 --- a/mm2src/coins/z_coin/z_coin_errors.rs +++ b/mm2src/coins/z_coin/z_coin_errors.rs @@ -1,3 +1,4 @@ +use super::storage::LockedNotesStorageError; use crate::my_tx_history_v2::MyTxHistoryErrorV2; use crate::utxo::rpc_clients::UtxoRpcError; use crate::utxo::utxo_builder::UtxoCoinBuildError; @@ -9,6 +10,7 @@ use common::jsonrpc_client::JsonRpcError; #[cfg(not(target_arch = "wasm32"))] use db_common::sqlite::rusqlite::Error as SqliteError; use derive_more::Display; +use enum_derives::EnumFromStringify; use http::uri::InvalidUri; #[cfg(target_arch = "wasm32")] use mm2_db::indexed_db::cursor_prelude::*; @@ -100,7 +102,7 @@ pub enum UrlIterError { ConnectionFailure(tonic::transport::Error), } -#[derive(Debug, Display)] +#[derive(Debug, Display, EnumFromStringify)] pub enum GenTxError { DecryptedOutputNotFound, GetWitnessErr(GetUnspentWitnessErr), @@ -130,6 +132,8 @@ pub enum GenTxError { FailedToCreateNote, SpendableNotesError(String), Internal(String), + #[from_stringify("LockedNotesStorageError")] + SaveLockedNotesError(String), } impl From for GenTxError { @@ -177,7 +181,8 @@ impl From for WithdrawError { | GenTxError::LightClientErr(_) | GenTxError::SpendableNotesError(_) | GenTxError::FailedToCreateNote - | GenTxError::Internal(_) => WithdrawError::InternalError(gen_tx.to_string()), + | GenTxError::Internal(_) + | GenTxError::SaveLockedNotesError(_) => WithdrawError::InternalError(gen_tx.to_string()), } } } @@ -231,10 +236,11 @@ impl From for GetUnspentWitnessErr { fn from(err: SqliteError) -> GetUnspentWitnessErr { GetUnspentWitnessErr::ZcashDBError(err.to_string()) } } -#[derive(Debug, Display)] +#[derive(Debug, Display, EnumFromStringify)] pub enum ZCoinBuildError { UtxoBuilderError(UtxoCoinBuildError), GetAddressError, + #[from_stringify("LockedNotesStorageError")] ZcashDBError(String), Rpc(UtxoRpcError), #[display(fmt = "Sapling cache DB does not exist at {}. Please download it.", path)] diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index aba2d0d55f..9b35081f7c 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -1,9 +1,10 @@ use super::{z_coin_errors::*, BlockDbImpl, CheckPointBlockInfo, WalletDbShared, ZCoinBuilder, ZcoinConsensusParams}; -use crate::utxo::rpc_clients::NO_TX_ERROR_CODE; use crate::utxo::utxo_builder::{UtxoCoinBuilderCommonOps, DAY_IN_SECONDS}; +use crate::z_coin::storage::z_locked_notes::LockedNotesStorage; use crate::z_coin::storage::{BlockProcessingMode, DataConnStmtCacheWrapper}; use crate::z_coin::SyncStartPoint; use crate::RpcCommonOps; + use async_trait::async_trait; use common::executor::Timer; use common::executor::{spawn_abortable, AbortOnDropHandle}; @@ -338,13 +339,11 @@ impl ZRpcOps for LightRpcClient { match client.get_transaction(request).await { Ok(_) => break, Err(e) => { - error!("Error on getting tx {}", tx_id); - if e.message().contains(NO_TX_ERROR_CODE) { - if attempts >= 3 { - return false; - } - attempts += 1; + error!("Error on getting tx {}: err: {}", tx_id, e.to_string()); + if attempts >= 5 { + return false; } + attempts += 1; Timer::sleep(30.).await; }, } @@ -475,16 +474,16 @@ impl ZRpcOps for NativeClient { async fn check_tx_existence(&self, tx_id: TxId) -> bool { let mut attempts = 0; loop { - match self.get_raw_transaction_bytes(&H256Json::from(tx_id.0)).compat().await { + let tx_hash = H256Json::from(tx_id.0).reversed(); + let tx = self.get_raw_transaction_bytes(&tx_hash).compat().await; + match tx { Ok(_) => break, Err(e) => { - error!("Error on getting tx {}", tx_id); - if e.to_string().contains(NO_TX_ERROR_CODE) { - if attempts >= 3 { - return false; - } - attempts += 1; + error!("Error on getting tx {}: err: {}", tx_id, e.to_string()); + if attempts >= 5 { + return false; } + attempts += 1; Timer::sleep(30.).await; }, } @@ -507,6 +506,7 @@ pub(super) async fn init_light_client<'a>( blocks_db: BlockDbImpl, sync_params: &Option, skip_sync_params: bool, + locked_notes_db: LockedNotesStorage, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); @@ -568,6 +568,7 @@ pub(super) async fn init_light_client<'a>( scan_interval_ms: builder.z_coin_params.scan_interval_ms, first_sync_block: first_sync_block.clone(), streaming_manager: builder.ctx.event_stream_manager.clone(), + locked_notes_db, }; let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(light_rpc_clients))); @@ -583,6 +584,7 @@ pub(super) async fn init_native_client<'a>( builder: &ZCoinBuilder<'a>, native_client: NativeClient, blocks_db: BlockDbImpl, + locked_notes_db: LockedNotesStorage, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); @@ -614,6 +616,7 @@ pub(super) async fn init_native_client<'a>( scan_interval_ms: builder.z_coin_params.scan_interval_ms, first_sync_block: first_sync_block.clone(), streaming_manager: builder.ctx.event_stream_manager.clone(), + locked_notes_db, }; let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(native_client))); @@ -700,6 +703,7 @@ pub struct SaplingSyncLoopHandle { current_block: BlockHeight, blocks_db: BlockDbImpl, wallet_db: WalletDbShared, + locked_notes_db: LockedNotesStorage, consensus_params: ZcoinConsensusParams, /// Notifies about sync status without stopping the loop, e.g. on coin activation sync_status_notifier: AsyncSender, @@ -800,6 +804,7 @@ impl SaplingSyncLoopHandle { BlockProcessingMode::Validate, wallet_ops.get_max_height_hash().await?, None, + &self.locked_notes_db, ) .await { @@ -844,6 +849,7 @@ impl SaplingSyncLoopHandle { BlockProcessingMode::Scan(scan, self.streaming_manager.clone()), None, Some(self.scan_blocks_per_iteration), + &self.locked_notes_db, ) .await?; @@ -914,7 +920,7 @@ async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle, mut c let walletdb = &sync_handle.wallet_db; if let Ok(is_tx_imported) = walletdb.is_tx_imported(tx_id).await { if !is_tx_imported { - info!("Tx {} is not imported yet", tx_id); + error!("Tx {} is not imported yet", tx_id); Timer::sleep(10.).await; continue; } diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 560156e148..b2fe520cfb 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -98,6 +98,7 @@ serialization_derive = { path = "../mm2_bitcoin/serialization_derive" } spv_validation = { path = "../mm2_bitcoin/spv_validation" } sp-runtime-interface.workspace = true sp-trie.workspace = true +tempfile.workspace = true trie-db.workspace = true trie-root.workspace = true uuid.workspace = true diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs index d513ada2d2..6a483767af 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -239,7 +239,7 @@ impl CoinDockerOps for ZCoinAssetDockerOps { impl ZCoinAssetDockerOps { pub fn new() -> ZCoinAssetDockerOps { - let (ctx, coin) = block_on(z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe")); + let (ctx, coin) = block_on(z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe", "fe")); ZCoinAssetDockerOps { ctx, coin } } diff --git a/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs index f9cd8e99af..1fa139ab31 100644 --- a/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs @@ -1,6 +1,7 @@ use bitcrypto::dhash160; use coins::z_coin::{z_coin_from_conf_and_params_with_docker, z_send_dex_fee, ZCoin, ZcoinActivationParams, ZcoinRpcMode}; +use coins::DexFeeBurnDestination; use coins::{coin_errors::ValidatePaymentError, CoinProtocol, DexFee, PrivKeyBuildPolicy, RefundPaymentArgs, SendPaymentArgs, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, ValidateFeeArgs}; use common::now_sec; @@ -8,22 +9,33 @@ use lazy_static::lazy_static; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_number::MmNumber; use mm2_test_helpers::for_tests::zombie_conf_for_docker; +use tempfile::TempDir; use tokio::sync::Mutex; // https://github.com/KomodoPlatform/librustzcash/blob/4e030a0f44cc17f100bf5f019563be25c5b8755f/zcash_client_backend/src/data_api/wallet.rs#L72-L73 lazy_static! { - static ref TEST_MUTEX: Mutex<()> = Mutex::new(()); + /// For secret....fe + static ref GEN_TX_LOCK_MUTEX: Mutex<()> = Mutex::new(()); + /// For secret....we + static ref GEN_TX_LOCK_MUTEX_ADDR2: Mutex<()> = Mutex::new(()); + /// This `TempDir` is created once on first use and cleaned up when the process exits. + static ref TEMP_DIR: Mutex = Mutex::new(TempDir::new().unwrap()); } -/// Build asset `ZCoin` from ticker and spendingkey str without filling the balance. -pub async fn z_coin_from_spending_key(spending_key: &str) -> (MmArc, ZCoin) { - let ctx = MmCtxBuilder::new().into_mm_arc(); +/// Build asset `ZCoin` from ticker and spending_key. +pub async fn z_coin_from_spending_key<'a>(spending_key: &str, path: &'a str) -> (MmArc, ZCoin) { + let tmp = TEMP_DIR.lock().await; + let db_path = tmp.path().join(format!("ZOMBIE_DB_{path}")); + std::fs::create_dir_all(&db_path).unwrap(); + let ctx = MmCtxBuilder::new().with_conf(json!({ "dbdir": db_path})).into_mm_arc(); + let mut conf = zombie_conf_for_docker(); let params = ZcoinActivationParams { mode: ZcoinRpcMode::Native, ..Default::default() }; let pk_data = [1; 32]; + let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { CoinProtocol::ZHTLC(protocol_info) => protocol_info, other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), @@ -44,17 +56,24 @@ pub async fn z_coin_from_spending_key(spending_key: &str) -> (MmArc, ZCoin) { (ctx, coin) } -#[ignore] -#[tokio::test(flavor = "multi_thread")] +#[tokio::test(flavor = "current_thread")] +async fn prepare_zombie_sapling_cache() { + let _lock = GEN_TX_LOCK_MUTEX.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe", "fe").await; + assert!(coin.is_sapling_state_synced().await); + drop(_lock) +} + +#[tokio::test(flavor = "current_thread")] async fn zombie_coin_send_and_refund_maker_payment() { - let _lock = TEST_MUTEX.lock().await; + let _lock = GEN_TX_LOCK_MUTEX.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe", "fe").await; + + assert!(coin.is_sapling_state_synced().await); - let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; let time_lock = now_sec() - 3600; let secret_hash = [0; 20]; - let maker_uniq_data = [3; 32]; - let taker_uniq_data = [5; 32]; let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); let taker_pub = taker_key_pair.public(); @@ -90,12 +109,12 @@ async fn zombie_coin_send_and_refund_maker_payment() { drop(_lock); } -#[ignore] -#[tokio::test(flavor = "multi_thread")] +#[tokio::test(flavor = "current_thread")] async fn zombie_coin_send_and_spend_maker_payment() { - let _lock = TEST_MUTEX.lock().await; + let _lock = GEN_TX_LOCK_MUTEX_ADDR2.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1qvqstxphqyqqpqqnh3hstqpdjzkpadeed6u7fz230jmm2mxl0aacrtu9vt7a7rmr2w5az5u79d24t0rudak3newknrz5l0m3dsd8m4dffqh5xwyldc5qwz8pnalrnhlxdzf900x83jazc52y25e9hvyd4kepaze6nlcvk8sd8a4qjh3e9j5d6730t7ctzhhrhp0zljjtwuptadnksxf8a8y5axwdhass5pjaxg0hzhg7z25rx0rll7a6txywl32s6cda0s5kexr03uqdtelwe", "we").await; - let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; + assert!(coin.is_sapling_state_synced().await); let lock_time = now_sec() - 1000; let secret = [0; 32]; @@ -139,37 +158,55 @@ async fn zombie_coin_send_and_spend_maker_payment() { drop(_lock); } -#[ignore] -#[tokio::test(flavor = "multi_thread")] -async fn prepare_zombie_sapling_cache() { - let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; +#[tokio::test(flavor = "current_thread")] +async fn zombie_coin_send_standard_dex_fee() { + let _lock = GEN_TX_LOCK_MUTEX_ADDR2.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1qvqstxphqyqqpqqnh3hstqpdjzkpadeed6u7fz230jmm2mxl0aacrtu9vt7a7rmr2w5az5u79d24t0rudak3newknrz5l0m3dsd8m4dffqh5xwyldc5qwz8pnalrnhlxdzf900x83jazc52y25e9hvyd4kepaze6nlcvk8sd8a4qjh3e9j5d6730t7ctzhhrhp0zljjtwuptadnksxf8a8y5axwdhass5pjaxg0hzhg7z25rx0rll7a6txywl32s6cda0s5kexr03uqdtelwe", "we").await; assert!(coin.is_sapling_state_synced().await); + + let tx = z_send_dex_fee(&coin, DexFee::Standard("0.01".into()), &[1; 16]) + .await + .unwrap(); + log!("dex fee tx {}", tx.txid()); + drop(_lock) } -#[ignore] -#[tokio::test(flavor = "multi_thread")] +#[tokio::test(flavor = "current_thread")] async fn zombie_coin_send_dex_fee() { - let _lock = TEST_MUTEX.lock().await; + let _lock = GEN_TX_LOCK_MUTEX_ADDR2.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1qvqstxphqyqqpqqnh3hstqpdjzkpadeed6u7fz230jmm2mxl0aacrtu9vt7a7rmr2w5az5u79d24t0rudak3newknrz5l0m3dsd8m4dffqh5xwyldc5qwz8pnalrnhlxdzf900x83jazc52y25e9hvyd4kepaze6nlcvk8sd8a4qjh3e9j5d6730t7ctzhhrhp0zljjtwuptadnksxf8a8y5axwdhass5pjaxg0hzhg7z25rx0rll7a6txywl32s6cda0s5kexr03uqdtelwe", "we").await; - let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; + assert!(coin.is_sapling_state_synced().await); - let dex_fee = DexFee::Standard("0.01".into()); + let dex_fee = DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); log!("dex fee tx {}", tx.txid()); drop(_lock); } -#[ignore] -#[tokio::test(flavor = "multi_thread")] +#[tokio::test(flavor = "current_thread")] async fn zombie_coin_validate_dex_fee() { - let _lock = TEST_MUTEX.lock().await; - let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; + let _lock = GEN_TX_LOCK_MUTEX.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe", "fe").await; - // let balance = coin.my_balance().compat().await; + assert!(coin.is_sapling_state_synced().await); - let dex_fee = DexFee::Standard("0.01".into()); - let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); + let tx = z_send_dex_fee( + &coin, + DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }, + &[1; 16], + ) + .await + .unwrap(); log!("dex fee tx {}", tx.txid()); let tx = tx.into(); @@ -177,52 +214,78 @@ async fn zombie_coin_validate_dex_fee() { fee_tx: &tx, expected_sender: &[], dex_fee: &DexFee::Standard(MmNumber::from("0.001")), - min_block_number: 4, + min_block_number: 12000, uuid: &[1; 16], }; // Invalid amount should return an error let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Dex fee has invalid amount")), + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), } // Invalid memo should return an error + let expected_fee = DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), - min_block_number: 10, + dex_fee: &expected_fee, + min_block_number: 12000, uuid: &[2; 16], }; + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Dex fee has invalid memo")), + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid memo")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), } - // Confirmed before min block + // Success validation let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), - min_block_number: 20000, + dex_fee: &expected_fee, + min_block_number: 12000, + uuid: &[1; 16], + }; + coin.validate_fee(validate_fee_args).await.unwrap(); + + // Test old standard dex fee with no burn output + // TODO: disable when the upgrade transition period ends + let tx_2 = z_send_dex_fee(&coin, DexFee::Standard("0.00879999".into()), &[1; 16]) + .await + .unwrap(); + log!("dex fee tx {}", tx_2.txid()); + let tx_2 = tx_2.into(); + + // Success validation + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx_2, + expected_sender: &[], + dex_fee: &DexFee::Standard("0.00999999".into()), + min_block_number: 12000, uuid: &[1; 16], }; let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("confirmed before min block")), + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), } // Success validation + let expected_std_fee = DexFee::Standard("0.00879999".into()); let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx, + fee_tx: &tx_2, expected_sender: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), - min_block_number: 20, + dex_fee: &expected_std_fee, + min_block_number: 12000, uuid: &[1; 16], }; coin.validate_fee(validate_fee_args).await.unwrap(); - drop(_lock); + drop(_lock) } diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 86697593a5..490d312269 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -480,11 +480,11 @@ pub enum Mm2InitPrivKeyPolicy { GlobalHDAccount, } -pub fn zombie_conf() -> Json { zombie_conf_inner(None) } +pub fn zombie_conf() -> Json { zombie_conf_inner(None, 0) } -pub fn zombie_conf_for_docker() -> Json { zombie_conf_inner(Some(10)) } +pub fn zombie_conf_for_docker() -> Json { zombie_conf_inner(Some(10), 1) } -pub fn zombie_conf_inner(custom_blocktime: Option) -> Json { +pub fn zombie_conf_inner(custom_blocktime: Option, required_confirmations: u8) -> Json { json!({ "coin":"ZOMBIE", "asset":"ZOMBIE", @@ -511,7 +511,7 @@ pub fn zombie_conf_inner(custom_blocktime: Option) -> Json { "z_derivation_path": "m/32'/133'", } }, - "required_confirmations":0, + "required_confirmations": required_confirmations, "derivation_path": "m/44'/133'", }) } From dc39db704a74a5dcdb7f5693fc556af346bcd850 Mon Sep 17 00:00:00 2001 From: dimxy Date: Mon, 16 Jun 2025 10:57:35 +0500 Subject: [PATCH 30/36] feat(tests): zcoin unit test to validate dex fee (#2460) This commit introduces a framework for creating zcoin unit tests and makes the following key changes: - A new unit test has been added to validate the address-checking logic within the `validate_dex_fee_output` function. - The multicore feature has been removed from the zcash_proofs dependency for the wasm32 target to resolve test errors. --- Cargo.toml | 2 +- mm2src/coins/Cargo.toml | 7 +- mm2src/coins/z_coin.rs | 66 +--- mm2src/coins/z_coin/z_coin_native_tests.rs | 366 --------------------- mm2src/coins/z_coin/z_unit_tests.rs | 344 +++++++++++++++++++ 5 files changed, 366 insertions(+), 419 deletions(-) delete mode 100644 mm2src/coins/z_coin/z_coin_native_tests.rs create mode 100644 mm2src/coins/z_coin/z_unit_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 1ce0e81764..dd317f0d87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -228,7 +228,7 @@ zcash_client_backend = { git = "https://github.com/komodoplatform/librustzcash.g zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } zcash_extras = { git = "https://github.com/komodoplatform/librustzcash.git", tag = "k-1.4.2" } zcash_primitives = { git = "https://github.com/komodoplatform/librustzcash.git", tag = "k-1.4.2", features = ["transparent-inputs"] } -zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2", default-features = false, features = ["local-prover", "multicore"] } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2", default-features = false } x25519-dalek = { version = "2.0", features = ["static_secrets"] } zeroize = { version = "1.5", features = ["zeroize_derive"] } diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index c099395605..3e49769c7e 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -135,7 +135,7 @@ wasm-bindgen.workspace = true wasm-bindgen-futures.workspace = true wasm-bindgen-test.workspace = true web-sys = { workspace = true, features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } -zcash_proofs.workspace = true +zcash_proofs = { workspace = true, features = ["local-prover"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dirs.workspace = true @@ -155,7 +155,7 @@ tokio-rustls.workspace = true tonic = { workspace = true, features = ["codegen", "prost", "gzip", "tls", "tls-webpki-roots"] } webpki-roots.workspace = true zcash_client_sqlite.workspace = true -zcash_proofs.workspace = true +zcash_proofs = { workspace = true, features = ["local-prover", "multicore"] } [target.'cfg(windows)'.dependencies] winapi.workspace = true @@ -164,6 +164,9 @@ winapi.workspace = true mm2_test_helpers = { path = "../mm2_test_helpers" } mocktopus.workspace = true mm2_p2p = { path = "../mm2_p2p", features = ["application"] } +ff.workspace = true +jubjub.workspace = true +reqwest.workspace = true [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wagyu-zcash-parameters.workspace = true diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 243d6f89e6..2d4d3bde42 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -7,6 +7,7 @@ mod z_coin_errors; mod z_htlc; mod z_rpc; mod z_tx_history; +#[cfg(all(test, not(target_arch = "wasm32")))] mod z_unit_tests; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::hd_wallet::{HDAddressSelector, HDPathAccountToAddressId}; @@ -130,6 +131,8 @@ const DEX_FEE_OVK: OutgoingViewingKey = OutgoingViewingKey([7; 32]); const DEX_FEE_Z_ADDR: &str = "zs1rp6426e9r6jkq2nsanl66tkd34enewrmr0uvj0zelhkcwmsy0uvxz2fhm9eu9rl3ukxvgzy2v9f"; const DEX_BURN_Z_ADDR: &str = "zs1ntx28kyurgvsc7rxgkdhasz8p6wzv63nqpcayvnh7c4r6cs4wfkz8ztkwazjzdsxkgaq6erscyl"; cfg_native!( + #[cfg(test)] + const DOWNLOAD_URL: &str = "https://komodoplatform.com/downloads"; const SAPLING_OUTPUT_NAME: &str = "sapling-output.params"; const SAPLING_SPEND_NAME: &str = "sapling-spend.params"; const BLOCKS_TABLE: &str = "blocks"; @@ -809,6 +812,8 @@ pub enum ZcoinRpcMode { /// Will use `sync_params` if no last synced block found. skip_sync_params: Option, }, + #[cfg(test)] + UnitTests, } #[derive(Clone, Deserialize)] @@ -967,6 +972,8 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { ) .await? }, + #[cfg(test)] + ZcoinRpcMode::UnitTests => z_unit_tests::create_test_sync_connector(&self).await, }; let z_fields = Arc::new(ZCoinFields { @@ -1011,6 +1018,12 @@ impl<'a> ZCoinBuilder<'a> { min_connected: *min_connected, max_connected: *max_connected, }, + #[cfg(test)] + ZcoinRpcMode::UnitTests => UtxoRpcMode::Electrum { + servers: vec![], + min_connected: None, + max_connected: Some(1), + }, }; let utxo_params = UtxoActivationParams { mode: utxo_mode, @@ -1073,6 +1086,9 @@ impl<'a> ZCoinBuilder<'a> { Some(file_path) => PathBuf::from(file_path), }; + #[cfg(test)] + z_unit_tests::download_parameters_for_tests(¶ms_dir).await; + async_blocking(move || { let (spend_path, output_path) = get_spend_output_paths(params_dir)?; let verification_successful = verify_checksum_zcash_params(&spend_path, &output_path)?; @@ -2197,53 +2213,3 @@ fn rseed_to_string(rseed: &Rseed) -> String { Rseed::AfterZip212(rseed) => jubjub::Fr::from_bytes_wide(prf_expand(rseed, &INPUT).as_array()).to_string(), } } - -#[test] -fn derive_z_key_from_mm_seed() { - use crypto::privkey::key_pair_from_seed; - use zcash_client_backend::encoding::encode_extended_spending_key; - - let seed = "spice describe gravity federal blast come thank unfair canal monkey style afraid"; - let secp_keypair = key_pair_from_seed(seed).unwrap(); - let z_spending_key = ExtendedSpendingKey::master(&*secp_keypair.private().secret); - let encoded = encode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, &z_spending_key); - assert_eq!(encoded, "secret-extended-key-main1qqqqqqqqqqqqqqytwz2zjt587n63kyz6jawmflttqu5rxavvqx3lzfs0tdr0w7g5tgntxzf5erd3jtvva5s52qx0ms598r89vrmv30r69zehxy2r3vesghtqd6dfwdtnauzuj8u8eeqfx7qpglzu6z54uzque6nzzgnejkgq569ax4lmk0v95rfhxzxlq3zrrj2z2kqylx2jp8g68lqu6alczdxd59lzp4hlfuj3jp54fp06xsaaay0uyass992g507tdd7psua5w6q76dyq3"); - - let (_, address) = z_spending_key.default_address().unwrap(); - let encoded_addr = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &address); - assert_eq!( - encoded_addr, - "zs182ht30wnnnr8jjhj2j9v5dkx3qsknnr5r00jfwk2nczdtqy7w0v836kyy840kv2r8xle5gcl549" - ); - - let seed = "also shoot benefit prefer juice shell elder veteran woman mimic image kidney"; - let secp_keypair = key_pair_from_seed(seed).unwrap(); - let z_spending_key = ExtendedSpendingKey::master(&*secp_keypair.private().secret); - let encoded = encode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, &z_spending_key); - assert_eq!(encoded, "secret-extended-key-main1qqqqqqqqqqqqqq8jnhc9stsqwts6pu5ayzgy4szplvy03u227e50n3u8e6dwn5l0q5s3s8xfc03r5wmyh5s5dq536ufwn2k89ngdhnxy64sd989elwas6kr7ygztsdkw6k6xqyvhtu6e0dhm4mav8rus0fy8g0hgy9vt97cfjmus0m2m87p4qz5a00um7gwjwk494gul0uvt3gqyjujcclsqry72z57kr265jsajactgfn9m3vclqvx8fsdnwp4jwj57ffw560vvwks9g9hpu"); - - let (_, address) = z_spending_key.default_address().unwrap(); - let encoded_addr = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &address); - assert_eq!( - encoded_addr, - "zs1funuwrjr2stlr6fnhkdh7fyz3p7n0p8rxase9jnezdhc286v5mhs6q3myw0phzvad5mvqgfxpam" - ); -} - -#[test] -fn test_interpret_memo_string() { - use std::str::FromStr; - use zcash_primitives::memo::Memo; - - let actual = interpret_memo_string("68656c6c6f207a63617368").unwrap(); - let expected = Memo::from_str("68656c6c6f207a63617368").unwrap().encode(); - assert_eq!(actual, expected); - - let actual = interpret_memo_string("A custom memo").unwrap(); - let expected = Memo::from_str("A custom memo").unwrap().encode(); - assert_eq!(actual, expected); - - let actual = interpret_memo_string("0x68656c6c6f207a63617368").unwrap(); - let expected = MemoBytes::from_bytes(&hex::decode("68656c6c6f207a63617368").unwrap()).unwrap(); - assert_eq!(actual, expected); -} diff --git a/mm2src/coins/z_coin/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs deleted file mode 100644 index 892da5401e..0000000000 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ /dev/null @@ -1,366 +0,0 @@ -//! Native tests for zcoin -//! -//! To run zcoin tests in this source you need `--features zhtlc-native-tests` -//! ZOMBIE chain must be running for zcoin tests: -//! komodod -ac_name=ZOMBIE -ac_supply=0 -ac_reward=25600000000 -ac_halving=388885 -ac_private=1 -ac_sapling=1 -testnode=1 -addnode=65.21.51.116 -addnode=116.203.120.163 -addnode=168.119.236.239 -addnode=65.109.1.121 -addnode=159.69.125.84 -addnode=159.69.10.44 -//! Also check the test z_key (spending key) has balance: -//! `komodo-cli -ac_name=ZOMBIE z_getbalance zs10hvyxf3ajm82e4gvxem3zjlf9xf3yxhjww9fvz3mfqza9zwumvluzy735e29c3x5aj2nu0ua6n0` -//! If no balance, you may mine some transparent coins and send to the test z_key. -//! When tests are run for the first time (or have not been run for a long) synching to fill ZOMBIE_wallet.db is started which may take hours. -//! So it is recommended to run prepare_zombie_sapling_cache to sync ZOMBIE_wallet.db before running zcoin tests: -//! cargo test -p coins --features zhtlc-native-tests -- --nocapture prepare_zombie_sapling_cache -//! If you did not run prepare_zombie_sapling_cache waiting for ZOMBIE_wallet.db sync will be done in the first call to ZCoin::gen_tx. -//! In tests, for ZOMBIE_wallet.db to be filled, another database ZOMBIE_cache.db is created in memory, -//! so if db sync in tests is cancelled and restarted this would cause restarting of building ZOMBIE_cache.db in memory -//! -//! Note that during the ZOMBIE_wallet.db sync an error may be reported: -//! 'error trying to connect: tcp connect error: Can't assign requested address (os error 49)'. -//! Also during the sync other apps like ssh or komodo-cli may return same error or even crash. TODO: fix this problem, maybe it is due to too much load on TCP stack -//! Errors like `No one seems interested in SyncStatus: send failed because channel is full` in the debug log may be ignored (means that update status is temporarily not watched) -//! -//! To monitor sync status in logs you may add logging support into the beginning of prepare_zombie_sapling_cache test (or other tests): -//! common::log::UnifiedLoggerBuilder::default().init(); -//! and run cargo test with var RUST_LOG=debug - -use bitcrypto::dhash160; -use common::{block_on, now_sec}; -use mm2_core::mm_ctx::MmCtxBuilder; -use mm2_test_helpers::for_tests::zombie_conf; -use std::time::Duration; -use zcash_client_backend::encoding::decode_extended_spending_key; - -use super::{z_coin_from_conf_and_params_with_z_key, z_mainnet_constants, PrivKeyBuildPolicy, RefundPaymentArgs, - SendPaymentArgs, SpendPaymentArgs, SwapOps, ValidateFeeArgs, ValidatePaymentError, ZTransaction}; -use crate::z_coin::{z_htlc::z_send_dex_fee, ZcoinActivationParams, ZcoinRpcMode}; -use crate::{CoinProtocol, SwapTxTypeWithSecretHash}; -use crate::{DexFee, DexFeeBurnDestination}; -use mm2_number::MmNumber; - -fn native_zcoin_activation_params() -> ZcoinActivationParams { - ZcoinActivationParams { - mode: ZcoinRpcMode::Native, - ..Default::default() - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn zombie_coin_send_and_refund_maker_payment() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let pk_data = [1; 32]; - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = z_coin_from_conf_and_params_with_z_key( - &ctx, - "ZOMBIE", - &conf, - ¶ms, - PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), - z_key, - protocol_info, - ) - .await - .unwrap(); - - let time_lock = now_sec() - 3600; - let maker_uniq_data = [3; 32]; - - let taker_uniq_data = [5; 32]; - let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); - let taker_pub = taker_key_pair.public(); - - let secret_hash = [0; 20]; - - let args = SendPaymentArgs { - time_lock_duration: 0, - time_lock, - other_pubkey: taker_pub, - secret_hash: &secret_hash, - amount: "0.01".parse().unwrap(), - swap_contract_address: &None, - swap_unique_data: maker_uniq_data.as_slice(), - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - let tx = coin.send_maker_payment(args).await.unwrap(); - log!("swap tx {}", hex::encode(tx.tx_hash_as_bytes().0)); - - let refund_args = RefundPaymentArgs { - payment_tx: &tx.tx_hex(), - time_lock, - other_pubkey: taker_pub, - tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { - maker_secret_hash: &secret_hash, - }, - swap_contract_address: &None, - swap_unique_data: maker_uniq_data.as_slice(), - watcher_reward: false, - }; - let refund_tx = coin.send_maker_refunds_payment(refund_args).await.unwrap(); - log!("refund tx {}", hex::encode(refund_tx.tx_hash_as_bytes().0)); -} - -#[tokio::test(flavor = "multi_thread")] -async fn zombie_coin_send_and_spend_maker_payment() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let pk_data = [1; 32]; - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = z_coin_from_conf_and_params_with_z_key( - &ctx, - "ZOMBIE", - &conf, - ¶ms, - PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), - z_key, - protocol_info, - ) - .await - .unwrap(); - - let lock_time = now_sec() - 1000; - - let maker_uniq_data = [3; 32]; - let maker_key_pair = coin.derive_htlc_key_pair(maker_uniq_data.as_slice()); - let maker_pub = maker_key_pair.public(); - - let taker_uniq_data = [5; 32]; - let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); - let taker_pub = taker_key_pair.public(); - - let secret = [0; 32]; - let secret_hash = dhash160(&secret); - - let maker_payment_args = SendPaymentArgs { - time_lock_duration: 0, - time_lock: lock_time, - other_pubkey: taker_pub, - secret_hash: secret_hash.as_slice(), - amount: "0.01".parse().unwrap(), - swap_contract_address: &None, - swap_unique_data: maker_uniq_data.as_slice(), - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - - let tx = coin.send_maker_payment(maker_payment_args).await.unwrap(); - log!("swap tx {}", hex::encode(tx.tx_hash_as_bytes().0)); - - let spends_payment_args = SpendPaymentArgs { - other_payment_tx: &tx.tx_hex(), - time_lock: lock_time, - other_pubkey: maker_pub, - secret: &secret, - secret_hash: secret_hash.as_slice(), - swap_contract_address: &None, - swap_unique_data: taker_uniq_data.as_slice(), - watcher_reward: false, - }; - let spend_tx = coin.send_taker_spends_maker_payment(spends_payment_args).await.unwrap(); - log!("spend tx {}", hex::encode(spend_tx.tx_hash_as_bytes().0)); -} - -#[tokio::test(flavor = "multi_thread")] -async fn zombie_coin_send_dex_fee() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, z_key, protocol_info) - .await - .unwrap(); - - let dex_fee = DexFee::WithBurn { - fee_amount: "0.0075".into(), - burn_amount: "0.0025".into(), - burn_destination: DexFeeBurnDestination::PreBurnAccount, - }; - let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); - log!("dex fee tx {}", tx.txid()); -} - -#[tokio::test(flavor = "multi_thread")] -async fn zombie_coin_send_standard_dex_fee() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, z_key, protocol_info) - .await - .unwrap(); - - let dex_fee = DexFee::Standard("0.01".into()); - let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); - log!("dex fee tx {}", tx.txid()); -} - -/// Use to create ZOMBIE_wallet.db -#[test] -fn prepare_zombie_sapling_cache() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = block_on(z_coin_from_conf_and_params_with_z_key( - &ctx, - "ZOMBIE", - &conf, - ¶ms, - priv_key, - z_key, - protocol_info, - )) - .unwrap(); - - while !block_on(coin.is_sapling_state_synced()) { - std::thread::sleep(Duration::from_secs(1)); - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn zombie_coin_validate_dex_fee() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, z_key, protocol_info) - .await - .unwrap(); - - // https://zombie.explorer.lordofthechains.com/tx/9390a26810342151f48f455b09e5d087a5429cbba08f2381b02c43b76f813e29 - let tx_hex = "0400008085202f8900000000000001030c00e8030000000000000169e7017fbd969be53da2c1b8812002baaf59ce98b230a9c1001397ba7f4db8676bd77e8ea644b67067d1f996d8d81c279961343f00a10095bccbddc341c98539287c900cf969688ddc574786e0e34bd6d3ec2ffaab5e2d472848781b116906669786c14c5c608b20dc23c9566fd46861f6a258b5ffc6de73495b56f4823e098c8664eab895d5cd31c013428ae2cbe940dc236ca40465ea2b912ce6c36555b2affb1f38b99b28dc593d865b0b948d567f9315df666d2e65e666d829b9823154bae0410bd885582b4a8a6eb4b9ae214b59ffd9b1167b7cd48f48a11cbd67c08f4e01ed4fd78fc91d0c9e70baa4f25761ef6c78cd7268b307aaa6ece2b443937eb4beac2c8843279a8879adbe0b381e65d0b674f2feeb54b78f80b377f66baab72c4cf9f10dde48f343c001df91a1a6d252ad8eca26eea0fdee49ad7024b505e55b4e082e94616794ddd7c2b852594b4b7af2292f0aa9e34f38322f548f1a21c015e92dbfd239ce18144f3b8045e9efa3de6b4c6b338f01d0adeb26a088a3c8c00503b67b2980b7663e97541e2944e4ad3588554966b6a930d2dc01d9fc7f8a846583fcf3b721f979705eff5bb9bb1fb0cad9ad941ceb3f581710efd8c50713a53751a0a196322ef8618bf1e097383666e91b5133ba81645d2b542181476eba2326cd02fb29a9f09edc46ea04b32ed9243597318d23b955a2570d78cbfb46cc26c1807eddd1de4785b6e752f859f7e25fc67f9e8a00feafac6fd7781eb72a663d9b80c10e9c387abc4d41294b3573785fd53bc56ccac2edf5c7bbb99cb3bcf87161fa893d2e1aabfee75754767cef07a12e44bb707720e727e585a258356cc797ecee8263c0f61cfc8ffa0360c758f1348ac44c186e12ce0f4faad43b4638abd4a0bc9fd4a6fa4352c20cc771241f95c26f1671ca95c8f4a63a8318dc43299f54e8a899df78ccfd3112a0d5ea637847dd2e3b05be8c0658dd0d7d814473fa5369957c00e84df600df23faaee5faa17b9ededad4731e5e9c1099dfddf5264756800dcfcad4b006b736d1d47c59a019acde4dc22249fc40846b77b43294e32a21db745e1bec790324c3d505edc79388a6e44b02841b26306ed48cfce1e941642c30792315016dba03797c8e4e279eec5b78aad602620471f24c25aea3aaa57509aa9eef2057f11bc95bad708918f2f0df74ac179d7dffc772b2c603dd89e7aea0e8f94f1a8bab4a4fba10bf05c88fbe4b021b3faff3d558e32e4bc20be4bed62d653674ce697390e098e590a3e354cb4a1e703474de8aab30cd76cf7e237f2e66bf486c4fc6c22028764e95adf7d8fa018f44b51ae6acfa3bf80f14c45c06623b916d79649abe0a2b229f96e60e421f6e734160da37f01e915cf73d1cacd1eb7f06c26c33b4d8e4dde264f3cfe84bada0601d1c03aa31c5938750ca0b852f3177883cae9f285d582a4eb38c05f8ef6e5cff5be0745e1ec66e20752bfd5bd5a1590fa280ace3e9786e0022e7ae3c48bcca14e9c5513bc8b57e15820a685f8348159862be0579a35d8ac9d1abaf36d9274c7e750fd9ad265c0d8f08c95ed9ce69eef3a55aef05f2d5d601f80f472689f3428e4f0095829a459813d5dace7e6137a752ae5567982e67b2092afeba99561fbe4e716f67bd1b4e8de1f376dec30eed27371bcc42d7de2ea0f4288054618e9afa002a2d1996b7a70a9683229f28bab811b67629dad527f325c0f12e19d92bac51e5924f27048fa118673b52b296b3642ec946d9915ded0ae84e1a2236da65f672bdad75a22cc0ea751c07e56d2ec22caa41afc98ec6b37a8c1b6a5378a81f2cdb2228f4efb8d7f35c0086a955e1b04bd09bd7e056c949fab1805f733a8b2061adad0c2b7fae33d21363de911e517b21a1539dfa1b3cbb1ea0dbfa3ffff23bbac01183f852de41e798fca5a278b711893175aeaded90873574d8de30b360f39ea239492c630eda4a811d3bb7a125054d5ca74bb6698aeea1a417ad19415ca0e5ca36abc2f96725986f73bcbe3113e391010d08f58f05979c7cef26ff92506c5d1eb2a2f6f5689e9a39957f0723bef3262f5190de996234d4f00b73ed74d78fdf1e6bf31161e16bd083bc6fbddc4eba85c17067e15f08019e5ed943de8e23a974d516abc641e85e641b03779816c30b3449a16b142417c1ff93ab7fa8f96a175e9ef73b3f06ac76788c27889d426efa78d5b8ce35be4591902f7766fe579a0aa28229235a920d26264c09625dea807f619a040f08931d6e1fe57ff0c48ea476be93a16d1fc8de3617984eeebcf14b63c839b41f8f9305402d1288c8e481a4fa5c3302bb1f83e3f0dc8ff9550f9bacb44bccb58f3de152abef5d578afed1c29dc89495b9e54a0c6d00f1dba45a2cf68c9512d9a9ff0b2531e58e47428a99cb246ca23f867b660dc71785b57407cc292f735634c602409792c4640831809f1f1e51903273b623aa0ae0cdd335c7b9db360b0bceb0d15f2313e1944800f30f82ed5bb07cfa1c4740c2bf2806539a4afac1f79d779b923ad8dc2493ebb2d2fce9aea58a009d64e7d1b71ca6893b076e41f7e88a4b51b5402e3fa6c60fa65a686adea229f0164318c9fa1b6d2d2218e5ada710daffecb6b7dd8bf7447658795c4c7a0ad710c4f02fd19017a0575f9467600cdca019793f2f49d197dbfc937828e5790b90929e5ca16037ec79734b64feec36b36c220a2979c45dd51e24c9fb21d8634471aac20c6f179f90c0d61c7b3d89826d146b157bedd8f6b66f6edfabfe04b49f2f2d999fc2e578a440bafd524c82ae614dc8017e379cf926e042f4fbd6f0628fde52de18d764ba8385b77569eda30d5a3617fb0a0c7fd26c821308c3ae98498d33b974cb318a04af3ea3fbcb13fc62fc952aaef095423da9ec7bdc7b77adbd403931189ddc98fe19a06711415b40a9a68812bb7c5453b7b2377910c7b89c99b379e038a7940487c0fd2405456ee55ab6ead3ef25a8a5b1abcae479c24f5e6869057e0bdabcdf352b4a64a3e385171a6e14c8102b2a187034e21705e3a457167fe0dc0d63d6e8d489c9a18c9d84b541504d36b086c2c63cc1a34c0080122c5d60ca33ab60289d16f21e1ded753607267c2093b1c587b89da9df65584fbe3ff9eb7f91d64e33912b8e91adc27191d22f8e835be6bb24546f21488f7abcb29339c34058d4f4093096144b17b8ab76a346275b7e7c80bca59d20e0bb482bb2a9cc3c9515cc1b5be17348c65c73e9fb1ed77d423c509f7cff0e355a34d080d310f3b848dbc209bbba6b6b109fb8d9556dca0fab086e197327ab423d5d762b68961244d8d22c30a8a3a116770bb15b5a0a347091a843b68d6a8e0f1c79f12523a7561c1233cd44db90f6cd3c1ce5fc13f8382177b5522aae028379269b71ae2a42f41dff7374ed7e83c89566f57297b82478b04359a2c199ce8f842112b7450cc1e2e2e394cda4c67e0b2302e21f6af997607ceefd067f77be8900bb3ecb3e30782477aa76861b286b9ddc9e36fcebb50f04f9516e02da31e6219bb5bcb81ee673d95be14c1bd2be4909556d6dbca0365292c582dedcafcc60b255ab7bcd9d977a4139f394ca1da81040e784fd8e7534f230bc5201e7f1db47eadc30f37609d5bbaba624157d98d65029bbab766b6c23c3049a32b894c0cfcb40913ba1cd2d5acda7d2acc920fd01c36f28fc6b7ffd01a37b17fc3235d0dbe9b8098530bed6894b288604b8689f4aafc22cdf211fb95ef5c90cae62a250234e6f790e9a15012acac88305dc4f91fd564a9ab8bb27c057ec5dd46fe952a7be557caea9b7b1d6118aa42df79b8c207e2bae6c34d67dc32b4360ad20b3e609e9caeb7f432ad51cfce139f2d4eb9ed219f4323acd5685e0e0409939eb662175a83fa083f500516dbcb091a3448cb24c3198c8fc547fbda3cb0894edeceef7ccb4ad746aa06f4038b63ab4095a9c390656520561ba3763b1057b3af7cb548342a2bfc2ab725b01b12a7adfc30d7d9632acafd2595cde406b8637a911b7c86f7b09b11f58acec3f1a1bd7cf6853331b48d7907ed699d91fbdbcab8001e3d8d3a26b491b6e2d98c5e149847a07a2b7faa1f567cd4bc9c83ad553339632f3dcacb890c5222656b3349ddd5c8eacaa490ac0b2b38f8a26da9ce7789f5601769a7f10b93125cb93b589bda4ddb4e8795817b60cc149af7c0699b2bbbf655f2f5ec170d6af51213e8c725e699d181923ecf10c6f1069f46e6bc89c7a29d2ebe133b5c0c4b67826a93add7d4824e60b4c5f0cee358abedb50c54a59e95185d7a80081f2dddba5c7c7c637b2dfe8575ddaa71306a2725c9ec17b8e4e1f271a442f6798cc21bbd55c2d69819ddde37a8e8d6a812c41a3e58719b7c96e9375155c4a873ed698ad37144ef32e3fe41cce9c48bbe31441dbbeec7b97734769063d6d04cd8d4963f09f7101bf57cb97a83452cc5de873c5ac0ce001c471c9fcd3275d90a118dd4c25a525d9fb358ff85104b98136850786b387fa17cc1a1d128bc5f7c365ec7920ea677e4c8023071a958647d9fbd27e29d7d099b4dfbbac086ac2af00407fd12092ef1f4847bf8988d839e49a6b5b42482c3dde77022ace66e1ca15b46f2df88d053c1bc3623110b3be74b08749eba6d22f87a44cf7cc1997e7e45d0e"; - let tx_bytes = hex::decode(tx_hex).unwrap(); - let tx = ZTransaction::read(tx_bytes.as_slice()).unwrap(); - let tx = tx.into(); - - let expected_fee = DexFee::WithBurn { - fee_amount: "0.0075".into(), - burn_amount: "0.0025".into(), - burn_destination: DexFeeBurnDestination::PreBurnAccount, - }; - - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx, - expected_sender: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.001")), - min_block_number: 12000, - uuid: &[1; 16], - }; - // Invalid amount should return an error - let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); - match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), - _ => panic!("Expected `WrongPaymentTx`: {:?}", err), - } - - // Invalid memo should return an error - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx, - expected_sender: &[], - dex_fee: &expected_fee, - min_block_number: 12000, - uuid: &[2; 16], - }; - let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); - match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid memo")), - _ => panic!("Expected `WrongPaymentTx`: {:?}", err), - } - - /* Fix realtime min_block_number to run this test: - // Confirmed before min block - let min_block_number = 451208; - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx, - expected_sender: &[], - dex_fee: &expected_fee, - min_block_number: , - uuid: &[1; 16], - }; - let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); - match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("confirmed before min block")), - _ => panic!("Expected `WrongPaymentTx`: {:?}", err), - } */ - - // Success validation - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx, - expected_sender: &[], - dex_fee: &expected_fee, - min_block_number: 12000, - uuid: &[1; 16], - }; - coin.validate_fee(validate_fee_args).await.unwrap(); - - // Test old standard dex fee with no burn output - // TODO: disable when the upgrade transition period ends - - // https://zombie.explorer.lordofthechains.com/tx/9eb7fc697b280499df33e5838af6e67540d436fd8f565f47a7f03e6013e8342c - let tx_2_hex = "0400008085202f8900000000000006030c00e80300000000000001c167d6e78e09dfbac2973bfd8acac75fc603f6ffb481377e3ec790f1cc812a8a3979ecfb8a0c7c3a966d90675261568550f9363f9384a21390d7f58bde6f7b03270d88e1fa61d739c27d7f585c9bbc81a3d522fbb88fe8dc8567e27a048d475ce14fdfd11455fd54c577538438decbf6954f1ffba86c78896178ce514c5f1762a7de9e83552533eb4c558c4f9950b1806f266b25d6437f5aac08048d6f48100d49ecb2253e85c3b555a7cd84c9628ae58e5d68ddad61e69edfcdc0fa12170dd80340c417bff9e1711bf6e9728a6a52c42598d7ffd00c35679b1555cab075e54b134901d02ca9b07bb20c5719b2728faa020fb844c183c2ae649034a5476c4d129c3f97cd00a87be1ca7e73d027188cdab57fbb34b5addb7432f51454299b8cf47b389f98bad8abd42d82a2f8c2d11312e39272d44409540bcfa4c6b445e8e6dc63cc2fd5db1448875adb055ea8665c863bd07bf3aa8eb210f638287789957c96c54819061ee215eb7ba7b6048591a57f097a3e5da06b6359325d830d5b74c20c025996a113e4bb9fd2c853b7360d4961396cd99c23a13de972097eede3a955a5d5d8c8695a7290581a248fc03ea87606e71564d8e8fb00ebb8d5c10fc8fefe1660171524264060d15363fc2dc0ac0ab21fcbae1dc53786873cb9e8716f3ada651e79c3306ad49adeeb354213cc37499e217fa1c0f219e85bd22cf493f5e76f053543dd3b36bd180b1dcf17f781e35d6955c33c06426a885138f1e21b78ee87a27624f33b6567bfa6a0fe43e2d623578f6917d300a408c4dd48683213ffad453de1003e120fbfa74a6db4628af9d446e26492fde67bf52d034fcaf2b9b959472404fd631ef599815c6f190807b75f638e134148a5813424ba6cf59cf86ce515a14b95f7b8f80b1aa1b3cbbc091fa2a686277a9cc613e48b2c227aed7b4b093ac8b12a238bc99f9983c8bac21bb0f897eada35bf0e01b1436cf6d44b959595bdcdfd4676e28b500b9ad6b8a5825c3d3c0c38a4a5a2c3ded205584439621eaa7ee639b09aca1f533bb4892b29d761d94887fa78f605b9b8f5b3ab44ea578d9329bd78d7a6ae903f1960e16007a924be79ab31ea6ed7466485488b5c71eb02d6b99f345f2f61cb3cd994045c502d19f615233b3ebb263981de26674de082d384cc04c09a309567780f7f24298847fc2dff5f22082074684aa9efa260b8aaf4357bff2e9d32f8918b16876051b5459136dcc8788aba7b2ead435c3bf662f9f1acddd4a8a71b593e99ed50e158028946195ee991666bf88f4cf4d30a04c877ce8a9e6d224aed662e85a32f5cb9029a3dd4ba663b6f6314ef58fbce623171946d01d1ff456f90131159e5209cb41329061a0dd8a5fc35576108681e783fb173f67dda33134a9b1f07494a1d6273810fd77a25c92f7444d6226738d5c7161b7b198be069ac65d50a22d728292e95d1859e0c646db62aa3f401e55026a551b1edfe8fd5eca8e4c6836bd09429b5e22f64a09db4c6935b6febcbac6430f66dc0280c9be046133795f1f59ec32cbf4511749984f7b2ba131588f86f82322901ee7d709550ecadb5b915d5cfb2e950d2a8c5eda57da49d2ac9562b851f81e70a32178989e83807f04a6324cf7320a26a91b41e31a06c706431794ffb8b9ce5f3d853fb9106c8a98ea3b2948356948bfbbd63eb30e3cb68d7e373df80221d1b1211c717afe8b7b0b46a3208859254d9ae3517b8e031f413178c0fd408e76ccdc580a9a19edf4b3c70c273f4c8c626fad225e5aeee890c65328437b8bf316066e54a4741d8ac8ab9b5555f09b89b79165f9aa08a59be8f10c121b1b425bd5e3a64b6e4db3e1cacb00a5867fd05b454b75ff1eb8560770f21af7680107560a2209373d2999eb21bed2a10bafe1eaf5a31c18e69c63cce9b8c6cddcfc1088f956bcf3c9adeb77ef0589ab6405f0a9ba5650819a48fb42597fcd2f4ad67bdc89870d82eaa0d8dbd298a59ff552576dedb539834de725638e0f68307d4ac203d8e2e4649e31abc4e8748251c8fb6df3459300d1badfc19ad4d2f680f466b02680bb3e5a13c0c8a5db3665bc9fc2093c4d38acb176754db556ebd1663c23f284bec95279957b112131f8aa09af15ff26eebea3215c96b9df43c9fc9134d9db4e588aff293f3084db13e1d92bc33ca07a1b534b4a4e5fcbf098be7d26f9312db7f9d6b160318a4562c3c3b0c87688c59f402e0032242324339ef33713bf39c2110e7eb155bf926888385fe4b18bf3ef13dc2601b76def3d763f5b2ddea363f7e3697112194fb6332be96540a53a86e1e34fd70429dcfc39c5e2f68fa72e0045fe4ef12b965f0827c5bee9cd4f0c9b4cf6468316384fe33df5703c7742f9b409b9a508e94faa8be3c27ad75d21f85ee31753c96deb909221befd62bae084885c890d89f775dc0eee940ffbcad0aa65c08a71d09e234ad150e82610ba03deb608d44e9019d8579f9e9351daa6f3bcbbc8ec170c8b700bcb495c333b32136721f6417a3f3b12500641eb7af9e5813fafd27794a7b2476320fde18f3019302d49d77c3536af214e6c8357a36029a37a07011d1cdbe0db3fe7443a6908f5d3b6e08d61f33bad2a0bfbc9db86022d4f91b0ba6ef1b5ec30f0187f4c540eeb117c4d3d78659e46540df4b9301c6fce031d7e438abeb13a747be6ce9c0a33a2bd6f6092d0a26d5ba138bb6f2c3113ea6cff868853dacfc5df0433049a59d2b365e9a87ee6a6203e52121d60bc709feb1c1a30e95fbc600f648dfa5fadc8cf324a4c5d91e1f80501661aa51a518b381933932a1367e4369e07943f291012f5a9394692d9984fc2dc55c0ec4fe3d18a4a0b9f9d7c9d3f57b2e2a0c31f08f17ffe7355fec963b8ae364ed8cff046aa8220dc813f2dc78405069c707afadb77cfc8d64803a25eab7ebc74c738b41f9b3f2d881f1e2b77d37f38c1b5991daf5c911c04947891909f9c3e50e1314884207f0ea99d9310c9cfe93fea53fb57c93efbd412702e283e61196b9158de774333893b51c768ae48ec086e47b105d0b21357bd14f85b9f145fbfd63c0e998d6e54900915c8ffaf1234fa910ede3035e5e47ee9b22559459d0ea2b0f3242c5ec2782d09a7b477b560b1ecfd14d82f24600334d2c85dc2def0f457ea199e266c52fb9a596de02da05a9df8e4731cf941e1ada11c66d0954742745d5ef1b36dc7628614ed28ba9358ab38c2d007aa90147906270ab35ae26fa3473ec5881f8e6ed04c592a403386c4061becc70b5735531f8d249abb079317f43f111de58c6678e62a6d2dc83193acef928c906"; - let tx_2_bytes = hex::decode(tx_2_hex).unwrap(); - let tx_2 = ZTransaction::read(tx_2_bytes.as_slice()).unwrap(); - let tx_2 = tx_2.into(); - - // Success validation - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx_2, - expected_sender: &[], - dex_fee: &DexFee::Standard("0.00999999".into()), - min_block_number: 12000, - uuid: &[1; 16], - }; - let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); - match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), - _ => panic!("Expected `WrongPaymentTx`: {:?}", err), - } - - // Success validation - let expected_std_fee = DexFee::Standard("0.01".into()); - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx_2, - expected_sender: &[], - dex_fee: &expected_std_fee, - min_block_number: 12000, - uuid: &[1; 16], - }; - coin.validate_fee(validate_fee_args).await.unwrap(); -} diff --git a/mm2src/coins/z_coin/z_unit_tests.rs b/mm2src/coins/z_coin/z_unit_tests.rs new file mode 100644 index 0000000000..25ce04ff3b --- /dev/null +++ b/mm2src/coins/z_coin/z_unit_tests.rs @@ -0,0 +1,344 @@ +use super::*; +use crate::utxo::rpc_clients::ElectrumClient; +use crate::utxo::rpc_clients::UtxoRpcClientOps; +use crate::z_coin::storage::WalletDbShared; +use crate::CoinProtocol; +use crate::DexFeeBurnDestination; +use common::executor::spawn_abortable; +use core::convert::AsRef; +use ff::{Field, PrimeField}; +use futures::channel::mpsc::channel; +use futures::lock::Mutex as AsyncMutex; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +use mm2_net::transport::slurp_url_with_headers; +use mm2_test_helpers::for_tests::zombie_conf; +use mocktopus::mocking::*; +use rand::rngs::OsRng; +use rand::RngCore; +use std::fs::{self, create_dir}; +use std::path::Path; +use url::Url; +use zcash_primitives::merkle_tree::CommitmentTree; +use zcash_primitives::merkle_tree::IncrementalWitness; +use zcash_primitives::sapling::Node; +use zcash_primitives::sapling::Rseed; +use zcash_primitives::transaction::components::amount::DEFAULT_FEE; + +const GITHUB_CLIENT_USER_AGENT: &str = "mm2"; + +/// Download zcash params from komodo repo +async fn fetch_and_save_params(param: &str, fname: &Path) -> Result<(), String> { + let url = Url::parse(&format!("{}/", DOWNLOAD_URL)).unwrap().join(param).unwrap(); + println!("downloading zcash params {}...", url); + let data = slurp_url_with_headers(url.as_str(), vec![( + http::header::USER_AGENT.as_str(), + GITHUB_CLIENT_USER_AGENT, + )]) + .await + .map_err(|err| format!("could not download zcash params: {}", err))? + .2; + println!("saving zcash params to file {}...", fname.display()); + fs::write(fname, data).map_err(|err| format!("could not save zcash params: {}", err)) +} + +/// download zcash params, if not exist +pub(super) async fn download_parameters_for_tests(z_params_path: &Path) { + let sapling_spend_fname = z_params_path.join(SAPLING_SPEND_NAME); + let sapling_output_fname = z_params_path.join(SAPLING_OUTPUT_NAME); + if !sapling_spend_fname.exists() + || !sapling_output_fname.exists() + || !verify_checksum_zcash_params(&sapling_spend_fname, &sapling_output_fname).is_ok_and(|r| r) + { + let _ = create_dir(z_params_path); + fetch_and_save_params(SAPLING_SPEND_NAME, sapling_spend_fname.as_path()) + .await + .unwrap(); + fetch_and_save_params(SAPLING_OUTPUT_NAME, sapling_output_fname.as_path()) + .await + .unwrap(); + } +} + +pub(super) async fn create_test_sync_connector<'a>( + builder: &ZCoinBuilder<'a>, +) -> (AsyncMutex, WalletDbShared) { + let wallet_db = WalletDbShared::new(builder, None, true).await.unwrap(); // Note: assuming we have a spending key in the builder + let (_, sync_watcher) = channel(1); + let (on_tx_gen_notifier, _) = channel(1); + let abort_handle = spawn_abortable(futures::future::ready(())); + let first_sync_block = FirstSyncBlock { + requested: 0, + is_pre_sapling: false, + actual: 0, + }; + let sync_state_connector = + SaplingSyncConnector::new_mutex_wrapped(sync_watcher, on_tx_gen_notifier, abort_handle, first_sync_block); + (sync_state_connector, wallet_db) +} + +#[allow(clippy::too_many_arguments)] +async fn z_coin_from_conf_and_params_for_tests( + ctx: &MmArc, + ticker: &str, + conf: &Json, + params: &ZcoinActivationParams, + priv_key_policy: PrivKeyBuildPolicy, + protocol_info: ZcoinProtocolInfo, + spending_key: &str, +) -> Result> { + use zcash_client_backend::encoding::decode_extended_spending_key; + let z_spending_key = + decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, spending_key) + .unwrap() + .unwrap(); + + let builder = ZCoinBuilder::new( + ctx, + ticker, + conf, + params, + priv_key_policy, + Some(z_spending_key), + protocol_info, + )?; + + builder.build().await +} + +/// Build asset `ZCoin` for unit tests. +async fn z_coin_from_spending_key_for_unit_test(spending_key: &str) -> (MmArc, ZCoin) { + let ctx = MmCtxBuilder::new().into_mm_arc(); + let mut conf = zombie_conf(); + let params = ZcoinActivationParams { + mode: ZcoinRpcMode::UnitTests, + ..Default::default() + }; + let pk_data = [1; 32]; + let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { + CoinProtocol::ZHTLC(protocol_info) => protocol_info, + other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), + }; + + let coin = z_coin_from_conf_and_params_for_tests( + &ctx, + "ZOMBIE", + &conf, + ¶ms, + PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), + protocol_info, + spending_key, + ) + .await + .unwrap(); + (ctx, coin) +} + +fn add_test_spend(coin: &ZCoin, tx_builder: &mut ZTxBuilder, amount: u64) { + let extsk = coin.z_fields.z_spending_key.clone(); + let extfvk = coin.z_fields.evk.clone(); + let to = extfvk.default_address().unwrap().1; + let mut rng = OsRng; + let note1 = to + .create_note(amount, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng))) + .unwrap(); + let cmu1 = Node::new(note1.cmu().to_repr()); + let mut tree = CommitmentTree::empty(); + tree.append(cmu1).unwrap(); + let witness1 = IncrementalWitness::from_tree(&tree); + + tx_builder + .add_sapling_spend(extsk, *to.diversifier(), note1, witness1.path().unwrap()) + .unwrap(); +} + +async fn validate_fee_caller( + coin: &ZCoin, + dex_params: (PaymentAddress, u64), + burn_params: Option<(PaymentAddress, u64)>, + dex_fee: &DexFee, +) -> ValidatePaymentResult<()> { + let uuid = &[1; 16]; + let mut z_outputs = vec![]; + let mut tx_builder = ZTxBuilder::new(coin.consensus_params(), BlockHeight::from_u32(1)); + + add_test_spend( + coin, + &mut tx_builder, + dex_params.1 + + if let Some(ref burn_params) = burn_params { + burn_params.1 + } else { + 0 + } + + u64::from(DEFAULT_FEE), + ); + + let dex_fee_out = ZOutput { + to_addr: dex_params.0, + amount: Amount::from_u64(dex_params.1).unwrap(), + viewing_key: Some(DEX_FEE_OVK), + memo: Some(MemoBytes::from_bytes(uuid).expect("uuid length < 512")), + }; + z_outputs.push(dex_fee_out); + + // add output to the dex burn address: + if let Some(burn_params) = burn_params { + let dex_burn_out = ZOutput { + to_addr: burn_params.0, + amount: Amount::from_u64(burn_params.1).unwrap(), + viewing_key: Some(DEX_FEE_OVK), + memo: Some(MemoBytes::from_bytes(uuid).expect("uuid length < 512")), + }; + z_outputs.push(dex_burn_out); + } + for z_out in z_outputs { + tx_builder + .add_sapling_output(z_out.viewing_key, z_out.to_addr, z_out.amount, z_out.memo) + .unwrap(); + } + let (tx, _) = async_blocking({ + let prover = coin.z_fields.z_tx_prover.clone(); + move || tx_builder.build(BranchId::Sapling, prover.as_ref()) + }) + .await + .unwrap(); + + let tx: TransactionEnum = tx.into(); + let tx_ret = tx.clone(); + ElectrumClient::get_verbose_transaction.mock_safe(move |_, txid| { + let bytes: BytesJson = tx_ret.tx_hex().into(); + MockResult::Return(Box::new(futures01::future::ok(RpcTransaction { + txid: *txid, + hash: None, + blockhash: H256Json::default(), + confirmations: 0, + time: 0, + blocktime: 0, + hex: bytes, + vout: vec![], + vin: vec![], + size: None, + version: 4, + locktime: 0, + vsize: None, + rawconfirmations: None, + height: None, + }))) + }); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &[], + dex_fee, + min_block_number: 1, + uuid: &[1; 16], + }; + coin.validate_fee(validate_fee_args).await +} + +#[test] +fn derive_z_key_from_mm_seed() { + use crypto::privkey::key_pair_from_seed; + use zcash_client_backend::encoding::encode_extended_spending_key; + + let seed = "spice describe gravity federal blast come thank unfair canal monkey style afraid"; + let secp_keypair = key_pair_from_seed(seed).unwrap(); + let z_spending_key = ExtendedSpendingKey::master(&*secp_keypair.private().secret); + let encoded = encode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, &z_spending_key); + assert_eq!(encoded, "secret-extended-key-main1qqqqqqqqqqqqqqytwz2zjt587n63kyz6jawmflttqu5rxavvqx3lzfs0tdr0w7g5tgntxzf5erd3jtvva5s52qx0ms598r89vrmv30r69zehxy2r3vesghtqd6dfwdtnauzuj8u8eeqfx7qpglzu6z54uzque6nzzgnejkgq569ax4lmk0v95rfhxzxlq3zrrj2z2kqylx2jp8g68lqu6alczdxd59lzp4hlfuj3jp54fp06xsaaay0uyass992g507tdd7psua5w6q76dyq3"); + + let (_, address) = z_spending_key.default_address().unwrap(); + let encoded_addr = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &address); + assert_eq!( + encoded_addr, + "zs182ht30wnnnr8jjhj2j9v5dkx3qsknnr5r00jfwk2nczdtqy7w0v836kyy840kv2r8xle5gcl549" + ); + + let seed = "also shoot benefit prefer juice shell elder veteran woman mimic image kidney"; + let secp_keypair = key_pair_from_seed(seed).unwrap(); + let z_spending_key = ExtendedSpendingKey::master(&*secp_keypair.private().secret); + let encoded = encode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, &z_spending_key); + assert_eq!(encoded, "secret-extended-key-main1qqqqqqqqqqqqqq8jnhc9stsqwts6pu5ayzgy4szplvy03u227e50n3u8e6dwn5l0q5s3s8xfc03r5wmyh5s5dq536ufwn2k89ngdhnxy64sd989elwas6kr7ygztsdkw6k6xqyvhtu6e0dhm4mav8rus0fy8g0hgy9vt97cfjmus0m2m87p4qz5a00um7gwjwk494gul0uvt3gqyjujcclsqry72z57kr265jsajactgfn9m3vclqvx8fsdnwp4jwj57ffw560vvwks9g9hpu"); + + let (_, address) = z_spending_key.default_address().unwrap(); + let encoded_addr = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &address); + assert_eq!( + encoded_addr, + "zs1funuwrjr2stlr6fnhkdh7fyz3p7n0p8rxase9jnezdhc286v5mhs6q3myw0phzvad5mvqgfxpam" + ); +} + +#[test] +fn test_interpret_memo_string() { + use std::str::FromStr; + use zcash_primitives::memo::Memo; + + let actual = interpret_memo_string("68656c6c6f207a63617368").unwrap(); + let expected = Memo::from_str("68656c6c6f207a63617368").unwrap().encode(); + assert_eq!(actual, expected); + + let actual = interpret_memo_string("A custom memo").unwrap(); + let expected = Memo::from_str("A custom memo").unwrap().encode(); + assert_eq!(actual, expected); + + let actual = interpret_memo_string("0x68656c6c6f207a63617368").unwrap(); + let expected = MemoBytes::from_bytes(&hex::decode("68656c6c6f207a63617368").unwrap()).unwrap(); + assert_eq!(actual, expected); +} + +#[tokio::test] +async fn test_validate_zcoin_dex_fee() { + let (_ctx, coin) = z_coin_from_spending_key_for_unit_test("secret-extended-key-main1qvqstxphqyqqpqqnh3hstqpdjzkpadeed6u7fz230jmm2mxl0aacrtu9vt7a7rmr2w5az5u79d24t0rudak3newknrz5l0m3dsd8m4dffqh5xwyldc5qwz8pnalrnhlxdzf900x83jazc52y25e9hvyd4kepaze6nlcvk8sd8a4qjh3e9j5d6730t7ctzhhrhp0zljjtwuptadnksxf8a8y5axwdhass5pjaxg0hzhg7z25rx0rll7a6txywl32s6cda0s5kexr03uqdtelwe").await; + + let std_fee = DexFee::Standard("0.001".into()); + let with_burn = DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + assert!( + validate_fee_caller(&coin, (coin.z_fields.dex_fee_addr.clone(), 100000), None, &std_fee) + .await + .is_ok() + ); + assert!(validate_fee_caller( + &coin, + (coin.z_fields.dex_fee_addr.clone(), 750000), + Some((coin.z_fields.dex_burn_addr.clone(), 250000)), + &with_burn + ) + .await + .is_ok()); + // try reverted addresses + assert!(validate_fee_caller( + &coin, + (coin.z_fields.dex_burn_addr.clone(), 750000), + Some((coin.z_fields.dex_fee_addr.clone(), 250000)), + &with_burn + ) + .await + .is_err()); + let other_addr = decode_payment_address( + coin.z_fields.consensus_params.hrp_sapling_payment_address(), + "zs182ht30wnnnr8jjhj2j9v5dkx3qsknnr5r00jfwk2nczdtqy7w0v836kyy840kv2r8xle5gcl549", + ) + .expect("valid z address format") + .expect("valid z address"); + // try invalid dex address + assert!(validate_fee_caller( + &coin, + (other_addr.clone(), 750000), + Some((coin.z_fields.dex_burn_addr.clone(), 250000)), + &with_burn + ) + .await + .is_err()); + // try invalid burn address + assert!(validate_fee_caller( + &coin, + (coin.z_fields.dex_fee_addr.clone(), 750000), + Some((other_addr.clone(), 250000)), + &with_burn + ) + .await + .is_err()); +} From 43346d61d1a799cfe2acfa74ec0e56f2989ebd77 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Mon, 16 Jun 2025 09:21:36 +0300 Subject: [PATCH 31/36] chore(release): bump kdf version to 2.5.0-beta (#2492) --- Cargo.lock | 2 +- mm2src/mm2_bin_lib/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9748b76b83..c6cd5560d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3875,7 +3875,7 @@ dependencies = [ [[package]] name = "mm2_bin_lib" -version = "2.4.0-beta" +version = "2.5.0-beta" dependencies = [ "chrono", "common", diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index 002592e745..b2300f509a 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "mm2_bin_lib" -version = "2.4.0-beta" +version = "2.5.0-beta" authors = ["James Lee", "Artem Pikulin", "Artem Grinblat", "Omar S.", "Onur Ozkan", "Alina Sharon", "Caglar Kaya", "Cipi", "Sergey Boiko", "Samuel Onoja", "Roman Sztergbaum", "Kadan Stadelmann ", "Dimxy", "Omer Yacine", "DeckerSU"] edition = "2018" default-run = "kdf" From f9de870eb877ad4bf0befcbff025ea0307aca9ae Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:13:32 +0300 Subject: [PATCH 32/36] chore(release): add changelog entries for v2.5.0-beta (#2494) --- CHANGELOG.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 132dda9919..ba9c8792c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,103 @@ +## v2.5.0-beta - 2025-06-23 + +### Features: + +**WalletConnect Integration**: +- WalletConnect v2 support for EVM and Cosmos coins was implemented, enabling wallet connection and transaction signing via the WalletConnect protocol. [#2223](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2223) [#2485](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2485) + +--- + +### Work in Progress (WIP) Features: + +**Cosmos Network and IBC Swaps**: +- Pre-swap validation logic was implemented for maker order creation, requiring HTLC assets and healthy IBC channels on the Cosmos network, with all changes gated behind the `ibc-routing-for-swaps` feature. [#2459](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2459) +- The taker and maker order types were extended with an `order_metadata` field to carry protocol/IBC details, and cross-checks for IBC channels were added (also feature-gated), enabling both parties to validate IBC routing before a swap. [#2476](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2476) + +**TRON Integration**: +- Initial groundwork for TRON integration was started, including the addition of basic structures and boilerplate code; no end-to-end functionality is yet available. [#2425](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2425) + +--- + +### Enhancements/Fixes: + +**Event Streaming**: +- Streamer IDs in the event-streaming system were strongly typed to improve type safety and code clarity. [#2441](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2441) + +**Peer-to-Peer Network**: +- Hardcoded seed node IP addresses were removed from the peer-to-peer network configuration to improve maintainability. [#2439](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2439) + +**Orders and Trading Protocol**: +- The minimum trading volume logic was revised to remove BTC-specific volume behavior, standardizing the calculation across all coins. [#2483](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2483) + +**Tendermint / Cosmos**: +- A helper for generating internal transaction IDs for Tendermint transactions was introduced. [#2438](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2438) +- The IBC channel handler was improved to enhance safety and reliability when interacting with IBC channels. [#2298](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2298) + +**Wallet**: +- Unconfirmed z-coin notes are now correctly tracked. [#2331](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2331) +- HD multi-address support for message signing was implemented, allowing message signatures from multiple derived addresses. [#2432](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2432) [#2474](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2474) + +**UTXO**: +- Validation of expected public keys for p2pk inputs was corrected, resolving an error in p2pk transaction processing. [#2408](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2408) +- Transaction fee calculation and minimum relay fee handling for UTXO coins were improved for accurate fee estimation. [#2316](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2316) + +**EVM / ERC20**: +- ETH address serialization in event streaming was updated to use the `AddrToString` trait for consistency. [#2440](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2440) + +**Pubkey Banning**: +- Expirable bans for pubkeys were introduced, allowing temporary exclusion of certain public keys. [#2455](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2455) + +**RPC Interface**: +- A unified interface was implemented for legacy and current RPC methods. [#2450](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2450) + +**DNS Resolution**: +- IP resolution logic was improved to fail only if no IPv4 address is found. [#2487](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2487) + +**Database and File System**: +- More replacements of `dbdir` with `address_dir` were made as part of an ongoing improvement to database architecture. [#2398](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2398) + +**Build and Dependency Management**: +- Duplicated mm2 build artifacts were removed from the build process to reduce clutter. [#2448](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2448) +- Static CRT linking was enabled for MSVC builds, improving the portability of Windows binaries. [#2464](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2464) +- The `timed-map` dependency was bumped to version `1.4.1` for improved performance and stability. [#2413](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2413) [#2481](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2481) +- The `base58` crate was removed and replaced with `bs58` throughout the codebase for consistency and security. [#2427](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2427) +- Dependencies were reorganized using the `workspace.dependencies` feature for centralized management. [#2449](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2449) + +--- + +### Other Changes: + +**Documentation**: +- Old URLs referencing atomicDEX or previous documentation pages were updated throughout the documentation. [#2428](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2428) +- A DeepWiki badge was added to the README to highlight documentation resources. [#2463](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2463) + +**Core Maintenance**: +- Workspace dependencies were organized for consistent dependency management across the project. [#2449](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2449) +- A unit test was added to validate DEX fee handling for ZCoin. [#2460](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2460) +- Improved ERC20 token lookup to use platform ticker alongside contract address for proper token identification across platforms. [#2445](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2445) + +--- + +### NB - Backwards compatibility breaking changes: + +**WalletConnect/EVM Coin Activation Policy**: +- The `priv_key_policy` field for EVM coin activation now requires the new enum variant format: `"priv_key_policy": { "type": "ContextPrivKey" }`. [#2223](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2223) + +**TRON/EVM Chain Specification**: +- EVM coin configurations must now include `chain_id` inside the `protocol_data` field. Legacy `chain_id` fields are deprecated. [#2425](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2425) + +**mm2 Build Artifacts**: +- The `mm2` binaries have been removed from build outputs. Users must reference new artifact locations. [#2448](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2448) + +**Seednode Configuration**: +- Hardcoded seed nodes were removed. KDF will no longer connect to 8762 mainnet by default without proper `seednodes` configuration. [#2439](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2439) + +**IBC/Cosmos Changes**: +- The `ibc_chains` and `ibc_transfer_channels` RPC endpoints have been removed. [#2459](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2459) +- The `ibc_source_channel` field now requires numeric values only (e.g., `12` instead of `channel-12`). [#2459](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2459) + +--- + ## v2.4.0-beta - 2025-05-02 ### Features: From d62c20b4c978bf2a8389b31bfbe45b3c0b2cd29f Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:11:52 +0300 Subject: [PATCH 33/36] feat(wallet): add `delete_wallet` RPC (#2497) This commit does the following: - Introduces a `delete_wallet` RPC to securely remove any wallet after password confirmation; it also prevents deletion of the active wallet. - It add a requirement for `CryptoCtx` to be initialized with a passphrase if a seednodes is being initialized, preventing startup without persistant p2p keypair. - It also improves WASM test documentation and removes/deactivates outdated QRC20 tests. --- docs/DEV_ENVIRONMENT.md | 40 ++++-- mm2src/coins/qrc20/qrc20_tests.rs | 1 + mm2src/mm2_main/src/lp_native_dex.rs | 5 +- mm2src/mm2_main/src/lp_wallet.rs | 126 ++++++++++++++---- .../src/lp_wallet/mnemonics_storage.rs | 31 +++-- .../src/lp_wallet/mnemonics_wasm_db.rs | 24 ++-- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 3 +- mm2src/mm2_main/src/wasm_tests.rs | 100 +++++++++++++- .../tests/mm2_tests/mm2_tests_inner.rs | 124 ++++++++++++++++- mm2src/mm2_test_helpers/src/for_tests.rs | 19 +++ 10 files changed, 404 insertions(+), 69 deletions(-) diff --git a/docs/DEV_ENVIRONMENT.md b/docs/DEV_ENVIRONMENT.md index 5e4f6d1659..8782769079 100644 --- a/docs/DEV_ENVIRONMENT.md +++ b/docs/DEV_ENVIRONMENT.md @@ -66,16 +66,34 @@ CC=/opt/homebrew/opt/llvm/bin/clang AR=/opt/homebrew/opt/llvm/bin/llvm-ar wasm-pack test --firefox --headless mm2src/mm2_main ``` Please note `CC` and `AR` must be specified in the same line as `wasm-pack test mm2src/mm2_main`. -#### Running specific WASM tests with Cargo
- - Install `wasm-bindgen-cli`:
- Make sure you have wasm-bindgen-cli installed with a version that matches the one specified in your Cargo.toml file. - You can install it using Cargo with the following command: - ``` - cargo install -f wasm-bindgen-cli --version - ``` - - Run - ``` - cargo test --target wasm32-unknown-unknown --package coins --lib utxo::utxo_block_header_storage::wasm::indexeddb_block_header_storage - ``` + +#### Running specific WASM tests + +There are two primary methods for running specific tests: + +* **Method 1: Using `wasm-pack` (Recommended for browser-based tests)** + + To filter tests, append `--` to the `wasm-pack test` command, followed by the name of the test you want to run. This will execute only the tests whose names contain the provided string. + + General Example: + ```shell + wasm-pack test --firefox --headless mm2src/mm2_main -- + ``` + + > **Note for macOS users:** You must prepend the `CC` and `AR` environment variables to the command if they weren't already exported, just as you would when running all tests. For example: `CC=... AR=... wasm-pack test ...` + +* **Method 2: Using `cargo test` (For non-browser tests)** + + This method uses the standard Cargo test runner with a wasm target and is useful for tests that do not require a browser environment. + + a. **Install `wasm-bindgen-cli`**: Make sure you have `wasm-bindgen-cli` installed with a version that matches the one specified in your `Cargo.toml` file. + ```shell + cargo install -f wasm-bindgen-cli --version + ``` + + b. **Run the test**: Append `--` to the `cargo test` command, followed by the test path. + ```shell + cargo test --target wasm32-unknown-unknown --package coins --lib -- utxo::utxo_block_header_storage::wasm::indexeddb_block_header_storage + ``` PS If you notice that this guide is outdated, please submit a PR. diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 01cf4adcad..f4dd5a7121 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -429,6 +429,7 @@ fn test_validate_fee() { } #[test] +#[ignore] fn test_wait_for_tx_spend_malicious() { // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG let priv_key = [ diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index a24a75d8ce..4877ef8de8 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -113,7 +113,6 @@ impl From for P2PInitError { } } } - #[derive(Clone, Debug, Display, EnumFromTrait, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum MmInitError { @@ -534,6 +533,10 @@ fn p2p_precheck(ctx: &MmArc) -> P2PResult<()> { } } + if is_seed_node && !CryptoCtx::is_init(ctx).unwrap_or(false) { + return precheck_err("Seed node requires a persistent identity to generate its P2P key."); + } + Ok(()) } diff --git a/mm2src/mm2_main/src/lp_wallet.rs b/mm2src/mm2_main/src/lp_wallet.rs index 8bb64690e2..7743b05039 100644 --- a/mm2src/mm2_main/src/lp_wallet.rs +++ b/mm2src/mm2_main/src/lp_wallet.rs @@ -14,14 +14,14 @@ cfg_wasm32! { use crate::lp_wallet::mnemonics_wasm_db::{WalletsDb, WalletsDBError}; use mm2_core::mm_ctx::from_ctx; use mm2_db::indexed_db::{ConstructibleDb, DbLocked, InitDbResult}; - use mnemonics_wasm_db::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase}; + use mnemonics_wasm_db::{delete_wallet, read_all_wallet_names, read_encrypted_passphrase, save_encrypted_passphrase}; use std::sync::Arc; type WalletsDbLocked<'a> = DbLocked<'a, WalletsDb>; } cfg_native! { - use mnemonics_storage::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase, WalletsStorageError}; + use mnemonics_storage::{delete_wallet, read_all_wallet_names, read_encrypted_passphrase, save_encrypted_passphrase, WalletsStorageError}; } #[cfg(not(target_arch = "wasm32"))] mod mnemonics_storage; #[cfg(target_arch = "wasm32")] mod mnemonics_wasm_db; @@ -69,6 +69,8 @@ pub enum ReadPassphraseError { WalletsStorageError(String), #[display(fmt = "Error decrypting passphrase: {}", _0)] DecryptionError(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), } impl From for WalletInitError { @@ -76,6 +78,7 @@ impl From for WalletInitError { match e { ReadPassphraseError::WalletsStorageError(e) => WalletInitError::WalletsStorageError(e), ReadPassphraseError::DecryptionError(e) => WalletInitError::MnemonicError(e), + ReadPassphraseError::Internal(e) => WalletInitError::InternalError(e), } } } @@ -121,25 +124,39 @@ async fn encrypt_and_save_passphrase( .mm_err(|e| WalletInitError::WalletsStorageError(e.to_string())) } -/// Reads and decrypts the passphrase from a file associated with the given wallet name, if available. -/// -/// This function first checks if a passphrase is available. If a passphrase is found, -/// since it is stored in an encrypted format, it decrypts it before returning. If no passphrase is found, -/// it returns `None`. -/// -/// # Returns -/// `MmInitResult` - The decrypted passphrase or an error if any operation fails. +/// A convenience wrapper that calls [`try_load_wallet_passphrase`] for the currently active wallet. +async fn try_load_active_wallet_passphrase( + ctx: &MmArc, + wallet_password: &str, +) -> MmResult, ReadPassphraseError> { + let wallet_name = ctx + .wallet_name + .get() + .ok_or(ReadPassphraseError::Internal( + "`wallet_name` not initialized yet!".to_string(), + ))? + .clone() + .ok_or_else(|| { + ReadPassphraseError::Internal("Cannot read stored passphrase: no active wallet is set.".to_string()) + })?; + + try_load_wallet_passphrase(ctx, &wallet_name, wallet_password).await +} + +/// Loads (reads from storage and decrypts) a passphrase for a specific wallet by name. /// -/// # Errors -/// Returns specific `MmInitError` variants for different failure scenarios. -async fn read_and_decrypt_passphrase_if_available( +/// Returns `Ok(None)` if the passphrase is not found in storage. This is an expected +/// outcome for a new wallet or when using a legacy config where the passphrase is not saved. +async fn try_load_wallet_passphrase( ctx: &MmArc, + wallet_name: &str, wallet_password: &str, ) -> MmResult, ReadPassphraseError> { - match read_encrypted_passphrase_if_available(ctx) + let encrypted = read_encrypted_passphrase(ctx, wallet_name) .await - .mm_err(|e| ReadPassphraseError::WalletsStorageError(e.to_string()))? - { + .mm_err(|e| ReadPassphraseError::WalletsStorageError(e.to_string()))?; + + match encrypted { Some(encrypted_passphrase) => { let mnemonic = decrypt_mnemonic(&encrypted_passphrase, wallet_password) .mm_err(|e| ReadPassphraseError::DecryptionError(e.to_string()))?; @@ -171,7 +188,7 @@ async fn retrieve_or_create_passphrase( wallet_name: &str, wallet_password: &str, ) -> WalletInitResult> { - match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? { + match try_load_active_wallet_passphrase(ctx, wallet_password).await? { Some(passphrase_from_file) => { // If an existing passphrase is found, return it Ok(Some(passphrase_from_file)) @@ -202,7 +219,7 @@ async fn confirm_or_encrypt_and_store_passphrase( passphrase: &str, wallet_password: &str, ) -> WalletInitResult> { - match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? { + match try_load_active_wallet_passphrase(ctx, wallet_password).await? { Some(passphrase_from_file) if passphrase == passphrase_from_file => { // If an existing passphrase is found and it matches the provided passphrase, return it Ok(Some(passphrase_from_file)) @@ -238,7 +255,7 @@ async fn decrypt_validate_or_save_passphrase( // Decrypt the provided encrypted passphrase let decrypted_passphrase = decrypt_mnemonic(&encrypted_passphrase_data, wallet_password)?; - match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? { + match try_load_active_wallet_passphrase(ctx, wallet_password).await? { Some(passphrase_from_file) if decrypted_passphrase == passphrase_from_file => { // If an existing passphrase is found and it matches the decrypted passphrase, return it Ok(Some(decrypted_passphrase)) @@ -476,7 +493,13 @@ impl From for MnemonicRpcError { } impl From for MnemonicRpcError { - fn from(e: ReadPassphraseError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) } + fn from(e: ReadPassphraseError) -> Self { + match e { + ReadPassphraseError::DecryptionError(e) => MnemonicRpcError::InvalidPassword(e), + ReadPassphraseError::WalletsStorageError(e) => MnemonicRpcError::WalletsStorageError(e), + ReadPassphraseError::Internal(e) => MnemonicRpcError::Internal(e), + } + } } /// Retrieves the wallet mnemonic in the requested format. @@ -513,7 +536,19 @@ impl From for MnemonicRpcError { pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult { match req.mnemonic_format { MnemonicFormat::Encrypted => { - let encrypted_mnemonic = read_encrypted_passphrase_if_available(&ctx) + let wallet_name = ctx + .wallet_name + .get() + .ok_or(MnemonicRpcError::Internal( + "`wallet_name` not initialized yet!".to_string(), + ))? + .as_ref() + .ok_or_else(|| { + MnemonicRpcError::Internal( + "Cannot get encrypted mnemonic: This operation requires an active named wallet.".to_string(), + ) + })?; + let encrypted_mnemonic = read_encrypted_passphrase(&ctx, wallet_name) .await? .ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; Ok(GetMnemonicResponse { @@ -521,7 +556,7 @@ pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult { - let plaintext_mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &wallet_password) + let plaintext_mnemonic = try_load_active_wallet_passphrase(&ctx, &wallet_password) .await? .ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; Ok(GetMnemonicResponse { @@ -584,7 +619,7 @@ pub async fn change_mnemonic_password(ctx: MmArc, req: ChangeMnemonicPasswordReq .as_ref() .ok_or_else(|| MnemonicRpcError::Internal("`wallet_name` cannot be None!".to_string()))?; // read mnemonic for a wallet_name using current user's password. - let mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &req.current_password) + let mnemonic = try_load_active_wallet_passphrase(&ctx, &req.current_password) .await? .ok_or(MmError::new(MnemonicRpcError::Internal(format!( "{wallet_name}: wallet mnemonic file not found" @@ -596,3 +631,48 @@ pub async fn change_mnemonic_password(ctx: MmArc, req: ChangeMnemonicPasswordReq Ok(()) } + +#[derive(Debug, Deserialize)] +pub struct DeleteWalletRequest { + /// The name of the wallet to be deleted. + pub wallet_name: String, + /// The password to confirm wallet deletion. + pub password: String, +} + +/// Deletes a wallet. Requires password confirmation. +/// The active wallet cannot be deleted. +pub async fn delete_wallet_rpc(ctx: MmArc, req: DeleteWalletRequest) -> MmResult<(), MnemonicRpcError> { + let active_wallet = ctx + .wallet_name + .get() + .ok_or(MnemonicRpcError::Internal( + "`wallet_name` not initialized yet!".to_string(), + ))? + .as_ref(); + + if active_wallet == Some(&req.wallet_name) { + return MmError::err(MnemonicRpcError::InvalidRequest(format!( + "Cannot delete wallet '{}' as it is currently active.", + req.wallet_name + ))); + } + + // Verify the password by attempting to decrypt the mnemonic. + let maybe_mnemonic = try_load_wallet_passphrase(&ctx, &req.wallet_name, &req.password).await?; + + match maybe_mnemonic { + Some(_) => { + // Password is correct, proceed with deletion. + delete_wallet(&ctx, &req.wallet_name).await?; + Ok(()) + }, + None => { + // This case implies no mnemonic file was found for the given wallet. + MmError::err(MnemonicRpcError::InvalidRequest(format!( + "Wallet '{}' not found.", + req.wallet_name + ))) + }, + } +} diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs index 25e77c27d1..f2636be4a0 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs @@ -57,27 +57,22 @@ pub(super) async fn save_encrypted_passphrase( /// Reads the encrypted passphrase data from the file associated with the given wallet name, if available. /// -/// This function is responsible for retrieving the encrypted passphrase data from a file, if it exists. +/// This function is responsible for retrieving the encrypted passphrase data from a file for a specific wallet. /// The data is expected to be in the format of `EncryptedData`, which includes /// all necessary components for decryption, such as the encryption algorithm, key derivation /// /// # Returns -/// `io::Result` - The encrypted passphrase data or an error if the -/// reading process fails. +/// `WalletsStorageResult>` - The encrypted passphrase data or an error if the +/// reading process fails. An `Ok(None)` is returned if the wallet file does not exist. /// /// # Errors -/// Returns an `io::Error` if the file cannot be read or the data cannot be deserialized into +/// Returns a `WalletsStorageError` if the file cannot be read or the data cannot be deserialized into /// `EncryptedData`. -pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> WalletsStorageResult> { - let wallet_name = ctx - .wallet_name - .get() - .ok_or(WalletsStorageError::Internal( - "`wallet_name` not initialized yet!".to_string(), - ))? - .clone() - .ok_or_else(|| WalletsStorageError::Internal("`wallet_name` cannot be None!".to_string()))?; - let wallet_path = wallet_file_path(ctx, &wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?; +pub(super) async fn read_encrypted_passphrase( + ctx: &MmArc, + wallet_name: &str, +) -> WalletsStorageResult> { + let wallet_path = wallet_file_path(ctx, wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?; mm2_io::fs::read_json(&wallet_path).await.mm_err(|e| { WalletsStorageError::FsReadError(format!( "Error reading passphrase from file {}: {}", @@ -93,3 +88,11 @@ pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsStorageResult WalletsStorageResult<()> { + let wallet_path = wallet_file_path(ctx, wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?; + mm2_io::fs::remove_file_async(&wallet_path) + .await + .mm_err(|e| WalletsStorageError::FsWriteError(e.to_string())) +} diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs index e4733a132d..6eeaebc8d4 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs @@ -119,21 +119,16 @@ pub(super) async fn save_encrypted_passphrase( Ok(()) } -pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> WalletsDBResult> { +pub(super) async fn read_encrypted_passphrase( + ctx: &MmArc, + wallet_name: &str, +) -> WalletsDBResult> { let wallets_ctx = WalletsContext::from_ctx(ctx).map_to_mm(WalletsDBError::Internal)?; let db = wallets_ctx.wallets_db().await?; let transaction = db.transaction().await?; let table = transaction.table::().await?; - let wallet_name = ctx - .wallet_name - .get() - .ok_or(WalletsDBError::Internal( - "`wallet_name` not initialized yet!".to_string(), - ))? - .clone() - .ok_or_else(|| WalletsDBError::Internal("`wallet_name` can't be None!".to_string()))?; table .get_item_by_unique_index("wallet_name", wallet_name) .await? @@ -160,3 +155,14 @@ pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsDBResult WalletsDBResult<()> { + let wallets_ctx = WalletsContext::from_ctx(ctx).map_to_mm(WalletsDBError::Internal)?; + + let db = wallets_ctx.wallets_db().await?; + let transaction = db.transaction().await?; + let table = transaction.table::().await?; + + table.delete_item_by_unique_index("wallet_name", wallet_name).await?; + Ok(()) +} diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 924b87f387..4b18118a7d 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -11,7 +11,7 @@ use crate::lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, s stop_version_stat_collection, update_version_stat_collection}; use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swap_status_rpc}; use crate::lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}; -use crate::lp_wallet::{change_mnemonic_password, get_mnemonic_rpc, get_wallet_names_rpc}; +use crate::lp_wallet::{change_mnemonic_password, delete_wallet_rpc, get_mnemonic_rpc, get_wallet_names_rpc}; use crate::rpc::lp_commands::db_id::get_shared_db_id; use crate::rpc::lp_commands::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc, one_inch_v6_0_classic_swap_create_rpc, @@ -201,6 +201,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, get_token_allowance_rpc).await, "best_orders" => handle_mmrpc(ctx, request, best_orders_rpc_v2).await, "clear_nft_db" => handle_mmrpc(ctx, request, clear_nft_db).await, + "delete_wallet" => handle_mmrpc(ctx, request, delete_wallet_rpc).await, "enable_bch_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, "enable_slp" => handle_mmrpc(ctx, request, enable_token::).await, "enable_eth_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, diff --git a/mm2src/mm2_main/src/wasm_tests.rs b/mm2src/mm2_main/src/wasm_tests.rs index 11d31fb91c..a8768b675b 100644 --- a/mm2src/mm2_main/src/wasm_tests.rs +++ b/mm2src/mm2_main/src/wasm_tests.rs @@ -2,18 +2,19 @@ use crate::{lp_init, lp_run}; use common::executor::{spawn, spawn_abortable, spawn_local_abortable, AbortOnDropHandle, Timer}; use common::log::warn; use common::log::wasm_log::register_wasm_log; +use http::StatusCode; use mm2_core::mm_ctx::MmArc; use mm2_number::BigDecimal; use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::electrums::{doc_electrums, marty_electrums}; -use mm2_test_helpers::for_tests::{check_recent_swaps, enable_electrum_json, enable_utxo_v2_electrum, +use mm2_test_helpers::for_tests::{check_recent_swaps, delete_wallet, enable_electrum_json, enable_utxo_v2_electrum, enable_z_coin_light, get_wallet_names, morty_conf, pirate_conf, rick_conf, start_swaps, test_qrc20_history_impl, wait_for_swaps_finish_and_check_status, MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, ARRR, MORTY, PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, RICK}; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::{Bip44Chain, EnableCoinBalance, HDAccountAddressId}; -use serde_json::json; +use serde_json::{json, Value as Json}; use wasm_bindgen_test::wasm_bindgen_test; const PIRATE_TEST_BALANCE_SEED: &str = "pirate test seed"; @@ -80,9 +81,6 @@ async fn test_mm2_stops_immediately() { test_mm2_stops_impl(pairs, 1., 1., 0.0001).await; } -#[wasm_bindgen_test] -async fn test_qrc20_tx_history() { test_qrc20_history_impl(Some(wasm_start)).await } - async fn trade_base_rel_electrum( mut mm_bob: MarketMakerIt, mut mm_alice: MarketMakerIt, @@ -303,3 +301,95 @@ async fn test_get_wallet_names() { .await .unwrap(); } + +#[wasm_bindgen_test] +async fn test_delete_wallet_rpc() { + register_wasm_log(); + + const DB_NAMESPACE_NUM: u64 = 2; + + let coins = json!([]); + let wallet_1_name = "wallet_to_be_deleted"; + let wallet_1_pass = "pass1"; + let wallet_1_conf = Mm2TestConf::seednode_with_wallet_name(&coins, wallet_1_name, wallet_1_pass); + let mm_wallet_1 = MarketMakerIt::start_with_db( + wallet_1_conf.conf, + wallet_1_conf.rpc_password, + Some(wasm_start), + DB_NAMESPACE_NUM, + ) + .await + .unwrap(); + + let get_wallet_names_1 = get_wallet_names(&mm_wallet_1).await; + assert_eq!(get_wallet_names_1.wallet_names, vec![wallet_1_name]); + assert_eq!(get_wallet_names_1.activated_wallet.as_deref(), Some(wallet_1_name)); + + mm_wallet_1 + .stop_and_wait_for_ctx_is_dropped(STOP_TIMEOUT_MS) + .await + .unwrap(); + + let wallet_2_name = "active_wallet"; + let wallet_2_pass = "pass2"; + let wallet_2_conf = Mm2TestConf::seednode_with_wallet_name(&coins, wallet_2_name, wallet_2_pass); + let mm_wallet_2 = MarketMakerIt::start_with_db( + wallet_2_conf.conf, + wallet_2_conf.rpc_password, + Some(wasm_start), + DB_NAMESPACE_NUM, + ) + .await + .unwrap(); + + let wallet_names = get_wallet_names(&mm_wallet_2).await.wallet_names; + assert_eq!(wallet_names, vec![wallet_2_name, wallet_1_name]); + let activated_wallet = get_wallet_names(&mm_wallet_2).await.activated_wallet; + assert_eq!(activated_wallet.as_deref(), Some(wallet_2_name)); + + // Try to delete the active wallet - should fail + let (_, body, _) = delete_wallet(&mm_wallet_2, wallet_2_name, wallet_2_pass).await; + let error: Json = serde_json::from_str(&body).unwrap(); + assert_eq!(error["error_type"], "InvalidRequest"); + assert!(error["error_data"] + .as_str() + .unwrap() + .contains("Cannot delete wallet 'active_wallet' as it is currently active.")); + + // Try to delete with the wrong password - should fail + let (_, body, _) = delete_wallet(&mm_wallet_2, wallet_1_name, "wrong_pass").await; + let error: Json = serde_json::from_str(&body).unwrap(); + assert_eq!(error["error_type"], "InvalidPassword"); + assert!(error["error_data"] + .as_str() + .unwrap() + .contains("Error decrypting mnemonic")); + + // Try to delete a non-existent wallet - should fail + let (_, body, _) = delete_wallet(&mm_wallet_2, "non_existent_wallet", "any_pass").await; + let error: Json = serde_json::from_str(&body).unwrap(); + assert_eq!(error["error_type"], "InvalidRequest"); + assert!(error["error_data"] + .as_str() + .unwrap() + .contains("Wallet 'non_existent_wallet' not found.")); + + // Delete the inactive wallet with the correct password - should succeed + let (_, body, _) = delete_wallet(&mm_wallet_2, wallet_1_name, wallet_1_pass).await; + let response: Json = serde_json::from_str(&body).expect("Response should be valid JSON"); + assert!( + response["result"].is_null(), + "Expected a successful response with null result, but got error: {}", + body + ); + + // Verify the wallet is deleted + let get_wallet_names_3 = get_wallet_names(&mm_wallet_2).await; + assert_eq!(get_wallet_names_3.wallet_names, vec![wallet_2_name]); + assert_eq!(get_wallet_names_3.activated_wallet.as_deref(), Some(wallet_2_name)); + + mm_wallet_2 + .stop_and_wait_for_ctx_is_dropped(STOP_TIMEOUT_MS) + .await + .unwrap(); +} diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 117fff3558..392a046838 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -13,10 +13,10 @@ use mm2_test_helpers::electrums::*; #[cfg(all(not(target_arch = "wasm32"), not(feature = "zhtlc-native-tests")))] use mm2_test_helpers::for_tests::wait_check_stats_swap_status; use mm2_test_helpers::for_tests::{account_balance, btc_segwit_conf, btc_with_spv_conf, btc_with_sync_starting_header, - check_recent_swaps, enable_qrc20, enable_utxo_v2_electrum, eth_dev_conf, - find_metrics_in_json, from_env_file, get_new_address, get_shared_db_id, - get_wallet_names, mm_spat, morty_conf, my_balance, rick_conf, sign_message, - start_swaps, tbtc_conf, tbtc_segwit_conf, tbtc_with_spv_conf, + check_recent_swaps, delete_wallet, enable_qrc20, enable_utxo_v2_electrum, + eth_dev_conf, find_metrics_in_json, from_env_file, get_new_address, + get_shared_db_id, get_wallet_names, mm_spat, morty_conf, my_balance, rick_conf, + sign_message, start_swaps, tbtc_conf, tbtc_segwit_conf, tbtc_with_spv_conf, test_qrc20_history_impl, tqrc20_conf, verify_message, wait_for_swaps_finish_and_check_status, wait_till_history_has_records, MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, RaiiDump, @@ -3751,6 +3751,7 @@ fn test_get_raw_transaction() { } #[test] +#[ignore] #[cfg(not(target_arch = "wasm32"))] fn test_qrc20_tx_history() { block_on(test_qrc20_history_impl(None)); } @@ -6357,7 +6358,7 @@ fn test_change_mnemonic_password_rpc() { .unwrap(); assert_eq!( request.0, - StatusCode::INTERNAL_SERVER_ERROR, + StatusCode::BAD_REQUEST, "'change_mnemonic_password' failed: {}", request.1 ); @@ -6381,6 +6382,119 @@ fn test_change_mnemonic_password_rpc() { ); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_delete_wallet_rpc() { + let coins = json!([]); + let wallet_1_name = "wallet_to_be_deleted"; + let wallet_1_pass = "pass1"; + let wallet_1_conf = Mm2TestConf::seednode_with_wallet_name(&coins, wallet_1_name, wallet_1_pass); + let mm_wallet_1 = MarketMakerIt::start(wallet_1_conf.conf, wallet_1_conf.rpc_password, None).unwrap(); + + let get_wallet_names_1 = block_on(get_wallet_names(&mm_wallet_1)); + assert_eq!(get_wallet_names_1.wallet_names, vec![wallet_1_name]); + assert_eq!(get_wallet_names_1.activated_wallet.as_deref(), Some(wallet_1_name)); + + let wallet_2_name = "active_wallet"; + let wallet_2_pass = "pass2"; + let mut wallet_2_conf = Mm2TestConf::seednode_with_wallet_name(&coins, wallet_2_name, wallet_2_pass); + wallet_2_conf.conf["dbdir"] = mm_wallet_1.folder.join("DB").to_str().unwrap().into(); + + block_on(mm_wallet_1.stop()).unwrap(); + + let mm_wallet_2 = MarketMakerIt::start(wallet_2_conf.conf, wallet_2_conf.rpc_password, None).unwrap(); + + let get_wallet_names_2 = block_on(get_wallet_names(&mm_wallet_2)); + assert_eq!(get_wallet_names_2.wallet_names, vec![ + "active_wallet", + "wallet_to_be_deleted" + ]); + assert_eq!(get_wallet_names_2.activated_wallet.as_deref(), Some(wallet_2_name)); + + // Try to delete the active wallet - should fail + let (status, body, _) = block_on(delete_wallet(&mm_wallet_2, wallet_2_name, wallet_2_pass)); + assert_eq!(status, StatusCode::BAD_REQUEST); + assert!(body.contains("Cannot delete wallet 'active_wallet' as it is currently active.")); + + // Try to delete with the wrong password - should fail + let (status, body, _) = block_on(delete_wallet(&mm_wallet_2, wallet_1_name, "wrong_pass")); + assert_eq!(status, StatusCode::BAD_REQUEST); + assert!(body.contains("Invalid password")); + + // Try to delete a non-existent wallet - should fail + let (status, body, _) = block_on(delete_wallet(&mm_wallet_2, "non_existent_wallet", "any_pass")); + assert_eq!(status, StatusCode::BAD_REQUEST); + assert!(body.contains("Wallet 'non_existent_wallet' not found.")); + + // Delete the inactive wallet with the correct password - should succeed + let (status, body, _) = block_on(delete_wallet(&mm_wallet_2, wallet_1_name, wallet_1_pass)); + assert_eq!(status, StatusCode::OK, "Body: {}", body); + + // Verify the wallet is deleted + let get_wallet_names_3 = block_on(get_wallet_names(&mm_wallet_2)); + assert_eq!(get_wallet_names_3.wallet_names, vec![wallet_2_name]); + assert_eq!(get_wallet_names_3.activated_wallet.as_deref(), Some(wallet_2_name)); + + block_on(mm_wallet_2.stop()).unwrap(); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_delete_wallet_in_no_login_mode() { + // 0. Setup a seednode to be able to run a no-login node. + let seednode_conf = Mm2TestConf::seednode_with_wallet_name(&json!([]), "seednode_wallet", "seednode_pass"); + let mm_seednode = MarketMakerIt::start(seednode_conf.conf, seednode_conf.rpc_password, None).unwrap(); + let seednode_ip = mm_seednode.ip.to_string(); + + // 1. Setup: Create a wallet to be deleted later. + let wallet_to_delete_name = "wallet_for_no_login_test"; + let wallet_to_delete_pass = "password123"; + let coins = json!([]); + + let wallet_conf = Mm2TestConf::seednode_with_wallet_name(&coins, wallet_to_delete_name, wallet_to_delete_pass); + let mm_setup = MarketMakerIt::start(wallet_conf.conf.clone(), wallet_conf.rpc_password, None).unwrap(); + + let wallet_names_before = block_on(get_wallet_names(&mm_setup)); + assert_eq!(wallet_names_before.wallet_names, vec![wallet_to_delete_name]); + let db_dir = mm_setup.folder.join("DB"); + block_on(mm_setup.stop()).unwrap(); + + // 2. Execution: Start in no-login mode, connecting to the seednode. + let mut no_login_conf = Mm2TestConf::no_login_node(&coins, &[&seednode_ip]); + no_login_conf.conf["dbdir"] = db_dir.to_str().unwrap().into(); + + let mm_no_login = MarketMakerIt::start(no_login_conf.conf, no_login_conf.rpc_password, None).unwrap(); + + let wallet_names_no_login = block_on(get_wallet_names(&mm_no_login)); + assert!(wallet_names_no_login + .wallet_names + .contains(&wallet_to_delete_name.to_string())); + + let (status, body, _) = block_on(delete_wallet( + &mm_no_login, + wallet_to_delete_name, + wallet_to_delete_pass, + )); + assert_eq!(status, StatusCode::OK, "Delete failed with body: {}", body); + + block_on(mm_no_login.stop()).unwrap(); + + // 3. Verification: Start another instance to check if the wallet is gone. + let mut verification_conf = Mm2TestConf::seednode_with_wallet_name(&coins, "verification_wallet", "pass"); + verification_conf.conf["dbdir"] = db_dir.to_str().unwrap().into(); + let mm_verify = MarketMakerIt::start(verification_conf.conf, verification_conf.rpc_password, None).unwrap(); + + let wallet_names_after = block_on(get_wallet_names(&mm_verify)); + assert!(!wallet_names_after + .wallet_names + .contains(&wallet_to_delete_name.to_string())); + + block_on(mm_verify.stop()).unwrap(); + + // 4. Teardown: Stop the seednode. + block_on(mm_seednode.stop()).unwrap(); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_sign_raw_transaction_rick() { diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 490d312269..f98fe3041c 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -427,6 +427,11 @@ impl Mm2TestConf { } pub fn no_login_node(coins: &Json, seednodes: &[&str]) -> Self { + assert!( + !seednodes.is_empty(), + "Invalid Test Setup: A no-login node requires at least one seednode." + ); + Mm2TestConf { conf: json!({ "gui": "nogui", @@ -2985,6 +2990,20 @@ pub async fn get_wallet_names(mm: &MarketMakerIt) -> GetWalletNamesResult { res.result } +pub async fn delete_wallet(mm: &MarketMakerIt, wallet_name: &str, password: &str) -> (StatusCode, String, HeaderMap) { + mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "delete_wallet", + "mmrpc": "2.0", + "params": { + "wallet_name": wallet_name, + "password": password, + } + })) + .await + .unwrap() +} + pub async fn max_maker_vol(mm: &MarketMakerIt, coin: &str) -> RpcResponse { let rc = mm .rpc(&json!({ From a25aea61495534adbadec94068fc67525575ea4d Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 28 Jun 2025 04:26:42 +0200 Subject: [PATCH 34/36] fix(hw-wallet): avoid calling `get_enabled_address` in trezor-based coin init (#2504) This commit makes it so we don't call get_enabled_address before initializing the coin, but rather after we made sure we initialized the account and address that the enabled address belongs to. --- mm2src/coins/coin_balance.rs | 42 +++++++++++++++++++ mm2src/coins/hd_wallet/coin_ops.rs | 12 +++++- mm2src/coins/hd_wallet/errors.rs | 5 +++ mm2src/coins/hd_wallet/mod.rs | 2 +- mm2src/coins/utxo/bch.rs | 12 +++++- mm2src/coins/utxo/qtum.rs | 12 +++++- .../utxo/utxo_builder/utxo_coin_builder.rs | 10 ++--- mm2src/coins/utxo/utxo_common.rs | 21 ++++++++++ mm2src/coins/utxo/utxo_standard.rs | 12 +++++- 9 files changed, 116 insertions(+), 12 deletions(-) diff --git a/mm2src/coins/coin_balance.rs b/mm2src/coins/coin_balance.rs index 3ec047ee80..dbcf02c343 100644 --- a/mm2src/coins/coin_balance.rs +++ b/mm2src/coins/coin_balance.rs @@ -506,6 +506,27 @@ pub mod common_impl { params.min_addresses_number.max(Some(path_to_address.address_id + 1)), ) .await?; + drop(new_account); + + if coin.is_trezor() { + let enabled_address = + hd_wallet + .get_enabled_address() + .await + .ok_or(EnableCoinBalanceError::NewAddressDerivingError( + NewAddressDerivingError::Internal( + "Couldn't find enabled address after it has already been enabled".to_string(), + ), + ))?; + coin.received_enabled_address_from_hw_wallet(enabled_address) + .await + .map_err(|e| { + EnableCoinBalanceError::NewAddressDerivingError(NewAddressDerivingError::Internal(format!( + "Coin rejected the enabled address derived from the hardware wallet: {}", + e + ))) + })?; + } // Todo: The enabled address should be indicated in the response. result.accounts.push(account_balance); return Ok(result); @@ -538,6 +559,27 @@ pub mod common_impl { .await?; result.accounts.push(account_balance); } + drop(accounts); + + if coin.is_trezor() { + let enabled_address = + hd_wallet + .get_enabled_address() + .await + .ok_or(EnableCoinBalanceError::NewAddressDerivingError( + NewAddressDerivingError::Internal( + "Couldn't find enabled address after it has already been enabled".to_string(), + ), + ))?; + coin.received_enabled_address_from_hw_wallet(enabled_address) + .await + .map_err(|e| { + EnableCoinBalanceError::NewAddressDerivingError(NewAddressDerivingError::Internal(format!( + "Coin rejected the enabled address derived from the hardware wallet: {}", + e + ))) + })?; + } Ok(result) } diff --git a/mm2src/coins/hd_wallet/coin_ops.rs b/mm2src/coins/hd_wallet/coin_ops.rs index 27b92d0aa6..7bf6a7392b 100644 --- a/mm2src/coins/hd_wallet/coin_ops.rs +++ b/mm2src/coins/hd_wallet/coin_ops.rs @@ -1,7 +1,7 @@ use super::{inner_impl, AccountUpdatingError, AddressDerivingError, DisplayAddress, ExtendedPublicKeyOps, HDAccountOps, HDCoinExtendedPubkey, HDCoinHDAccount, HDCoinHDAddress, HDConfirmAddress, HDWalletOps, NewAddressDeriveConfirmError, NewAddressDerivingError}; -use crate::hd_wallet::{HDAddressOps, HDWalletStorageOps, TrezorCoinError}; +use crate::hd_wallet::{errors::SettingEnabledAddressError, HDAddressOps, HDWalletStorageOps, TrezorCoinError}; use async_trait::async_trait; use bip32::{ChildNumber, DerivationPath}; use crypto::Bip44Chain; @@ -231,4 +231,14 @@ pub trait HDWalletCoinOps { /// Returns the Trezor coin name for this coin. fn trezor_coin(&self) -> MmResult; + + /// Informs the coin of the enabled address provided/derived by the hardware wallet. + async fn received_enabled_address_from_hw_wallet( + &self, + _enabled_address: HDCoinHDAddress, + ) -> MmResult<(), SettingEnabledAddressError> { + // By default, the default implementation is doing nothing. + // Different coins can use this hook to perform additional actions if needed. + Ok(()) + } } diff --git a/mm2src/coins/hd_wallet/errors.rs b/mm2src/coins/hd_wallet/errors.rs index 8b517bc609..89ab2002de 100644 --- a/mm2src/coins/hd_wallet/errors.rs +++ b/mm2src/coins/hd_wallet/errors.rs @@ -242,3 +242,8 @@ impl From for NewAddressDeriveConfirmError { NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::Internal(e.to_string())) } } + +#[derive(Display)] +pub enum SettingEnabledAddressError { + Internal(String), +} diff --git a/mm2src/coins/hd_wallet/mod.rs b/mm2src/coins/hd_wallet/mod.rs index 1fedf35fcd..5691f98c2c 100644 --- a/mm2src/coins/hd_wallet/mod.rs +++ b/mm2src/coins/hd_wallet/mod.rs @@ -30,7 +30,7 @@ pub use confirm_address::{HDConfirmAddress, HDConfirmAddressError}; mod errors; pub use errors::{AccountUpdatingError, AddressDerivingError, HDExtractPubkeyError, HDWithdrawError, InvalidBip44ChainError, NewAccountCreationError, NewAddressDeriveConfirmError, - NewAddressDerivingError, TrezorCoinError}; + NewAddressDerivingError, SettingEnabledAddressError, TrezorCoinError}; mod pubkey; pub use pubkey::{ExtendedPublicKeyOps, ExtractExtendedPubkey, HDXPubExtractor, RpcTaskXPubExtractor}; diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 8694710907..062f15fc66 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -2,7 +2,8 @@ use super::*; use crate::coin_balance::{EnableCoinBalanceError, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::hd_wallet::{ExtractExtendedPubkey, HDAddressSelector, HDCoinAddress, HDCoinWithdrawOps, - HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; + HDExtractPubkeyError, HDXPubExtractor, SettingEnabledAddressError, TrezorCoinError, + WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxDetailsBuilder, TxHistoryStorage}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; @@ -1388,6 +1389,15 @@ impl HDWalletCoinOps for BchCoin { } fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } + + async fn received_enabled_address_from_hw_wallet( + &self, + enabled_address: UtxoHDAddress, + ) -> MmResult<(), SettingEnabledAddressError> { + utxo_common::received_enabled_address_from_hw_wallet(self, enabled_address.address) + .await + .mm_err(SettingEnabledAddressError::Internal) + } } impl HDCoinWithdrawOps for BchCoin {} diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index a45b4a9491..7a29ab81c5 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -4,7 +4,8 @@ use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams HDWalletBalance, HDWalletBalanceOps}; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::hd_wallet::{ExtractExtendedPubkey, HDAddressSelector, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, - HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; + HDExtractPubkeyError, HDXPubExtractor, SettingEnabledAddressError, TrezorCoinError, + WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, @@ -1036,6 +1037,15 @@ impl HDWalletCoinOps for QtumCoin { } fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } + + async fn received_enabled_address_from_hw_wallet( + &self, + enabled_address: UtxoHDAddress, + ) -> MmResult<(), SettingEnabledAddressError> { + utxo_common::received_enabled_address_from_hw_wallet(self, enabled_address.address) + .await + .mm_err(SettingEnabledAddressError::Internal) + } } impl HDCoinWithdrawOps for QtumCoin {} diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 03ccd6fd8b..05513dd93b 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -1,4 +1,4 @@ -use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDWallet, HDWalletCoinStorage, HDWalletOps, +use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDWallet, HDWalletCoinStorage, HDWalletStorageError, DEFAULT_GAP_LIMIT}; use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientSettings, ElectrumConnectionSettings, EstimateFeeMethod, UtxoRpcClientEnum}; @@ -321,12 +321,8 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { address_format, }; - let my_address = hd_wallet - .get_enabled_address() - .await - .ok_or_else(|| UtxoCoinBuildError::Internal("Failed to get enabled address from HD wallet".to_owned()))?; - let my_script_pubkey = output_script(&my_address.address).map(|script| script.to_bytes())?; - let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)); + // TODO: Creating a dummy output script for now. We better set it to the enabled address output script. + let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(Default::default())); // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, // all spawned futures related to this `UTXO` coin will be aborted as well. diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 73f7043784..6d13af3c0d 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -138,6 +138,27 @@ where }) } +pub(crate) async fn received_enabled_address_from_hw_wallet( + coin: &Coin, + enabled_address: Address, +) -> MmResult<(), String> +where + Coin: AsRef, +{ + let my_script_pubkey = match output_script(&enabled_address) { + Ok(script) => script.to_bytes(), + Err(e) => { + return MmError::err(format!( + "Error generating the output_script for the enabled_address={}: {}", + enabled_address, e + )); + }, + }; + let mut recently_spent_outputs = coin.as_ref().recently_spent_outpoints.lock().await; + *recently_spent_outputs = RecentlySpentOutPoints::new(my_script_pubkey); + Ok(()) +} + pub async fn produce_hd_address_scanner(coin: &T) -> BalanceResult where T: AsRef, diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 49e4ea9d60..dae16222de 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -4,7 +4,8 @@ use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams HDWalletBalance, HDWalletBalanceOps}; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::hd_wallet::{ExtractExtendedPubkey, HDAddressSelector, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, - HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; + HDExtractPubkeyError, HDXPubExtractor, SettingEnabledAddressError, TrezorCoinError, + WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, @@ -1123,6 +1124,15 @@ impl HDWalletCoinOps for UtxoStandardCoin { } fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } + + async fn received_enabled_address_from_hw_wallet( + &self, + enabled_address: UtxoHDAddress, + ) -> MmResult<(), SettingEnabledAddressError> { + utxo_common::received_enabled_address_from_hw_wallet(self, enabled_address.address) + .await + .mm_err(SettingEnabledAddressError::Internal) + } } impl HDCoinWithdrawOps for UtxoStandardCoin {} From 46ab1fbe99048a1aff2aeebc203fdfef85e193e1 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:04:38 +0300 Subject: [PATCH 35/36] fix(walletconnect): centralize connection and retry logic (#2508) This commit fixes a critical race condition in the WalletConnect module that causes Client(WebsocketClient(NotConnected)) errors during initialization, particularly for the wc_new_connection RPC call. It's done by refactoring the connection management logic into a robust, centralized state machine. The scattered, per-function retry loops have been removed in favor of a more reliable architecture. --- .../src/connection_handler.rs | 37 +-- mm2src/kdf_walletconnect/src/lib.rs | 238 +++++++++--------- 2 files changed, 119 insertions(+), 156 deletions(-) diff --git a/mm2src/kdf_walletconnect/src/connection_handler.rs b/mm2src/kdf_walletconnect/src/connection_handler.rs index 2ac7d7b7c3..3bd57b6fdc 100644 --- a/mm2src/kdf_walletconnect/src/connection_handler.rs +++ b/mm2src/kdf_walletconnect/src/connection_handler.rs @@ -1,9 +1,5 @@ -use crate::WalletConnectCtxImpl; - -use common::executor::Timer; -use common::log::{debug, error, info}; -use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use futures::StreamExt; +use common::log::{debug, error}; +use futures::channel::mpsc::UnboundedSender; use relay_client::error::ClientError; use relay_client::websocket::{CloseFrame, ConnectionHandler, PublishedMessage}; @@ -67,32 +63,3 @@ impl ConnectionHandler for Handler { } } } - -/// Handles unexpected disconnections from WalletConnect relay server. -/// Implements exponential backoff retry mechanism for reconnection attempts. -/// After successful reconnection, resubscribes to previous topics to restore full functionality. -pub(crate) async fn handle_disconnections( - this: &WalletConnectCtxImpl, - mut connection_live_rx: UnboundedReceiver>, -) { - let mut backoff = 1; - - while let Some(msg) = connection_live_rx.next().await { - info!("WalletConnect disconnected with message: {msg:?}. Attempting to reconnect..."); - loop { - match this.reconnect_and_subscribe().await { - Ok(_) => { - info!("Reconnection process complete."); - backoff = 1; - break; - }, - Err(e) => { - error!("Reconnection attempt failed: {:?}. Retrying in {:?}...", e, backoff); - Timer::sleep(backoff as f64).await; - // Exponentially increase backoff, but cap it at MAX_BACKOFF - backoff = std::cmp::min(backoff * 2, MAX_BACKOFF); - }, - } - } - } -} diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs index 8f8740e081..8acf4895a2 100644 --- a/mm2src/kdf_walletconnect/src/lib.rs +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -7,15 +7,13 @@ mod metadata; pub mod session; mod storage; -use crate::connection_handler::{handle_disconnections, MAX_BACKOFF}; +use crate::connection_handler::{Handler, MAX_BACKOFF}; use crate::session::rpc::propose::send_proposal_request; use chain::{WcChainId, WcRequestMethods, SUPPORTED_PROTOCOL}; use common::custom_futures::timeout::FutureTimerExt; use common::executor::abortable_queue::AbortableQueue; -use common::executor::{AbortableSystem, Timer}; -use common::log::{debug, info, LogOnError}; -use common::{executor::SpawnFuture, log::error}; -use connection_handler::Handler; +use common::executor::{AbortableSystem, SpawnFuture, Timer}; +use common::log::{debug, error, info, LogOnError}; use error::WalletConnectError; use futures::channel::mpsc::{unbounded, UnboundedReceiver}; use futures::StreamExt; @@ -39,16 +37,24 @@ use session::{key::SymKeyPair, SessionManager}; use session::{EncodingAlgo, Session, SessionProperties, FIVE_MINUTES}; use std::collections::BTreeSet; use std::ops::Deref; -use std::{sync::{Arc, Mutex}, - time::Duration}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; use storage::SessionStorageDb; use storage::WalletConnectStorageOps; use timed_map::TimedMap; -use tokio::sync::oneshot; +use tokio::sync::{oneshot, watch}; use wc_common::{decode_and_decrypt_type0, encrypt_and_encode, EnvelopeType, SymKey}; const PUBLISH_TIMEOUT_SECS: f64 = 6.; -const MAX_RETRIES: usize = 5; +const CONNECTION_TIMEOUT_S: f64 = 30.; + +/// Broadcast by the lifecycle task so every RPC can cheaply await connectivity. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ConnectionState { + Connecting, + Connected, + Disconnected, +} #[async_trait::async_trait] pub trait WalletConnectOps { @@ -92,6 +98,7 @@ pub struct WalletConnectCtxImpl { message_id_generator: MessageIdGenerator, pending_requests: Mutex>>, abortable_system: AbortableQueue, + connection_state_rx: watch::Receiver, } /// A newtype wrapper around a thread-safe reference to `WalletConnectCtxImpl`. @@ -117,6 +124,7 @@ impl WalletConnectCtx { }; let (inbound_message_tx, inbound_message_rx) = unbounded(); let (conn_live_sender, conn_live_receiver) = unbounded(); + let (connection_state_tx, connection_state_rx) = watch::channel(ConnectionState::Disconnected); let (client, _) = Client::new_with_callback( Handler::new("KDF", inbound_message_tx, conn_live_sender), |receiver, handler| { @@ -137,13 +145,15 @@ impl WalletConnectCtx { pending_requests: Default::default(), message_id_generator, abortable_system, + connection_state_rx, }); - // Connect to relayer client and spawn a watcher loop for disconnection. - context - .abortable_system - .weak_spawner() - .spawn(context.clone().spawn_connection_initialization_fut(conn_live_receiver)); + // Spawn the relayer connection lifecycle task. + context.abortable_system.weak_spawner().spawn( + context + .clone() + .connection_lifecycle_task(conn_live_receiver, connection_state_tx), + ); // spawn message handler event loop context @@ -163,42 +173,75 @@ impl WalletConnectCtx { } impl WalletConnectCtxImpl { - /// Establishes initial connection to WalletConnect relay server with linear retry mechanism. - /// Uses increasing delay between retry attempts starting from 1sec and increase exponentially. - /// After successful connection, attempts to restore previous session state from storage. - pub(crate) async fn spawn_connection_initialization_fut( + /// Centralised task owning **all** connection logic (connect → monitor → reconnect). + async fn connection_lifecycle_task( self: Arc, - connection_live_rx: UnboundedReceiver>, + mut conn_status_rx: UnboundedReceiver>, + connection_state_tx: watch::Sender, ) { - info!("Initializing WalletConnect connection"); - let mut retry_count = 0; - let mut retry_secs = 1; - - // Connect to WalletConnect relay client(retry until successful) before proceeeding with other initializations. - while let Err(err) = self.connect_client().await { - retry_count += 1; - error!( - "Error during initial connection attempt {}: {:?}. Retrying in {retry_secs} seconds...", - retry_count, err - ); - Timer::sleep(retry_secs as f64).await; - retry_secs = std::cmp::min(retry_secs * 2, MAX_BACKOFF); - } - - // Initialize storage if let Err(err) = self.session_manager.storage().init().await { error!("Failed to initialize WalletConnect storage, shutting down: {err:?}"); + connection_state_tx.send(ConnectionState::Disconnected).error_log(); self.abortable_system.abort_all().error_log(); - }; + return; + } - // load session from storage - if let Err(err) = self.load_session_from_storage().await { + if let Err(err) = self.load_sessions_from_storage().await { error!("Failed to load sessions from storage, shutting down: {err:?}"); + connection_state_tx.send(ConnectionState::Disconnected).error_log(); self.abortable_system.abort_all().error_log(); + return; + } + + loop { + connection_state_tx.send(ConnectionState::Connecting).error_log(); + info!("WalletConnect: connecting…"); + + let mut backoff = 1; + while let Err(e) = self.connect_and_subscribe().await { + error!("Connection attempt failed: {e:?}; retrying in {backoff}s"); + Timer::sleep(backoff as f64).await; + backoff = std::cmp::min(backoff * 2, MAX_BACKOFF); + } + + connection_state_tx.send(ConnectionState::Connected).error_log(); + info!("WalletConnect: online."); + + if let Some(msg) = conn_status_rx.next().await { + info!("WalletConnect: disconnected with message: {msg:?}, will reconnect."); + connection_state_tx.send(ConnectionState::Disconnected).error_log(); + } else { + connection_state_tx.send(ConnectionState::Disconnected).error_log(); + self.abortable_system.abort_all().error_log(); + break; + } + } + } + + /// Waits until the current state is `Connected`. + async fn await_connection(&self) -> MmResult<(), WalletConnectError> { + let mut rx = self.connection_state_rx.clone(); + + let wait_for_connected = async move { + loop { + if *rx.borrow() == ConnectionState::Connected { + return Ok(()); + } + + if rx.changed().await.is_err() { + let last_state = *rx.borrow(); + return MmError::err(WalletConnectError::InternalError(format!( + "Connection task dropped, last state was: {:?}", + last_state + ))); + } + } }; - // Spawn session disconnection watcher. - handle_disconnections(&self, connection_live_rx).await; + Box::pin(wait_for_connected) + .timeout_secs(CONNECTION_TIMEOUT_S) + .await + .map_to_mm(|_timeout_err| WalletConnectError::TimeoutError)? } /// Attempt to connect to a wallet connection relay server. @@ -217,8 +260,8 @@ impl WalletConnectCtxImpl { Ok(()) } - /// Re-connect to WalletConnect relayer and re-subscribes to previously active session topics after reconnection. - pub(crate) async fn reconnect_and_subscribe(&self) -> MmResult<(), WalletConnectError> { + /// Connects to WalletConnect relayer and re-subscribes to previously active session topics if it's a reconnection. + pub(crate) async fn connect_and_subscribe(&self) -> MmResult<(), WalletConnectError> { self.connect_client().await?; let sessions = self .session_manager @@ -239,6 +282,8 @@ impl WalletConnectCtxImpl { required_namespaces: serde_json::Value, optional_namespaces: Option, ) -> MmResult { + self.await_connection().await?; + let required_namespaces = serde_json::from_value(required_namespaces)?; let optional_namespaces = match optional_namespaces { Some(value) => serde_json::from_value(value)?, @@ -248,26 +293,18 @@ impl WalletConnectCtxImpl { info!("[{topic}] Subscribing to topic"); - for attempt in 0..MAX_RETRIES { - match self - .client - .subscribe(topic.clone()) - .timeout_secs(PUBLISH_TIMEOUT_SECS) - .await - { - Ok(res) => { - res.map_to_mm(|err| err.into())?; - info!("[{topic}] Subscribed to topic"); - send_proposal_request(self, &topic, required_namespaces, optional_namespaces).await?; - return Ok(url); - }, - Err(_) => self.wait_until_client_is_online_loop(attempt).await, - } - } + self.client + .subscribe(topic.clone()) + .timeout_secs(PUBLISH_TIMEOUT_SECS) + .await + .map_to_mm(|_| WalletConnectError::TimeoutError)? + .map_to_mm(|e| e)?; - MmError::err(WalletConnectError::InternalError( - "client connection timeout".to_string(), - )) + info!("[{topic}] Subscribed to topic"); + + send_proposal_request(self, &topic, required_namespaces, optional_namespaces).await?; + + Ok(url) } /// Get symmetric key associated with a for `topic`. @@ -312,7 +349,7 @@ impl WalletConnectCtxImpl { } /// Loads sessions from storage, activates valid ones, and deletes expired. - async fn load_session_from_storage(&self) -> MmResult<(), WalletConnectError> { + async fn load_sessions_from_storage(&self) -> MmResult<(), WalletConnectError> { info!("Loading WalletConnect session from storage"); let now = chrono::Utc::now().timestamp() as u64; let sessions = self @@ -321,8 +358,6 @@ impl WalletConnectCtxImpl { .get_all_sessions() .await .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; - let mut valid_topics = Vec::with_capacity(sessions.len()); - let mut pairing_topics = Vec::with_capacity(sessions.len()); // bring most recent active session to the back. for session in sessions.into_iter().rev() { @@ -337,19 +372,8 @@ impl WalletConnectCtxImpl { continue; }; - let topic = session.topic.clone(); - let pairing_topic = session.pairing_topic.clone(); - debug!("[{topic}] Session found! activating"); + debug!("[{}] Session found! activating", session.topic); self.session_manager.add_session(session); - - valid_topics.push(topic); - pairing_topics.push(pairing_topic); - } - - let all_topics = valid_topics.into_iter().chain(pairing_topics).collect::>(); - - if !all_topics.is_empty() { - self.client.batch_subscribe(all_topics).await?; } info!("Loaded WalletConnect session from storage"); @@ -429,6 +453,8 @@ impl WalletConnectCtxImpl { irn_metadata: IrnMetadata, payload: Payload, ) -> MmResult<(), WalletConnectError> { + self.await_connection().await?; + info!("[{topic}] Publishing message={payload:?}"); let message = { let sym_key = self.sym_key(topic)?; @@ -436,52 +462,22 @@ impl WalletConnectCtxImpl { encrypt_and_encode(EnvelopeType::Type0, payload, &sym_key)? }; - for attempt in 0..MAX_RETRIES { - match self - .client - .publish( - topic.clone(), - &*message, - None, - irn_metadata.tag, - Duration::from_secs(irn_metadata.ttl), - irn_metadata.prompt, - ) - .timeout_secs(PUBLISH_TIMEOUT_SECS) - .await - { - Ok(Ok(_)) => { - info!("[{topic}] Message published successfully"); - return Ok(()); - }, - Ok(Err(err)) => return MmError::err(err.into()), - Err(_) => self.wait_until_client_is_online_loop(attempt).await, - } - } - - MmError::err(WalletConnectError::InternalError( - "[{topic}] client connection timeout".to_string(), - )) - } + self.client + .publish( + topic.clone(), + &*message, + None, + irn_metadata.tag, + Duration::from_secs(irn_metadata.ttl), + irn_metadata.prompt, + ) + .timeout_secs(PUBLISH_TIMEOUT_SECS) + .await + .map_to_mm(|_| WalletConnectError::TimeoutError)? + .map_to_mm(|e| e)?; - /// Persistent reconnection and retry strategy keeps the WebSocket connection active, - /// allowing the client to automatically resume operations after network interruptions or disconnections. - /// Since TCP handles connection timeouts (which can be lengthy and it's determined by the OS), we're using a shorter timeout here - /// to detect issues quickly and reconnect as needed. - async fn wait_until_client_is_online_loop(&self, attempt: usize) { - debug!("Attempt {} failed due to timeout. Reconnecting...", attempt + 1); - loop { - match self.reconnect_and_subscribe().await { - Ok(_) => { - info!("Reconnected and subscribed successfully."); - break; - }, - Err(reconnect_err) => { - error!("Reconnection attempt failed: {reconnect_err:?}. Retrying..."); - Timer::sleep(1.5).await; - }, - } - } + info!("[{topic}] Message published successfully"); + Ok(()) } /// Checks if the current session is connected to a Ledger device. From 895ac4ad95d268c77546699cd6c7213ac3c38758 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Fri, 4 Jul 2025 06:00:04 +0300 Subject: [PATCH 36/36] chore(release): finalize changelog for v2.5.0-beta (#2524) --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba9c8792c6..831bc7bc68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ -## v2.5.0-beta - 2025-06-23 +## v2.5.0-beta - 2025-07-04 ### Features: **WalletConnect Integration**: -- WalletConnect v2 support for EVM and Cosmos coins was implemented, enabling wallet connection and transaction signing via the WalletConnect protocol. [#2223](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2223) [#2485](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2485) +- WalletConnect v2 support for EVM and Cosmos coins was implemented, enabling wallet connection and transaction signing via the WalletConnect protocol. [#2223](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2223) [#2485](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2485) [#2508](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2508) --- @@ -36,6 +36,8 @@ **Wallet**: - Unconfirmed z-coin notes are now correctly tracked. [#2331](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2331) - HD multi-address support for message signing was implemented, allowing message signatures from multiple derived addresses. [#2432](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2432) [#2474](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2474) +- A `delete_wallet` RPC was introduced to securely remove wallets after password confirmation, while preventing the deletion of the currently active wallet. [#2497](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2497) +- A race condition during the initialization of Trezor-based hardware wallets was resolved by ensuring the correct account and address are loaded before fetching the enabled address, preventing startup errors. [#2504](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2504) **UTXO**: - Validation of expected public keys for p2pk inputs was corrected, resolving an error in p2pk transaction processing. [#2408](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2408)