From d4d5add3ffbb0d0dc1863d4b01d0b973a9aadcb7 Mon Sep 17 00:00:00 2001 From: Rozhkov Dmitrii Date: Wed, 26 Jul 2023 05:50:22 +0500 Subject: [PATCH 01/17] feat(adex-cli): Set config file permissions to 660 in unix (#1913) The file permissions of the cli config file is now set to 660 to disallow reading by other users. --- mm2src/adex_cli/src/adex_config.rs | 10 +++++++++- mm2src/adex_cli/src/helpers.rs | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/mm2src/adex_cli/src/adex_config.rs b/mm2src/adex_cli/src/adex_config.rs index 20bcbb8fdf..aee4fec2d7 100644 --- a/mm2src/adex_cli/src/adex_config.rs +++ b/mm2src/adex_cli/src/adex_config.rs @@ -9,6 +9,7 @@ use std::path::{Path, PathBuf}; use crate::adex_proc::SmartFractPrecision; use crate::helpers::rewrite_json_file; +#[cfg(unix)] use crate::helpers::set_file_permissions; use crate::logging::{error_anyhow, warn_bail}; const PROJECT_QUALIFIER: &str = "com"; @@ -22,6 +23,8 @@ const VOLUME_PRECISION_MIN: usize = 2; const VOLUME_PRECISION_MAX: usize = 5; const VOLUME_PRECISION: SmartFractPrecision = (VOLUME_PRECISION_MIN, VOLUME_PRECISION_MAX); const PRICE_PRECISION: SmartFractPrecision = (PRICE_PRECISION_MIN, PRICE_PRECISION_MAX); +#[cfg(unix)] +const CFG_FILE_PERM_MODE: u32 = 0o660; pub(super) fn get_config() { let Ok(adex_cfg) = AdexConfigImpl::from_config_path() else { return; }; @@ -151,7 +154,12 @@ impl AdexConfigImpl { let adex_path_str = cfg_path .to_str() .ok_or_else(|| error_anyhow!("Failed to get cfg_path as str"))?; - rewrite_json_file(self, adex_path_str) + rewrite_json_file(self, adex_path_str)?; + #[cfg(unix)] + { + set_file_permissions(adex_path_str, CFG_FILE_PERM_MODE)?; + } + Ok(()) } fn set_rpc_password(&mut self, rpc_password: String) { self.rpc_password.replace(rpc_password); } diff --git a/mm2src/adex_cli/src/helpers.rs b/mm2src/adex_cli/src/helpers.rs index a6c93a7774..6d6cef2a90 100644 --- a/mm2src/adex_cli/src/helpers.rs +++ b/mm2src/adex_cli/src/helpers.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::io::Write; use std::ops::Deref; +#[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::Path; use crate::error_anyhow; @@ -22,6 +23,15 @@ where writer .write(&data) .map_err(|error| error_anyhow!("Failed to write data into {file}: {error}"))?; + + Ok(()) +} + +#[cfg(unix)] +pub(crate) fn set_file_permissions(file: &str, unix_mode: u32) -> Result<()> { + let mut perms = fs::metadata(file)?.permissions(); + perms.set_mode(unix_mode); + fs::set_permissions(file, perms)?; Ok(()) } From 80f7e6f14585661849d88fe2b4cfea4761764330 Mon Sep 17 00:00:00 2001 From: Rozhkov Dmitrii Date: Wed, 26 Jul 2023 05:54:49 +0500 Subject: [PATCH 02/17] feat(adex-cli): activation request types (#1912) Activation types in cli have been introduced by this commit to ensure that if a malicious person substituted them in the activation scheme file it would not lead to any unexpected action. --- .../activation_scheme_impl.rs | 14 +- .../adex_cli/src/adex_proc/adex_proc_impl.rs | 6 +- mm2src/adex_cli/src/main.rs | 1 + mm2src/adex_cli/src/rpc_data.rs | 88 ++++++ mm2src/adex_cli/src/tests/mod.rs | 9 +- mm2src/coins/eth.rs | 18 +- mm2src/coins/eth/v2_activation.rs | 2 +- mm2src/coins/utxo.rs | 10 +- mm2src/coins/utxo/rpc_clients.rs | 25 +- mm2src/coins/utxo/utxo_common.rs | 10 - mm2src/mm2_rpc/src/data/legacy.rs | 255 +----------------- mm2src/mm2_rpc/src/data/legacy/activation.rs | 26 ++ .../mm2_rpc/src/data/legacy/activation/eth.rs | 16 ++ .../src/data/legacy/activation/utxo.rs | 35 +++ mm2src/mm2_rpc/src/data/legacy/orders.rs | 200 ++++++++++++++ mm2src/mm2_rpc/src/data/legacy/utility.rs | 14 + mm2src/mm2_rpc/src/data/legacy/wallet.rs | 11 + mm2src/mm2_test_helpers/src/for_tests.rs | 15 +- 18 files changed, 430 insertions(+), 325 deletions(-) create mode 100644 mm2src/adex_cli/src/rpc_data.rs create mode 100644 mm2src/mm2_rpc/src/data/legacy/activation.rs create mode 100644 mm2src/mm2_rpc/src/data/legacy/activation/eth.rs create mode 100644 mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs create mode 100644 mm2src/mm2_rpc/src/data/legacy/orders.rs create mode 100644 mm2src/mm2_rpc/src/data/legacy/utility.rs create mode 100644 mm2src/mm2_rpc/src/data/legacy/wallet.rs diff --git a/mm2src/adex_cli/src/activation_scheme_db/activation_scheme_impl.rs b/mm2src/adex_cli/src/activation_scheme_db/activation_scheme_impl.rs index a2bc0ba451..63c89e5efc 100644 --- a/mm2src/adex_cli/src/activation_scheme_db/activation_scheme_impl.rs +++ b/mm2src/adex_cli/src/activation_scheme_db/activation_scheme_impl.rs @@ -1,11 +1,13 @@ use anyhow::{anyhow, bail, Result}; -use log::{debug, error}; use serde_json::Value as Json; use std::collections::HashMap; +use common::log::{debug, error}; + use super::init_activation_scheme::get_activation_scheme_path; use crate::helpers::read_json_file; use crate::logging::{error_anyhow, error_bail}; +use crate::rpc_data::ActivationRequest; #[derive(Default)] pub(crate) struct ActivationScheme { @@ -13,7 +15,15 @@ pub(crate) struct ActivationScheme { } impl ActivationScheme { - pub(crate) fn get_activation_method(&self, coin: &str) -> Option<&Json> { self.scheme.get(coin) } + pub(crate) fn get_activation_method(&self, coin: &str) -> Result { + let method_json = self + .scheme + .get(coin) + .ok_or_else(|| error_anyhow!("Coin is not in activation scheme data: {}", coin))?; + let method: ActivationRequest = serde_json::from_value(method_json.clone()) + .map_err(|error| error_anyhow!("Failed to deserialize json data: {:?}, error: {}", method_json, error))?; + Ok(method) + } fn init(&mut self) -> Result<()> { let mut scheme_source: Vec = Self::load_json_file()?; diff --git a/mm2src/adex_cli/src/adex_proc/adex_proc_impl.rs b/mm2src/adex_cli/src/adex_proc/adex_proc_impl.rs index 926ee784ff..33d4fbfb62 100644 --- a/mm2src/adex_cli/src/adex_proc/adex_proc_impl.rs +++ b/mm2src/adex_cli/src/adex_proc/adex_proc_impl.rs @@ -10,7 +10,7 @@ use super::OrderbookConfig; use crate::activation_scheme_db::get_activation_scheme; use crate::adex_config::AdexConfig; use crate::transport::Transport; -use crate::{error_anyhow, error_bail, warn_anyhow, warn_bail}; +use crate::{error_anyhow, error_bail, warn_anyhow}; pub(crate) struct AdexProc<'trp, 'hand, 'cfg, T: Transport, H: ResponseHandler, C: AdexConfig + ?Sized> { pub(crate) transport: Option<&'trp T>, @@ -37,9 +37,7 @@ impl AdexProc<'_, '_, info!("Enabling asset: {asset}"); let activation_scheme = get_activation_scheme()?; - let Some(activation_method) = activation_scheme.get_activation_method(asset) else { - warn_bail!("Asset is not known: {asset}") - }; + let activation_method = activation_scheme.get_activation_method(asset)?; let enable = Command::builder() .flatten_data(activation_method) diff --git a/mm2src/adex_cli/src/main.rs b/mm2src/adex_cli/src/main.rs index 613597cb57..4a6da7bacf 100644 --- a/mm2src/adex_cli/src/main.rs +++ b/mm2src/adex_cli/src/main.rs @@ -5,6 +5,7 @@ #[cfg(not(target_arch = "wasm32"))] mod cli; #[cfg(not(target_arch = "wasm32"))] mod helpers; mod logging; +#[cfg(not(target_arch = "wasm32"))] mod rpc_data; #[cfg(not(target_arch = "wasm32"))] mod scenarios; #[cfg(all(not(target_arch = "wasm32"), test))] mod tests; #[cfg(not(target_arch = "wasm32"))] mod transport; diff --git a/mm2src/adex_cli/src/rpc_data.rs b/mm2src/adex_cli/src/rpc_data.rs new file mode 100644 index 0000000000..2f534e74c1 --- /dev/null +++ b/mm2src/adex_cli/src/rpc_data.rs @@ -0,0 +1,88 @@ +//! Contains rpc data layer structures that are not ready to become a part of the mm2_rpc::data module +//! +//! *Note: it's expected that the following data types will be moved to mm2_rpc::data when mm2 is refactored to be able to handle them* +//! + +use mm2_rpc::data::legacy::{ElectrumProtocol, GasStationPricePolicy, UtxoMergeParams}; +use serde::ser::SerializeSeq; +use serde::{Deserialize, Serialize, Serializer}; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "method", rename_all = "lowercase")] +pub(crate) enum ActivationRequest { + Enable(EnableRequest), + Electrum(ElectrumRequest), +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct EnableRequest { + coin: String, + #[serde(default, serialize_with = "serialize_urls", skip_serializing_if = "Vec::is_empty")] + urls: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + swap_contract_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + fallback_swap_contract: Option, + #[serde(skip_serializing_if = "Option::is_none")] + gas_station_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + gas_station_decimals: Option, + #[serde(skip_serializing_if = "Option::is_none")] + gas_station_policy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + mm2: Option, + #[serde(default)] + tx_history: bool, + #[serde(skip_serializing_if = "Option::is_none")] + required_confirmations: Option, + #[serde(skip_serializing_if = "Option::is_none")] + requires_notarization: Option, + #[serde(default)] + contract_supports_watchers: Option, +} + +fn serialize_urls(urls: &Vec, s: S) -> Result +where + S: Serializer, +{ + let mut s_seq = s.serialize_seq(None)?; + for url in urls { + s_seq.serialize_element(url.url.as_str())?; + } + s_seq.end() +} + +#[derive(Debug, Deserialize)] +struct EnableUrl { + url: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct ElectrumRequest { + coin: String, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub(super) servers: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + mm2: Option, + #[serde(default)] + tx_history: bool, + #[serde(skip_serializing_if = "Option::is_none")] + required_confirmations: Option, + #[serde(skip_serializing_if = "Option::is_none")] + requires_notarization: Option, + #[serde(skip_serializing_if = "Option::is_none")] + swap_contract_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + fallback_swap_contract: Option, + #[serde(skip_serializing_if = "Option::is_none")] + utxo_merge_params: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(super) struct Server { + url: String, + #[serde(default)] + protocol: ElectrumProtocol, + #[serde(default)] + disable_cert_verification: bool, +} diff --git a/mm2src/adex_cli/src/tests/mod.rs b/mm2src/adex_cli/src/tests/mod.rs index 1946a838ad..9777a268da 100644 --- a/mm2src/adex_cli/src/tests/mod.rs +++ b/mm2src/adex_cli/src/tests/mod.rs @@ -7,6 +7,7 @@ use crate::activation_scheme_db::{get_activation_scheme, get_activation_scheme_p use crate::adex_config::AdexConfigImpl; use crate::adex_proc::ResponseHandlerImpl; use crate::cli::Cli; +use crate::rpc_data::ActivationRequest; const FAKE_SERVER_COOLDOWN_TIMEOUT_MS: u64 = 10; const FAKE_SERVER_WARMUP_TIMEOUT_MS: u64 = 100; @@ -146,10 +147,10 @@ async fn test_activation_scheme() { init_activation_scheme().await.unwrap(); let scheme = get_activation_scheme().unwrap(); let kmd_scheme = scheme.get_activation_method("KMD"); - assert!(kmd_scheme.is_some()); - let kmd_scheme = kmd_scheme.unwrap(); - assert_eq!(kmd_scheme.get("method").unwrap().as_str().unwrap(), "electrum"); - assert_ne!(kmd_scheme.get("servers").unwrap().as_array().unwrap().iter().count(), 0); + let Ok(ActivationRequest::Electrum(electrum)) = kmd_scheme else { + panic!("Failed to get electrum scheme") + }; + assert_ne!(electrum.servers.len(), 0); } #[tokio::test] diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 59c5736358..8e6b762a4b 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -52,6 +52,7 @@ use mm2_err_handle::prelude::*; use mm2_net::transport::{slurp_url, GuiAuthValidation, GuiAuthValidationGenerator, SlurpError}; use mm2_number::bigdecimal_custom::CheckedDivision; use mm2_number::{BigDecimal, MmNumber}; +use mm2_rpc::data::legacy::GasStationPricePolicy; #[cfg(test)] use mocktopus::macros::*; use rand::seq::SliceRandom; use rpc::v1::types::Bytes as BytesJson; @@ -4247,7 +4248,7 @@ impl EthCoin { // TODO refactor to error_log_passthrough once simple maker bot is merged let gas_station_price = match &coin.gas_station_url { Some(url) => { - match GasStationData::get_gas_price(url, coin.gas_station_decimals, coin.gas_station_policy) + match GasStationData::get_gas_price(url, coin.gas_station_decimals, coin.gas_station_policy.clone()) .compat() .await { @@ -5000,21 +5001,6 @@ pub struct GasStationData { fast: MmNumber, } -/// Using tagged representation to allow adding variants with coefficients, percentage, etc in the future. -#[derive(Clone, Copy, Debug, Deserialize)] -#[serde(tag = "policy", content = "additional_data")] -pub enum GasStationPricePolicy { - /// Use mean between average and fast values, default and recommended to use on ETH mainnet due to - /// gas price big spikes. - MeanAverageFast, - /// Use average value only. Useful for non-heavily congested networks (Matic, etc.) - Average, -} - -impl Default for GasStationPricePolicy { - fn default() -> Self { GasStationPricePolicy::MeanAverageFast } -} - impl GasStationData { fn average_gwei(&self, decimals: u8, gas_price_policy: GasStationPricePolicy) -> NumConversResult { let gas_price = match gas_price_policy { diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index f3eda4d4ff..856af882c9 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -204,7 +204,7 @@ impl EthCoin { ticker, gas_station_url: self.gas_station_url.clone(), gas_station_decimals: self.gas_station_decimals, - gas_station_policy: self.gas_station_policy, + gas_station_policy: self.gas_station_policy.clone(), web3, web3_instances, history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 90faca52dd..84c4550047 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -67,6 +67,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; +use mm2_rpc::data::legacy::UtxoMergeParams; #[cfg(test)] use mocktopus::macros::*; use num_traits::ToPrimitive; use primitives::hash::{H160, H256, H264}; @@ -1341,15 +1342,6 @@ impl RpcTransportEventHandler for ElectrumProtoVerifier { } } -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct UtxoMergeParams { - pub merge_at: usize, - #[serde(default = "common::ten_f64")] - pub check_every: f64, - #[serde(default = "common::one_hundred")] - pub max_merge_at_once: usize, -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct UtxoActivationParams { pub mode: UtxoRpcMode, diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index 4ee1139a15..c4cb848dde 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -31,6 +31,7 @@ use keys::hash::H256; use keys::{Address, Type as ScriptType}; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, BigInt, MmNumber}; +use mm2_rpc::data::legacy::ElectrumProtocol; #[cfg(test)] use mocktopus::macros::*; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; use serde_json::{self as json, Value as Json}; @@ -1384,30 +1385,6 @@ pub fn electrum_script_hash(script: &[u8]) -> Vec { result } -#[allow(clippy::upper_case_acronyms)] -#[derive(Clone, Debug, Deserialize, Serialize)] -/// Deserializable Electrum protocol representation for RPC -pub enum ElectrumProtocol { - /// TCP - TCP, - /// SSL/TLS - SSL, - /// Insecure WebSocket. - WS, - /// Secure WebSocket. - WSS, -} - -#[cfg(not(target_arch = "wasm32"))] -impl Default for ElectrumProtocol { - fn default() -> Self { ElectrumProtocol::TCP } -} - -#[cfg(target_arch = "wasm32")] -impl Default for ElectrumProtocol { - fn default() -> Self { ElectrumProtocol::WS } -} - #[derive(Debug, Deserialize, Serialize)] /// Deserializable Electrum protocol version representation for RPC /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html#server.version diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 5d359dad9a..b28915eec9 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -32,7 +32,6 @@ use chain::{OutPoint, TransactionOutput}; use common::executor::Timer; use common::jsonrpc_client::JsonRpcErrorType; use common::log::{error, warn}; -use common::{one_hundred, ten_f64}; use crypto::{Bip32DerPathOps, Bip44Chain, RpcDerivationPath, StandardHDPath, StandardHDPathError}; use futures::compat::Future01CompatExt; use futures::future::{FutureExt, TryFutureExt}; @@ -86,15 +85,6 @@ lazy_static! { pub const HISTORY_TOO_LARGE_ERR_CODE: i64 = -1; -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct UtxoMergeParams { - merge_at: usize, - #[serde(default = "ten_f64")] - check_every: f64, - #[serde(default = "one_hundred")] - max_merge_at_once: usize, -} - pub async fn get_tx_fee(coin: &UtxoCoinFields) -> UtxoRpcResult { let conf = &coin.conf; match &coin.tx_fee { diff --git a/mm2src/mm2_rpc/src/data/legacy.rs b/mm2src/mm2_rpc/src/data/legacy.rs index 562f8c37a7..c16a328847 100644 --- a/mm2src/mm2_rpc/src/data/legacy.rs +++ b/mm2src/mm2_rpc/src/data/legacy.rs @@ -1,10 +1,19 @@ +#[path = "legacy/activation.rs"] mod activation; +#[path = "legacy/orders.rs"] mod orders; +#[path = "legacy/utility.rs"] mod utility; +#[path = "legacy/wallet.rs"] mod wallet; + +pub use activation::{eth::GasStationPricePolicy, + utxo::{ElectrumProtocol, UtxoMergeParams}, + CoinInitResponse, EnabledCoin, GetEnabledResponse}; +pub use orders::{AggregatedOrderbookEntry, MatchBy, OrderConfirmationsSettings, OrderType, OrderbookRequest, + OrderbookResponse, RpcOrderbookEntry, SellBuyRequest, SellBuyResponse, TakerAction, + TakerRequestForRpc}; +pub use utility::{MmVersionResponse, Status}; +pub use wallet::BalanceResponse; + use common::serde_derive::{Deserialize, Serialize}; -use derive_more::Display; -use mm2_number::{construct_detailed, BigDecimal, BigRational, Fraction, MmNumber}; -use rpc::v1::types::H256 as H256Json; -use std::collections::HashSet; use std::ops::Deref; -use uuid::Uuid; #[derive(Serialize, Deserialize)] pub struct Mm2RpcResult { @@ -19,239 +28,3 @@ impl Deref for Mm2RpcResult { type Target = T; fn deref(&self) -> &Self::Target { &self.result } } - -#[derive(Serialize, Deserialize)] -pub struct BalanceResponse { - pub coin: String, - pub balance: BigDecimal, - pub unspendable_balance: BigDecimal, - pub address: String, -} - -#[derive(Serialize, Deserialize)] -pub struct OrderbookRequest { - pub base: String, - pub rel: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct OrderbookResponse { - #[serde(rename = "askdepth")] - pub ask_depth: u32, - pub asks: Vec, - pub base: String, - #[serde(rename = "biddepth")] - pub bid_depth: u32, - pub bids: Vec, - pub netid: u16, - #[serde(rename = "numasks")] - pub num_asks: usize, - #[serde(rename = "numbids")] - pub num_bids: usize, - pub rel: String, - pub timestamp: u64, - #[serde(flatten)] - pub total_asks_base: TotalAsksBaseVol, - #[serde(flatten)] - pub total_asks_rel: TotalAsksRelVol, - #[serde(flatten)] - pub total_bids_base: TotalBidsBaseVol, - #[serde(flatten)] - pub total_bids_rel: TotalBidsRelVol, -} - -construct_detailed!(TotalAsksBaseVol, total_asks_base_vol); -construct_detailed!(TotalAsksRelVol, total_asks_rel_vol); -construct_detailed!(TotalBidsBaseVol, total_bids_base_vol); -construct_detailed!(TotalBidsRelVol, total_bids_rel_vol); - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RpcOrderbookEntry { - pub coin: String, - pub address: String, - pub price: BigDecimal, - pub price_rat: BigRational, - pub price_fraction: Fraction, - #[serde(rename = "maxvolume")] - pub max_volume: BigDecimal, - pub max_volume_rat: BigRational, - pub max_volume_fraction: Fraction, - pub min_volume: BigDecimal, - pub min_volume_rat: BigRational, - pub min_volume_fraction: Fraction, - pub pubkey: String, - pub age: u64, - pub uuid: Uuid, - pub is_mine: bool, - #[serde(flatten)] - pub base_max_volume: DetailedBaseMaxVolume, - #[serde(flatten)] - pub base_min_volume: DetailedBaseMinVolume, - #[serde(flatten)] - pub rel_max_volume: DetailedRelMaxVolume, - #[serde(flatten)] - pub rel_min_volume: DetailedRelMinVolume, - #[serde(flatten)] - pub conf_settings: Option, -} - -construct_detailed!(DetailedBaseMaxVolume, base_max_volume); -construct_detailed!(DetailedBaseMinVolume, base_min_volume); -construct_detailed!(DetailedRelMaxVolume, rel_max_volume); -construct_detailed!(DetailedRelMinVolume, rel_min_volume); - -#[derive(Debug, Serialize, Deserialize)] -pub struct AggregatedOrderbookEntry { - #[serde(flatten)] - pub entry: RpcOrderbookEntry, - #[serde(flatten)] - pub base_max_volume_aggr: AggregatedBaseVol, - #[serde(flatten)] - pub rel_max_volume_aggr: AggregatedRelVol, -} - -construct_detailed!(AggregatedBaseVol, base_max_volume_aggr); -construct_detailed!(AggregatedRelVol, rel_max_volume_aggr); - -#[derive(Deserialize, Serialize, Debug)] -pub struct SellBuyRequest { - pub base: String, - pub rel: String, - pub price: MmNumber, - pub volume: MmNumber, - pub timeout: Option, - /// Not used. Deprecated. - #[allow(dead_code)] - pub duration: Option, - pub method: String, - #[allow(dead_code)] - pub gui: Option, - #[serde(rename = "destpubkey")] - #[serde(default)] - #[allow(dead_code)] - pub dest_pub_key: H256Json, - #[serde(default)] - pub match_by: MatchBy, - #[serde(default)] - pub order_type: OrderType, - pub base_confs: Option, - pub base_nota: Option, - pub rel_confs: Option, - pub rel_nota: Option, - pub min_volume: Option, - #[serde(default = "get_true")] - pub save_in_history: bool, -} - -#[derive(Serialize, Deserialize)] -pub struct SellBuyResponse { - #[serde(flatten)] - pub request: TakerRequestForRpc, - pub order_type: OrderType, - #[serde(flatten)] - pub min_volume: DetailedMinVolume, - pub base_orderbook_ticker: Option, - pub rel_orderbook_ticker: Option, -} - -construct_detailed!(DetailedMinVolume, min_volume); - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TakerRequestForRpc { - pub base: String, - pub rel: String, - pub base_amount: BigDecimal, - pub base_amount_rat: BigRational, - pub rel_amount: BigDecimal, - pub rel_amount_rat: BigRational, - pub action: TakerAction, - pub uuid: Uuid, - pub method: String, - pub sender_pubkey: H256Json, - pub dest_pub_key: H256Json, - pub match_by: MatchBy, - pub conf_settings: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum TakerAction { - Buy, - Sell, -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", content = "data")] -pub enum OrderType { - FillOrKill, - GoodTillCancelled, -} - -impl Default for OrderType { - fn default() -> Self { OrderType::GoodTillCancelled } -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", content = "data")] -pub enum MatchBy { - Any, - Orders(HashSet), - Pubkeys(HashSet), -} - -impl Default for MatchBy { - fn default() -> Self { MatchBy::Any } -} - -#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct OrderConfirmationsSettings { - pub base_confs: u64, - pub base_nota: bool, - pub rel_confs: u64, - pub rel_nota: bool, -} - -impl OrderConfirmationsSettings { - pub fn reversed(&self) -> OrderConfirmationsSettings { - OrderConfirmationsSettings { - base_confs: self.rel_confs, - base_nota: self.rel_nota, - rel_confs: self.base_confs, - rel_nota: self.base_nota, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct CoinInitResponse { - pub result: String, - pub address: String, - pub balance: BigDecimal, - pub unspendable_balance: BigDecimal, - pub coin: String, - pub required_confirmations: u64, - pub requires_notarization: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub mature_confirmations: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct EnabledCoin { - pub ticker: String, - pub address: String, -} - -pub type GetEnabledResponse = Vec; - -#[derive(Serialize, Deserialize, Display)] -#[serde(rename_all = "lowercase")] -pub enum Status { - Success, -} - -#[derive(Serialize, Deserialize)] -pub struct MmVersionResponse { - pub result: String, - pub datetime: String, -} - -fn get_true() -> bool { true } diff --git a/mm2src/mm2_rpc/src/data/legacy/activation.rs b/mm2src/mm2_rpc/src/data/legacy/activation.rs new file mode 100644 index 0000000000..a43f531b38 --- /dev/null +++ b/mm2src/mm2_rpc/src/data/legacy/activation.rs @@ -0,0 +1,26 @@ +#[path = "activation/eth.rs"] pub mod eth; +#[path = "activation/utxo.rs"] pub mod utxo; + +use common::serde_derive::{Deserialize, Serialize}; +use mm2_number::BigDecimal; + +#[derive(Serialize, Deserialize)] +pub struct EnabledCoin { + pub ticker: String, + pub address: String, +} + +pub type GetEnabledResponse = Vec; + +#[derive(Debug, Serialize, Deserialize)] +pub struct CoinInitResponse { + pub result: String, + pub address: String, + pub balance: BigDecimal, + pub unspendable_balance: BigDecimal, + pub coin: String, + pub required_confirmations: u64, + pub requires_notarization: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub mature_confirmations: Option, +} diff --git a/mm2src/mm2_rpc/src/data/legacy/activation/eth.rs b/mm2src/mm2_rpc/src/data/legacy/activation/eth.rs new file mode 100644 index 0000000000..75146a9c2d --- /dev/null +++ b/mm2src/mm2_rpc/src/data/legacy/activation/eth.rs @@ -0,0 +1,16 @@ +use common::serde_derive::{Deserialize, Serialize}; + +/// Using tagged representation to allow adding variants with coefficients, percentage, etc in the future. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "policy", content = "additional_data")] +pub enum GasStationPricePolicy { + /// Use mean between average and fast values, default and recommended to use on ETH mainnet due to + /// gas price big spikes. + MeanAverageFast, + /// Use average value only. Useful for non-heavily congested networks (Matic, etc.) + Average, +} + +impl Default for GasStationPricePolicy { + fn default() -> Self { GasStationPricePolicy::MeanAverageFast } +} diff --git a/mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs b/mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs new file mode 100644 index 0000000000..ff7c685bd2 --- /dev/null +++ b/mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs @@ -0,0 +1,35 @@ +use common::serde_derive::{Deserialize, Serialize}; +use common::{one_hundred, ten_f64}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UtxoMergeParams { + pub merge_at: usize, + #[serde(default = "ten_f64")] + pub check_every: f64, + #[serde(default = "one_hundred")] + pub max_merge_at_once: usize, +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Clone, Debug, Deserialize, Serialize)] +/// Deserializable Electrum protocol representation for RPC +pub enum ElectrumProtocol { + /// TCP + TCP, + /// SSL/TLS + SSL, + /// Insecure WebSocket. + WS, + /// Secure WebSocket. + WSS, +} + +#[cfg(not(target_arch = "wasm32"))] +impl Default for ElectrumProtocol { + fn default() -> Self { ElectrumProtocol::TCP } +} + +#[cfg(target_arch = "wasm32")] +impl Default for ElectrumProtocol { + fn default() -> Self { ElectrumProtocol::WS } +} diff --git a/mm2src/mm2_rpc/src/data/legacy/orders.rs b/mm2src/mm2_rpc/src/data/legacy/orders.rs new file mode 100644 index 0000000000..8f50966da9 --- /dev/null +++ b/mm2src/mm2_rpc/src/data/legacy/orders.rs @@ -0,0 +1,200 @@ +use rpc::v1::types::H256 as H256Json; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use uuid::Uuid; + +use common::true_f; +use mm2_number::{construct_detailed, BigDecimal, BigRational, Fraction, MmNumber}; + +#[derive(Deserialize, Serialize, Debug)] +pub struct SellBuyRequest { + pub base: String, + pub rel: String, + pub price: MmNumber, + pub volume: MmNumber, + pub timeout: Option, + /// Not used. Deprecated. + #[allow(dead_code)] + pub duration: Option, + pub method: String, + #[allow(dead_code)] + pub gui: Option, + #[serde(rename = "destpubkey")] + #[serde(default)] + #[allow(dead_code)] + pub dest_pub_key: H256Json, + #[serde(default)] + pub match_by: MatchBy, + #[serde(default)] + pub order_type: OrderType, + pub base_confs: Option, + pub base_nota: Option, + pub rel_confs: Option, + pub rel_nota: Option, + pub min_volume: Option, + #[serde(default = "true_f")] + pub save_in_history: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct SellBuyResponse { + #[serde(flatten)] + pub request: TakerRequestForRpc, + pub order_type: OrderType, + #[serde(flatten)] + pub min_volume: DetailedMinVolume, + pub base_orderbook_ticker: Option, + pub rel_orderbook_ticker: Option, +} + +construct_detailed!(DetailedMinVolume, min_volume); + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TakerRequestForRpc { + pub base: String, + pub rel: String, + pub base_amount: BigDecimal, + pub base_amount_rat: BigRational, + pub rel_amount: BigDecimal, + pub rel_amount_rat: BigRational, + pub action: TakerAction, + pub uuid: Uuid, + pub method: String, + pub sender_pubkey: H256Json, + pub dest_pub_key: H256Json, + pub match_by: MatchBy, + pub conf_settings: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum TakerAction { + Buy, + Sell, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "data")] +pub enum OrderType { + FillOrKill, + GoodTillCancelled, +} + +impl Default for OrderType { + fn default() -> Self { OrderType::GoodTillCancelled } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "data")] +pub enum MatchBy { + Any, + Orders(HashSet), + Pubkeys(HashSet), +} + +impl Default for MatchBy { + fn default() -> Self { MatchBy::Any } +} + +#[derive(Serialize, Deserialize)] +pub struct OrderbookRequest { + pub base: String, + pub rel: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OrderbookResponse { + #[serde(rename = "askdepth")] + pub ask_depth: u32, + pub asks: Vec, + pub base: String, + #[serde(rename = "biddepth")] + pub bid_depth: u32, + pub bids: Vec, + pub netid: u16, + #[serde(rename = "numasks")] + pub num_asks: usize, + #[serde(rename = "numbids")] + pub num_bids: usize, + pub rel: String, + pub timestamp: u64, + #[serde(flatten)] + pub total_asks_base: TotalAsksBaseVol, + #[serde(flatten)] + pub total_asks_rel: TotalAsksRelVol, + #[serde(flatten)] + pub total_bids_base: TotalBidsBaseVol, + #[serde(flatten)] + pub total_bids_rel: TotalBidsRelVol, +} + +construct_detailed!(TotalAsksBaseVol, total_asks_base_vol); +construct_detailed!(TotalAsksRelVol, total_asks_rel_vol); +construct_detailed!(TotalBidsBaseVol, total_bids_base_vol); +construct_detailed!(TotalBidsRelVol, total_bids_rel_vol); + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RpcOrderbookEntry { + pub coin: String, + pub address: String, + pub price: BigDecimal, + pub price_rat: BigRational, + pub price_fraction: Fraction, + #[serde(rename = "maxvolume")] + pub max_volume: BigDecimal, + pub max_volume_rat: BigRational, + pub max_volume_fraction: Fraction, + pub min_volume: BigDecimal, + pub min_volume_rat: BigRational, + pub min_volume_fraction: Fraction, + pub pubkey: String, + pub age: u64, + pub uuid: Uuid, + pub is_mine: bool, + #[serde(flatten)] + pub base_max_volume: DetailedBaseMaxVolume, + #[serde(flatten)] + pub base_min_volume: DetailedBaseMinVolume, + #[serde(flatten)] + pub rel_max_volume: DetailedRelMaxVolume, + #[serde(flatten)] + pub rel_min_volume: DetailedRelMinVolume, + #[serde(flatten)] + pub conf_settings: Option, +} + +construct_detailed!(DetailedBaseMaxVolume, base_max_volume); +construct_detailed!(DetailedBaseMinVolume, base_min_volume); +construct_detailed!(DetailedRelMaxVolume, rel_max_volume); +construct_detailed!(DetailedRelMinVolume, rel_min_volume); + +#[derive(Debug, Serialize, Deserialize)] +pub struct AggregatedOrderbookEntry { + #[serde(flatten)] + pub entry: RpcOrderbookEntry, + #[serde(flatten)] + pub base_max_volume_aggr: AggregatedBaseVol, + #[serde(flatten)] + pub rel_max_volume_aggr: AggregatedRelVol, +} + +construct_detailed!(AggregatedBaseVol, base_max_volume_aggr); +construct_detailed!(AggregatedRelVol, rel_max_volume_aggr); + +#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct OrderConfirmationsSettings { + pub base_confs: u64, + pub base_nota: bool, + pub rel_confs: u64, + pub rel_nota: bool, +} + +impl OrderConfirmationsSettings { + pub fn reversed(&self) -> OrderConfirmationsSettings { + OrderConfirmationsSettings { + base_confs: self.rel_confs, + base_nota: self.rel_nota, + rel_confs: self.base_confs, + rel_nota: self.base_nota, + } + } +} diff --git a/mm2src/mm2_rpc/src/data/legacy/utility.rs b/mm2src/mm2_rpc/src/data/legacy/utility.rs new file mode 100644 index 0000000000..b6cef85f7c --- /dev/null +++ b/mm2src/mm2_rpc/src/data/legacy/utility.rs @@ -0,0 +1,14 @@ +use derive_more::Display; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Display)] +#[serde(rename_all = "lowercase")] +pub enum Status { + Success, +} + +#[derive(Serialize, Deserialize)] +pub struct MmVersionResponse { + pub result: String, + pub datetime: String, +} diff --git a/mm2src/mm2_rpc/src/data/legacy/wallet.rs b/mm2src/mm2_rpc/src/data/legacy/wallet.rs new file mode 100644 index 0000000000..ddcb5c2354 --- /dev/null +++ b/mm2src/mm2_rpc/src/data/legacy/wallet.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +use mm2_number::BigDecimal; + +#[derive(Serialize, Deserialize)] +pub struct BalanceResponse { + pub coin: String, + pub balance: BigDecimal, + pub unspendable_balance: BigDecimal, + pub address: String, +} diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index b83211556a..4004cde892 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -14,7 +14,7 @@ use lazy_static::lazy_static; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_metrics::{MetricType, MetricsJson}; use mm2_number::BigDecimal; -use mm2_rpc::data::legacy::BalanceResponse; +use mm2_rpc::data::legacy::{BalanceResponse, ElectrumProtocol}; use rand::Rng; use serde::{Deserialize, Serialize}; use serde_json::{self as json, json, Value as Json}; @@ -1647,19 +1647,6 @@ pub async fn enable_slp(mm: &MarketMakerIt, coin: &str) -> Json { json::from_str(&enable.1).unwrap() } -#[allow(clippy::upper_case_acronyms)] -#[derive(Serialize)] -pub enum ElectrumProtocol { - /// TCP - TCP, - /// SSL/TLS - SSL, - /// Insecure WebSocket. - WS, - /// Secure WebSocket. - WSS, -} - #[derive(Serialize)] pub struct ElectrumRpcRequest { pub url: String, From 7b2955386683e925ccff951a4ee8d9f52902fa34 Mon Sep 17 00:00:00 2001 From: John Nash Date: Wed, 2 Aug 2023 11:15:45 +1000 Subject: [PATCH 03/17] fix(posv): fix missing n_time in posv transactions (#1925) This commit fixes PoSV coins withdrawal issue. The issue was a missing n_time field in the generated transaction. The fix now correctly considers when n_time is required, and the rawtransaction can be broadcasted. --- mm2src/coins/utxo.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 84c4550047..0783d6cdec 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -778,7 +778,11 @@ impl UtxoCoinFields { None }; - let n_time = if self.conf.is_pos { Some(now_sec_u32()) } else { None }; + let n_time = if self.conf.is_pos || self.conf.is_posv { + Some(now_sec_u32()) + } else { + None + }; TransactionInputSigner { version: self.conf.tx_version, From e333635e56251ddf3adc7c7195f6e6b2aac17380 Mon Sep 17 00:00:00 2001 From: Onur Date: Wed, 2 Aug 2023 21:33:42 +0300 Subject: [PATCH 04/17] fix(ibc-test): use latest relayer channel for tendermint test (#1929) Signed-off-by: ozkanonur --- mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs index 7b5e33fe8b..05e89f74fb 100644 --- a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs @@ -34,7 +34,7 @@ fn test_tendermint_activation_and_balance() { let result: RpcV2Response = json::from_value(activation_result).unwrap(); assert_eq!(result.result.address, expected_address); - let expected_balance: BigDecimal = "8.0959".parse().unwrap(); + let expected_balance: BigDecimal = "0.575457".parse().unwrap(); assert_eq!(result.result.balance.unwrap().spendable, expected_balance); let my_balance = block_on(my_balance(&mm, ATOM_TICKER)); @@ -179,7 +179,9 @@ fn test_custom_gas_limit_on_tendermint_withdraw() { #[test] fn test_tendermint_ibc_withdraw() { - const IBC_SOURCE_CHANNEL: &str = "channel-81"; + // visit `{rpc_url}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels + const IBC_SOURCE_CHANNEL: &str = "channel-93"; + const IBC_TARGET_ADDRESS: &str = "cosmos1r5v5srda7xfth3hn2s26txvrcrntldjumt8mhl"; const MY_ADDRESS: &str = "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2"; From 867a01ab04a57bab93ed52adf5729d165a43698a Mon Sep 17 00:00:00 2001 From: Rozhkov Dmitrii Date: Thu, 3 Aug 2023 04:19:06 +0500 Subject: [PATCH 05/17] feat(adex-cli): add support for https connection (#1910) --- mm2src/adex_cli/Cargo.lock | 75 ++++++++ mm2src/adex_cli/Cargo.toml | 6 +- mm2src/adex_cli/src/transport.rs | 63 ++++++- mm2src/mm2_net/src/native_http.rs | 280 ++++++++++++++++++++++-------- 4 files changed, 343 insertions(+), 81 deletions(-) diff --git a/mm2src/adex_cli/Cargo.lock b/mm2src/adex_cli/Cargo.lock index 37367f7ed8..26fd951793 100644 --- a/mm2src/adex_cli/Cargo.lock +++ b/mm2src/adex_cli/Cargo.lock @@ -24,6 +24,8 @@ dependencies = [ "env_logger 0.7.1", "gstuff", "http 0.2.9", + "hyper", + "hyper-rustls", "inquire", "itertools", "log 0.4.17", @@ -32,6 +34,7 @@ dependencies = [ "mm2_rpc", "passwords", "rpc", + "rustls 0.20.8", "serde", "serde_json", "sysinfo", @@ -560,6 +563,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -1328,7 +1341,9 @@ checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http 0.2.9", "hyper", + "log 0.4.17", "rustls 0.20.8", + "rustls-native-certs", "tokio", "tokio-rustls", "webpki-roots", @@ -1944,6 +1959,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "option-ext" version = "0.2.0" @@ -2637,17 +2658,48 @@ version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ + "log 0.4.17", "ring", "sct 0.7.0", "webpki 0.22.0", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + [[package]] name = "ryu" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2738,6 +2790,29 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.9.0" diff --git a/mm2src/adex_cli/Cargo.toml b/mm2src/adex_cli/Cargo.toml index 9af19a3ab0..ec0e0e5283 100644 --- a/mm2src/adex_cli/Cargo.toml +++ b/mm2src/adex_cli/Cargo.toml @@ -15,6 +15,8 @@ derive_more = "0.99" directories = "5.0" env_logger = "0.7.1" http = "0.2" +hyper = { version = "0.14.26", features = ["client", "http2", "tcp"] } +hyper-rustls = "^0.23.0" gstuff = { version = "=0.7.4" , features = [ "nightly" ]} inquire = "0.6" itertools = "0.10" @@ -23,14 +25,14 @@ mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } mm2_rpc = { path = "../mm2_rpc"} passwords = "3.1" +rpc = { path = "../mm2_bitcoin/rpc" } +rustls = { version = "^0.20.4", 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", features = [ "macros" ] } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } -rpc = { path = "../mm2_bitcoin/rpc" } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.3", features = ["processthreadsapi", "winnt"] } - diff --git a/mm2src/adex_cli/src/transport.rs b/mm2src/adex_cli/src/transport.rs index f2726908fd..92e28c0655 100644 --- a/mm2src/adex_cli/src/transport.rs +++ b/mm2src/adex_cli/src/transport.rs @@ -1,10 +1,12 @@ use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; use http::{HeaderMap, StatusCode}; -use log::{error, warn}; -use mm2_net::native_http::slurp_post_json; use serde::{Deserialize, Serialize}; +use common::log::{error, warn}; +use hyper_dangerous::get_hyper_client_dangerous; +use mm2_net::native_http::SlurpHttpClient; + use crate::{error_anyhow, error_bail, warn_bail}; #[async_trait] @@ -32,8 +34,10 @@ impl Transport for SlurpTransport { OkT: for<'a> Deserialize<'a>, ErrT: for<'a> Deserialize<'a>, { - let data = serde_json::to_string(&req).expect("Failed to serialize enable request"); - match slurp_post_json(&self.rpc_uri, data).await { + let data = serde_json::to_string(&req) + .map_err(|error| error_anyhow!("Failed to serialize data being sent: {error}"))?; + let client = get_hyper_client_dangerous()?; + match client.slurp_post_json(&self.rpc_uri, data).await { Err(error) => error_bail!("Failed to send json: {error}"), Ok(resp) => resp.process::(), } @@ -78,3 +82,54 @@ impl Response for (StatusCode, HeaderMap, Vec) { } } } + +mod hyper_dangerous { + + use hyper::{client::HttpConnector, Body, Client}; + use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; + use rustls::client::{ServerCertVerified, ServerCertVerifier}; + use rustls::{RootCertStore, DEFAULT_CIPHER_SUITES, DEFAULT_VERSIONS}; + use std::sync::Arc; + use std::time::SystemTime; + + use super::*; + + pub(super) fn get_hyper_client_dangerous() -> Result>> { + let mut config = rustls::ClientConfig::builder() + .with_cipher_suites(DEFAULT_CIPHER_SUITES) + .with_safe_default_kx_groups() + .with_protocol_versions(DEFAULT_VERSIONS) + .map_err(|error| error_anyhow!("Inconsistent cipher-suite/versions selected: {error}"))? + .with_root_certificates(RootCertStore::empty()) + .with_no_client_auth(); + + config + .dangerous() + .set_certificate_verifier(Arc::new(NoCertificateVerification {})); + + let https_connector = HttpsConnectorBuilder::default() + .with_tls_config(config) + .https_or_http() + .enable_http1() + .enable_http2() + .build(); + + Ok(Client::builder().build::<_, Body>(https_connector)) + } + + struct NoCertificateVerification {} + + impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _: &rustls::Certificate, + _: &[rustls::Certificate], + _: &rustls::ServerName, + _: &mut dyn Iterator, + _: &[u8], + _: SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + } +} diff --git a/mm2src/mm2_net/src/native_http.rs b/mm2src/mm2_net/src/native_http.rs index 700bdb1efa..9a79611835 100644 --- a/mm2src/mm2_net/src/native_http.rs +++ b/mm2src/mm2_net/src/native_http.rs @@ -1,12 +1,212 @@ -use crate::transport::{SlurpError, SlurpResult, SlurpResultJson}; -use common::wio::{drive03, HYPER}; -use common::APPLICATION_JSON; +//! Facilitates execution of http requests +//! +//! # Layout +//! +//! This module contains several service functions like [slurp_post_json] or [slurp_url_with_headers] +//! for executing http protocol requests, allowing you to set certain headers, make a [`Request`] in JSON or [`Body`] format. +//! +//! These methods are wrappers over [`HYPER`], which is actually `Client>` that implements +//! [`SlurpHttpClient`] trait designed to provide http capabilities through it. +//! +//! There are also facilities for constructing [SlurpError] from the [hyper::Error] +//! + +use async_trait::async_trait; use futures::channel::oneshot::Canceled; use http::{header, HeaderValue, Request}; -use hyper::Body; -use mm2_err_handle::prelude::*; +use hyper::client::connect::Connect; +use hyper::client::ResponseFuture; +use hyper::{Body, Client}; use serde_json::Value as Json; +use common::wio::{drive03, HYPER}; +use common::APPLICATION_JSON; +use mm2_err_handle::prelude::*; + +use super::transport::{SlurpError, SlurpResult, SlurpResultJson}; + +/// Provides requesting http through it +/// +/// Initially designed to be used with [hyper::Client] that could be constructed in different specific ways. +/// one of which is using with statically defined [HYPER] that is common client able to request https or https urls +/// In the other case it can be a dangerous client that does not verify self signed signature +/// +/// # Examples +/// +/// Request over both http or https using common [hyper_rustls::HttpsConnectorBuilder] +/// +/// ```rust +/// let https = HttpsConnectorBuilder::new() +/// .with_webpki_roots() +/// .https_or_http() +/// .enable_http1() +/// .enable_http2() +/// .build(); +/// let client = Client::builder().pool_max_idle_per_host(0).build(https) +/// client.slurp_url(`https://komodoproject.com`) +/// ``` +/// +/// Request over https with self-signed certificate +/// +/// ```rust +/// let data = serde_json::to_string(&req).map_err(|error| error_anyhow!("Failed to serialize data being sent: {error}"))?; +/// match HYPER_DANGEROUS.slurp_post_json(&self.rpc_uri, data).await { +/// Err(error) => error_bail!("Failed to send json: {error}"), +/// Ok(resp) => resp.process::(), +/// } +/// +/// mod hyper_dangerous { +/// use hyper::{client::HttpConnector, Body, Client}; +/// use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +/// use lazy_static::lazy_static; +/// use rustls::client::{ServerCertVerified, ServerCertVerifier}; +/// use rustls::{RootCertStore, DEFAULT_CIPHER_SUITES, DEFAULT_VERSIONS}; +/// use std::sync::Arc; +/// use std::time::SystemTime; +/// +/// lazy_static! { +/// pub(super) static ref HYPER_DANGEROUS: Client> = get_hyper_client_dangerous(); +/// } +/// +/// fn get_hyper_client_dangerous() -> Client> { +/// let mut config = rustls::ClientConfig::builder() +/// .with_cipher_suites(&DEFAULT_CIPHER_SUITES) +/// .with_safe_default_kx_groups() +/// .with_protocol_versions(&DEFAULT_VERSIONS) +/// .expect("inconsistent cipher-suite/versions selected") +/// .with_root_certificates(RootCertStore::empty()) +/// .with_no_client_auth(); +/// +/// config +/// .dangerous() +/// .set_certificate_verifier(Arc::new(NoCertificateVerification {})); +/// +/// let https_connector = HttpsConnectorBuilder::default() +/// .with_tls_config(config) +/// .https_or_http() +/// .enable_http1() +/// .build(); +/// +/// Client::builder().build::<_, Body>(https_connector) +/// } +/// +/// struct NoCertificateVerification {} +/// +/// impl ServerCertVerifier for NoCertificateVerification { +/// fn verify_server_cert( +/// &self, +/// _: &rustls::Certificate, +/// _: &[rustls::Certificate], +/// _: &rustls::ServerName, +/// _: &mut dyn Iterator, +/// _: &[u8], +/// _: SystemTime, +/// ) -> Result { +/// Ok(ServerCertVerified::assertion()) +/// } +/// } +/// } +/// ``` +#[async_trait] +pub trait SlurpHttpClient { + /// Provides a [ResponseFuture] that could be spawned and processed asynchronously + fn request(&self, req: Request) -> ResponseFuture; + + /// Executes a POST request, returning the response status, headers and body. + async fn slurp_post_json(&self, url: &str, body: String) -> SlurpResult { + let request = Request::builder() + .method("POST") + .uri(url) + .header(header::CONTENT_TYPE, APPLICATION_JSON) + .body(body.into())?; + self.slurp_req(request).await + } + + /// Executes a GET request, returning the response status, headers and body. + async fn slurp_url(&self, url: &str) -> SlurpResult { + let req = Request::builder().uri(url).body(Vec::new())?; + self.slurp_req(req).await + } + + /// Executes a GET request with additional headers. + /// Returning the response status, headers and body. + async fn slurp_url_with_headers(&self, url: &str, headers: Vec<(&'static str, &'static str)>) -> SlurpResult { + let mut req = Request::builder(); + let h = req + .headers_mut() + .or_mm_err(|| SlurpError::Internal("An error occurred when accessing the request headers".to_string()))?; + + for (key, value) in headers { + h.insert(key, HeaderValue::from_static(value)); + } + + let req = req.uri(url).body(Vec::new())?; + self.slurp_req(req).await + } + + /// Executes a Hyper request, requires [`Request`] and return the response status, headers and body as Json. + async fn slurp_req_body(&self, request: Request) -> SlurpResultJson { + let uri = request.uri().to_string(); + + let request_f = self.request(request); + let response = drive03(request_f) + .await? + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + let status = response.status(); + let headers = response.headers().clone(); + let body_bytes = hyper::body::to_bytes(response.into_body()) + .await + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + let body_str = String::from_utf8(body_bytes.to_vec()).map_to_mm(|e| SlurpError::Internal(e.to_string()))?; + let body: Json = serde_json::from_str(&body_str)?; + Ok((status, headers, body)) + } + + /// Executes a Hyper request, returning the response status, headers and body. + async fn slurp_req(&self, request: Request>) -> SlurpResult { + let uri = request.uri().to_string(); + let (head, body) = request.into_parts(); + let request = Request::from_parts(head, Body::from(body)); + let request_f = self.request(request); + let response = drive03(request_f) + .await? + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + let status = response.status(); + let headers = response.headers().clone(); + let body = response.into_body(); + let output = hyper::body::to_bytes(body) + .await + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + Ok((status, headers, output.to_vec())) + } +} + +#[async_trait] +impl SlurpHttpClient for Client +where + C: Connect + Clone + Send + Sync + 'static, +{ + fn request(&self, req: Request) -> ResponseFuture { Client::::request(self, req) } +} + +/// Executes a Hyper request, returning the response status, headers and body. +pub async fn slurp_req(request: Request>) -> SlurpResult { HYPER.slurp_req(request).await } + +/// Executes a Hyper request, requires [`Request`] and return the response status, headers and body as Json. +pub async fn slurp_req_body(request: Request) -> SlurpResultJson { HYPER.slurp_req_body(request).await } + +/// Executes a GET request, returning the response status, headers and body. +pub async fn slurp_url(url: &str) -> SlurpResult { HYPER.slurp_url(url).await } + +/// Executes a GET request with additional headers. +/// Returning the response status, headers and body. +pub async fn slurp_url_with_headers(url: &str, headers: Vec<(&'static str, &'static str)>) -> SlurpResult { + HYPER.slurp_url_with_headers(url, headers).await +} + +/// Executes a POST request, returning the response status, headers and body. +pub async fn slurp_post_json(url: &str, body: String) -> SlurpResult { HYPER.slurp_post_json(url, body).await } + impl From for SlurpError { fn from(_: Canceled) -> Self { SlurpError::Internal("Spawned Slurp future has been canceled".to_owned()) } } @@ -31,76 +231,6 @@ impl From for SlurpError { fn from(e: http::Error) -> Self { SlurpError::InvalidRequest(e.to_string()) } } -/// Executes a Hyper request, returning the response status, headers and body. -pub async fn slurp_req(request: Request>) -> SlurpResult { - let uri = request.uri().to_string(); - let (head, body) = request.into_parts(); - let request = Request::from_parts(head, Body::from(body)); - - let request_f = HYPER.request(request); - let response = drive03(request_f) - .await? - .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; - let status = response.status(); - let headers = response.headers().clone(); - let body = response.into_body(); - let output = hyper::body::to_bytes(body) - .await - .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; - Ok((status, headers, output.to_vec())) -} - -/// Executes a Hyper request, requires [`Request`] and return the response status, headers and body as Json. -pub async fn slurp_req_body(request: Request) -> SlurpResultJson { - let uri = request.uri().to_string(); - - let request_f = HYPER.request(request); - let response = drive03(request_f) - .await? - .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; - let status = response.status(); - let headers = response.headers().clone(); - // Get the response body bytes. - let body_bytes = hyper::body::to_bytes(response.into_body()) - .await - .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; - let body_str = String::from_utf8(body_bytes.to_vec()).map_to_mm(|e| SlurpError::Internal(e.to_string()))?; - let body: Json = serde_json::from_str(&body_str)?; - Ok((status, headers, body)) -} - -/// Executes a GET request, returning the response status, headers and body. -pub async fn slurp_url(url: &str) -> SlurpResult { - let req = Request::builder().uri(url).body(Vec::new())?; - slurp_req(req).await -} - -/// Executes a GET request with additional headers. -/// Returning the response status, headers and body. -pub async fn slurp_url_with_headers(url: &str, headers: Vec<(&'static str, &'static str)>) -> SlurpResult { - let mut req = Request::builder(); - let h = req - .headers_mut() - .or_mm_err(|| SlurpError::Internal("An error occured while accessing to the request headers.".to_string()))?; - - for (key, value) in headers { - h.insert(key, HeaderValue::from_static(value)); - } - - let req = req.uri(url).body(Vec::new())?; - slurp_req(req).await -} - -/// Executes a POST request, returning the response status, headers and body. -pub async fn slurp_post_json(url: &str, body: String) -> SlurpResult { - let request = Request::builder() - .method("POST") - .uri(url) - .header(header::CONTENT_TYPE, APPLICATION_JSON) - .body(body.into())?; - slurp_req(request).await -} - #[cfg(test)] mod tests { use crate::native_http::slurp_url; From 54dce3c6d8cdb2231539a45e5a995049297abb49 Mon Sep 17 00:00:00 2001 From: smk762 <35845239+smk762@users.noreply.github.com> Date: Fri, 4 Aug 2023 07:02:06 +0800 Subject: [PATCH 06/17] fix(endpoints): update prices url (#1928) --- mm2src/coins/lp_price.rs | 2 +- mm2src/mm2_main/src/lp_ordermatch/simple_market_maker.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mm2src/coins/lp_price.rs b/mm2src/coins/lp_price.rs index 1960aa7b16..c0834c2efe 100644 --- a/mm2src/coins/lp_price.rs +++ b/mm2src/coins/lp_price.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use std::str::Utf8Error; const PRICE_ENDPOINTS: [&str; 2] = [ - "https://prices.komodo.earth:1313/api/v2/tickers", + "https://prices.komodo.earth/api/v2/tickers", "https://prices.cipig.net:1717/api/v2/tickers", ]; diff --git a/mm2src/mm2_main/src/lp_ordermatch/simple_market_maker.rs b/mm2src/mm2_main/src/lp_ordermatch/simple_market_maker.rs index bec6262a7a..4fde30b8ee 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/simple_market_maker.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/simple_market_maker.rs @@ -23,7 +23,7 @@ use std::collections::{HashMap, HashSet}; use uuid::Uuid; // !< constants -pub const KMD_PRICE_ENDPOINT: &str = "https://prices.komodo.earth:1313/api/v2/tickers"; +pub const KMD_PRICE_ENDPOINT: &str = "https://prices.komodo.earth/api/v2/tickers"; pub const BOT_DEFAULT_REFRESH_RATE: f64 = 30.0; pub const PRECISION_FOR_NOTIFICATION: u64 = 8; const LATEST_SWAPS_LIMIT: usize = 1000; From 92372cbf73422a4c65e6c65d3a9f7b744f6dbc76 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Mon, 7 Aug 2023 06:32:54 +0300 Subject: [PATCH 07/17] fix(nft): add log_index to history table and use in PK (#1926) - This commmit fixes transactions that transfer multiple NFT tokens in db. These transactions cause errors when adding due to the db constraint on tx hash uniqueness. To solve this, the PR uses log_index as part of the transfers history table primary key. - nft_tx_history table is now called nft_transfer_history and tx/txs are renamed to transfer/transfers throughout the NFT code since what's added/retrieved from DB is NFT transfers not transactions (Multiple NFT transfers can be in one transaction). By renaming the table, there are no need for db migrations due to the addition of log_index column. Although NFT is not used in production yet, if anybody used it, transfers will be re-fetched and saved to the new DB table when using the related API methods. --- mm2src/coins/nft.rs | 231 ++++++++-------- mm2src/coins/nft/nft_structs.rs | 18 +- mm2src/coins/nft/nft_tests.rs | 42 +-- mm2src/coins/nft/storage/db_test_helpers.rs | 248 ++++++++++++------ mm2src/coins/nft/storage/mod.rs | 41 +-- mm2src/coins/nft/storage/sql_storage.rs | 208 ++++++++------- mm2src/coins/nft/storage/wasm/nft_idb.rs | 4 +- mm2src/coins/nft/storage/wasm/wasm_storage.rs | 221 +++++++++------- 8 files changed, 584 insertions(+), 429 deletions(-) diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 3d84103f3f..e679947517 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -11,14 +11,14 @@ pub(crate) mod storage; use crate::{get_my_address, MyAddressReq, WithdrawError}; use nft_errors::{GetInfoFromUriError, GetNftInfoError, UpdateNftError}; use nft_structs::{Chain, ContractType, ConvertChain, Nft, NftFromMoralis, NftList, NftListReq, NftMetadataReq, - NftTransferHistory, NftTransfersReq, NftTxHistoryFromMoralis, NftsTransferHistoryList, + NftTransferHistory, NftTransferHistoryFromMoralis, NftTransfersReq, NftsTransferHistoryList, TransactionNftDetails, UpdateNftReq, WithdrawNftReq}; use crate::eth::{eth_addr_to_hex, get_eth_address, withdraw_erc1155, withdraw_erc721}; use crate::nft::nft_errors::ProtectFromSpamError; -use crate::nft::nft_structs::{NftCommon, NftCtx, NftTransferCommon, RefreshMetadataReq, TransferStatus, TxMeta, +use crate::nft::nft_structs::{NftCommon, NftCtx, NftTransferCommon, RefreshMetadataReq, TransferMeta, TransferStatus, UriMeta}; -use crate::nft::storage::{NftListStorageOps, NftStorageBuilder, NftTxHistoryStorageOps}; +use crate::nft::storage::{NftListStorageOps, NftStorageBuilder, NftTransferHistoryStorageOps}; use common::{parse_rfc3339_to_timestamp, APPLICATION_JSON}; use ethereum_types::Address; use http::header::ACCEPT; @@ -91,16 +91,16 @@ pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult MmResult<(), UpdateNft let storage = NftStorageBuilder::new(&ctx).build()?; for chain in req.chains.iter() { - let tx_history_initialized = NftTxHistoryStorageOps::is_initialized(&storage, chain).await?; + let transfer_history_initialized = NftTransferHistoryStorageOps::is_initialized(&storage, chain).await?; - let from_block = if tx_history_initialized { - let last_tx_block = NftTxHistoryStorageOps::get_last_block_number(&storage, chain).await?; - last_tx_block.map(|b| b + 1) + let from_block = if transfer_history_initialized { + let last_transfer_block = NftTransferHistoryStorageOps::get_last_block_number(&storage, chain).await?; + last_transfer_block.map(|b| b + 1) } else { - NftTxHistoryStorageOps::init(&storage, chain).await?; + NftTransferHistoryStorageOps::init(&storage, chain).await?; None }; let nft_transfers = get_moralis_nft_transfers(&ctx, chain, from_block, &req.url).await?; - storage.add_txs_to_history(chain, nft_transfers).await?; + storage.add_transfers_to_history(chain, nft_transfers).await?; let nft_block = match NftListStorageOps::get_last_block_number(&storage, chain).await { Ok(Some(block)) => block, Ok(None) => { // if there are no rows in NFT LIST table we can try to get all info from moralis. let nfts = cache_nfts_from_moralis(&ctx, &storage, chain, &req.url).await?; - update_meta_in_txs(&storage, chain, nfts).await?; - update_txs_with_empty_meta(&storage, chain, &req.url).await?; + update_meta_in_transfers(&storage, chain, nfts).await?; + update_transfers_with_empty_meta(&storage, chain, &req.url).await?; continue; }, Err(_) => { // if there is an error, then NFT LIST table doesnt exist, so we need to cache from mroalis. NftListStorageOps::init(&storage, chain).await?; let nft_list = get_moralis_nft_list(&ctx, chain, &req.url).await?; - let last_scanned_block = NftTxHistoryStorageOps::get_last_block_number(&storage, chain) + let last_scanned_block = NftTransferHistoryStorageOps::get_last_block_number(&storage, chain) .await? .unwrap_or(0); storage .add_nfts_to_list(chain, nft_list.clone(), last_scanned_block) .await?; - update_meta_in_txs(&storage, chain, nft_list).await?; - update_txs_with_empty_meta(&storage, chain, &req.url).await?; + update_meta_in_transfers(&storage, chain, nft_list).await?; + update_transfers_with_empty_meta(&storage, chain, &req.url).await?; continue; }, }; @@ -166,7 +166,7 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft }); } update_nft_list(ctx.clone(), &storage, chain, scanned_block + 1, &req.url).await?; - update_txs_with_empty_meta(&storage, chain, &req.url).await?; + update_transfers_with_empty_meta(&storage, chain, &req.url).await?; } Ok(()) } @@ -204,8 +204,10 @@ pub async fn refresh_nft_metadata(ctx: MmArc, req: RefreshMetadataReq) -> MmResu storage .refresh_nft_metadata(&moralis_meta.chain, nft_db.clone()) .await?; - let tx_meta = TxMeta::from(nft_db.clone()); - storage.update_txs_meta_by_token_addr_id(&nft_db.chain, tx_meta).await?; + let transfer_meta = TransferMeta::from(nft_db.clone()); + storage + .update_transfers_meta_by_token_addr_id(&nft_db.chain, transfer_meta) + .await?; Ok(()) } @@ -292,12 +294,13 @@ async fn get_moralis_nft_transfers( let response = send_request_to_uri(uri.as_str()).await?; if let Some(transfer_list) = response["result"].as_array() { for transfer in transfer_list { - let transfer_moralis: NftTxHistoryFromMoralis = serde_json::from_str(&transfer.to_string())?; + let transfer_moralis: NftTransferHistoryFromMoralis = serde_json::from_str(&transfer.to_string())?; let contract_type = match transfer_moralis.contract_type { Some(contract_type) => contract_type, None => continue, }; - let status = get_tx_status(&wallet_address, ð_addr_to_hex(&transfer_moralis.common.to_address)); + let status = + get_transfer_status(&wallet_address, ð_addr_to_hex(&transfer_moralis.common.to_address)); let block_timestamp = parse_rfc3339_to_timestamp(&transfer_moralis.block_timestamp)?; let transfer_history = NftTransferHistory { common: NftTransferCommon { @@ -477,8 +480,8 @@ async fn get_uri_meta(token_uri: Option<&str>, metadata: Option<&str>) -> UriMet uri_meta } -fn get_tx_status(my_wallet: &str, to_address: &str) -> TransferStatus { - // if my_wallet == from_address && my_wallet == to_address it is incoming tx, so we can check just to_address. +fn get_transfer_status(my_wallet: &str, to_address: &str) -> TransferStatus { + // if my_wallet == from_address && my_wallet == to_address it is incoming transfer, so we can check just to_address. if my_wallet.to_lowercase() == to_address.to_lowercase() { TransferStatus::Receive } else { @@ -488,96 +491,98 @@ fn get_tx_status(my_wallet: &str, to_address: &str) -> TransferStatus { /// `update_nft_list` function gets nft transfers from NFT HISTORY table, iterates through them /// and updates NFT LIST table info. -async fn update_nft_list( +async fn update_nft_list( ctx: MmArc, storage: &T, chain: &Chain, scan_from_block: u64, url: &Url, ) -> MmResult<(), UpdateNftError> { - let txs = storage.get_txs_from_block(chain, scan_from_block).await?; + let transfers = storage.get_transfers_from_block(chain, scan_from_block).await?; let req = MyAddressReq { coin: chain.to_ticker(), }; let my_address = get_my_address(ctx.clone(), req).await?.wallet_address.to_lowercase(); - for tx in txs.into_iter() { - handle_nft_tx(storage, chain, url, tx, &my_address).await?; + for transfer in transfers.into_iter() { + handle_nft_transfer(storage, chain, url, transfer, &my_address).await?; } Ok(()) } -async fn handle_nft_tx( +async fn handle_nft_transfer( storage: &T, chain: &Chain, url: &Url, - tx: NftTransferHistory, + transfer: NftTransferHistory, my_address: &str, ) -> MmResult<(), UpdateNftError> { - match (tx.status, tx.contract_type) { - (TransferStatus::Send, ContractType::Erc721) => handle_send_erc721(storage, chain, tx).await, + match (transfer.status, transfer.contract_type) { + (TransferStatus::Send, ContractType::Erc721) => handle_send_erc721(storage, chain, transfer).await, (TransferStatus::Receive, ContractType::Erc721) => { - handle_receive_erc721(storage, chain, tx, url, my_address).await + handle_receive_erc721(storage, chain, transfer, url, my_address).await }, - (TransferStatus::Send, ContractType::Erc1155) => handle_send_erc1155(storage, chain, tx).await, + (TransferStatus::Send, ContractType::Erc1155) => handle_send_erc1155(storage, chain, transfer).await, (TransferStatus::Receive, ContractType::Erc1155) => { - handle_receive_erc1155(storage, chain, tx, url, my_address).await + handle_receive_erc1155(storage, chain, transfer, url, my_address).await }, } } -async fn handle_send_erc721( +async fn handle_send_erc721( storage: &T, chain: &Chain, - tx: NftTransferHistory, + transfer: NftTransferHistory, ) -> MmResult<(), UpdateNftError> { let nft_db = storage .get_nft( chain, - eth_addr_to_hex(&tx.common.token_address), - tx.common.token_id.clone(), + eth_addr_to_hex(&transfer.common.token_address), + transfer.common.token_id.clone(), ) .await? .ok_or_else(|| UpdateNftError::TokenNotFoundInWallet { - token_address: eth_addr_to_hex(&tx.common.token_address), - token_id: tx.common.token_id.to_string(), + token_address: eth_addr_to_hex(&transfer.common.token_address), + token_id: transfer.common.token_id.to_string(), })?; - let tx_meta = TxMeta::from(nft_db); - storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + let transfer_meta = TransferMeta::from(nft_db); + storage + .update_transfers_meta_by_token_addr_id(chain, transfer_meta) + .await?; storage .remove_nft_from_list( chain, - eth_addr_to_hex(&tx.common.token_address), - tx.common.token_id, - tx.block_number, + eth_addr_to_hex(&transfer.common.token_address), + transfer.common.token_id, + transfer.block_number, ) .await?; Ok(()) } -async fn handle_receive_erc721( +async fn handle_receive_erc721( storage: &T, chain: &Chain, - tx: NftTransferHistory, + transfer: NftTransferHistory, url: &Url, my_address: &str, ) -> MmResult<(), UpdateNftError> { let nft = match storage .get_nft( chain, - eth_addr_to_hex(&tx.common.token_address), - tx.common.token_id.clone(), + eth_addr_to_hex(&transfer.common.token_address), + transfer.common.token_id.clone(), ) .await? { Some(mut nft_db) => { // An error is raised if user tries to receive an identical ERC-721 token they already own // and if owner address != from address - if my_address != eth_addr_to_hex(&tx.common.from_address) { + if my_address != eth_addr_to_hex(&transfer.common.from_address) { return MmError::err(UpdateNftError::AttemptToReceiveAlreadyOwnedErc721 { - tx_hash: tx.common.transaction_hash, + tx_hash: transfer.common.transaction_hash, }); } - nft_db.block_number = tx.block_number; + nft_db.block_number = transfer.block_number; drop_mutability!(nft_db); storage .update_nft_amount_and_block_number(chain, nft_db.clone()) @@ -587,8 +592,8 @@ async fn handle_receive_erc721( // If token isn't in NFT LIST table then add nft to the table. None => { let mut nft = get_moralis_metadata( - eth_addr_to_hex(&tx.common.token_address), - tx.common.token_id, + eth_addr_to_hex(&transfer.common.token_address), + transfer.common.token_id, chain, url, ) @@ -597,86 +602,90 @@ async fn handle_receive_erc721( // than History by Wallet update nft.common.owner_of = Address::from_str(my_address).map_to_mm(|e| UpdateNftError::InvalidHexString(e.to_string()))?; - nft.block_number = tx.block_number; + nft.block_number = transfer.block_number; drop_mutability!(nft); storage - .add_nfts_to_list(chain, vec![nft.clone()], tx.block_number) + .add_nfts_to_list(chain, vec![nft.clone()], transfer.block_number) .await?; nft }, }; - let tx_meta = TxMeta::from(nft); - storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + let transfer_meta = TransferMeta::from(nft); + storage + .update_transfers_meta_by_token_addr_id(chain, transfer_meta) + .await?; Ok(()) } -async fn handle_send_erc1155( +async fn handle_send_erc1155( storage: &T, chain: &Chain, - tx: NftTransferHistory, + transfer: NftTransferHistory, ) -> MmResult<(), UpdateNftError> { let mut nft_db = storage .get_nft( chain, - eth_addr_to_hex(&tx.common.token_address), - tx.common.token_id.clone(), + eth_addr_to_hex(&transfer.common.token_address), + transfer.common.token_id.clone(), ) .await? .ok_or_else(|| UpdateNftError::TokenNotFoundInWallet { - token_address: eth_addr_to_hex(&tx.common.token_address), - token_id: tx.common.token_id.to_string(), + token_address: eth_addr_to_hex(&transfer.common.token_address), + token_id: transfer.common.token_id.to_string(), })?; - match nft_db.common.amount.cmp(&tx.common.amount) { + match nft_db.common.amount.cmp(&transfer.common.amount) { Ordering::Equal => { storage .remove_nft_from_list( chain, - eth_addr_to_hex(&tx.common.token_address), - tx.common.token_id, - tx.block_number, + eth_addr_to_hex(&transfer.common.token_address), + transfer.common.token_id, + transfer.block_number, ) .await?; }, Ordering::Greater => { - nft_db.common.amount -= tx.common.amount; + nft_db.common.amount -= transfer.common.amount; storage - .update_nft_amount(chain, nft_db.clone(), tx.block_number) + .update_nft_amount(chain, nft_db.clone(), transfer.block_number) .await?; }, Ordering::Less => { return MmError::err(UpdateNftError::InsufficientAmountInCache { amount_list: nft_db.common.amount.to_string(), - amount_history: tx.common.amount.to_string(), + amount_history: transfer.common.amount.to_string(), }); }, } - let tx_meta = TxMeta::from(nft_db); - storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + let transfer_meta = TransferMeta::from(nft_db); + storage + .update_transfers_meta_by_token_addr_id(chain, transfer_meta) + .await?; Ok(()) } -async fn handle_receive_erc1155( +async fn handle_receive_erc1155( storage: &T, chain: &Chain, - tx: NftTransferHistory, + transfer: NftTransferHistory, url: &Url, my_address: &str, ) -> MmResult<(), UpdateNftError> { let nft = match storage .get_nft( chain, - eth_addr_to_hex(&tx.common.token_address), - tx.common.token_id.clone(), + eth_addr_to_hex(&transfer.common.token_address), + transfer.common.token_id.clone(), ) .await? { Some(mut nft_db) => { // if owner address == from address, then owner sent tokens to themself, // which means that the amount will not change. - if my_address != eth_addr_to_hex(&tx.common.from_address) { - nft_db.common.amount += tx.common.amount; + if my_address != eth_addr_to_hex(&transfer.common.from_address) { + nft_db.common.amount += transfer.common.amount; } - nft_db.block_number = tx.block_number; + nft_db.block_number = transfer.block_number; drop_mutability!(nft_db); storage .update_nft_amount_and_block_number(chain, nft_db.clone()) @@ -686,8 +695,8 @@ async fn handle_receive_erc1155( // If token isn't in NFT LIST table then add nft to the table. None => { let moralis_meta = get_moralis_metadata( - eth_addr_to_hex(&tx.common.token_address), - tx.common.token_id.clone(), + eth_addr_to_hex(&transfer.common.token_address), + transfer.common.token_id.clone(), chain, url, ) @@ -698,7 +707,7 @@ async fn handle_receive_erc1155( common: NftCommon { token_address: moralis_meta.common.token_address, token_id: moralis_meta.common.token_id, - amount: tx.common.amount, + amount: transfer.common.amount, owner_of: Address::from_str(my_address) .map_to_mm(|e| UpdateNftError::InvalidHexString(e.to_string()))?, token_hash: moralis_meta.common.token_hash, @@ -713,16 +722,20 @@ async fn handle_receive_erc1155( }, chain: *chain, block_number_minted: moralis_meta.block_number_minted, - block_number: tx.block_number, + block_number: transfer.block_number, contract_type: moralis_meta.contract_type, uri_meta, }; - storage.add_nfts_to_list(chain, [nft.clone()], tx.block_number).await?; + storage + .add_nfts_to_list(chain, [nft.clone()], transfer.block_number) + .await?; nft }, }; - let tx_meta = TxMeta::from(nft); - storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + let transfer_meta = TransferMeta::from(nft); + storage + .update_transfers_meta_by_token_addr_id(chain, transfer_meta) + .await?; Ok(()) } @@ -751,14 +764,14 @@ pub(crate) async fn find_wallet_nft_amount( Ok(nft_meta.common.amount) } -async fn cache_nfts_from_moralis( +async fn cache_nfts_from_moralis( ctx: &MmArc, storage: &T, chain: &Chain, url: &Url, ) -> MmResult, UpdateNftError> { let nft_list = get_moralis_nft_list(ctx, chain, url).await?; - let last_scanned_block = NftTxHistoryStorageOps::get_last_block_number(storage, chain) + let last_scanned_block = NftTransferHistoryStorageOps::get_last_block_number(storage, chain) .await? .unwrap_or(0); storage @@ -767,28 +780,32 @@ async fn cache_nfts_from_moralis( Ok(nft_list) } -/// `update_meta_in_txs` function updates only txs related to current nfts in wallet. -async fn update_meta_in_txs(storage: &T, chain: &Chain, nfts: Vec) -> MmResult<(), UpdateNftError> +/// `update_meta_in_transfers` function updates only transfers related to current nfts in wallet. +async fn update_meta_in_transfers(storage: &T, chain: &Chain, nfts: Vec) -> MmResult<(), UpdateNftError> where - T: NftListStorageOps + NftTxHistoryStorageOps, + T: NftListStorageOps + NftTransferHistoryStorageOps, { for nft in nfts.into_iter() { - let tx_meta = TxMeta::from(nft); - storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + let transfer_meta = TransferMeta::from(nft); + storage + .update_transfers_meta_by_token_addr_id(chain, transfer_meta) + .await?; } Ok(()) } -/// `update_txs_with_empty_meta` function updates empty metadata in transfers. -async fn update_txs_with_empty_meta(storage: &T, chain: &Chain, url: &Url) -> MmResult<(), UpdateNftError> +/// `update_transfers_with_empty_meta` function updates empty metadata in transfers. +async fn update_transfers_with_empty_meta(storage: &T, chain: &Chain, url: &Url) -> MmResult<(), UpdateNftError> where - T: NftListStorageOps + NftTxHistoryStorageOps, + T: NftListStorageOps + NftTransferHistoryStorageOps, { - let nft_token_addr_id = storage.get_txs_with_empty_meta(chain).await?; + let nft_token_addr_id = storage.get_transfers_with_empty_meta(chain).await?; for addr_id_pair in nft_token_addr_id.into_iter() { let nft_meta = get_moralis_metadata(addr_id_pair.token_address, addr_id_pair.token_id, chain, url).await?; - let tx_meta = TxMeta::from(nft_meta); - storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + let transfer_meta = TransferMeta::from(nft_meta); + storage + .update_transfers_meta_by_token_addr_id(chain, transfer_meta) + .await?; } Ok(()) } @@ -818,12 +835,12 @@ fn check_and_redact_if_spam(text: &mut Option) -> Result MmResult<(), ProtectFromSpamError> { - let collection_name_spam = check_and_redact_if_spam(&mut tx.collection_name)?; - let token_name_spam = check_and_redact_if_spam(&mut tx.token_name)?; +fn protect_from_history_spam(transfer: &mut NftTransferHistory) -> MmResult<(), ProtectFromSpamError> { + let collection_name_spam = check_and_redact_if_spam(&mut transfer.collection_name)?; + let token_name_spam = check_and_redact_if_spam(&mut transfer.token_name)?; if collection_name_spam || token_name_spam { - tx.common.possible_spam = true; + transfer.common.possible_spam = true; } Ok(()) } diff --git a/mm2src/coins/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs index 3db093540c..9d7a07b89a 100644 --- a/mm2src/coins/nft/nft_structs.rs +++ b/mm2src/coins/nft/nft_structs.rs @@ -325,7 +325,7 @@ pub struct TransactionNftDetails { #[derive(Debug, Deserialize)] pub struct NftTransfersReq { pub(crate) chains: Vec, - pub(crate) filters: Option, + pub(crate) filters: Option, #[serde(default)] pub(crate) max: bool, #[serde(default = "ten")] @@ -368,14 +368,14 @@ impl fmt::Display for TransferStatus { } } -/// [`NftTransferCommon`] structure contains common fields from [`NftTransferHistory`] and [`NftTxHistoryFromMoralis`] +/// [`NftTransferCommon`] structure contains common fields from [`NftTransferHistory`] and [`NftTransferHistoryFromMoralis`] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct NftTransferCommon { pub(crate) block_hash: Option, /// Transaction hash in hexadecimal format pub(crate) transaction_hash: String, pub(crate) transaction_index: Option, - pub(crate) log_index: Option, + pub(crate) log_index: u32, pub(crate) value: Option, pub(crate) transaction_type: Option, pub(crate) token_address: Address, @@ -404,9 +404,9 @@ pub struct NftTransferHistory { pub(crate) status: TransferStatus, } -/// This structure is for deserializing moralis NFT transaction json to struct. +/// This structure is for deserializing moralis NFT transfer json to struct. #[derive(Debug, Deserialize)] -pub(crate) struct NftTxHistoryFromMoralis { +pub(crate) struct NftTransferHistoryFromMoralis { #[serde(flatten)] pub(crate) common: NftTransferCommon, pub(crate) block_number: SerdeStringWrap, @@ -422,7 +422,7 @@ pub struct NftsTransferHistoryList { } #[derive(Copy, Clone, Debug, Deserialize)] -pub struct NftTxHistoryFilters { +pub struct NftTransferHistoryFilters { #[serde(default)] pub receive: bool, #[serde(default)] @@ -444,7 +444,7 @@ pub struct NftTokenAddrId { } #[derive(Debug)] -pub struct TxMeta { +pub struct TransferMeta { pub(crate) token_address: String, pub(crate) token_id: BigDecimal, pub(crate) token_uri: Option, @@ -453,9 +453,9 @@ pub struct TxMeta { pub(crate) token_name: Option, } -impl From for TxMeta { +impl From for TransferMeta { fn from(nft_db: Nft) -> Self { - TxMeta { + TransferMeta { token_address: eth_addr_to_hex(&nft_db.common.token_address), token_id: nft_db.common.token_id, token_uri: nft_db.common.token_uri, diff --git a/mm2src/coins/nft/nft_tests.rs b/mm2src/coins/nft/nft_tests.rs index 7beb9e5f12..02710e0ac4 100644 --- a/mm2src/coins/nft/nft_tests.rs +++ b/mm2src/coins/nft/nft_tests.rs @@ -6,7 +6,7 @@ const TEST_WALLET_ADDR_EVM: &str = "0x394d86994f954ed931b86791b62fe64f4c5dac37"; #[cfg(all(test, not(target_arch = "wasm32")))] mod native_tests { use crate::eth::eth_addr_to_hex; - use crate::nft::nft_structs::{NftFromMoralis, NftTxHistoryFromMoralis, UriMeta}; + use crate::nft::nft_structs::{NftFromMoralis, NftTransferHistoryFromMoralis, UriMeta}; use crate::nft::nft_tests::{NFT_HISTORY_URL_TEST, NFT_LIST_URL_TEST, NFT_METADATA_URL_TEST, TEST_WALLET_ADDR_EVM}; use crate::nft::storage::db_test_helpers::*; use crate::nft::{check_and_redact_if_spam, check_moralis_ipfs_bafy, check_nft_metadata_for_spam, @@ -70,11 +70,12 @@ mod native_tests { assert_eq!(TEST_WALLET_ADDR_EVM, eth_addr_to_hex(&nft_moralis.common.owner_of)); } - let response_tx_history = block_on(send_request_to_uri(NFT_HISTORY_URL_TEST)).unwrap(); - let mut transfer_list = response_tx_history["result"].as_array().unwrap().clone(); + let response_transfer_history = block_on(send_request_to_uri(NFT_HISTORY_URL_TEST)).unwrap(); + let mut transfer_list = response_transfer_history["result"].as_array().unwrap().clone(); assert!(!transfer_list.is_empty()); - let first_tx = transfer_list.remove(transfer_list.len() - 1); - let transfer_moralis: NftTxHistoryFromMoralis = serde_json::from_str(&first_tx.to_string()).unwrap(); + let first_transfer = transfer_list.remove(transfer_list.len() - 1); + let transfer_moralis: NftTransferHistoryFromMoralis = + serde_json::from_str(&first_transfer.to_string()).unwrap(); assert_eq!( TEST_WALLET_ADDR_EVM, eth_addr_to_hex(&transfer_moralis.common.to_address) @@ -107,25 +108,25 @@ mod native_tests { fn test_nft_amount() { block_on(test_nft_amount_impl()) } #[test] - fn test_add_get_txs() { block_on(test_add_get_txs_impl()) } + fn test_add_get_transfers() { block_on(test_add_get_transfers_impl()) } #[test] - fn test_last_tx_block() { block_on(test_last_tx_block_impl()) } + fn test_last_transfer_block() { block_on(test_last_transfer_block_impl()) } #[test] - fn test_tx_history() { block_on(test_tx_history_impl()) } + fn test_transfer_history() { block_on(test_transfer_history_impl()) } #[test] - fn test_tx_history_filters() { block_on(test_tx_history_filters_impl()) } + fn test_transfer_history_filters() { block_on(test_transfer_history_filters_impl()) } #[test] - fn test_get_update_tx_meta() { block_on(test_get_update_tx_meta_impl()) } + fn test_get_update_transfer_meta() { block_on(test_get_update_transfer_meta_impl()) } } #[cfg(target_arch = "wasm32")] mod wasm_tests { use crate::eth::eth_addr_to_hex; - use crate::nft::nft_structs::{NftFromMoralis, NftTxHistoryFromMoralis}; + use crate::nft::nft_structs::{NftFromMoralis, NftTransferHistoryFromMoralis}; use crate::nft::nft_tests::{NFT_HISTORY_URL_TEST, NFT_LIST_URL_TEST, NFT_METADATA_URL_TEST, TEST_WALLET_ADDR_EVM}; use crate::nft::send_request_to_uri; use crate::nft::storage::db_test_helpers::*; @@ -142,11 +143,12 @@ mod wasm_tests { assert_eq!(TEST_WALLET_ADDR_EVM, eth_addr_to_hex(&nft_moralis.common.owner_of)); } - let response_tx_history = send_request_to_uri(NFT_HISTORY_URL_TEST).await.unwrap(); - let mut transfer_list = response_tx_history["result"].as_array().unwrap().clone(); + let response_transfer_history = send_request_to_uri(NFT_HISTORY_URL_TEST).await.unwrap(); + let mut transfer_list = response_transfer_history["result"].as_array().unwrap().clone(); assert!(!transfer_list.is_empty()); - let first_tx = transfer_list.remove(transfer_list.len() - 1); - let transfer_moralis: NftTxHistoryFromMoralis = serde_json::from_str(&first_tx.to_string()).unwrap(); + let first_transfer = transfer_list.remove(transfer_list.len() - 1); + let transfer_moralis: NftTransferHistoryFromMoralis = + serde_json::from_str(&first_transfer.to_string()).unwrap(); assert_eq!( TEST_WALLET_ADDR_EVM, eth_addr_to_hex(&transfer_moralis.common.to_address) @@ -176,17 +178,17 @@ mod wasm_tests { async fn test_refresh_metadata() { test_refresh_metadata_impl().await } #[wasm_bindgen_test] - async fn test_add_get_txs() { test_add_get_txs_impl().await } + async fn test_add_get_transfers() { test_add_get_transfers_impl().await } #[wasm_bindgen_test] - async fn test_last_tx_block() { test_last_tx_block_impl().await } + async fn test_last_transfer_block() { test_last_transfer_block_impl().await } #[wasm_bindgen_test] - async fn test_tx_history() { test_tx_history_impl().await } + async fn test_transfer_history() { test_transfer_history_impl().await } #[wasm_bindgen_test] - async fn test_tx_history_filters() { test_tx_history_filters_impl().await } + async fn test_transfer_history_filters() { test_transfer_history_filters_impl().await } #[wasm_bindgen_test] - async fn test_get_update_tx_meta() { test_get_update_tx_meta_impl().await } + async fn test_get_update_transfer_meta() { test_get_update_transfer_meta_impl().await } } diff --git a/mm2src/coins/nft/storage/db_test_helpers.rs b/mm2src/coins/nft/storage/db_test_helpers.rs index 44e0ede9e1..7961d4c1ea 100644 --- a/mm2src/coins/nft/storage/db_test_helpers.rs +++ b/mm2src/coins/nft/storage/db_test_helpers.rs @@ -1,7 +1,7 @@ use crate::eth::eth_addr_to_hex; use crate::nft::nft_structs::{Chain, ContractType, Nft, NftCommon, NftTransferCommon, NftTransferHistory, - NftTxHistoryFilters, TransferStatus, TxMeta, UriMeta}; -use crate::nft::storage::{NftListStorageOps, NftStorageBuilder, NftTxHistoryStorageOps, RemoveNftResult}; + NftTransferHistoryFilters, TransferMeta, TransferStatus, UriMeta}; +use crate::nft::storage::{NftListStorageOps, NftStorageBuilder, NftTransferHistoryStorageOps, RemoveNftResult}; use ethereum_types::Address; use mm2_number::BigDecimal; use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; @@ -17,6 +17,7 @@ cfg_wasm32! { const TOKEN_ADD: &str = "0xfd913a305d70a60aac4faac70c739563738e1f81"; const TOKEN_ID: &str = "214300044414"; const TX_HASH: &str = "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe"; +const LOG_INDEX: u32 = 495; pub(crate) fn nft() -> Nft { Nft { @@ -56,13 +57,13 @@ pub(crate) fn nft() -> Nft { } } -fn tx() -> NftTransferHistory { +fn transfer() -> NftTransferHistory { NftTransferHistory { common: NftTransferCommon { block_hash: Some("0x3d68b78391fb3cf8570df27036214f7e9a5a6a45d309197936f51d826041bfe7".to_string()), transaction_hash: "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe".to_string(), transaction_index: Some(198), - log_index: Some(495), + log_index: 495, value: Default::default(), transaction_type: Some("Single".to_string()), token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), @@ -158,6 +159,44 @@ fn nft_list() -> Vec { }; let nft2 = Nft { + common: NftCommon { + token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), + token_id: BigDecimal::from_str("214300047253").unwrap(), + amount: BigDecimal::from_str("1").unwrap(), + owner_of: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), + token_hash: Some("c5d1cfd75a0535b0ec750c0156e6ddfe".to_string()), + collection_name: Some("Binance NFT Mystery Box-Back to Blockchain Future".to_string()), + symbol: Some("BMBBBF".to_string()), + token_uri: Some("https://public.nftstatic.com/static/nft/BSC/BMBBBF/214300047252".to_string()), + metadata: Some( + "{\"image\":\"https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png\"}" + .to_string(), + ), + last_token_uri_sync: Some("2023-02-16T16:35:52.392Z".to_string()), + last_metadata_sync: Some("2023-02-16T16:36:04.283Z".to_string()), + minter_address: Some("0xdbdeb0895f3681b87fb3654b5cf3e05546ba24a9".to_string()), + possible_spam: false, + }, + chain: Chain::Bsc, + + block_number_minted: Some(25721963), + block_number: 28056726, + contract_type: ContractType::Erc721, + uri_meta: UriMeta { + image_url: Some( + "https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png".to_string(), + ), + raw_image_url: None, + token_name: Some("Nebula Nodes".to_string()), + description: Some("Interchain nodes".to_string()), + attributes: None, + animation_url: None, + external_url: None, + image_details: None, + }, + }; + + let nft3 = Nft { common: NftCommon { token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), token_id: BigDecimal::from_str("214300044414").unwrap(), @@ -194,16 +233,16 @@ fn nft_list() -> Vec { image_details: None, }, }; - vec![nft, nft1, nft2] + vec![nft, nft1, nft2, nft3] } -fn nft_tx_history() -> Vec { - let tx = NftTransferHistory { +fn nft_transfer_history() -> Vec { + let transfer = NftTransferHistory { common: NftTransferCommon { block_hash: Some("0xcb41654fc5cf2bf5d7fd3f061693405c74d419def80993caded0551ecfaeaae5".to_string()), transaction_hash: "0x9c16b962f63eead1c5d2355cc9037dde178b14b53043c57eb40c27964d22ae6a".to_string(), transaction_index: Some(57), - log_index: Some(139), + log_index: 139, value: Default::default(), transaction_type: Some("Single".to_string()), token_address: Address::from_str("0x5c7d6712dfaf0cb079d48981781c8705e8417ca0").unwrap(), @@ -226,12 +265,12 @@ fn nft_tx_history() -> Vec { status: TransferStatus::Receive, }; - let tx1 = NftTransferHistory { + let transfer1 = NftTransferHistory { common: NftTransferCommon { block_hash: Some("0x3d68b78391fb3cf8570df27036214f7e9a5a6a45d309197936f51d826041bfe7".to_string()), transaction_hash: "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe".to_string(), transaction_index: Some(198), - log_index: Some(495), + log_index: 495, value: Default::default(), transaction_type: Some("Single".to_string()), token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), @@ -256,12 +295,43 @@ fn nft_tx_history() -> Vec { status: TransferStatus::Receive, }; - let tx2 = NftTransferHistory { + // Same as transfer1 but with different log_index, meaning that transfer1 and transfer2 are part of one batch/multi token transaction + let transfer2 = NftTransferHistory { + common: NftTransferCommon { + block_hash: Some("0x3d68b78391fb3cf8570df27036214f7e9a5a6a45d309197936f51d826041bfe7".to_string()), + transaction_hash: "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe".to_string(), + transaction_index: Some(198), + log_index: 496, + value: Default::default(), + transaction_type: Some("Single".to_string()), + token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), + token_id: BigDecimal::from_str("214300047253").unwrap(), + from_address: Address::from_str("0x6fad0ec6bb76914b2a2a800686acc22970645820").unwrap(), + to_address: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), + amount: BigDecimal::from_str("1").unwrap(), + verified: Some(1), + operator: None, + possible_spam: false, + }, + chain: Chain::Bsc, + block_number: 28056726, + block_timestamp: 1683627432, + contract_type: ContractType::Erc721, + + token_uri: None, + collection_name: None, + image_url: None, + token_name: None, + + status: TransferStatus::Receive, + }; + + let transfer3 = NftTransferHistory { common: NftTransferCommon { block_hash: Some("0x326db41c5a4fd5f033676d95c590ced18936ef2ef6079e873b23af087fd966c6".to_string()), transaction_hash: "0x981bad702cc6e088f0e9b5e7287ff7a3487b8d269103cee3b9e5803141f63f91".to_string(), transaction_index: Some(83), - log_index: Some(201), + log_index: 201, value: Default::default(), transaction_type: Some("Single".to_string()), token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), @@ -286,10 +356,10 @@ fn nft_tx_history() -> Vec { status: TransferStatus::Receive, }; - vec![tx, tx1, tx2] + vec![transfer, transfer1, transfer2, transfer3] } -async fn init_nft_list_storage(chain: &Chain) -> impl NftListStorageOps + NftTxHistoryStorageOps { +async fn init_nft_list_storage(chain: &Chain) -> impl NftListStorageOps + NftTransferHistoryStorageOps { let ctx = mm_ctx_with_custom_db(); let storage = NftStorageBuilder::new(&ctx).build().unwrap(); NftListStorageOps::init(&storage, chain).await.unwrap(); @@ -298,11 +368,13 @@ async fn init_nft_list_storage(chain: &Chain) -> impl NftListStorageOps + NftTxH storage } -async fn init_nft_history_storage(chain: &Chain) -> impl NftListStorageOps + NftTxHistoryStorageOps { +async fn init_nft_history_storage(chain: &Chain) -> impl NftListStorageOps + NftTransferHistoryStorageOps { let ctx = mm_ctx_with_custom_db(); let storage = NftStorageBuilder::new(&ctx).build().unwrap(); - NftTxHistoryStorageOps::init(&storage, chain).await.unwrap(); - let is_initialized = NftTxHistoryStorageOps::is_initialized(&storage, chain).await.unwrap(); + NftTransferHistoryStorageOps::init(&storage, chain).await.unwrap(); + let is_initialized = NftTransferHistoryStorageOps::is_initialized(&storage, chain) + .await + .unwrap(); assert!(is_initialized); storage } @@ -344,14 +416,14 @@ pub(crate) async fn test_nft_list_impl() { storage.add_nfts_to_list(&chain, nft_list, 28056726).await.unwrap(); let nft_list = storage - .get_nft_list(vec![chain], false, 1, Some(NonZeroUsize::new(2).unwrap())) + .get_nft_list(vec![chain], false, 1, Some(NonZeroUsize::new(3).unwrap())) .await .unwrap(); assert_eq!(nft_list.nfts.len(), 1); let nft = nft_list.nfts.get(0).unwrap(); assert_eq!(nft.block_number, 28056721); - assert_eq!(nft_list.skipped, 1); - assert_eq!(nft_list.total, 3); + assert_eq!(nft_list.skipped, 2); + assert_eq!(nft_list.total, 4); } pub(crate) async fn test_remove_nft_impl() { @@ -372,7 +444,7 @@ pub(crate) async fn test_remove_nft_impl() { .unwrap() .nfts .len(); - assert_eq!(list_len, 2); + assert_eq!(list_len, 3); let last_scanned_block = storage.get_last_scanned_block(&chain).await.unwrap().unwrap(); assert_eq!(last_scanned_block, 28056800); } @@ -435,126 +507,126 @@ pub(crate) async fn test_refresh_metadata_impl() { assert_eq!(new_symbol.to_string(), nft_upd.common.symbol.unwrap()); } -pub(crate) async fn test_add_get_txs_impl() { +pub(crate) async fn test_add_get_transfers_impl() { let chain = Chain::Bsc; let storage = init_nft_history_storage(&chain).await; - let txs = nft_tx_history(); - storage.add_txs_to_history(&chain, txs).await.unwrap(); + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(&chain, transfers).await.unwrap(); let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); - let tx1 = storage - .get_txs_by_token_addr_id(&chain, TOKEN_ADD.to_string(), token_id) + let transfer1 = storage + .get_transfers_by_token_addr_id(&chain, TOKEN_ADD.to_string(), token_id) .await .unwrap() .get(0) .unwrap() .clone(); - assert_eq!(tx1.block_number, 28056721); - let tx2 = storage - .get_tx_by_tx_hash(&chain, TX_HASH.to_string()) + assert_eq!(transfer1.block_number, 28056721); + let transfer2 = storage + .get_transfer_by_tx_hash_and_log_index(&chain, TX_HASH.to_string(), LOG_INDEX) .await .unwrap() .unwrap(); - assert_eq!(tx2.block_number, 28056726); - let tx_from = storage.get_txs_from_block(&chain, 28056721).await.unwrap(); - assert_eq!(tx_from.len(), 2); + assert_eq!(transfer2.block_number, 28056726); + let transfer_from = storage.get_transfers_from_block(&chain, 28056721).await.unwrap(); + assert_eq!(transfer_from.len(), 3); } -pub(crate) async fn test_last_tx_block_impl() { +pub(crate) async fn test_last_transfer_block_impl() { let chain = Chain::Bsc; let storage = init_nft_history_storage(&chain).await; - let txs = nft_tx_history(); - storage.add_txs_to_history(&chain, txs).await.unwrap(); + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(&chain, transfers).await.unwrap(); - let last_block = NftTxHistoryStorageOps::get_last_block_number(&storage, &chain) + let last_block = NftTransferHistoryStorageOps::get_last_block_number(&storage, &chain) .await .unwrap() .unwrap(); assert_eq!(last_block, 28056726); } -pub(crate) async fn test_tx_history_impl() { +pub(crate) async fn test_transfer_history_impl() { let chain = Chain::Bsc; let storage = init_nft_history_storage(&chain).await; - let txs = nft_tx_history(); - storage.add_txs_to_history(&chain, txs).await.unwrap(); + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(&chain, transfers).await.unwrap(); - let tx_history = storage - .get_tx_history(vec![chain], false, 1, Some(NonZeroUsize::new(2).unwrap()), None) + let transfer_history = storage + .get_transfer_history(vec![chain], false, 1, Some(NonZeroUsize::new(3).unwrap()), None) .await .unwrap(); - assert_eq!(tx_history.transfer_history.len(), 1); - let tx = tx_history.transfer_history.get(0).unwrap(); - assert_eq!(tx.block_number, 28056721); - assert_eq!(tx_history.skipped, 1); - assert_eq!(tx_history.total, 3); + assert_eq!(transfer_history.transfer_history.len(), 1); + let transfer = transfer_history.transfer_history.get(0).unwrap(); + assert_eq!(transfer.block_number, 28056721); + assert_eq!(transfer_history.skipped, 2); + assert_eq!(transfer_history.total, 4); } -pub(crate) async fn test_tx_history_filters_impl() { +pub(crate) async fn test_transfer_history_filters_impl() { let chain = Chain::Bsc; let storage = init_nft_history_storage(&chain).await; - let txs = nft_tx_history(); - storage.add_txs_to_history(&chain, txs).await.unwrap(); + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(&chain, transfers).await.unwrap(); - let filters = NftTxHistoryFilters { + let filters = NftTransferHistoryFilters { receive: true, send: false, from_date: None, to_date: None, }; - let filters1 = NftTxHistoryFilters { + let filters1 = NftTransferHistoryFilters { receive: false, send: false, from_date: None, to_date: Some(1677166110), }; - let filters2 = NftTxHistoryFilters { + let filters2 = NftTransferHistoryFilters { receive: false, send: false, from_date: Some(1677166110), to_date: Some(1683627417), }; - let tx_history = storage - .get_tx_history(vec![chain], true, 1, None, Some(filters)) + let transfer_history = storage + .get_transfer_history(vec![chain], true, 1, None, Some(filters)) .await .unwrap(); - assert_eq!(tx_history.transfer_history.len(), 3); - let tx = tx_history.transfer_history.get(0).unwrap(); - assert_eq!(tx.block_number, 28056726); + assert_eq!(transfer_history.transfer_history.len(), 4); + let transfer = transfer_history.transfer_history.get(0).unwrap(); + assert_eq!(transfer.block_number, 28056726); - let tx_history1 = storage - .get_tx_history(vec![chain], true, 1, None, Some(filters1)) + let transfer_history1 = storage + .get_transfer_history(vec![chain], true, 1, None, Some(filters1)) .await .unwrap(); - assert_eq!(tx_history1.transfer_history.len(), 1); - let tx1 = tx_history1.transfer_history.get(0).unwrap(); - assert_eq!(tx1.block_number, 25919780); + assert_eq!(transfer_history1.transfer_history.len(), 1); + let transfer1 = transfer_history1.transfer_history.get(0).unwrap(); + assert_eq!(transfer1.block_number, 25919780); - let tx_history2 = storage - .get_tx_history(vec![chain], true, 1, None, Some(filters2)) + let transfer_history2 = storage + .get_transfer_history(vec![chain], true, 1, None, Some(filters2)) .await .unwrap(); - assert_eq!(tx_history2.transfer_history.len(), 2); - let tx_0 = tx_history2.transfer_history.get(0).unwrap(); - assert_eq!(tx_0.block_number, 28056721); - let tx_1 = tx_history2.transfer_history.get(1).unwrap(); - assert_eq!(tx_1.block_number, 25919780); + assert_eq!(transfer_history2.transfer_history.len(), 2); + let transfer_0 = transfer_history2.transfer_history.get(0).unwrap(); + assert_eq!(transfer_0.block_number, 28056721); + let transfer_1 = transfer_history2.transfer_history.get(1).unwrap(); + assert_eq!(transfer_1.block_number, 25919780); } -pub(crate) async fn test_get_update_tx_meta_impl() { +pub(crate) async fn test_get_update_transfer_meta_impl() { let chain = Chain::Bsc; let storage = init_nft_history_storage(&chain).await; - let txs = nft_tx_history(); - storage.add_txs_to_history(&chain, txs).await.unwrap(); + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(&chain, transfers).await.unwrap(); - let vec_token_add_id = storage.get_txs_with_empty_meta(&chain).await.unwrap(); - assert_eq!(vec_token_add_id.len(), 2); + let vec_token_add_id = storage.get_transfers_with_empty_meta(&chain).await.unwrap(); + assert_eq!(vec_token_add_id.len(), 3); let token_add = "0x5c7d6712dfaf0cb079d48981781c8705e8417ca0".to_string(); - let tx_meta = TxMeta { + let transfer_meta = TransferMeta { token_address: token_add.clone(), token_id: Default::default(), token_uri: None, @@ -562,20 +634,26 @@ pub(crate) async fn test_get_update_tx_meta_impl() { image_url: None, token_name: Some("Tiki box".to_string()), }; - storage.update_txs_meta_by_token_addr_id(&chain, tx_meta).await.unwrap(); - let tx_upd = storage - .get_txs_by_token_addr_id(&chain, token_add, Default::default()) + storage + .update_transfers_meta_by_token_addr_id(&chain, transfer_meta) + .await + .unwrap(); + let transfer_upd = storage + .get_transfers_by_token_addr_id(&chain, token_add, Default::default()) .await .unwrap(); - let tx_upd = tx_upd.get(0).unwrap(); - assert_eq!(tx_upd.token_name, Some("Tiki box".to_string())); + let transfer_upd = transfer_upd.get(0).unwrap(); + assert_eq!(transfer_upd.token_name, Some("Tiki box".to_string())); - let tx_meta = tx(); - storage.update_tx_meta_by_hash(&chain, tx_meta).await.unwrap(); - let tx_by_hash = storage - .get_tx_by_tx_hash(&chain, TX_HASH.to_string()) + let transfer_meta = transfer(); + storage + .update_transfer_meta_by_hash_and_log_index(&chain, transfer_meta) + .await + .unwrap(); + let transfer_by_hash = storage + .get_transfer_by_tx_hash_and_log_index(&chain, TX_HASH.to_string(), LOG_INDEX) .await .unwrap() .unwrap(); - assert_eq!(tx_by_hash.token_name, Some("Nebula Nodes".to_string())) + assert_eq!(transfer_by_hash.token_name, Some("Nebula Nodes".to_string())) } diff --git a/mm2src/coins/nft/storage/mod.rs b/mm2src/coins/nft/storage/mod.rs index d5f4319a2a..0a2e906ccc 100644 --- a/mm2src/coins/nft/storage/mod.rs +++ b/mm2src/coins/nft/storage/mod.rs @@ -1,5 +1,5 @@ -use crate::nft::nft_structs::{Chain, Nft, NftList, NftTokenAddrId, NftTransferHistory, NftTxHistoryFilters, - NftsTransferHistoryList, TxMeta}; +use crate::nft::nft_structs::{Chain, Nft, NftList, NftTokenAddrId, NftTransferHistory, NftTransferHistoryFilters, + NftsTransferHistoryList, TransferMeta}; use crate::WithdrawError; use async_trait::async_trait; use derive_more::Display; @@ -88,7 +88,7 @@ pub trait NftListStorageOps { } #[async_trait] -pub trait NftTxHistoryStorageOps { +pub trait NftTransferHistoryStorageOps { type Error: NftStorageError; /// Initializes tables in storage for the specified chain type. @@ -97,48 +97,57 @@ pub trait NftTxHistoryStorageOps { /// Whether tables are initialized for the specified chain. async fn is_initialized(&self, chain: &Chain) -> MmResult; - async fn get_tx_history( + async fn get_transfer_history( &self, chains: Vec, max: bool, limit: usize, page_number: Option, - filters: Option, + filters: Option, ) -> MmResult; - async fn add_txs_to_history(&self, chain: &Chain, txs: I) -> MmResult<(), Self::Error> + async fn add_transfers_to_history(&self, chain: &Chain, transfers: I) -> MmResult<(), Self::Error> where I: IntoIterator + Send + 'static, I::IntoIter: Send; async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error>; - /// `get_txs_from_block` function returns transfers sorted by + /// `get_transfers_from_block` function returns transfers sorted by /// block_number in ascending order. It is needed to update the NFT LIST table correctly. - async fn get_txs_from_block( + async fn get_transfers_from_block( &self, chain: &Chain, from_block: u64, ) -> MmResult, Self::Error>; - async fn get_txs_by_token_addr_id( + async fn get_transfers_by_token_addr_id( &self, chain: &Chain, token_address: String, token_id: BigDecimal, ) -> MmResult, Self::Error>; - async fn get_tx_by_tx_hash( + async fn get_transfer_by_tx_hash_and_log_index( &self, chain: &Chain, transaction_hash: String, + log_index: u32, ) -> MmResult, Self::Error>; - async fn update_tx_meta_by_hash(&self, chain: &Chain, tx: NftTransferHistory) -> MmResult<(), Self::Error>; + async fn update_transfer_meta_by_hash_and_log_index( + &self, + chain: &Chain, + transfer: NftTransferHistory, + ) -> MmResult<(), Self::Error>; - async fn update_txs_meta_by_token_addr_id(&self, chain: &Chain, tx_meta: TxMeta) -> MmResult<(), Self::Error>; + async fn update_transfers_meta_by_token_addr_id( + &self, + chain: &Chain, + transfer_meta: TransferMeta, + ) -> MmResult<(), Self::Error>; - async fn get_txs_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error>; + async fn get_transfers_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error>; } #[derive(Debug, Deserialize, Display, Serialize)] @@ -155,7 +164,7 @@ impl From for WithdrawError { } /// `NftStorageBuilder` is used to create an instance that implements the [`NftListStorageOps`] -/// and [`NftTxHistoryStorageOps`] traits.Also has guard to lock write operations. +/// and [`NftTransferHistoryStorageOps`] traits.Also has guard to lock write operations. pub struct NftStorageBuilder<'a> { ctx: &'a MmArc, } @@ -164,9 +173,9 @@ impl<'a> NftStorageBuilder<'a> { #[inline] pub fn new(ctx: &MmArc) -> NftStorageBuilder<'_> { NftStorageBuilder { ctx } } - /// `build` function is used to build nft storage which implements [`NftListStorageOps`] and [`NftTxHistoryStorageOps`] traits. + /// `build` function is used to build nft storage which implements [`NftListStorageOps`] and [`NftTransferHistoryStorageOps`] traits. #[inline] - pub fn build(&self) -> MmResult { + pub fn build(&self) -> MmResult { #[cfg(target_arch = "wasm32")] return wasm::wasm_storage::IndexedDbNftStorage::new(self.ctx); #[cfg(not(target_arch = "wasm32"))] diff --git a/mm2src/coins/nft/storage/sql_storage.rs b/mm2src/coins/nft/storage/sql_storage.rs index e37780111d..9179704467 100644 --- a/mm2src/coins/nft/storage/sql_storage.rs +++ b/mm2src/coins/nft/storage/sql_storage.rs @@ -1,8 +1,8 @@ use crate::nft::eth_addr_to_hex; use crate::nft::nft_structs::{Chain, ConvertChain, Nft, NftList, NftTokenAddrId, NftTransferHistory, - NftTxHistoryFilters, NftsTransferHistoryList, TxMeta}; + NftTransferHistoryFilters, NftsTransferHistoryList, TransferMeta}; use crate::nft::storage::{get_offset_limit, CreateNftStorageError, NftListStorageOps, NftStorageError, - NftTxHistoryStorageOps, RemoveNftResult}; + NftTransferHistoryStorageOps, RemoveNftResult}; use async_trait::async_trait; use common::async_blocking; use db_common::sql_build::{SqlCondition, SqlQuery}; @@ -22,7 +22,7 @@ use std::sync::{Arc, Mutex}; fn nft_list_table_name(chain: &Chain) -> String { chain.to_ticker() + "_nft_list" } -fn nft_tx_history_table_name(chain: &Chain) -> String { chain.to_ticker() + "_nft_tx_history" } +fn nft_transfer_history_table_name(chain: &Chain) -> String { chain.to_ticker() + "_nft_transfer_history" } fn scanned_nft_blocks_table_name() -> String { "scanned_nft_blocks".to_string() } @@ -45,12 +45,13 @@ fn create_nft_list_table_sql(chain: &Chain) -> MmResult { Ok(sql) } -fn create_tx_history_table_sql(chain: &Chain) -> MmResult { - let table_name = nft_tx_history_table_name(chain); +fn create_transfer_history_table_sql(chain: &Chain) -> MmResult { + let table_name = nft_transfer_history_table_name(chain); validate_table_name(&table_name)?; let sql = format!( "CREATE TABLE IF NOT EXISTS {} ( - transaction_hash VARCHAR(256) PRIMARY KEY, + transaction_hash VARCHAR(256) NOT NULL, + log_index INTEGER NOT NULL, chain TEXT NOT NULL, block_number INTEGER NOT NULL, block_timestamp INTEGER NOT NULL, @@ -63,7 +64,8 @@ fn create_tx_history_table_sql(chain: &Chain) -> MmResult { collection_name TEXT, image_url TEXT, token_name TEXT, - details_json TEXT + details_json TEXT, + PRIMARY KEY (transaction_hash, log_index) );", table_name ); @@ -121,14 +123,14 @@ fn get_nft_list_builder_preimage(chains: Vec) -> MmResult, - filters: Option, + filters: Option, ) -> MmResult { let union_sql_strings = chains .into_iter() .map(|chain| { - let table_name = nft_tx_history_table_name(&chain); + let table_name = nft_transfer_history_table_name(&chain); validate_table_name(&table_name)?; let sql_builder = nft_history_table_builder_preimage(table_name.as_str(), filters)?; let sql_string = sql_builder @@ -148,7 +150,7 @@ fn get_nft_tx_builder_preimage( fn nft_history_table_builder_preimage( table_name: &str, - filters: Option, + filters: Option, ) -> Result { let mut sql_builder = SqlBuilder::select_from(table_name); if let Some(filters) = filters { @@ -201,7 +203,7 @@ fn nft_from_row(row: &Row<'_>) -> Result { json::from_str(&json_string).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e))) } -fn tx_history_from_row(row: &Row<'_>) -> Result { +fn transfer_history_from_row(row: &Row<'_>) -> Result { let json_string: String = row.get(0)?; json::from_str(&json_string).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e))) } @@ -231,16 +233,16 @@ fn insert_nft_in_list_sql(chain: &Chain) -> MmResult { Ok(sql) } -fn insert_tx_in_history_sql(chain: &Chain) -> MmResult { - let table_name = nft_tx_history_table_name(chain); +fn insert_transfer_in_history_sql(chain: &Chain) -> MmResult { + let table_name = nft_transfer_history_table_name(chain); validate_table_name(&table_name)?; let sql = format!( "INSERT INTO {} ( - transaction_hash, chain, block_number, block_timestamp, contract_type, + transaction_hash, log_index, chain, block_number, block_timestamp, contract_type, token_address, token_id, status, amount, collection_name, image_url, token_name, details_json ) VALUES ( - ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13 + ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14 );", table_name ); @@ -271,12 +273,12 @@ where Ok(sql) } -fn update_meta_by_tx_hash_sql(chain: &Chain) -> MmResult { - let table_name = nft_tx_history_table_name(chain); +fn update_meta_by_tx_hash_and_log_index_sql(chain: &Chain) -> MmResult { + let table_name = nft_transfer_history_table_name(chain); validate_table_name(&table_name)?; let sql = format!( - "UPDATE {} SET token_uri = ?1, collection_name = ?2, image_url = ?3, token_name = ?4, details_json = ?5 WHERE transaction_hash = ?6;", + "UPDATE {} SET token_uri = ?1, collection_name = ?2, image_url = ?3, token_name = ?4, details_json = ?5 WHERE transaction_hash = ?6 AND log_index = ?7;", table_name ); Ok(sql) @@ -367,12 +369,12 @@ fn block_number_from_row(row: &Row<'_>) -> Result { row.get::<_, fn nft_amount_from_row(row: &Row<'_>) -> Result { row.get(0) } -fn get_txs_from_block_builder<'a>( +fn get_transfers_from_block_builder<'a>( conn: &'a Connection, chain: &'a Chain, from_block: u64, ) -> MmResult, SqlError> { - let table_name = nft_tx_history_table_name(chain); + let table_name = nft_transfer_history_table_name(chain); validate_table_name(table_name.as_str())?; let mut sql_builder = SqlQuery::select_from(conn, table_name.as_str())?; sql_builder @@ -384,8 +386,11 @@ fn get_txs_from_block_builder<'a>( Ok(sql_builder) } -fn get_txs_by_token_addr_id_statement<'a>(conn: &'a Connection, chain: &'a Chain) -> MmResult, SqlError> { - let table_name = nft_tx_history_table_name(chain); +fn get_transfers_by_token_addr_id_statement<'a>( + conn: &'a Connection, + chain: &'a Chain, +) -> MmResult, SqlError> { + let table_name = nft_transfer_history_table_name(chain); validate_table_name(table_name.as_str())?; let sql_query = format!( "SELECT details_json FROM {} WHERE token_address = ? AND token_id = ?", @@ -395,8 +400,11 @@ fn get_txs_by_token_addr_id_statement<'a>(conn: &'a Connection, chain: &'a Chain Ok(stmt) } -fn get_txs_with_empty_meta_builder<'a>(conn: &'a Connection, chain: &'a Chain) -> MmResult, SqlError> { - let table_name = nft_tx_history_table_name(chain); +fn get_transfers_with_empty_meta_builder<'a>( + conn: &'a Connection, + chain: &'a Chain, +) -> MmResult, SqlError> { + let table_name = nft_transfer_history_table_name(chain); validate_table_name(table_name.as_str())?; let mut sql_builder = SqlQuery::select_from(conn, table_name.as_str())?; sql_builder @@ -412,10 +420,13 @@ fn get_txs_with_empty_meta_builder<'a>(conn: &'a Connection, chain: &'a Chain) - Ok(sql_builder) } -fn get_tx_by_tx_hash_sql(chain: &Chain) -> MmResult { - let table_name = nft_tx_history_table_name(chain); +fn get_transfer_by_tx_hash_and_log_index_sql(chain: &Chain) -> MmResult { + let table_name = nft_transfer_history_table_name(chain); validate_table_name(&table_name)?; - let sql = format!("SELECT details_json FROM {} WHERE transaction_hash=?1", table_name); + let sql = format!( + "SELECT details_json FROM {} WHERE transaction_hash=?1 AND log_index = ?2", + table_name + ); Ok(sql) } @@ -675,22 +686,22 @@ impl NftListStorageOps for SqliteNftStorage { } #[async_trait] -impl NftTxHistoryStorageOps for SqliteNftStorage { +impl NftTransferHistoryStorageOps for SqliteNftStorage { type Error = SqlError; async fn init(&self, chain: &Chain) -> MmResult<(), Self::Error> { let selfi = self.clone(); - let sql_tx_history = create_tx_history_table_sql(chain)?; + let sql_transfer_history = create_transfer_history_table_sql(chain)?; async_blocking(move || { let conn = selfi.0.lock().unwrap(); - conn.execute(&sql_tx_history, []).map(|_| ())?; + conn.execute(&sql_transfer_history, []).map(|_| ())?; Ok(()) }) .await } async fn is_initialized(&self, chain: &Chain) -> MmResult { - let table_name = nft_tx_history_table_name(chain); + let table_name = nft_transfer_history_table_name(chain); validate_table_name(&table_name)?; let selfi = self.clone(); async_blocking(move || { @@ -701,18 +712,18 @@ impl NftTxHistoryStorageOps for SqliteNftStorage { .await } - async fn get_tx_history( + async fn get_transfer_history( &self, chains: Vec, max: bool, limit: usize, page_number: Option, - filters: Option, + filters: Option, ) -> MmResult { let selfi = self.clone(); async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let sql_builder = get_nft_tx_builder_preimage(chains, filters)?; + let sql_builder = get_nft_transfer_builder_preimage(chains, filters)?; let total_count_builder_sql = sql_builder .clone() .count("*") @@ -725,12 +736,12 @@ impl NftTxHistoryStorageOps for SqliteNftStorage { let (offset, limit) = get_offset_limit(max, limit, page_number, count_total); let sql = finalize_nft_history_sql_builder(sql_builder, offset, limit)?; - let txs = conn + let transfers = conn .prepare(&sql)? - .query_map([], tx_history_from_row)? + .query_map([], transfer_history_from_row)? .collect::, _>>()?; let result = NftsTransferHistoryList { - transfer_history: txs, + transfer_history: transfers, skipped: offset, total: count_total, }; @@ -739,7 +750,7 @@ impl NftTxHistoryStorageOps for SqliteNftStorage { .await } - async fn add_txs_to_history(&self, chain: &Chain, txs: I) -> MmResult<(), Self::Error> + async fn add_transfers_to_history(&self, chain: &Chain, transfers: I) -> MmResult<(), Self::Error> where I: IntoIterator + Send + 'static, I::IntoIter: Send, @@ -750,24 +761,25 @@ impl NftTxHistoryStorageOps for SqliteNftStorage { let mut conn = selfi.0.lock().unwrap(); let sql_transaction = conn.transaction()?; - for tx in txs { - let tx_json = json::to_string(&tx).expect("serialization should not fail"); + for transfer in transfers { + let transfer_json = json::to_string(&transfer).expect("serialization should not fail"); let params = [ - Some(tx.common.transaction_hash), - Some(tx.chain.to_string()), - Some(tx.block_number.to_string()), - Some(tx.block_timestamp.to_string()), - Some(tx.contract_type.to_string()), - Some(eth_addr_to_hex(&tx.common.token_address)), - Some(tx.common.token_id.to_string()), - Some(tx.status.to_string()), - Some(tx.common.amount.to_string()), - tx.collection_name, - tx.image_url, - tx.token_name, - Some(tx_json), + Some(transfer.common.transaction_hash), + Some(transfer.common.log_index.to_string()), + Some(transfer.chain.to_string()), + Some(transfer.block_number.to_string()), + Some(transfer.block_timestamp.to_string()), + Some(transfer.contract_type.to_string()), + Some(eth_addr_to_hex(&transfer.common.token_address)), + Some(transfer.common.token_id.to_string()), + Some(transfer.status.to_string()), + Some(transfer.common.amount.to_string()), + transfer.collection_name, + transfer.image_url, + transfer.token_name, + Some(transfer_json), ]; - sql_transaction.execute(&insert_tx_in_history_sql(&chain)?, params)?; + sql_transaction.execute(&insert_transfer_in_history_sql(&chain)?, params)?; } sql_transaction.commit()?; Ok(()) @@ -776,7 +788,7 @@ impl NftTxHistoryStorageOps for SqliteNftStorage { } async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error> { - let sql = select_last_block_number_sql(chain, nft_tx_history_table_name)?; + let sql = select_last_block_number_sql(chain, nft_transfer_history_table_name)?; let selfi = self.clone(); async_blocking(move || { let conn = selfi.0.lock().unwrap(); @@ -788,7 +800,7 @@ impl NftTxHistoryStorageOps for SqliteNftStorage { .map_to_mm(|e| SqlError::FromSqlConversionFailure(2, Type::Integer, Box::new(e))) } - async fn get_txs_from_block( + async fn get_transfers_from_block( &self, chain: &Chain, from_block: u64, @@ -797,14 +809,14 @@ impl NftTxHistoryStorageOps for SqliteNftStorage { let chain = *chain; async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let sql_builder = get_txs_from_block_builder(&conn, &chain, from_block)?; - let txs = sql_builder.query(tx_history_from_row)?; - Ok(txs) + let sql_builder = get_transfers_from_block_builder(&conn, &chain, from_block)?; + let transfers = sql_builder.query(transfer_history_from_row)?; + Ok(transfers) }) .await } - async fn get_txs_by_token_addr_id( + async fn get_transfers_by_token_addr_id( &self, chain: &Chain, token_address: String, @@ -814,39 +826,51 @@ impl NftTxHistoryStorageOps for SqliteNftStorage { let chain = *chain; async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let mut stmt = get_txs_by_token_addr_id_statement(&conn, &chain)?; - let txs = stmt - .query_map([token_address, token_id.to_string()], tx_history_from_row)? + let mut stmt = get_transfers_by_token_addr_id_statement(&conn, &chain)?; + let transfers = stmt + .query_map([token_address, token_id.to_string()], transfer_history_from_row)? .collect::, _>>()?; - Ok(txs) + Ok(transfers) }) .await } - async fn get_tx_by_tx_hash( + async fn get_transfer_by_tx_hash_and_log_index( &self, chain: &Chain, transaction_hash: String, + log_index: u32, ) -> MmResult, Self::Error> { - let sql = get_tx_by_tx_hash_sql(chain)?; + let sql = get_transfer_by_tx_hash_and_log_index_sql(chain)?; let selfi = self.clone(); async_blocking(move || { let conn = selfi.0.lock().unwrap(); - query_single_row(&conn, &sql, [transaction_hash], tx_history_from_row).map_to_mm(SqlError::from) + query_single_row( + &conn, + &sql, + [transaction_hash, log_index.to_string()], + transfer_history_from_row, + ) + .map_to_mm(SqlError::from) }) .await } - async fn update_tx_meta_by_hash(&self, chain: &Chain, tx: NftTransferHistory) -> MmResult<(), Self::Error> { - let sql = update_meta_by_tx_hash_sql(chain)?; - let tx_json = json::to_string(&tx).expect("serialization should not fail"); + async fn update_transfer_meta_by_hash_and_log_index( + &self, + chain: &Chain, + transfer: NftTransferHistory, + ) -> MmResult<(), Self::Error> { + let sql = update_meta_by_tx_hash_and_log_index_sql(chain)?; + let transfer_json = json::to_string(&transfer).expect("serialization should not fail"); let params = [ - tx.token_uri, - tx.collection_name, - tx.image_url, - tx.token_name, - Some(tx_json), - Some(tx.common.transaction_hash), + transfer.token_uri, + transfer.collection_name, + transfer.image_url, + transfer.token_name, + Some(transfer_json), + Some(transfer.common.transaction_hash), + Some(transfer.common.log_index.to_string()), ]; let selfi = self.clone(); async_blocking(move || { @@ -859,28 +883,34 @@ impl NftTxHistoryStorageOps for SqliteNftStorage { .await } - async fn update_txs_meta_by_token_addr_id(&self, chain: &Chain, tx_meta: TxMeta) -> MmResult<(), Self::Error> { + async fn update_transfers_meta_by_token_addr_id( + &self, + chain: &Chain, + transfer_meta: TransferMeta, + ) -> MmResult<(), Self::Error> { let selfi = self.clone(); - let txs = selfi - .get_txs_by_token_addr_id(chain, tx_meta.token_address, tx_meta.token_id) + let transfers = selfi + .get_transfers_by_token_addr_id(chain, transfer_meta.token_address, transfer_meta.token_id) .await?; - for mut tx in txs.into_iter() { - tx.token_uri = tx_meta.token_uri.clone(); - tx.collection_name = tx_meta.collection_name.clone(); - tx.image_url = tx_meta.image_url.clone(); - tx.token_name = tx_meta.token_name.clone(); - drop_mutability!(tx); - selfi.update_tx_meta_by_hash(chain, tx).await?; + for mut transfer in transfers.into_iter() { + transfer.token_uri = transfer_meta.token_uri.clone(); + transfer.collection_name = transfer_meta.collection_name.clone(); + transfer.image_url = transfer_meta.image_url.clone(); + transfer.token_name = transfer_meta.token_name.clone(); + drop_mutability!(transfer); + selfi + .update_transfer_meta_by_hash_and_log_index(chain, transfer) + .await?; } Ok(()) } - async fn get_txs_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error> { + async fn get_transfers_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error> { let selfi = self.clone(); let chain = *chain; async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let sql_builder = get_txs_with_empty_meta_builder(&conn, &chain)?; + let sql_builder = get_transfers_with_empty_meta_builder(&conn, &chain)?; let token_addr_id_pair = sql_builder.query(token_address_id_from_row)?; Ok(token_addr_id_pair) }) diff --git a/mm2src/coins/nft/storage/wasm/nft_idb.rs b/mm2src/coins/nft/storage/wasm/nft_idb.rs index 287bcab30a..0d7758d61a 100644 --- a/mm2src/coins/nft/storage/wasm/nft_idb.rs +++ b/mm2src/coins/nft/storage/wasm/nft_idb.rs @@ -1,4 +1,4 @@ -use crate::nft::storage::wasm::wasm_storage::{LastScannedBlockTable, NftListTable, NftTxHistoryTable}; +use crate::nft::storage::wasm::wasm_storage::{LastScannedBlockTable, NftListTable, NftTransferHistoryTable}; use async_trait::async_trait; use mm2_db::indexed_db::InitDbResult; use mm2_db::indexed_db::{DbIdentifier, DbInstance, DbLocked, IndexedDb, IndexedDbBuilder}; @@ -19,7 +19,7 @@ impl DbInstance for NftCacheIDB { let inner = IndexedDbBuilder::new(db_id) .with_version(DB_VERSION) .with_table::() - .with_table::() + .with_table::() .with_table::() .build() .await?; diff --git a/mm2src/coins/nft/storage/wasm/wasm_storage.rs b/mm2src/coins/nft/storage/wasm/wasm_storage.rs index 6fb341c26e..058b6cdffd 100644 --- a/mm2src/coins/nft/storage/wasm/wasm_storage.rs +++ b/mm2src/coins/nft/storage/wasm/wasm_storage.rs @@ -1,10 +1,10 @@ use crate::eth::eth_addr_to_hex; use crate::nft::nft_structs::{Chain, ContractType, Nft, NftCtx, NftList, NftTransferHistory, NftsTransferHistoryList, - TransferStatus, TxMeta}; + TransferMeta, TransferStatus}; use crate::nft::storage::wasm::nft_idb::{NftCacheIDB, NftCacheIDBLocked}; use crate::nft::storage::wasm::{WasmNftCacheError, WasmNftCacheResult}; use crate::nft::storage::{get_offset_limit, CreateNftStorageError, NftListStorageOps, NftTokenAddrId, - NftTxHistoryFilters, NftTxHistoryStorageOps, RemoveNftResult}; + NftTransferHistoryFilters, NftTransferHistoryStorageOps, RemoveNftResult}; use async_trait::async_trait; use common::is_initial_upgrade; use mm2_core::mm_ctx::MmArc; @@ -52,54 +52,54 @@ impl IndexedDbNftStorage { }) } - fn take_txs_according_to_paging_opts( - mut txs: Vec, + fn take_transfers_according_to_paging_opts( + mut transfers: Vec, max: bool, limit: usize, page_number: Option, ) -> WasmNftCacheResult { - let total_count = txs.len(); - txs.sort_by(|a, b| b.block_timestamp.cmp(&a.block_timestamp)); + let total_count = transfers.len(); + transfers.sort_by(|a, b| b.block_timestamp.cmp(&a.block_timestamp)); let (offset, limit) = get_offset_limit(max, limit, page_number, total_count); Ok(NftsTransferHistoryList { - transfer_history: txs.into_iter().skip(offset).take(limit).collect(), + transfer_history: transfers.into_iter().skip(offset).take(limit).collect(), skipped: offset, total: total_count, }) } - fn take_txs_according_to_filters( - txs: I, - filters: Option, + fn take_transfers_according_to_filters( + transfers: I, + filters: Option, ) -> WasmNftCacheResult> where - I: Iterator, + I: Iterator, { - let mut filtered_txs = Vec::new(); - for tx_table in txs { - let tx = tx_details_from_item(tx_table)?; + let mut filtered_transfers = Vec::new(); + for transfers_table in transfers { + let transfer = transfer_details_from_item(transfers_table)?; if let Some(filters) = &filters { - if filters.is_status_match(&tx) && filters.is_date_match(&tx) { - filtered_txs.push(tx); + if filters.is_status_match(&transfer) && filters.is_date_match(&transfer) { + filtered_transfers.push(transfer); } } else { - filtered_txs.push(tx); + filtered_transfers.push(transfer); } } - Ok(filtered_txs) + Ok(filtered_transfers) } } -impl NftTxHistoryFilters { - fn is_status_match(&self, tx: &NftTransferHistory) -> bool { +impl NftTransferHistoryFilters { + fn is_status_match(&self, transfer: &NftTransferHistory) -> bool { (!self.receive && !self.send) - || (self.receive && tx.status == TransferStatus::Receive) - || (self.send && tx.status == TransferStatus::Send) + || (self.receive && transfer.status == TransferStatus::Receive) + || (self.send && transfer.status == TransferStatus::Send) } - fn is_date_match(&self, tx: &NftTransferHistory) -> bool { - self.from_date.map_or(true, |from| tx.block_timestamp >= from) - && self.to_date.map_or(true, |to| tx.block_timestamp <= to) + fn is_date_match(&self, transfer: &NftTransferHistory) -> bool { + self.from_date.map_or(true, |from| transfer.block_timestamp >= from) + && self.to_date.map_or(true, |to| transfer.block_timestamp <= to) } } @@ -318,48 +318,48 @@ impl NftListStorageOps for IndexedDbNftStorage { } #[async_trait] -impl NftTxHistoryStorageOps for IndexedDbNftStorage { +impl NftTransferHistoryStorageOps for IndexedDbNftStorage { type Error = WasmNftCacheError; async fn init(&self, _chain: &Chain) -> MmResult<(), Self::Error> { Ok(()) } async fn is_initialized(&self, _chain: &Chain) -> MmResult { Ok(true) } - async fn get_tx_history( + async fn get_transfer_history( &self, chains: Vec, max: bool, limit: usize, page_number: Option, - filters: Option, + filters: Option, ) -> MmResult { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; - let table = db_transaction.table::().await?; - let mut txs = Vec::new(); + let table = db_transaction.table::().await?; + let mut transfers = Vec::new(); for chain in chains { - let tx_tables = table + let transfer_tables = table .get_items("chain", chain.to_string()) .await? .into_iter() - .map(|(_item_id, tx)| tx); - let filtered = Self::take_txs_according_to_filters(tx_tables, filters)?; - txs.extend(filtered); + .map(|(_item_id, transfer)| transfer); + let filtered = Self::take_transfers_according_to_filters(transfer_tables, filters)?; + transfers.extend(filtered); } - Self::take_txs_according_to_paging_opts(txs, max, limit, page_number) + Self::take_transfers_according_to_paging_opts(transfers, max, limit, page_number) } - async fn add_txs_to_history(&self, _chain: &Chain, txs: I) -> MmResult<(), Self::Error> + async fn add_transfers_to_history(&self, _chain: &Chain, transfers: I) -> MmResult<(), Self::Error> where I: IntoIterator + Send + 'static, I::IntoIter: Send, { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; - let table = db_transaction.table::().await?; - for tx in txs { - let tx_item = NftTxHistoryTable::from_tx_history(&tx)?; - table.add_item(&tx_item).await?; + let table = db_transaction.table::().await?; + for transfer in transfers { + let transfer_item = NftTransferHistoryTable::from_transfer_history(&transfer)?; + table.add_item(&transfer_item).await?; } Ok(()) } @@ -367,24 +367,24 @@ impl NftTxHistoryStorageOps for IndexedDbNftStorage { async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; - let table = db_transaction.table::().await?; - get_last_block_from_table(chain, table, NftTxHistoryTable::CHAIN_BLOCK_NUMBER_INDEX).await + let table = db_transaction.table::().await?; + get_last_block_from_table(chain, table, NftTransferHistoryTable::CHAIN_BLOCK_NUMBER_INDEX).await } - async fn get_txs_from_block( + async fn get_transfers_from_block( &self, chain: &Chain, from_block: u64, ) -> MmResult, Self::Error> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; - let table = db_transaction.table::().await?; + let table = db_transaction.table::().await?; let items = table .cursor_builder() .only("chain", chain.to_string()) .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? .bound("block_number", BeBigUint::from(from_block), BeBigUint::from(u64::MAX)) - .open_cursor(NftTxHistoryTable::CHAIN_BLOCK_NUMBER_INDEX) + .open_cursor(NftTransferHistoryTable::CHAIN_BLOCK_NUMBER_INDEX) .await .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? .collect() @@ -393,13 +393,13 @@ impl NftTxHistoryStorageOps for IndexedDbNftStorage { let mut res = Vec::new(); for (_item_id, item) in items.into_iter() { - let tx = tx_details_from_item(item)?; - res.push(tx); + let transfer = transfer_details_from_item(item)?; + res.push(transfer); } Ok(res) } - async fn get_txs_by_token_addr_id( + async fn get_transfers_by_token_addr_id( &self, chain: &Chain, token_address: String, @@ -407,9 +407,9 @@ impl NftTxHistoryStorageOps for IndexedDbNftStorage { ) -> MmResult, Self::Error> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; - let table = db_transaction.table::().await?; + let table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftTxHistoryTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) .with_value(chain.to_string())? .with_value(&token_address)? .with_value(token_id.to_string())?; @@ -418,71 +418,83 @@ impl NftTxHistoryStorageOps for IndexedDbNftStorage { .get_items_by_multi_index(index_keys) .await? .into_iter() - .map(|(_item_id, item)| tx_details_from_item(item)) + .map(|(_item_id, item)| transfer_details_from_item(item)) .collect() } - async fn get_tx_by_tx_hash( + async fn get_transfer_by_tx_hash_and_log_index( &self, chain: &Chain, transaction_hash: String, + log_index: u32, ) -> MmResult, Self::Error> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; - let table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftTxHistoryTable::CHAIN_TX_HASH_INDEX) + let table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_INDEX) .with_value(chain.to_string())? - .with_value(&transaction_hash)?; + .with_value(&transaction_hash)? + .with_value(log_index)?; if let Some((_item_id, item)) = table.get_item_by_unique_multi_index(index_keys).await? { - Ok(Some(tx_details_from_item(item)?)) + Ok(Some(transfer_details_from_item(item)?)) } else { Ok(None) } } - async fn update_tx_meta_by_hash(&self, chain: &Chain, tx: NftTransferHistory) -> MmResult<(), Self::Error> { + async fn update_transfer_meta_by_hash_and_log_index( + &self, + chain: &Chain, + transfer: NftTransferHistory, + ) -> MmResult<(), Self::Error> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; - let table = db_transaction.table::().await?; + let table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftTxHistoryTable::CHAIN_TX_HASH_INDEX) + let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_INDEX) .with_value(chain.to_string())? - .with_value(&tx.common.transaction_hash)?; + .with_value(&transfer.common.transaction_hash)? + .with_value(transfer.common.log_index)?; - let item = NftTxHistoryTable::from_tx_history(&tx)?; + let item = NftTransferHistoryTable::from_transfer_history(&transfer)?; table.replace_item_by_unique_multi_index(index_keys, &item).await?; Ok(()) } - async fn update_txs_meta_by_token_addr_id(&self, chain: &Chain, tx_meta: TxMeta) -> MmResult<(), Self::Error> { - let txs: Vec = self - .get_txs_by_token_addr_id(chain, tx_meta.token_address, tx_meta.token_id) + async fn update_transfers_meta_by_token_addr_id( + &self, + chain: &Chain, + transfer_meta: TransferMeta, + ) -> MmResult<(), Self::Error> { + let transfers: Vec = self + .get_transfers_by_token_addr_id(chain, transfer_meta.token_address, transfer_meta.token_id) .await?; let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; - let table = db_transaction.table::().await?; - for mut tx in txs { - tx.token_uri = tx_meta.token_uri.clone(); - tx.collection_name = tx_meta.collection_name.clone(); - tx.image_url = tx_meta.image_url.clone(); - tx.token_name = tx_meta.token_name.clone(); - drop_mutability!(tx); - - let index_keys = MultiIndex::new(NftTxHistoryTable::CHAIN_TX_HASH_INDEX) + let table = db_transaction.table::().await?; + for mut transfer in transfers { + transfer.token_uri = transfer_meta.token_uri.clone(); + transfer.collection_name = transfer_meta.collection_name.clone(); + transfer.image_url = transfer_meta.image_url.clone(); + transfer.token_name = transfer_meta.token_name.clone(); + drop_mutability!(transfer); + + let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_INDEX) .with_value(chain.to_string())? - .with_value(&tx.common.transaction_hash)?; + .with_value(&transfer.common.transaction_hash)? + .with_value(transfer.common.log_index)?; - let item = NftTxHistoryTable::from_tx_history(&tx)?; + let item = NftTransferHistoryTable::from_transfer_history(&transfer)?; table.replace_item_by_unique_multi_index(index_keys, &item).await?; } Ok(()) } - async fn get_txs_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error> { + async fn get_transfers_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; - let table = db_transaction.table::().await?; + let table = db_transaction.table::().await?; let items = table .cursor_builder() .only("chain", chain.to_string()) @@ -553,7 +565,7 @@ impl BlockNumberTable for NftListTable { fn get_block_number(&self) -> &BeBigUint { &self.block_number } } -impl BlockNumberTable for NftTxHistoryTable { +impl BlockNumberTable for NftTransferHistoryTable { fn get_block_number(&self) -> &BeBigUint { &self.block_number } } @@ -607,8 +619,9 @@ impl TableSignature for NftListTable { } #[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct NftTxHistoryTable { +pub(crate) struct NftTransferHistoryTable { transaction_hash: String, + log_index: u32, chain: String, block_number: BeBigUint, block_timestamp: BeBigUint, @@ -624,36 +637,38 @@ pub(crate) struct NftTxHistoryTable { details_json: Json, } -impl NftTxHistoryTable { +impl NftTransferHistoryTable { const CHAIN_TOKEN_ADD_TOKEN_ID_INDEX: &str = "chain_token_add_token_id_index"; - const CHAIN_TX_HASH_INDEX: &str = "chain_tx_hash_index"; + const CHAIN_TX_HASH_LOG_INDEX_INDEX: &str = "chain_tx_hash_log_index_index"; const CHAIN_BLOCK_NUMBER_INDEX: &str = "chain_block_number_index"; - fn from_tx_history(tx: &NftTransferHistory) -> WasmNftCacheResult { - let details_json = json::to_value(tx).map_to_mm(|e| WasmNftCacheError::ErrorSerializing(e.to_string()))?; - Ok(NftTxHistoryTable { - transaction_hash: tx.common.transaction_hash.clone(), - chain: tx.chain.to_string(), - block_number: BeBigUint::from(tx.block_number), - block_timestamp: BeBigUint::from(tx.block_timestamp), - contract_type: tx.contract_type, - token_address: eth_addr_to_hex(&tx.common.token_address), - token_id: tx.common.token_id.to_string(), - status: tx.status, - amount: tx.common.amount.to_string(), - token_uri: tx.token_uri.clone(), - collection_name: tx.collection_name.clone(), - image_url: tx.image_url.clone(), - token_name: tx.token_name.clone(), + fn from_transfer_history(transfer: &NftTransferHistory) -> WasmNftCacheResult { + let details_json = + json::to_value(transfer).map_to_mm(|e| WasmNftCacheError::ErrorSerializing(e.to_string()))?; + Ok(NftTransferHistoryTable { + transaction_hash: transfer.common.transaction_hash.clone(), + log_index: transfer.common.log_index, + chain: transfer.chain.to_string(), + block_number: BeBigUint::from(transfer.block_number), + block_timestamp: BeBigUint::from(transfer.block_timestamp), + contract_type: transfer.contract_type, + token_address: eth_addr_to_hex(&transfer.common.token_address), + token_id: transfer.common.token_id.to_string(), + status: transfer.status, + amount: transfer.common.amount.to_string(), + token_uri: transfer.token_uri.clone(), + collection_name: transfer.collection_name.clone(), + image_url: transfer.image_url.clone(), + token_name: transfer.token_name.clone(), details_json, }) } } -impl TableSignature for NftTxHistoryTable { - fn table_name() -> &'static str { "nft_tx_history_cache_table" } +impl TableSignature for NftTransferHistoryTable { + fn table_name() -> &'static str { "nft_transfer_history_cache_table" } fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if is_initial_upgrade(old_version, new_version) { @@ -663,7 +678,11 @@ impl TableSignature for NftTxHistoryTable { &["chain", "token_address", "token_id"], false, )?; - table.create_multi_index(Self::CHAIN_TX_HASH_INDEX, &["chain", "transaction_hash"], true)?; + table.create_multi_index( + Self::CHAIN_TX_HASH_LOG_INDEX_INDEX, + &["chain", "transaction_hash", "log_index"], + true, + )?; table.create_multi_index(Self::CHAIN_BLOCK_NUMBER_INDEX, &["chain", "block_number"], false)?; table.create_index("block_number", false)?; table.create_index("chain", false)?; @@ -694,6 +713,6 @@ fn nft_details_from_item(item: NftListTable) -> WasmNftCacheResult { json::from_value(item.details_json).map_to_mm(|e| WasmNftCacheError::ErrorDeserializing(e.to_string())) } -fn tx_details_from_item(item: NftTxHistoryTable) -> WasmNftCacheResult { +fn transfer_details_from_item(item: NftTransferHistoryTable) -> WasmNftCacheResult { json::from_value(item.details_json).map_to_mm(|e| WasmNftCacheError::ErrorDeserializing(e.to_string())) } From 3cbb54def955f3c4dc9ee7a3d9f631ed45cb5bd2 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Tue, 15 Aug 2023 07:33:16 +0300 Subject: [PATCH 08/17] chore(release): bump mm2 version to 1.0.7-beta (#1937) --- 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 eba3d3c01d..8bdd4d52cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4079,7 +4079,7 @@ dependencies = [ [[package]] name = "mm2_bin_lib" -version = "1.0.6-beta" +version = "1.0.7-beta" dependencies = [ "chrono", "common", diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index e708e460aa..bd3bf5aac9 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 = "1.0.6-beta" +version = "1.0.7-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 "] edition = "2018" default-run = "mm2" From 483f04cddcb8e942426701215cbcab8a4b4d2de4 Mon Sep 17 00:00:00 2001 From: Artem Vitae Date: Wed, 23 Aug 2023 18:34:16 +0700 Subject: [PATCH 09/17] feat(trading-proto-upgrade): UTXO PoC + State machine refactor (#1927) SwapOpsV2 trait was added containing methods of the new protocol (WIP). SwapOpsV2 was implemented for UtxoStandardCoin. Dockerized integration tests added, sending and spending/refunding "dex fee + premium" UTXO. State machine was refactored as a preparation step for StorableStateMachine pattern extension. --------- Co-authored-by: Artem Vitae --- mm2src/coins/eth.rs | 21 +- mm2src/coins/eth/eth_tests.rs | 10 +- mm2src/coins/lightning.rs | 26 +- mm2src/coins/lp_coins.rs | 135 +++- mm2src/coins/qrc20.rs | 42 +- mm2src/coins/solana.rs | 22 +- mm2src/coins/solana/spl.rs | 16 +- mm2src/coins/tendermint/tendermint_coin.rs | 25 +- mm2src/coins/tendermint/tendermint_token.rs | 16 +- .../tendermint/tendermint_tx_history_v2.rs | 51 +- mm2src/coins/test_coin.rs | 12 +- mm2src/coins/utxo.rs | 3 + mm2src/coins/utxo/bch.rs | 18 +- mm2src/coins/utxo/qtum.rs | 10 +- mm2src/coins/utxo/slp.rs | 38 +- mm2src/coins/utxo/swap_proto_v2_scripts.rs | 50 ++ mm2src/coins/utxo/utxo_common.rs | 578 ++++++++++++++---- mm2src/coins/utxo/utxo_standard.rs | 71 ++- mm2src/coins/utxo/utxo_tx_history_v2.rs | 78 ++- mm2src/coins/utxo_signer/src/with_key_pair.rs | 18 +- mm2src/coins/z_coin.rs | 73 +-- mm2src/coins/z_coin/z_coin_native_tests.rs | 2 +- mm2src/common/patterns/state_machine.rs | 109 ++-- mm2src/mm2_main/src/lp_swap/maker_swap.rs | 43 +- mm2src/mm2_main/src/lp_swap/swap_watcher.rs | 45 +- mm2src/mm2_main/src/lp_swap/taker_swap.rs | 43 +- .../tests/docker_tests/docker_tests_common.rs | 3 + .../tests/docker_tests/docker_tests_inner.rs | 10 +- mm2src/mm2_main/tests/docker_tests/mod.rs | 1 + .../tests/docker_tests/qrc20_tests.rs | 25 +- .../tests/docker_tests/swap_proto_v2_tests.rs | 129 ++++ mm2src/mm2_net/src/wasm_ws.rs | 38 +- 32 files changed, 1235 insertions(+), 526 deletions(-) create mode 100644 mm2src/coins/utxo/swap_proto_v2_scripts.rs create mode 100644 mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 8e6b762a4b..b9cf3e3fad 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -110,6 +110,7 @@ use crate::nft::{find_wallet_nft_amount, WithdrawNftResult}; use v2_activation::{build_address_and_priv_key_policy, EthActivationV2Error}; mod nonce; +use crate::TransactionResult; use nonce::ParityNonce; /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol @@ -1095,18 +1096,18 @@ impl SwapOps for EthCoin { ) } - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - Box::new( - self.refund_hash_time_locked_payment(taker_refunds_payment_args) - .map(TransactionEnum::from), - ) + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + self.refund_hash_time_locked_payment(taker_refunds_payment_args) + .map(TransactionEnum::from) + .compat() + .await } - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - Box::new( - self.refund_hash_time_locked_payment(maker_refunds_payment_args) - .map(TransactionEnum::from), - ) + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + self.refund_hash_time_locked_payment(maker_refunds_payment_args) + .map(TransactionEnum::from) + .compat() + .await } fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentFut<()> { diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 5c2d844381..5ab37c7e9b 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -380,10 +380,7 @@ fn send_and_refund_erc20_payment() { swap_unique_data: &[], watcher_reward: false, }; - let refund = coin - .send_maker_refunds_payment(maker_refunds_payment_args) - .wait() - .unwrap(); + let refund = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); log!("{:?}", refund); let status = block_on( @@ -470,10 +467,7 @@ fn send_and_refund_eth_payment() { swap_unique_data: &[], watcher_reward: false, }; - let refund = coin - .send_maker_refunds_payment(maker_refunds_payment_args) - .wait() - .unwrap(); + let refund = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); log!("{:?}", refund); diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 02a0c41cfd..979276d230 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -22,10 +22,10 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, C RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, TransactionEnum, TransactionErr, - TransactionFut, TxMarshalingErr, UnexpectedDerivationMethod, UtxoStandardCoin, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, - WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + TransactionFut, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, UtxoStandardCoin, + ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, + WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use bitcoin::bech32::ToBase32; @@ -660,16 +660,22 @@ impl SwapOps for LightningCoin { self.spend_swap_payment(taker_spends_payment_args) } - fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut { - Box::new(futures01::future::err(TransactionErr::Plain( + async fn send_taker_refunds_payment( + &self, + _taker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { + Err(TransactionErr::Plain( "Doesn't need transaction broadcast to refund lightning HTLC".into(), - ))) + )) } - fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut { - Box::new(futures01::future::err(TransactionErr::Plain( + async fn send_maker_refunds_payment( + &self, + _maker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { + Err(TransactionErr::Plain( "Doesn't need transaction broadcast to refund lightning HTLC".into(), - ))) + )) } // Todo: This validates the dummy fee for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 8f123d6066..bb8bc360fc 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -291,6 +291,7 @@ pub mod z_coin; use z_coin::{ZCoin, ZcoinProtocolInfo}; pub type TransactionFut = Box + Send>; +pub type TransactionResult = Result; pub type BalanceResult = Result>; pub type BalanceFut = Box> + Send>; pub type NonZeroBalanceFut = Box> + Send>; @@ -310,6 +311,9 @@ pub type RawTransactionResult = Result = Box> + Send + 'a>; pub type RefundResult = Result>; +pub type GenAndSignDexFeeSpendResult = MmResult; +pub type ValidateDexFeeResult = MmResult<(), ValidateDexFeeError>; +pub type ValidateDexFeeSpendPreimageResult = MmResult<(), ValidateDexFeeSpendPreimageError>; pub type IguanaPrivKey = Secp256k1Secret; @@ -821,9 +825,9 @@ pub trait SwapOps { fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut; - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut; + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult; - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut; + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult; fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentFut<()>; @@ -989,6 +993,133 @@ pub trait WatcherOps { ) -> Result, MmError>; } +pub struct SendDexFeeWithPremiumArgs<'a> { + pub time_lock: u32, + pub secret_hash: &'a [u8], + pub other_pub: &'a [u8], + pub dex_fee_amount: BigDecimal, + pub premium_amount: BigDecimal, + pub swap_unique_data: &'a [u8], +} + +pub struct ValidateDexFeeArgs<'a> { + pub dex_fee_tx: &'a [u8], + pub time_lock: u32, + pub secret_hash: &'a [u8], + pub other_pub: &'a [u8], + pub dex_fee_amount: BigDecimal, + pub premium_amount: BigDecimal, + pub swap_unique_data: &'a [u8], +} + +pub struct GenDexFeeSpendArgs<'a> { + pub dex_fee_tx: &'a [u8], + pub time_lock: u32, + pub secret_hash: &'a [u8], + pub maker_pub: &'a [u8], + pub taker_pub: &'a [u8], + pub dex_fee_pub: &'a [u8], + pub dex_fee_amount: BigDecimal, + pub premium_amount: BigDecimal, +} + +pub struct TxPreimageWithSig { + preimage: Vec, + signature: Vec, +} + +#[derive(Debug)] +pub enum TxGenError { + Rpc(String), + NumConversion(String), + AddressDerivation(String), + TxDeserialization(String), + InvalidPubkey(String), + Signing(String), + MinerFeeExceedsPremium { miner_fee: BigDecimal, premium: BigDecimal }, + Legacy(String), +} + +impl From for TxGenError { + fn from(err: UtxoRpcError) -> Self { TxGenError::Rpc(err.to_string()) } +} + +impl From for TxGenError { + fn from(err: NumConversError) -> Self { TxGenError::NumConversion(err.to_string()) } +} + +impl From for TxGenError { + fn from(err: UtxoSignWithKeyPairError) -> Self { TxGenError::Signing(err.to_string()) } +} + +#[derive(Debug)] +pub enum ValidateDexFeeError { + InvalidDestinationOrAmount(String), + InvalidPubkey(String), + NumConversion(String), + Rpc(String), + TxBytesMismatch { from_rpc: BytesJson, actual: BytesJson }, + TxDeserialization(String), + TxLacksOfOutputs, +} + +impl From for ValidateDexFeeError { + fn from(err: NumConversError) -> Self { ValidateDexFeeError::NumConversion(err.to_string()) } +} + +impl From for ValidateDexFeeError { + fn from(err: UtxoRpcError) -> Self { ValidateDexFeeError::Rpc(err.to_string()) } +} + +#[derive(Debug)] +pub enum ValidateDexFeeSpendPreimageError { + InvalidPubkey(String), + InvalidTakerSignature, + InvalidPreimage(String), + SignatureVerificationFailure(String), + TxDeserialization(String), + TxGenError(String), +} + +impl From for ValidateDexFeeSpendPreimageError { + fn from(err: UtxoSignWithKeyPairError) -> Self { + ValidateDexFeeSpendPreimageError::SignatureVerificationFailure(err.to_string()) + } +} + +impl From for ValidateDexFeeSpendPreimageError { + fn from(err: TxGenError) -> Self { ValidateDexFeeSpendPreimageError::TxGenError(format!("{:?}", err)) } +} + +#[async_trait] +pub trait SwapOpsV2 { + async fn send_dex_fee_with_premium(&self, args: SendDexFeeWithPremiumArgs<'_>) -> TransactionResult; + + async fn validate_dex_fee_with_premium(&self, args: ValidateDexFeeArgs<'_>) -> ValidateDexFeeResult; + + async fn refund_dex_fee_with_premium(&self, args: RefundPaymentArgs<'_>) -> TransactionResult; + + async fn gen_and_sign_dex_fee_spend_preimage( + &self, + args: &GenDexFeeSpendArgs<'_>, + swap_unique_data: &[u8], + ) -> GenAndSignDexFeeSpendResult; + + async fn validate_dex_fee_spend_preimage( + &self, + gen_args: &GenDexFeeSpendArgs<'_>, + preimage: &TxPreimageWithSig, + ) -> ValidateDexFeeSpendPreimageResult; + + async fn sign_and_broadcast_dex_fee_spend( + &self, + preimage: &TxPreimageWithSig, + gen_args: &GenDexFeeSpendArgs<'_>, + secret: &[u8], + swap_unique_data: &[u8], + ) -> TransactionResult; +} + /// Operations that coins have independently from the MarketMaker. /// That is, things implemented by the coin wallets or public coin services. pub trait MarketCoinOps { diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index d46371ed6f..d00139b56c 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -22,12 +22,12 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, Coi RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionType, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, - WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; + TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, + TransactionResult, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, + ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, + WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; @@ -832,33 +832,23 @@ impl SwapOps for Qrc20Coin { } #[inline] - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { let payment_tx: UtxoTx = - try_tx_fus!(deserialize(taker_refunds_payment_args.payment_tx).map_err(|e| ERRL!("{:?}", e))); - let swap_contract_address = try_tx_fus!(taker_refunds_payment_args.swap_contract_address.try_to_address()); + try_tx_s!(deserialize(taker_refunds_payment_args.payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_s!(taker_refunds_payment_args.swap_contract_address.try_to_address()); - let selfi = self.clone(); - let fut = async move { - selfi - .refund_hash_time_locked_payment(swap_contract_address, payment_tx) - .await - }; - Box::new(fut.boxed().compat()) + self.refund_hash_time_locked_payment(swap_contract_address, payment_tx) + .await } #[inline] - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { let payment_tx: UtxoTx = - try_tx_fus!(deserialize(maker_refunds_payment_args.payment_tx).map_err(|e| ERRL!("{:?}", e))); - let swap_contract_address = try_tx_fus!(maker_refunds_payment_args.swap_contract_address.try_to_address()); + try_tx_s!(deserialize(maker_refunds_payment_args.payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_s!(maker_refunds_payment_args.swap_contract_address.try_to_address()); - let selfi = self.clone(); - let fut = async move { - selfi - .refund_hash_time_locked_payment(swap_contract_address, payment_tx) - .await - }; - Box::new(fut.boxed().compat()) + self.refund_hash_time_locked_payment(swap_contract_address, payment_tx) + .await } #[inline] diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs index 90e914711c..12ca7379e0 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -8,12 +8,12 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, - TransactionDetails, TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, - ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, - ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, - WaitForHTLCTxSpendArgs, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest, - WithdrawResult}; + TransactionDetails, TransactionFut, TransactionResult, TransactionType, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + VerificationResult, WaitForHTLCTxSpendArgs, WatcherReward, WatcherRewardError, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use base58::ToBase58; use bincode::{deserialize, serialize}; @@ -480,11 +480,17 @@ impl SwapOps for SolanaCoin { unimplemented!() } - fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + async fn send_taker_refunds_payment( + &self, + _taker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } - fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + async fn send_maker_refunds_payment( + &self, + _maker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs index d5d5b3250e..06b43688d7 100644 --- a/mm2src/coins/solana/spl.rs +++ b/mm2src/coins/solana/spl.rs @@ -7,8 +7,8 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPayment PaymentInstructions, PaymentInstructionsErr, RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SolanaCoin, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionFut, TransactionType, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionFut, TransactionResult, + TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, @@ -303,11 +303,19 @@ impl SwapOps for SplToken { unimplemented!() } - fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + async fn send_taker_refunds_payment( + &self, + _taker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } - fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { todo!() } + async fn send_maker_refunds_payment( + &self, + _maker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { + todo!() + } fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { unimplemented!() } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 5279e4fbef..09758242b9 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -22,12 +22,13 @@ use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, RefundError, RefundPaymentArgs, RefundResult, RpcCommonOps, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionType, - TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, - VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, - WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; + TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, + TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, + WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, + WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; @@ -2426,16 +2427,16 @@ impl SwapOps for TendermintCoin { Box::new(fut.boxed().compat()) } - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - Box::new(futures01::future::err(TransactionErr::Plain( + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + Err(TransactionErr::Plain( "Doesn't need transaction broadcast to refund IRIS HTLC".into(), - ))) + )) } - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - Box::new(futures01::future::err(TransactionErr::Plain( + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + Err(TransactionErr::Plain( "Doesn't need transaction broadcast to refund IRIS HTLC".into(), - ))) + )) } fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index f4c128114d..09417c210f 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -13,8 +13,8 @@ use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFu RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, - TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionType, TxFeeDetails, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, + TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, @@ -276,16 +276,16 @@ impl SwapOps for TendermintToken { .send_taker_spends_maker_payment(taker_spends_payment_args) } - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - Box::new(futures01::future::err(TransactionErr::Plain( + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + Err(TransactionErr::Plain( "Doesn't need transaction broadcast to be refunded".into(), - ))) + )) } - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - Box::new(futures01::future::err(TransactionErr::Plain( + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + Err(TransactionErr::Plain( "Doesn't need transaction broadcast to be refunded".into(), - ))) + )) } fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index e4851a626a..5b16fe04e3 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -10,6 +10,7 @@ use bitcrypto::sha256; use common::executor::Timer; use common::log; use common::state_machine::prelude::*; +use common::state_machine::StateMachineTrait; use cosmrs::tendermint::abci::Code as TxCode; use cosmrs::tendermint::abci::Event; use cosmrs::tx::Fee; @@ -98,7 +99,7 @@ impl CoinWithTxHistoryV2 for TendermintToken { } } -struct TendermintTxHistoryCtx { +struct TendermintTxHistoryStateMachine { coin: Coin, storage: Storage, balances: AllBalancesResult, @@ -106,6 +107,12 @@ struct TendermintTxHistoryCtx last_spent_page: u32, } +impl StateMachineTrait + for TendermintTxHistoryStateMachine +{ + type Result = (); +} + struct TendermintInit { phantom: std::marker::PhantomData<(Coin, Storage)>, } @@ -181,10 +188,12 @@ where Coin: CoinCapabilities, Storage: TxHistoryStorage, { - type Ctx = TendermintTxHistoryCtx; - type Result = (); + type StateMachine = TendermintTxHistoryStateMachine; - async fn on_changed(mut self: Box, _ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed( + mut self: Box, + _ctx: &mut TendermintTxHistoryStateMachine, + ) -> StateResult> { Timer::sleep(30.).await; // retry history fetching process from last saved block @@ -233,10 +242,12 @@ where Coin: CoinCapabilities, Storage: TxHistoryStorage, { - type Ctx = TendermintTxHistoryCtx; - type Result = (); + type StateMachine = TendermintTxHistoryStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed( + self: Box, + ctx: &mut TendermintTxHistoryStateMachine, + ) -> StateResult> { loop { Timer::sleep(30.).await; @@ -268,10 +279,12 @@ where Coin: CoinCapabilities, Storage: TxHistoryStorage, { - type Ctx = TendermintTxHistoryCtx; - type Result = (); + type StateMachine = TendermintTxHistoryStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed( + self: Box, + ctx: &mut TendermintTxHistoryStateMachine, + ) -> StateResult> { const TX_PAGE_SIZE: u8 = 50; const DEFAULT_TRANSFER_EVENT_COUNT: usize = 1; @@ -821,10 +834,12 @@ where Coin: CoinCapabilities, Storage: TxHistoryStorage, { - type Ctx = TendermintTxHistoryCtx; - type Result = (); + type StateMachine = TendermintTxHistoryStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed( + self: Box, + ctx: &mut TendermintTxHistoryStateMachine, + ) -> StateResult> { const INITIAL_SEARCH_HEIGHT: u64 = 0; ctx.coin.set_history_sync_state(HistorySyncState::NotStarted); @@ -855,10 +870,9 @@ where Coin: CoinCapabilities, Storage: TxHistoryStorage, { - type Ctx = TendermintTxHistoryCtx; - type Result = (); + type StateMachine = TendermintTxHistoryStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> Self::Result { + async fn on_changed(self: Box, ctx: &mut TendermintTxHistoryStateMachine) -> () { log::info!( "Stopping tx history fetching for {}. Reason: {:?}", ctx.coin.ticker(), @@ -887,7 +901,7 @@ pub async fn tendermint_history_loop( }, }; - let ctx = TendermintTxHistoryCtx { + let mut state_machine = TendermintTxHistoryStateMachine { coin, storage, balances, @@ -895,6 +909,5 @@ pub async fn tendermint_history_loop( last_spent_page: 1, }; - let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ctx); - state_machine.run(TendermintInit::new()).await; + state_machine.run(Box::new(TendermintInit::new())).await; } diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 1fd9694fc7..c731b12b01 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -7,7 +7,7 @@ use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPay NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TxMarshalingErr, UnexpectedDerivationMethod, + TradePreimageResult, TradePreimageValue, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, @@ -114,11 +114,17 @@ impl SwapOps for TestCoin { unimplemented!() } - fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + async fn send_taker_refunds_payment( + &self, + _taker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } - fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + async fn send_maker_refunds_payment( + &self, + _maker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 0783d6cdec..8c0459e63c 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -31,6 +31,7 @@ pub mod qtum; pub mod rpc_clients; pub mod slp; pub mod spv; +pub mod swap_proto_v2_scripts; pub mod utxo_block_header_storage; pub mod utxo_builder; pub mod utxo_common; @@ -973,6 +974,8 @@ pub trait UtxoCommonOps: utxo_tx_map: &'b mut HistoryUtxoTxMap, ) -> UtxoRpcResult<&'b mut HistoryUtxoTx>; + /// Generates a transaction spending P2SH vout (typically, with 0 index [`utxo_common::DEFAULT_SWAP_VOUT`]) of input.prev_transaction + /// Works only if single signature is required! async fn p2sh_spending_tx(&self, input: utxo_common::P2SHSpendingTxInput<'_>) -> Result; /// Loads verbose transactions from cache or requests it using RPC client. diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index b089af276d..8d0592797d 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -15,11 +15,11 @@ use crate::{BlockHeightAndTime, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBal PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradePreimageValue, TransactionFut, - TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawFut}; + TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, + WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; use common::log::warn; use derive_more::Display; @@ -864,13 +864,13 @@ impl SwapOps for BchCoin { } #[inline] - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - utxo_common::send_taker_refunds_payment(self.clone(), taker_refunds_payment_args) + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + utxo_common::send_taker_refunds_payment(self.clone(), taker_refunds_payment_args).await } #[inline] - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - utxo_common::send_maker_refunds_payment(self.clone(), maker_refunds_payment_args) + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + utxo_common::send_maker_refunds_payment(self.clone(), maker_refunds_payment_args).await } fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 9640e66184..cfe3f2f4c1 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -29,7 +29,7 @@ use crate::{eth, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithD NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, StakingInfosFut, - SwapOps, TakerSwapMakerCoin, TradePreimageValue, TransactionFut, TxMarshalingErr, + SwapOps, TakerSwapMakerCoin, TradePreimageValue, TransactionFut, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, @@ -550,13 +550,13 @@ impl SwapOps for QtumCoin { } #[inline] - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - utxo_common::send_taker_refunds_payment(self.clone(), taker_refunds_payment_args) + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + utxo_common::send_taker_refunds_payment(self.clone(), taker_refunds_payment_args).await } #[inline] - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - utxo_common::send_maker_refunds_payment(self.clone(), maker_refunds_payment_args) + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + utxo_common::send_maker_refunds_payment(self.clone(), maker_refunds_payment_args).await } fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 9d8d968b53..8f996fdf0f 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -20,7 +20,7 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, C RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, - TransactionEnum, TransactionErr, TransactionFut, TxFeeDetails, TxMarshalingErr, + TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, @@ -1285,40 +1285,32 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { let tx = taker_refunds_payment_args.payment_tx.to_owned(); - let maker_pub = try_tx_fus!(Public::from_slice(taker_refunds_payment_args.other_pubkey)); + let maker_pub = try_tx_s!(Public::from_slice(taker_refunds_payment_args.other_pubkey)); let secret_hash = taker_refunds_payment_args.secret_hash.to_owned(); let htlc_keypair = self.derive_htlc_key_pair(taker_refunds_payment_args.swap_unique_data); - let coin = self.clone(); let time_lock = taker_refunds_payment_args.time_lock; - let fut = async move { - let tx = try_s!( - coin.refund_htlc(&tx, &maker_pub, time_lock, &secret_hash, &htlc_keypair) - .await - ); - Ok(tx.into()) - }; - Box::new(fut.boxed().compat().map_err(TransactionErr::Plain)) + let tx = try_tx_s!( + self.refund_htlc(&tx, &maker_pub, time_lock, &secret_hash, &htlc_keypair) + .await + ); + Ok(tx.into()) } - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { let tx = maker_refunds_payment_args.payment_tx.to_owned(); - let taker_pub = try_tx_fus!(Public::from_slice(maker_refunds_payment_args.other_pubkey)); + let taker_pub = try_tx_s!(Public::from_slice(maker_refunds_payment_args.other_pubkey)); let secret_hash = maker_refunds_payment_args.secret_hash.to_owned(); let htlc_keypair = self.derive_htlc_key_pair(maker_refunds_payment_args.swap_unique_data); - let coin = self.clone(); let time_lock = maker_refunds_payment_args.time_lock; - let fut = async move { - let tx = try_tx_s!( - coin.refund_htlc(&tx, &taker_pub, time_lock, &secret_hash, &htlc_keypair) - .await - ); - Ok(tx.into()) - }; - Box::new(fut.boxed().compat()) + let tx = try_tx_s!( + self.refund_htlc(&tx, &taker_pub, time_lock, &secret_hash, &htlc_keypair) + .await + ); + Ok(tx.into()) } fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { diff --git a/mm2src/coins/utxo/swap_proto_v2_scripts.rs b/mm2src/coins/utxo/swap_proto_v2_scripts.rs new file mode 100644 index 0000000000..d204f7e26b --- /dev/null +++ b/mm2src/coins/utxo/swap_proto_v2_scripts.rs @@ -0,0 +1,50 @@ +/// This module contains functions building Bitcoins scripts for the "Swap protocol upgrade" feature +/// For more info, see https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895 +use bitcrypto::ripemd160; +use keys::Public; +use script::{Builder, Opcode, Script}; + +/// Builds a script for refundable dex_fee + premium taker transaction +pub fn dex_fee_script(time_lock: u32, secret_hash: &[u8], pub_0: &Public, pub_1: &Public) -> Script { + let mut builder = Builder::default() + // Dex fee refund path, same lock time as for taker payment + .push_opcode(Opcode::OP_IF) + .push_bytes(&time_lock.to_le_bytes()) + .push_opcode(Opcode::OP_CHECKLOCKTIMEVERIFY) + .push_opcode(Opcode::OP_DROP) + .push_bytes(pub_0) + .push_opcode(Opcode::OP_CHECKSIG) + // Dex fee redeem path, Maker needs to reveal the secret to prevent case of getting + // the premium but not proceeding with spending the taker payment + .push_opcode(Opcode::OP_ELSE) + .push_opcode(Opcode::OP_SIZE) + .push_bytes(&[32]) + .push_opcode(Opcode::OP_EQUALVERIFY) + .push_opcode(Opcode::OP_HASH160); + + if secret_hash.len() == 32 { + builder = builder.push_bytes(ripemd160(secret_hash).as_slice()); + } else { + builder = builder.push_bytes(secret_hash); + } + + builder + .push_opcode(Opcode::OP_EQUALVERIFY) + .push_opcode(Opcode::OP_2) + .push_bytes(pub_0) + .push_bytes(pub_1) + .push_opcode(Opcode::OP_2) + .push_opcode(Opcode::OP_CHECKMULTISIG) + .push_opcode(Opcode::OP_ENDIF) + .into_script() +} + +#[cfg(test)] +mod swap_proto_v2_scripts_tests { + use super::*; + + #[test] + fn it_builds_the_dex_fee_script() { + let _script = dex_fee_script(1689069073, &[0; 20], &Public::default(), &Public::default()); + } +} diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index b28915eec9..33b705599c 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -15,15 +15,18 @@ use crate::utxo::spv::SimplePaymentVerification; use crate::utxo::tx_cache::TxCacheResult; use crate::utxo::utxo_withdraw::{InitUtxoWithdraw, StandardUtxoWithdraw, UtxoWithdraw}; use crate::watcher_common::validate_watcher_reward; -use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, GetWithdrawSenderAddress, - HDAccountAddressId, RawTransactionError, RawTransactionRequest, RawTransactionRes, RefundPaymentArgs, - RewardTarget, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TradePreimageValue, TransactionFut, - TxFeeDetails, TxMarshalingErr, ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, - ValidatePaymentInput, VerificationError, VerificationResult, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFrom, WithdrawResult, - WithdrawSenderAddress, EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, - INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; +use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, GenAndSignDexFeeSpendResult, + GenDexFeeSpendArgs, GetWithdrawSenderAddress, HDAccountAddressId, RawTransactionError, + RawTransactionRequest, RawTransactionRes, RefundPaymentArgs, RewardTarget, SearchForSwapTxSpendInput, + SendDexFeeWithPremiumArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, + SignatureResult, SpendPaymentArgs, SwapOps, TradePreimageValue, TransactionFut, TransactionResult, + TxFeeDetails, TxGenError, TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, ValidateDexFeeArgs, + ValidateDexFeeError, ValidateDexFeeResult, ValidateDexFeeSpendPreimageError, + ValidateDexFeeSpendPreimageResult, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, + VerificationError, VerificationResult, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawFrom, WithdrawResult, WithdrawSenderAddress, + EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, + INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; use crate::{MmCoinEnum, WatcherReward, WatcherRewardError}; pub use bitcrypto::{dhash160, sha256, ChecksumType}; use bitcrypto::{dhash256, ripemd160}; @@ -55,7 +58,7 @@ use std::cmp::Ordering; use std::collections::hash_map::{Entry, HashMap}; use std::str::FromStr; use std::sync::atomic::Ordering as AtomicOrdering; -use utxo_signer::with_key_pair::p2sh_spend; +use utxo_signer::with_key_pair::{calc_and_sign_sighash, p2sh_spend, signature_hash_to_sign}; use utxo_signer::UtxoSignerOps; pub use chain::Transaction as UtxoTx; @@ -1092,11 +1095,25 @@ pub struct P2SHSpendingTxInput<'a> { keypair: &'a KeyPair, } -pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxInput<'_>) -> Result { - if input.prev_transaction.outputs.is_empty() { - return ERR!("Transaction doesn't have any output"); +enum LocktimeSetting { + CalcByHtlcLocktime(u32), + UseExact(u32), +} + +async fn p2sh_spending_tx_preimage( + coin: &T, + prev_tx: &UtxoTx, + lock_time: LocktimeSetting, + sequence: u32, + outputs: Vec, +) -> Result { + if prev_tx.outputs.is_empty() { + return ERR!("Previous transaction doesn't have any output"); } - let lock_time = try_s!(coin.p2sh_tx_locktime(input.lock_time).await); + let lock_time = match lock_time { + LocktimeSetting::CalcByHtlcLocktime(lock) => try_s!(coin.p2sh_tx_locktime(lock).await), + LocktimeSetting::UseExact(lock) => lock, + }; let n_time = if coin.as_ref().conf.is_pos { Some(now_sec_u32()) } else { @@ -1108,21 +1125,21 @@ pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxI None }; let hash_algo = coin.as_ref().tx_hash_algo.into(); - let unsigned = TransactionInputSigner { + Ok(TransactionInputSigner { lock_time, version: coin.as_ref().conf.tx_version, n_time, overwintered: coin.as_ref().conf.overwintered, inputs: vec![UnsignedTransactionInput { - sequence: input.sequence, + sequence, previous_output: OutPoint { - hash: input.prev_transaction.hash(), + hash: prev_tx.hash(), index: DEFAULT_SWAP_VOUT as u32, }, - amount: input.prev_transaction.outputs[0].value, + amount: prev_tx.outputs[0].value, witness: Vec::new(), }], - outputs: input.outputs, + outputs, expiry_height: 0, join_splits: vec![], shielded_spends: vec![], @@ -1134,7 +1151,20 @@ pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxI posv: coin.as_ref().conf.is_posv, str_d_zeel, hash_algo, - }; + }) +} + +pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxInput<'_>) -> Result { + let unsigned = try_s!( + p2sh_spending_tx_preimage( + coin, + &input.prev_transaction, + LocktimeSetting::CalcByHtlcLocktime(input.lock_time), + input.sequence, + input.outputs + ) + .await + ); let signed_input = try_s!(p2sh_spend( &unsigned, DEFAULT_SWAP_VOUT, @@ -1167,6 +1197,234 @@ pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxI }) } +pub type GenDexFeeSpendResult = MmResult; + +enum CalcPremiumBy { + DeductMinerFee, + UseExactAmount(u64), +} + +async fn gen_dex_fee_spend_preimage( + coin: &T, + args: &GenDexFeeSpendArgs<'_>, + lock_time: LocktimeSetting, + calc_premium: CalcPremiumBy, +) -> GenDexFeeSpendResult { + let mut prev_tx: UtxoTx = + deserialize(args.dex_fee_tx).map_to_mm(|e| TxGenError::TxDeserialization(e.to_string()))?; + prev_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; + drop_mutability!(prev_tx); + + let dex_fee_sat = sat_from_big_decimal(&args.dex_fee_amount, coin.as_ref().decimals)?; + let premium_sat = match calc_premium { + CalcPremiumBy::UseExactAmount(sat) => sat, + CalcPremiumBy::DeductMinerFee => { + let miner_fee = coin + .get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await?; + + let premium_sat = sat_from_big_decimal(&args.premium_amount, coin.as_ref().decimals)?; + if miner_fee + coin.as_ref().dust_amount > premium_sat { + return MmError::err(TxGenError::MinerFeeExceedsPremium { + miner_fee: big_decimal_from_sat_unsigned(miner_fee, coin.as_ref().decimals), + premium: args.premium_amount.clone(), + }); + } + premium_sat - miner_fee + }, + }; + + let dex_fee_address = address_from_raw_pubkey( + args.dex_fee_pub, + coin.as_ref().conf.pub_addr_prefix, + coin.as_ref().conf.pub_t_addr_prefix, + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.bech32_hrp.clone(), + coin.addr_format().clone(), + ) + .map_to_mm(|e| TxGenError::AddressDerivation(format!("Failed to derive dex_fee_address: {}", e)))?; + let dex_fee_output = TransactionOutput { + value: dex_fee_sat, + script_pubkey: Builder::build_p2pkh(&dex_fee_address.hash).to_bytes(), + }; + + let premium_address = address_from_raw_pubkey( + args.maker_pub, + coin.as_ref().conf.pub_addr_prefix, + coin.as_ref().conf.pub_t_addr_prefix, + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.bech32_hrp.clone(), + coin.addr_format().clone(), + ) + .map_to_mm(|e| TxGenError::AddressDerivation(format!("Failed to derive premium_address: {}", e)))?; + let premium_output = TransactionOutput { + value: premium_sat, + script_pubkey: Builder::build_p2pkh(&premium_address.hash).to_bytes(), + }; + + p2sh_spending_tx_preimage(coin, &prev_tx, lock_time, SEQUENCE_FINAL, vec![ + dex_fee_output, + premium_output, + ]) + .await + .map_to_mm(TxGenError::Legacy) +} + +pub async fn gen_and_sign_dex_fee_spend_preimage( + coin: &T, + args: &GenDexFeeSpendArgs<'_>, + htlc_keypair: &KeyPair, +) -> GenAndSignDexFeeSpendResult { + let maker_pub = Public::from_slice(args.maker_pub).map_to_mm(|e| TxGenError::InvalidPubkey(e.to_string()))?; + let taker_pub = Public::from_slice(args.taker_pub).map_to_mm(|e| TxGenError::InvalidPubkey(e.to_string()))?; + + let preimage = gen_dex_fee_spend_preimage( + coin, + args, + LocktimeSetting::CalcByHtlcLocktime(args.time_lock), + CalcPremiumBy::DeductMinerFee, + ) + .await?; + + let redeem_script = swap_proto_v2_scripts::dex_fee_script(args.time_lock, args.secret_hash, &taker_pub, &maker_pub); + let signature = calc_and_sign_sighash( + &preimage, + DEFAULT_SWAP_VOUT, + &redeem_script, + htlc_keypair, + coin.as_ref().conf.signature_version, + coin.as_ref().conf.fork_id, + )?; + let preimage_tx: UtxoTx = preimage.into(); + Ok(TxPreimageWithSig { + preimage: serialize(&preimage_tx).take(), + signature: signature.take(), + }) +} + +pub async fn validate_dex_fee_spend_preimage( + coin: &T, + gen_args: &GenDexFeeSpendArgs<'_>, + preimage: &TxPreimageWithSig, +) -> ValidateDexFeeSpendPreimageResult { + // TODO validate that preimage has exactly 2 outputs + let actual_preimage_tx: UtxoTx = deserialize(preimage.preimage.as_slice()) + .map_to_mm(|e| ValidateDexFeeSpendPreimageError::TxDeserialization(e.to_string()))?; + + let maker_pub = Public::from_slice(gen_args.maker_pub) + .map_to_mm(|e| ValidateDexFeeSpendPreimageError::InvalidPubkey(e.to_string()))?; + let taker_pub = Public::from_slice(gen_args.taker_pub) + .map_to_mm(|e| ValidateDexFeeSpendPreimageError::InvalidPubkey(e.to_string()))?; + + // TODO validate premium amount. Might be a bit tricky in the case of dynamic miner fee + // TODO validate that output amounts are larger than dust + + let premium = match actual_preimage_tx.outputs.get(1) { + Some(o) => o.value, + None => { + return MmError::err(ValidateDexFeeSpendPreimageError::InvalidPreimage( + "Preimage doesn't have output 1".into(), + )) + }, + }; + + // Here, we have to use the exact lock time and premium amount from the preimage because maker + // can get different values (e.g. if MTP advances during preimage exchange/fee rate changes) + let expected_preimage = gen_dex_fee_spend_preimage( + coin, + gen_args, + LocktimeSetting::UseExact(actual_preimage_tx.lock_time), + CalcPremiumBy::UseExactAmount(premium), + ) + .await?; + let redeem_script = + swap_proto_v2_scripts::dex_fee_script(gen_args.time_lock, gen_args.secret_hash, &taker_pub, &maker_pub); + let sig_hash = signature_hash_to_sign( + &expected_preimage, + DEFAULT_SWAP_VOUT, + &redeem_script, + coin.as_ref().conf.signature_version, + coin.as_ref().conf.fork_id, + )?; + + if !taker_pub + .verify(&sig_hash, &preimage.signature.clone().into()) + .map_to_mm(|e| ValidateDexFeeSpendPreimageError::SignatureVerificationFailure(e.to_string()))? + { + return MmError::err(ValidateDexFeeSpendPreimageError::InvalidTakerSignature); + }; + let expected_preimage_tx: UtxoTx = expected_preimage.into(); + if expected_preimage_tx != actual_preimage_tx { + return MmError::err(ValidateDexFeeSpendPreimageError::InvalidPreimage( + "Preimage is not equal to expected".into(), + )); + } + Ok(()) +} + +pub async fn sign_and_broadcast_dex_fee_spend( + coin: &T, + preimage: &TxPreimageWithSig, + gen_args: &GenDexFeeSpendArgs<'_>, + secret: &[u8], + htlc_keypair: &KeyPair, +) -> TransactionResult { + let taker_pub = try_tx_s!(Public::from_slice(gen_args.taker_pub)); + + let mut dex_fee_tx: UtxoTx = try_tx_s!(deserialize(gen_args.dex_fee_tx)); + dex_fee_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; + drop_mutability!(dex_fee_tx); + + let mut preimage_tx: UtxoTx = try_tx_s!(deserialize(preimage.preimage.as_slice())); + preimage_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; + drop_mutability!(preimage_tx); + + let secret_hash = dhash160(secret); + let redeem_script = swap_proto_v2_scripts::dex_fee_script( + gen_args.time_lock, + secret_hash.as_slice(), + &taker_pub, + htlc_keypair.public(), + ); + + let mut signer: TransactionInputSigner = preimage_tx.clone().into(); + signer.inputs[0].amount = dex_fee_tx.outputs[0].value; + signer.consensus_branch_id = coin.as_ref().conf.consensus_branch_id; + drop_mutability!(signer); + + let maker_signature = try_tx_s!(calc_and_sign_sighash( + &signer, + DEFAULT_SWAP_VOUT, + &redeem_script, + htlc_keypair, + coin.as_ref().conf.signature_version, + coin.as_ref().conf.fork_id + )); + let sig_hash_all_fork_id = 1 | coin.as_ref().conf.fork_id as u8; + let mut taker_signature_with_sighash = preimage.signature.clone(); + taker_signature_with_sighash.push(sig_hash_all_fork_id); + drop_mutability!(taker_signature_with_sighash); + + let mut maker_signature_with_sighash: Vec = maker_signature.take(); + maker_signature_with_sighash.push(sig_hash_all_fork_id); + drop_mutability!(maker_signature_with_sighash); + + let script_sig = Builder::default() + .push_opcode(Opcode::OP_0) + .push_data(&taker_signature_with_sighash) + .push_data(&maker_signature_with_sighash) + .push_data(secret) + .push_opcode(Opcode::OP_0) + .push_data(&redeem_script) + .into_bytes(); + let mut final_tx: UtxoTx = signer.into(); + final_tx.inputs[0].script_sig = script_sig; + drop_mutability!(final_tx); + + try_tx_s!(coin.broadcast_tx(&final_tx).await, final_tx); + Ok(final_tx.into()) +} + pub fn send_taker_fee(coin: T, fee_pub_key: &[u8], amount: BigDecimal) -> TransactionFut where T: UtxoCommonOps + GetUtxoListOps, @@ -1201,7 +1459,8 @@ where maker_htlc_key_pair.public_slice(), args.other_pubkey, args.secret_hash, - args.amount + args.amount, + SwapPaymentType::TakerOrMakerPayment, )); let send_fut = match &coin.as_ref().rpc_client { UtxoRpcClientEnum::Electrum(_) => Either::A(send_outputs_from_my_address(coin, outputs)), @@ -1237,7 +1496,8 @@ where taker_htlc_key_pair.public_slice(), args.other_pubkey, args.secret_hash, - total_amount + total_amount, + SwapPaymentType::TakerOrMakerPayment, )); let send_fut = match &coin.as_ref().rpc_client { @@ -1542,61 +1802,73 @@ pub fn send_taker_spends_maker_payment(coin: T, args Box::new(fut.boxed().compat()) } -pub fn send_taker_refunds_payment(coin: T, args: RefundPaymentArgs) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); +async fn refund_htlc_payment( + coin: T, + args: RefundPaymentArgs<'_>, + payment_type: SwapPaymentType, +) -> TransactionResult { + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); let mut prev_transaction: UtxoTx = - try_tx_fus!(deserialize(args.payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); + try_tx_s!(deserialize(args.payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); if prev_transaction.outputs.is_empty() { - return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); + return try_tx_s!(TX_PLAIN_ERR!("Transaction doesn't have any output")); } + let other_public = try_tx_s!(Public::from_slice(args.other_pubkey)); let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); - let redeem_script = payment_script( - args.time_lock, - args.secret_hash, - key_pair.public(), - &try_tx_fus!(Public::from_slice(args.other_pubkey)), - ) - .into(); + let redeem_script = match payment_type { + SwapPaymentType::TakerOrMakerPayment => { + payment_script(args.time_lock, args.secret_hash, key_pair.public(), &other_public).into() + }, + SwapPaymentType::DexFeeWithPremium => { + swap_proto_v2_scripts::dex_fee_script(args.time_lock, args.secret_hash, key_pair.public(), &other_public) + .into() + }, + }; let time_lock = args.time_lock; - let fut = async move { - let fee = try_tx_s!( - coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) - .await + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= prev_transaction.outputs[0].value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + prev_transaction.outputs[0].value ); - if fee >= prev_transaction.outputs[0].value { - return TX_PLAIN_ERR!( - "HTLC spend fee {} is greater than transaction output {}", - fee, - prev_transaction.outputs[0].value - ); - } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); - let output = TransactionOutput { - value: prev_transaction.outputs[0].value - fee, - script_pubkey, - }; + } + let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let output = TransactionOutput { + value: prev_transaction.outputs[0].value - fee, + script_pubkey, + }; - let input = P2SHSpendingTxInput { - prev_transaction, - redeem_script, - outputs: vec![output], - script_data, - sequence: SEQUENCE_FINAL - 1, - lock_time: time_lock, - keypair: &key_pair, - }; - let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + let input = P2SHSpendingTxInput { + prev_transaction, + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL - 1, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); - let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_tx_s!(tx_fut.await, transaction); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); + try_tx_s!(tx_fut.await, transaction); - Ok(transaction.into()) - }; - Box::new(fut.boxed().compat()) + Ok(transaction.into()) +} + +#[inline] +pub async fn send_taker_refunds_payment( + coin: T, + args: RefundPaymentArgs<'_>, +) -> TransactionResult { + refund_htlc_payment(coin, args, SwapPaymentType::TakerOrMakerPayment).await } pub fn send_taker_payment_refund_preimage( @@ -1618,59 +1890,12 @@ pub fn send_taker_payment_refund_preimage( Box::new(fut.boxed().compat()) } -pub fn send_maker_refunds_payment(coin: T, args: RefundPaymentArgs) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); - let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(args.payment_tx).map_err(|e| ERRL!("{:?}", e))); - prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; - drop_mutability!(prev_transaction); - if prev_transaction.outputs.is_empty() { - return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); - } - let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); - let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); - let redeem_script = payment_script( - args.time_lock, - args.secret_hash, - key_pair.public(), - &try_tx_fus!(Public::from_slice(args.other_pubkey)), - ) - .into(); - let time_lock = args.time_lock; - let fut = async move { - let fee = try_tx_s!( - coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) - .await - ); - if fee >= prev_transaction.outputs[0].value { - return TX_PLAIN_ERR!( - "HTLC spend fee {} is greater than transaction output {}", - fee, - prev_transaction.outputs[0].value - ); - } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); - let output = TransactionOutput { - value: prev_transaction.outputs[0].value - fee, - script_pubkey, - }; - - let input = P2SHSpendingTxInput { - prev_transaction, - redeem_script, - outputs: vec![output], - script_data, - sequence: SEQUENCE_FINAL - 1, - lock_time: time_lock, - keypair: &key_pair, - }; - let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); - - let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_tx_s!(tx_fut.await, transaction); - - Ok(transaction.into()) - }; - Box::new(fut.boxed().compat()) +#[inline] +pub async fn send_maker_refunds_payment( + coin: T, + args: RefundPaymentArgs<'_>, +) -> TransactionResult { + refund_htlc_payment(coin, args, SwapPaymentType::TakerOrMakerPayment).await } /// Extracts pubkey from script sig @@ -3471,9 +3696,16 @@ where let secret_hash = &[0; 20]; // H160 is 20 bytes // `generate_swap_payment_outputs` may fail due to either invalid `other_pub` or a number conversation error - let SwapPaymentOutputsResult { outputs, .. } = - generate_swap_payment_outputs(coin, time_lock, my_pub, other_pub, secret_hash, amount) - .map_to_mm(TradePreimageError::InternalError)?; + let SwapPaymentOutputsResult { outputs, .. } = generate_swap_payment_outputs( + coin, + time_lock, + my_pub, + other_pub, + secret_hash, + amount, + SwapPaymentType::TakerOrMakerPayment, + ) + .map_to_mm(TradePreimageError::InternalError)?; let gas_fee = None; let fee_amount = coin .preimage_trade_fee_required_to_send_outputs(outputs, fee_policy, gas_fee, &stage) @@ -3952,6 +4184,11 @@ struct SwapPaymentOutputsResult { outputs: Vec, } +enum SwapPaymentType { + TakerOrMakerPayment, + DexFeeWithPremium, +} + fn generate_swap_payment_outputs( coin: T, time_lock: u32, @@ -3959,17 +4196,19 @@ fn generate_swap_payment_outputs( other_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + payment_type: SwapPaymentType, ) -> Result where T: AsRef, { let my_public = try_s!(Public::from_slice(my_pub)); - let redeem_script = payment_script( - time_lock, - secret_hash, - &my_public, - &try_s!(Public::from_slice(other_pub)), - ); + let other_public = try_s!(Public::from_slice(other_pub)); + let redeem_script = match payment_type { + SwapPaymentType::TakerOrMakerPayment => payment_script(time_lock, secret_hash, &my_public, &other_public), + SwapPaymentType::DexFeeWithPremium => { + swap_proto_v2_scripts::dex_fee_script(time_lock, secret_hash, &my_public, &other_public) + }, + }; let redeem_script_hash = dhash160(&redeem_script); let amount = try_s!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); let htlc_out = TransactionOutput { @@ -4317,6 +4556,93 @@ where .collect() } +pub async fn send_dex_fee_with_premium(coin: T, args: SendDexFeeWithPremiumArgs<'_>) -> TransactionResult +where + T: UtxoCommonOps + GetUtxoListOps + SwapOps, +{ + let taker_htlc_key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); + let total_amount = &args.dex_fee_amount + &args.premium_amount; + + let SwapPaymentOutputsResult { + payment_address, + outputs, + } = try_tx_s!(generate_swap_payment_outputs( + &coin, + args.time_lock, + taker_htlc_key_pair.public_slice(), + args.other_pub, + args.secret_hash, + total_amount, + SwapPaymentType::DexFeeWithPremium, + )); + if let UtxoRpcClientEnum::Native(client) = &coin.as_ref().rpc_client { + let addr_string = try_tx_s!(payment_address.display_address()); + client + .import_address(&addr_string, &addr_string, false) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e))) + .compat() + .await?; + } + send_outputs_from_my_address(coin, outputs).compat().await +} + +pub async fn validate_dex_fee_with_premium(coin: &T, args: ValidateDexFeeArgs<'_>) -> ValidateDexFeeResult +where + T: UtxoCommonOps + SwapOps, +{ + let dex_fee_tx: UtxoTx = + deserialize(args.dex_fee_tx).map_to_mm(|e| ValidateDexFeeError::TxDeserialization(e.to_string()))?; + if dex_fee_tx.outputs.len() < 2 { + return MmError::err(ValidateDexFeeError::TxLacksOfOutputs); + } + + let taker_pub = + Public::from_slice(args.other_pub).map_to_mm(|e| ValidateDexFeeError::InvalidPubkey(e.to_string()))?; + let maker_htlc_key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); + let total_expected_amount = &args.dex_fee_amount + &args.premium_amount; + + let expected_amount_sat = sat_from_big_decimal(&total_expected_amount, coin.as_ref().decimals)?; + + let redeem_script = swap_proto_v2_scripts::dex_fee_script( + args.time_lock, + args.secret_hash, + &taker_pub, + maker_htlc_key_pair.public(), + ); + let expected_output = TransactionOutput { + value: expected_amount_sat, + script_pubkey: Builder::build_p2sh(&AddressHashEnum::AddressHash(dhash160(&redeem_script))).into(), + }; + + if dex_fee_tx.outputs[0] != expected_output { + return MmError::err(ValidateDexFeeError::InvalidDestinationOrAmount(format!( + "Expected {:?}, got {:?}", + expected_output, dex_fee_tx.outputs[0] + ))); + } + + let tx_bytes_from_rpc = coin + .as_ref() + .rpc_client + .get_transaction_bytes(&dex_fee_tx.hash().reversed().into()) + .compat() + .await?; + if tx_bytes_from_rpc.0 != args.dex_fee_tx { + return MmError::err(ValidateDexFeeError::TxBytesMismatch { + from_rpc: tx_bytes_from_rpc, + actual: args.dex_fee_tx.into(), + }); + } + Ok(()) +} + +pub async fn refund_dex_fee_with_premium(coin: T, args: RefundPaymentArgs<'_>) -> TransactionResult +where + T: UtxoCommonOps + GetUtxoListOps + SwapOps, +{ + refund_htlc_payment(coin, args, SwapPaymentType::DexFeeWithPremium).await +} + #[test] fn test_increase_by_percent() { assert_eq!(increase_by_percent(4300, 1.), 4343); diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 8c03e66a8c..6a0ca49dac 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -23,15 +23,17 @@ use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; use crate::{CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, - GetWithdrawSenderAddress, IguanaPrivKey, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, - PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, RefundError, - RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, - SendPaymentArgs, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradePreimageValue, - TransactionFut, TxMarshalingErr, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, - ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, - WithdrawSenderAddress}; + GenAndSignDexFeeSpendResult, GenDexFeeSpendArgs, GetWithdrawSenderAddress, IguanaPrivKey, + MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, + PaymentInstructionsErr, PrivKeyBuildPolicy, RefundError, RefundPaymentArgs, RefundResult, + SearchForSwapTxSpendInput, SendDexFeeWithPremiumArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SignatureResult, SpendPaymentArgs, SwapOps, SwapOpsV2, TakerSwapMakerCoin, TradePreimageValue, + TransactionFut, TransactionResult, TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, + ValidateDexFeeArgs, ValidateDexFeeResult, ValidateDexFeeSpendPreimageResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, + WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawFut, WithdrawSenderAddress}; use common::executor::{AbortableSystem, AbortedError}; use crypto::Bip44Chain; use futures::{FutureExt, TryFutureExt}; @@ -315,13 +317,13 @@ impl SwapOps for UtxoStandardCoin { } #[inline] - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - utxo_common::send_taker_refunds_payment(self.clone(), taker_refunds_payment_args) + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + utxo_common::send_taker_refunds_payment(self.clone(), taker_refunds_payment_args).await } #[inline] - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - utxo_common::send_maker_refunds_payment(self.clone(), maker_refunds_payment_args) + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + utxo_common::send_maker_refunds_payment(self.clone(), maker_refunds_payment_args).await } fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { @@ -579,6 +581,49 @@ impl WatcherOps for UtxoStandardCoin { } } +#[async_trait] +impl SwapOpsV2 for UtxoStandardCoin { + async fn send_dex_fee_with_premium(&self, args: SendDexFeeWithPremiumArgs<'_>) -> TransactionResult { + utxo_common::send_dex_fee_with_premium(self.clone(), args).await + } + + async fn validate_dex_fee_with_premium(&self, args: ValidateDexFeeArgs<'_>) -> ValidateDexFeeResult { + utxo_common::validate_dex_fee_with_premium(self, args).await + } + + async fn refund_dex_fee_with_premium(&self, args: RefundPaymentArgs<'_>) -> TransactionResult { + utxo_common::refund_dex_fee_with_premium(self.clone(), args).await + } + + async fn gen_and_sign_dex_fee_spend_preimage( + &self, + args: &GenDexFeeSpendArgs<'_>, + swap_unique_data: &[u8], + ) -> GenAndSignDexFeeSpendResult { + let key_pair = self.derive_htlc_key_pair(swap_unique_data); + utxo_common::gen_and_sign_dex_fee_spend_preimage(self, args, &key_pair).await + } + + async fn validate_dex_fee_spend_preimage( + &self, + gen_args: &GenDexFeeSpendArgs<'_>, + preimage: &TxPreimageWithSig, + ) -> ValidateDexFeeSpendPreimageResult { + utxo_common::validate_dex_fee_spend_preimage(self, gen_args, preimage).await + } + + async fn sign_and_broadcast_dex_fee_spend( + &self, + preimage: &TxPreimageWithSig, + gen_args: &GenDexFeeSpendArgs<'_>, + secret: &[u8], + swap_unique_data: &[u8], + ) -> TransactionResult { + let htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); + utxo_common::sign_and_broadcast_dex_fee_spend(self, preimage, gen_args, secret, &htlc_keypair).await + } +} + impl MarketCoinOps for UtxoStandardCoin { fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } diff --git a/mm2src/coins/utxo/utxo_tx_history_v2.rs b/mm2src/coins/utxo/utxo_tx_history_v2.rs index 8a3e47c100..ca3b3a6dcd 100644 --- a/mm2src/coins/utxo/utxo_tx_history_v2.rs +++ b/mm2src/coins/utxo/utxo_tx_history_v2.rs @@ -11,6 +11,7 @@ use async_trait::async_trait; use common::executor::Timer; use common::log::{error, info}; use common::state_machine::prelude::*; +use common::state_machine::StateMachineTrait; use derive_more::Display; use keys::Address; use mm2_err_handle::prelude::*; @@ -136,7 +137,7 @@ pub trait UtxoTxHistoryOps: CoinWithTxHistoryV2 + MarketCoinOps + Send + Sync + fn set_history_sync_state(&self, new_state: HistorySyncState); } -struct UtxoTxHistoryCtx { +struct UtxoTxHistoryStateMachine { coin: Coin, storage: Storage, metrics: MetricsArc, @@ -145,17 +146,21 @@ struct UtxoTxHistoryCtx { balances: HashMap, } -impl UtxoTxHistoryCtx +impl StateMachineTrait for UtxoTxHistoryStateMachine { + type Result = (); +} + +impl UtxoTxHistoryStateMachine where Coin: UtxoTxHistoryOps, Storage: TxHistoryStorage, { - /// Requests balances for every activated address, updates the balances in [`UtxoTxHistoryCtx::balances`] + /// Requests balances for every activated address, updates the balances in [`UtxoTxHistoryStateMachine::balances`] /// and returns the addresses whose balance has changed. /// /// # Note /// - /// [`UtxoTxHistoryCtx::balances`] is changed if we successfully handled all balances **only**. + /// [`UtxoTxHistoryStateMachine::balances`] is changed if we successfully handled all balances **only**. async fn updated_addresses(&mut self) -> BalanceResult> { let current_balances = self.coin.my_addresses_balances().await?; @@ -222,10 +227,12 @@ where Coin: UtxoTxHistoryOps, Storage: TxHistoryStorage, { - type Ctx = UtxoTxHistoryCtx; - type Result = (); + type StateMachine = UtxoTxHistoryStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed( + self: Box, + ctx: &mut UtxoTxHistoryStateMachine, + ) -> StateResult> { ctx.coin.set_history_sync_state(HistorySyncState::NotStarted); if let Err(e) = ctx.storage.init(&ctx.coin.history_wallet_id()).await { @@ -268,10 +275,12 @@ where Coin: UtxoTxHistoryOps, Storage: TxHistoryStorage, { - type Ctx = UtxoTxHistoryCtx; - type Result = (); + type StateMachine = UtxoTxHistoryStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed( + self: Box, + ctx: &mut UtxoTxHistoryStateMachine, + ) -> StateResult> { let wallet_id = ctx.coin.history_wallet_id(); if let Err(e) = ctx.storage.init(&wallet_id).await { return Self::change_state(Stopped::storage_error(e)); @@ -330,7 +339,7 @@ where } /// An I/O cooldown before `FetchingTxHashes` state. -/// States have to be generic over storage type because `UtxoTxHistoryCtx` is generic over it. +/// States have to be generic over storage type because `UtxoTxHistoryStateMachine` is generic over it. struct OnIoErrorCooldown { /// The list of addresses of those we need to fetch TX hashes at the upcoming `FetchingTxHashses` state. fetch_for_addresses: HashSet
, @@ -356,10 +365,12 @@ where Coin: UtxoTxHistoryOps, Storage: TxHistoryStorage, { - type Ctx = UtxoTxHistoryCtx; - type Result = (); + type StateMachine = UtxoTxHistoryStateMachine; - async fn on_changed(mut self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed( + mut self: Box, + ctx: &mut UtxoTxHistoryStateMachine, + ) -> StateResult> { loop { Timer::sleep(30.).await; @@ -406,10 +417,12 @@ where Coin: UtxoTxHistoryOps, Storage: TxHistoryStorage, { - type Ctx = UtxoTxHistoryCtx; - type Result = (); + type StateMachine = UtxoTxHistoryStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed( + self: Box, + ctx: &mut UtxoTxHistoryStateMachine, + ) -> StateResult> { let wallet_id = ctx.coin.history_wallet_id(); loop { Timer::sleep(30.).await; @@ -471,10 +484,12 @@ where Coin: UtxoTxHistoryOps, Storage: TxHistoryStorage, { - type Ctx = UtxoTxHistoryCtx; - type Result = (); + type StateMachine = UtxoTxHistoryStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed( + self: Box, + ctx: &mut UtxoTxHistoryStateMachine, + ) -> StateResult> { let wallet_id = ctx.coin.history_wallet_id(); let for_addresses = to_filtering_addresses(&self.requested_for_addresses); @@ -551,10 +566,12 @@ where Coin: UtxoTxHistoryOps, Storage: TxHistoryStorage, { - type Ctx = UtxoTxHistoryCtx; - type Result = (); + type StateMachine = UtxoTxHistoryStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed( + self: Box, + ctx: &mut UtxoTxHistoryStateMachine, + ) -> StateResult> { let ticker = ctx.coin.ticker(); let wallet_id = ctx.coin.history_wallet_id(); @@ -653,10 +670,9 @@ where Coin: UtxoTxHistoryOps, Storage: TxHistoryStorage, { - type Ctx = UtxoTxHistoryCtx; - type Result = (); + type StateMachine = UtxoTxHistoryStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> Self::Result { + async fn on_changed(self: Box, ctx: &mut UtxoTxHistoryStateMachine) -> () { info!( "Stopping tx history fetching for {}. Reason: {:?}", ctx.coin.ticker(), @@ -711,14 +727,13 @@ pub async fn bch_and_slp_history_loop( }, }; - let ctx = UtxoTxHistoryCtx { + let mut state_machine = UtxoTxHistoryStateMachine { coin, storage, metrics, balances, }; - let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ctx); - state_machine.run(Init::new()).await; + state_machine.run(Box::new(Init::new())).await; } pub async fn utxo_history_loop( @@ -730,14 +745,13 @@ pub async fn utxo_history_loop( Coin: UtxoTxHistoryOps, Storage: TxHistoryStorage, { - let ctx = UtxoTxHistoryCtx { + let mut state_machine = UtxoTxHistoryStateMachine { coin, storage, metrics, balances: current_balances, }; - let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ctx); - state_machine.run(Init::new()).await; + state_machine.run(Box::new(Init::new())).await; } fn to_filtering_addresses(addresses: &HashSet
) -> FilteringAddresses { diff --git a/mm2src/coins/utxo_signer/src/with_key_pair.rs b/mm2src/coins/utxo_signer/src/with_key_pair.rs index 3db094e2e8..a061fe1017 100644 --- a/mm2src/coins/utxo_signer/src/with_key_pair.rs +++ b/mm2src/coins/utxo_signer/src/with_key_pair.rs @@ -82,7 +82,7 @@ pub fn p2pk_spend( let unsigned_input = get_input(signer, input_index)?; let script = Builder::build_p2pk(key_pair.public()); - let signature = calc_and_sign_sighash(signer, input_index, script, key_pair, signature_version, fork_id)?; + let signature = calc_and_sign_sighash(signer, input_index, &script, key_pair, signature_version, fork_id)?; Ok(p2pk_spend_with_signature(unsigned_input, fork_id, signature)) } @@ -106,7 +106,7 @@ pub fn p2pkh_spend( }); } - let signature = calc_and_sign_sighash(signer, input_index, script, key_pair, signature_version, fork_id)?; + let signature = calc_and_sign_sighash(signer, input_index, &script, key_pair, signature_version, fork_id)?; Ok(p2pkh_spend_with_signature( unsigned_input, key_pair.public(), @@ -130,7 +130,7 @@ pub fn p2sh_spend( let signature = calc_and_sign_sighash( signer, input_index, - redeem_script.clone(), + &redeem_script, key_pair, signature_version, fork_id, @@ -164,7 +164,7 @@ pub fn p2wpkh_spend( }); } - let signature = calc_and_sign_sighash(signer, input_index, script, key_pair, signature_version, fork_id)?; + let signature = calc_and_sign_sighash(signer, input_index, &script, key_pair, signature_version, fork_id)?; Ok(p2wpkh_spend_with_signature( unsigned_input, key_pair.public(), @@ -174,10 +174,10 @@ pub fn p2wpkh_spend( } /// Calculates the input script hash and sign it using `key_pair`. -pub(crate) fn calc_and_sign_sighash( +pub fn calc_and_sign_sighash( signer: &TransactionInputSigner, input_index: usize, - output_script: Script, + output_script: &Script, key_pair: &KeyPair, signature_version: SignatureVersion, fork_id: u32, @@ -186,10 +186,10 @@ pub(crate) fn calc_and_sign_sighash( sign_message(&sighash, key_pair) } -fn signature_hash_to_sign( +pub fn signature_hash_to_sign( signer: &TransactionInputSigner, input_index: usize, - output_script: Script, + output_script: &Script, signature_version: SignatureVersion, fork_id: u32, ) -> UtxoSignWithKeyPairResult { @@ -199,7 +199,7 @@ fn signature_hash_to_sign( Ok(signer.signature_hash( input_index, input_amount, - &output_script, + output_script, signature_version, sighash_type, )) diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 4e3f6f2ecb..ee6f6a23d3 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -20,10 +20,10 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, Coi RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, - TransactionEnum, TransactionFut, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, - WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + TransactionEnum, TransactionFut, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, + WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; use crate::{Transaction, WithdrawError}; use async_trait::async_trait; @@ -1277,60 +1277,53 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut { - let tx = try_tx_fus!(ZTransaction::read(taker_refunds_payment_args.payment_tx)); + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + let tx = try_tx_s!(ZTransaction::read(taker_refunds_payment_args.payment_tx)); let key_pair = self.derive_htlc_key_pair(taker_refunds_payment_args.swap_unique_data); let time_lock = taker_refunds_payment_args.time_lock; let redeem_script = payment_script( time_lock, taker_refunds_payment_args.secret_hash, key_pair.public(), - &try_tx_fus!(Public::from_slice(taker_refunds_payment_args.other_pubkey)), + &try_tx_s!(Public::from_slice(taker_refunds_payment_args.other_pubkey)), ); let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_1).into_script(); - let selfi = self.clone(); - let fut = async move { - let tx_fut = z_p2sh_spend( - &selfi, - tx, - time_lock, - SEQUENCE_FINAL - 1, - redeem_script, - script_data, - &key_pair, - ); - let tx = try_ztx_s!(tx_fut.await); - Ok(tx.into()) - }; - Box::new(fut.boxed().compat()) + + let tx_fut = z_p2sh_spend( + self, + tx, + time_lock, + SEQUENCE_FINAL - 1, + redeem_script, + script_data, + &key_pair, + ); + let tx = try_ztx_s!(tx_fut.await); + Ok(tx.into()) } - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut { - let tx = try_tx_fus!(ZTransaction::read(maker_refunds_payment_args.payment_tx)); + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + let tx = try_tx_s!(ZTransaction::read(maker_refunds_payment_args.payment_tx)); let key_pair = self.derive_htlc_key_pair(maker_refunds_payment_args.swap_unique_data); let time_lock = maker_refunds_payment_args.time_lock; let redeem_script = payment_script( time_lock, maker_refunds_payment_args.secret_hash, key_pair.public(), - &try_tx_fus!(Public::from_slice(maker_refunds_payment_args.other_pubkey)), + &try_tx_s!(Public::from_slice(maker_refunds_payment_args.other_pubkey)), ); let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_1).into_script(); - let selfi = self.clone(); - let fut = async move { - let tx_fut = z_p2sh_spend( - &selfi, - tx, - time_lock, - SEQUENCE_FINAL - 1, - redeem_script, - script_data, - &key_pair, - ); - let tx = try_ztx_s!(tx_fut.await); - Ok(tx.into()) - }; - Box::new(fut.boxed().compat()) + let tx_fut = z_p2sh_spend( + self, + tx, + time_lock, + SEQUENCE_FINAL - 1, + redeem_script, + script_data, + &key_pair, + ); + let tx = try_ztx_s!(tx_fut.await); + Ok(tx.into()) } fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentFut<()> { diff --git a/mm2src/coins/z_coin/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs index ef4929a21a..00df86e612 100644 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ b/mm2src/coins/z_coin/z_coin_native_tests.rs @@ -65,7 +65,7 @@ fn zombie_coin_send_and_refund_maker_payment() { swap_unique_data: pk_data.as_slice(), watcher_reward: false, }; - let refund_tx = coin.send_maker_refunds_payment(refund_args).wait().unwrap(); + let refund_tx = block_on(coin.send_maker_refunds_payment(refund_args)).unwrap(); println!("refund tx {}", hex::encode(refund_tx.tx_hash().0)); } diff --git a/mm2src/common/patterns/state_machine.rs b/mm2src/common/patterns/state_machine.rs index a558378de0..bd00be8857 100644 --- a/mm2src/common/patterns/state_machine.rs +++ b/mm2src/common/patterns/state_machine.rs @@ -6,39 +6,28 @@ use crate::NotSame; use async_trait::async_trait; pub mod prelude { - pub use super::{LastState, State, StateExt, StateMachine, StateResult, TransitionFrom}; + pub use super::{LastState, State, StateExt, StateResult, TransitionFrom}; } -pub struct StateMachine { - /// The shared between states context. - ctx: Ctx, - phantom: std::marker::PhantomData, -} +pub trait TransitionFrom {} -impl StateMachine { - pub fn from_ctx(ctx: Ctx) -> Self { - StateMachine { - ctx, - phantom: std::marker::PhantomData::default(), - } - } +#[async_trait] +pub trait StateMachineTrait: Send + Sized + 'static { + type Result: Send; - pub async fn run(mut self, initial_state: impl State) -> Result { - let mut state: Box> = Box::new(initial_state); + async fn run(&mut self, mut state: Box>) -> Self::Result { loop { - let result = state.on_changed(&mut self.ctx).await; - let next_state = match result { - StateResult::ChangeState(ChangeGuard { next }) => next, + let result = state.on_changed(self).await; + match result { + StateResult::ChangeState(ChangeGuard { next }) => { + state = next; + }, StateResult::Finish(ResultGuard { result }) => return result, }; - - state = next_state; } } } -pub trait TransitionFrom {} - /// Prevent implementing [`TransitionFrom`] for `Next` If `T` implements `LastState` already. impl !TransitionFrom for Next where @@ -52,24 +41,22 @@ where impl !TransitionFrom for T {} #[async_trait] -pub trait State: Send + 'static { - type Ctx: Send; - type Result; - +pub trait State: Send + Sync + 'static { + type StateMachine: StateMachineTrait; /// An action is called on entering this state. /// To change the state to another one in the end of processing, use [`StateExt::change_state`]. /// For example: /// ```rust /// return Self::change_state(next_state); /// ``` - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult; + async fn on_changed(self: Box, ctx: &mut Self::StateMachine) -> StateResult; } pub trait StateExt { /// Change the state to the `next_state`. /// This function performs the compile-time validation whether this state can transition to the `Next` state, /// i.e checks if `Next` implements [`Transition::from(ThisState)`]. - fn change_state(next_state: Next) -> StateResult + fn change_state(next_state: Next) -> StateResult where Self: Sized, Next: State + TransitionFrom, @@ -81,41 +68,42 @@ pub trait StateExt { impl StateExt for T {} #[async_trait] -pub trait LastState: Send + 'static { - type Ctx: Send; - type Result; +pub trait LastState: Send + Sync + 'static { + type StateMachine: StateMachineTrait; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> Self::Result; + async fn on_changed( + self: Box, + ctx: &mut Self::StateMachine, + ) -> ::Result; } #[async_trait] impl State for T { - type Ctx = T::Ctx; - type Result = T::Result; + type StateMachine = T::StateMachine; /// The last state always returns the result of the state machine calculations. - async fn on_changed(self: Box, ctx: &mut T::Ctx) -> StateResult { + async fn on_changed(self: Box, ctx: &mut T::StateMachine) -> StateResult { let result = LastState::on_changed(self, ctx).await; StateResult::Finish(ResultGuard::new(result)) } } -pub enum StateResult { - ChangeState(ChangeGuard), - Finish(ResultGuard), +pub enum StateResult { + ChangeState(ChangeGuard), + Finish(ResultGuard), } /* vvv The access guards that prevents the user using this pattern from entering an invalid state vvv */ /// An instance of `ChangeGuard` can be initialized within `state_machine` module only. -pub struct ChangeGuard { +pub struct ChangeGuard { /// The private field. - next: Box>, + next: Box>, } -impl ChangeGuard { +impl ChangeGuard { /// The private constructor. - fn next>(next_state: Next) -> Self { + fn next>(next_state: Next) -> Self { ChangeGuard { next: Box::new(next_state), } @@ -152,10 +140,16 @@ mod tests { UnknownUser, } - struct AuthCtx { + struct AuthStateMachine { users: HashMap<(Login, Password), UserId>, } + type AuthResult = Result; + + impl StateMachineTrait for AuthStateMachine { + type Result = AuthResult; + } + struct ReadingState { rx: mpsc::Receiver, } @@ -181,26 +175,23 @@ mod tests { #[async_trait] impl LastState for SuccessfulState { - type Ctx = AuthCtx; - type Result = Result; + type StateMachine = AuthStateMachine; - async fn on_changed(self: Box, _ctx: &mut Self::Ctx) -> Self::Result { Ok(self.user_id) } + async fn on_changed(self: Box, _ctx: &mut AuthStateMachine) -> AuthResult { Ok(self.user_id) } } #[async_trait] impl LastState for ErrorState { - type Ctx = AuthCtx; - type Result = Result; + type StateMachine = AuthStateMachine; - async fn on_changed(self: Box, _ctx: &mut Self::Ctx) -> Self::Result { Err(self.error) } + async fn on_changed(self: Box, _ctx: &mut AuthStateMachine) -> AuthResult { Err(self.error) } } #[async_trait] impl State for ReadingState { - type Ctx = AuthCtx; - type Result = Result; + type StateMachine = AuthStateMachine; - async fn on_changed(mut self: Box, _ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed(mut self: Box, _ctx: &mut AuthStateMachine) -> StateResult { let mut line = String::with_capacity(80); while let Some(ch) = self.rx.next().await { line.push(ch); @@ -212,10 +203,9 @@ mod tests { #[async_trait] impl State for ParsingState { - type Ctx = AuthCtx; - type Result = Result; + type StateMachine = AuthStateMachine; - async fn on_changed(self: Box, _ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed(self: Box, _ctx: &mut AuthStateMachine) -> StateResult { // parse the line into two chunks: (login, password) let chunks: Vec<_> = self.line.split(' ').collect(); if chunks.len() == 2 { @@ -235,10 +225,9 @@ mod tests { #[async_trait] impl State for AuthenticationState { - type Ctx = AuthCtx; - type Result = Result; + type StateMachine = AuthStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed(self: Box, ctx: &mut AuthStateMachine) -> StateResult { let credentials = (self.login, self.password); match ctx.users.get(&credentials) { Some(user_id) => Self::change_state(SuccessfulState { user_id: *user_id }), @@ -266,8 +255,8 @@ mod tests { let fut = async move { let initial_state: ReadingState = ReadingState { rx }; - let state_machine = StateMachine::from_ctx(AuthCtx { users }); - state_machine.run(initial_state).await + let mut state_machine = AuthStateMachine { users }; + state_machine.run(Box::new(initial_state)).await }; block_on(fut) } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index bfd69634ab..e0fa771ff2 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -34,6 +34,7 @@ use parking_lot::Mutex as PaMutex; use primitives::hash::{H256, H264}; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264 as H264Json}; use std::any::TypeId; +use std::convert::TryInto; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -1216,17 +1217,31 @@ impl MakerSwap { } } - let spend_fut = self.maker_coin.send_maker_refunds_payment(RefundPaymentArgs { - payment_tx: &maker_payment, - time_lock: locktime as u32, - other_pubkey: &*self.r().other_maker_coin_htlc_pub, - secret_hash: self.secret_hash().as_slice(), - swap_contract_address: &self.r().data.maker_coin_swap_contract_address, - swap_unique_data: &self.unique_swap_data(), - watcher_reward: self.r().watcher_reward, - }); + let other_maker_coin_htlc_pub = self.r().other_maker_coin_htlc_pub; + let maker_coin_swap_contract_address = self.r().data.maker_coin_swap_contract_address.clone(); + let watcher_reward = self.r().watcher_reward; + let time_lock: u32 = match locktime.try_into() { + Ok(t) => t, + Err(e) => { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentRefundFailed(ERRL!("!locktime.try_into: {}", e.to_string()).into()), + ])) + }, + }; + let spend_result = self + .maker_coin + .send_maker_refunds_payment(RefundPaymentArgs { + payment_tx: &maker_payment, + time_lock, + other_pubkey: other_maker_coin_htlc_pub.as_slice(), + secret_hash: self.secret_hash().as_slice(), + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &self.unique_swap_data(), + watcher_reward, + }) + .await; - let transaction = match spend_fut.compat().await { + let transaction = match spend_result { Ok(t) => t, Err(err) => { if let Some(tx) = err.get_tx() { @@ -1531,7 +1546,7 @@ impl MakerSwap { watcher_reward, }); - let transaction = match fut.compat().await { + let transaction = match fut.await { Ok(t) => t, Err(err) => { if let Some(tx) = err.get_tx() { @@ -2394,7 +2409,7 @@ mod maker_swap_tests { static mut MAKER_REFUND_CALLED: bool = false; TestCoin::send_maker_refunds_payment.mock_safe(|_, _| { unsafe { MAKER_REFUND_CALLED = true }; - MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) + MockResult::Return(Box::pin(futures::future::ok(eth_tx_for_test().into()))) }); TestCoin::search_for_swap_tx_spend_my .mock_safe(|_, _| MockResult::Return(Box::pin(futures::future::ready(Ok(None))))); @@ -2428,7 +2443,7 @@ mod maker_swap_tests { static mut MAKER_REFUND_CALLED: bool = false; TestCoin::send_maker_refunds_payment.mock_safe(|_, _| { unsafe { MAKER_REFUND_CALLED = true }; - MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) + MockResult::Return(Box::pin(futures::future::ok(eth_tx_for_test().into()))) }); TestCoin::search_for_swap_tx_spend_my @@ -2698,7 +2713,7 @@ mod maker_swap_tests { static mut SEND_MAKER_REFUNDS_PAYMENT_CALLED: bool = false; TestCoin::send_maker_refunds_payment.mock_safe(|_, _| { unsafe { SEND_MAKER_REFUNDS_PAYMENT_CALLED = true } - MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) + MockResult::Return(Box::pin(futures::future::ok(eth_tx_for_test().into()))) }); let maker_coin = MmCoinEnum::Test(TestCoin::default()); diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index 1e6bcdf3f2..f85faf2bae 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -9,6 +9,7 @@ use coins::{CanRefundHtlc, ConfirmPaymentInput, FoundSwapTxSpend, MmCoinEnum, Re use common::executor::{AbortSettings, SpawnAbortable, Timer}; use common::log::{debug, error, info}; use common::state_machine::prelude::*; +use common::state_machine::StateMachineTrait; use common::{now_sec, DEX_FEE_ADDR_RAW_PUBKEY}; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; @@ -28,7 +29,7 @@ pub const MAKER_PAYMENT_SPEND_SENT_LOG: &str = "Maker payment spend sent"; pub const MAKER_PAYMENT_SPEND_FOUND_LOG: &str = "Maker payment spend found by watcher"; pub const TAKER_PAYMENT_REFUND_SENT_LOG: &str = "Taker payment refund sent"; -struct WatcherContext { +struct WatcherStateMachine { ctx: MmArc, taker_coin: MmCoinEnum, maker_coin: MmCoinEnum, @@ -38,7 +39,11 @@ struct WatcherContext { watcher_reward: bool, } -impl WatcherContext { +impl StateMachineTrait for WatcherStateMachine { + type Result = (); +} + +impl WatcherStateMachine { fn taker_locktime(&self) -> u64 { self.data.swap_started_at + self.data.lock_duration } fn wait_for_maker_payment_spend_deadline(&self) -> u64 { @@ -167,10 +172,9 @@ impl TransitionFrom for Stopped {} #[async_trait] impl State for ValidateTakerFee { - type Ctx = WatcherContext; - type Result = (); + type StateMachine = WatcherStateMachine; - async fn on_changed(self: Box, watcher_ctx: &mut WatcherContext) -> StateResult { + async fn on_changed(self: Box, watcher_ctx: &mut WatcherStateMachine) -> StateResult { let validated_f = watcher_ctx .taker_coin .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { @@ -195,10 +199,9 @@ impl State for ValidateTakerFee { // TODO: Validate also maker payment #[async_trait] impl State for ValidateTakerPayment { - type Ctx = WatcherContext; - type Result = (); + type StateMachine = WatcherStateMachine; - async fn on_changed(self: Box, watcher_ctx: &mut WatcherContext) -> StateResult { + async fn on_changed(self: Box, watcher_ctx: &mut WatcherStateMachine) -> StateResult { let taker_payment_spend_deadline = taker_payment_spend_deadline(watcher_ctx.data.swap_started_at, watcher_ctx.data.lock_duration); @@ -269,10 +272,9 @@ impl State for ValidateTakerPayment { #[async_trait] impl State for WaitForTakerPaymentSpend { - type Ctx = WatcherContext; - type Result = (); + type StateMachine = WatcherStateMachine; - async fn on_changed(self: Box, watcher_ctx: &mut WatcherContext) -> StateResult { + async fn on_changed(self: Box, watcher_ctx: &mut WatcherStateMachine) -> StateResult { let payment_search_interval = watcher_ctx.conf.search_interval; let wait_until = watcher_ctx.refund_start_time(); let search_input = WatcherSearchForSwapTxSpendInput { @@ -374,10 +376,9 @@ impl State for WaitForTakerPaymentSpend { #[async_trait] impl State for SpendMakerPayment { - type Ctx = WatcherContext; - type Result = (); + type StateMachine = WatcherStateMachine; - async fn on_changed(self: Box, watcher_ctx: &mut WatcherContext) -> StateResult { + async fn on_changed(self: Box, watcher_ctx: &mut WatcherStateMachine) -> StateResult { let spend_fut = watcher_ctx .maker_coin .send_maker_payment_spend_preimage(SendMakerPaymentSpendPreimageInput { @@ -426,10 +427,9 @@ impl State for SpendMakerPayment { #[async_trait] impl State for RefundTakerPayment { - type Ctx = WatcherContext; - type Result = (); + type StateMachine = WatcherStateMachine; - async fn on_changed(self: Box, watcher_ctx: &mut WatcherContext) -> StateResult { + async fn on_changed(self: Box, watcher_ctx: &mut WatcherStateMachine) -> StateResult { if std::env::var("USE_TEST_LOCKTIME").is_err() { loop { match watcher_ctx @@ -497,9 +497,9 @@ impl State for RefundTakerPayment { #[async_trait] impl LastState for Stopped { - type Ctx = WatcherContext; - type Result = (); - async fn on_changed(self: Box, _watcher_ctx: &mut Self::Ctx) -> Self::Result {} + type StateMachine = WatcherStateMachine; + + async fn on_changed(self: Box, _watcher_ctx: &mut WatcherStateMachine) -> () {} } pub fn process_watcher_msg(ctx: MmArc, msg: &[u8]) -> P2PRequestResult<()> { @@ -624,7 +624,7 @@ fn spawn_taker_swap_watcher(ctx: MmArc, watcher_data: TakerSwapWatcherData, veri let conf = json::from_value::(ctx.conf["watcher_conf"].clone()).unwrap_or_default(); let watcher_reward = maker_coin.is_eth(); - let watcher_ctx = WatcherContext { + let mut state_machine = WatcherStateMachine { ctx, maker_coin, taker_coin, @@ -633,8 +633,7 @@ fn spawn_taker_swap_watcher(ctx: MmArc, watcher_data: TakerSwapWatcherData, veri conf, watcher_reward, }; - let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(watcher_ctx); - state_machine.run(ValidateTakerFee {}).await; + state_machine.run(Box::new(ValidateTakerFee {})).await; // This allows to move the `taker_watcher_lock` value into this async block to keep it alive // until the Swap Watcher finishes. diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 1be5345b2d..ad7e9d0ca4 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -34,7 +34,7 @@ use parking_lot::Mutex as PaMutex; use primitives::hash::H264; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264 as H264Json}; use serde_json::{self as json, Value as Json}; -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -1828,17 +1828,32 @@ impl TakerSwap { } } - let refund_fut = self.taker_coin.send_taker_refunds_payment(RefundPaymentArgs { - payment_tx: &taker_payment, - time_lock: locktime as u32, - other_pubkey: &*self.r().other_taker_coin_htlc_pub, - secret_hash: &self.r().secret_hash.0, - swap_contract_address: &self.r().data.taker_coin_swap_contract_address, - swap_unique_data: &self.unique_swap_data(), - watcher_reward: self.r().watcher_reward, - }); + let other_taker_coin_htlc_pub = self.r().other_taker_coin_htlc_pub; + let secret_hash = self.r().secret_hash.clone(); + let swap_contract_address = self.r().data.taker_coin_swap_contract_address.clone(); + let watcher_reward = self.r().watcher_reward; + let time_lock: u32 = match locktime.try_into() { + Ok(t) => t, + Err(e) => { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentRefundFailed(ERRL!("!locktime.try_into: {}", e.to_string()).into()), + ])) + }, + }; + let refund_result = self + .taker_coin + .send_taker_refunds_payment(RefundPaymentArgs { + payment_tx: &taker_payment, + time_lock, + other_pubkey: other_taker_coin_htlc_pub.as_slice(), + secret_hash: &secret_hash, + swap_contract_address: &swap_contract_address, + swap_unique_data: &self.unique_swap_data(), + watcher_reward, + }) + .await; - let transaction = match refund_fut.compat().await { + let transaction = match refund_result { Ok(t) => t, Err(err) => { if let Some(tx) = err.get_tx() { @@ -2183,7 +2198,7 @@ impl TakerSwap { watcher_reward, }); - let transaction = match fut.compat().await { + let transaction = match fut.await { Ok(t) => t, Err(err) => { if let Some(tx) = err.get_tx() { @@ -2668,7 +2683,7 @@ mod taker_swap_tests { static mut TAKER_PAYMENT_REFUND_CALLED: bool = false; TestCoin::send_taker_refunds_payment.mock_safe(|_, _| { unsafe { TAKER_PAYMENT_REFUND_CALLED = true }; - MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) + MockResult::Return(Box::pin(futures::future::ok(eth_tx_for_test().into()))) }); let maker_coin = MmCoinEnum::Test(TestCoin::default()); let taker_coin = MmCoinEnum::Test(TestCoin::default()); @@ -2753,7 +2768,7 @@ mod taker_swap_tests { static mut REFUND_CALLED: bool = false; TestCoin::send_taker_refunds_payment.mock_safe(|_, _| { unsafe { REFUND_CALLED = true }; - MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) + MockResult::Return(Box::pin(futures::future::ok(eth_tx_for_test().into()))) }); let maker_coin = MmCoinEnum::Test(TestCoin::default()); let taker_coin = MmCoinEnum::Test(TestCoin::default()); 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 6c5db29d00..2079a76141 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -69,6 +69,9 @@ pub const UTXO_ASSET_DOCKER_IMAGE: &str = "docker.io/artempikulin/testblockchain pub const QTUM_ADDRESS_LABEL: &str = "MM2_ADDRESS_LABEL"; +pub const MYCOIN: &str = "MYCOIN"; +pub const _MYCOIN1: &str = "MYCOIN1"; + pub trait CoinDockerOps { fn rpc_client(&self) -> &UtxoRpcClientEnum; 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 94f1a8f6c2..4df7bf911a 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -60,10 +60,7 @@ fn test_search_for_swap_tx_spend_native_was_refunded_taker() { swap_unique_data: &[], watcher_reward: false, }; - let refund_tx = coin - .send_maker_refunds_payment(maker_refunds_payment_args) - .wait() - .unwrap(); + let refund_tx = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: refund_tx.tx_hex(), @@ -148,10 +145,7 @@ fn test_search_for_swap_tx_spend_native_was_refunded_maker() { swap_unique_data: &[], watcher_reward: false, }; - let refund_tx = coin - .send_maker_refunds_payment(maker_refunds_payment_args) - .wait() - .unwrap(); + let refund_tx = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: refund_tx.tx_hex(), diff --git a/mm2src/mm2_main/tests/docker_tests/mod.rs b/mm2src/mm2_main/tests/docker_tests/mod.rs index 4ac4e6541d..c608944faf 100644 --- a/mm2src/mm2_main/tests/docker_tests/mod.rs +++ b/mm2src/mm2_main/tests/docker_tests/mod.rs @@ -4,6 +4,7 @@ mod docker_ordermatch_tests; mod docker_tests_inner; pub mod qrc20_tests; mod slp_tests; +mod swap_proto_v2_tests; mod swap_watcher_tests; mod swaps_confs_settings_sync_tests; mod swaps_file_lock_tests; diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index b5c5526436..8a08865490 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -422,10 +422,7 @@ fn test_maker_refunds_payment() { swap_unique_data: &[], watcher_reward: false, }; - let refund = coin - .send_maker_refunds_payment(maker_refunds_payment_args) - .wait() - .unwrap(); + let refund = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); let refund_tx_hash = refund.tx_hash(); let refund_tx_hex = refund.tx_hex(); log!("Maker refunds payment: {:?}", refund_tx_hash); @@ -495,10 +492,7 @@ fn test_taker_refunds_payment() { swap_unique_data: &[], watcher_reward: false, }; - let refund = coin - .send_taker_refunds_payment(taker_refunds_payment_args) - .wait() - .unwrap(); + let refund = block_on(coin.send_taker_refunds_payment(taker_refunds_payment_args)).unwrap(); let refund_tx_hash = refund.tx_hash(); let refund_tx_hex = refund.tx_hex(); log!("Taker refunds payment: {:?}", refund_tx_hash); @@ -701,10 +695,7 @@ fn test_search_for_swap_tx_spend_maker_refunded() { swap_unique_data: &[], watcher_reward: false, }; - let refund = maker_coin - .send_maker_refunds_payment(maker_refunds_payment_args) - .wait() - .unwrap(); + let refund = block_on(maker_coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); let refund_tx_hash = refund.tx_hash(); let refund_tx_hex = refund.tx_hex(); log!("Maker refunds tx: {:?}", refund_tx_hash); @@ -1556,10 +1547,7 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_maker() { swap_unique_data: &[], watcher_reward: false, }; - let refund_tx = coin - .send_maker_refunds_payment(maker_refunds_payment_args) - .wait() - .unwrap(); + let refund_tx = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: refund_tx.tx_hex(), @@ -1625,10 +1613,7 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_taker() { swap_unique_data: &[], watcher_reward: false, }; - let refund_tx = coin - .send_maker_refunds_payment(maker_refunds_payment_args) - .wait() - .unwrap(); + let refund_tx = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: refund_tx.tx_hex(), diff --git a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs new file mode 100644 index 0000000000..a661b6cdc7 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs @@ -0,0 +1,129 @@ +use crate::{generate_utxo_coin_with_random_privkey, MYCOIN}; +use bitcrypto::dhash160; +use coins::utxo::UtxoCommonOps; +use coins::{GenDexFeeSpendArgs, RefundPaymentArgs, SendDexFeeWithPremiumArgs, SwapOpsV2, Transaction, TransactionEnum, + ValidateDexFeeArgs}; +use common::{block_on, now_sec_u32, DEX_FEE_ADDR_RAW_PUBKEY}; +use script::{Builder, Opcode}; + +#[test] +fn send_and_refund_dex_fee() { + let (_mm_arc, coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); + + let time_lock = now_sec_u32() - 1000; + let secret_hash = &[0; 20]; + let other_pub = coin.my_public_key().unwrap(); + + let send_args = SendDexFeeWithPremiumArgs { + time_lock, + secret_hash, + other_pub, + dex_fee_amount: "0.01".parse().unwrap(), + premium_amount: "0.1".parse().unwrap(), + swap_unique_data: &[], + }; + let dex_fee_tx = block_on(coin.send_dex_fee_with_premium(send_args)).unwrap(); + println!("{:02x}", dex_fee_tx.tx_hash()); + let dex_fee_utxo_tx = match dex_fee_tx { + TransactionEnum::UtxoTx(tx) => tx, + unexpected => panic!("Unexpected tx {:?}", unexpected), + }; + // tx must have 3 outputs: actual payment, OP_RETURN containing the secret hash and change + assert_eq!(3, dex_fee_utxo_tx.outputs.len()); + + // dex_fee_amount + premium_amount + let expected_amount = 11000000u64; + assert_eq!(expected_amount, dex_fee_utxo_tx.outputs[0].value); + + let expected_op_return = Builder::default() + .push_opcode(Opcode::OP_RETURN) + .push_data(&[0; 20]) + .into_bytes(); + assert_eq!(expected_op_return, dex_fee_utxo_tx.outputs[1].script_pubkey); + + let dex_fee_bytes = dex_fee_utxo_tx.tx_hex(); + + let validate_args = ValidateDexFeeArgs { + dex_fee_tx: &dex_fee_bytes, + time_lock, + secret_hash, + other_pub, + dex_fee_amount: "0.01".parse().unwrap(), + premium_amount: "0.1".parse().unwrap(), + swap_unique_data: &[], + }; + block_on(coin.validate_dex_fee_with_premium(validate_args)).unwrap(); + + let refund_args = RefundPaymentArgs { + payment_tx: &dex_fee_bytes, + time_lock, + other_pubkey: coin.my_public_key().unwrap(), + secret_hash: &[0; 20], + swap_unique_data: &[], + swap_contract_address: &None, + watcher_reward: false, + }; + + let refund_tx = block_on(coin.refund_dex_fee_with_premium(refund_args)).unwrap(); + println!("{:02x}", refund_tx.tx_hash()); +} + +#[test] +fn send_and_spend_dex_fee() { + let (_, taker_coin, _) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); + let (_, maker_coin, _) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); + + let time_lock = now_sec_u32() - 1000; + let secret = [1; 32]; + let secret_hash = dhash160(&secret); + let send_args = SendDexFeeWithPremiumArgs { + time_lock, + secret_hash: secret_hash.as_slice(), + other_pub: maker_coin.my_public_key().unwrap(), + dex_fee_amount: "0.01".parse().unwrap(), + premium_amount: "0.1".parse().unwrap(), + swap_unique_data: &[], + }; + let dex_fee_tx = block_on(taker_coin.send_dex_fee_with_premium(send_args)).unwrap(); + println!("dex_fee_tx hash {:02x}", dex_fee_tx.tx_hash()); + let dex_fee_utxo_tx = match dex_fee_tx { + TransactionEnum::UtxoTx(tx) => tx, + unexpected => panic!("Unexpected tx {:?}", unexpected), + }; + + let dex_fee_bytes = dex_fee_utxo_tx.tx_hex(); + let validate_args = ValidateDexFeeArgs { + dex_fee_tx: &dex_fee_bytes, + time_lock, + secret_hash: secret_hash.as_slice(), + other_pub: taker_coin.my_public_key().unwrap(), + dex_fee_amount: "0.01".parse().unwrap(), + premium_amount: "0.1".parse().unwrap(), + swap_unique_data: &[], + }; + block_on(maker_coin.validate_dex_fee_with_premium(validate_args)).unwrap(); + + let gen_preimage_args = GenDexFeeSpendArgs { + dex_fee_tx: &dex_fee_utxo_tx.tx_hex(), + time_lock, + secret_hash: secret_hash.as_slice(), + maker_pub: maker_coin.my_public_key().unwrap(), + taker_pub: taker_coin.my_public_key().unwrap(), + dex_fee_pub: &DEX_FEE_ADDR_RAW_PUBKEY, + dex_fee_amount: "0.01".parse().unwrap(), + premium_amount: "0.1".parse().unwrap(), + }; + let preimage_with_taker_sig = + block_on(taker_coin.gen_and_sign_dex_fee_spend_preimage(&gen_preimage_args, &[])).unwrap(); + + block_on(maker_coin.validate_dex_fee_spend_preimage(&gen_preimage_args, &preimage_with_taker_sig)).unwrap(); + + let dex_fee_spend = block_on(maker_coin.sign_and_broadcast_dex_fee_spend( + &preimage_with_taker_sig, + &gen_preimage_args, + &secret, + &[], + )) + .unwrap(); + println!("dex_fee_spend hash {:02x}", dex_fee_spend.tx_hash()); +} diff --git a/mm2src/mm2_net/src/wasm_ws.rs b/mm2src/mm2_net/src/wasm_ws.rs index bf8008de54..fefdfa74cf 100644 --- a/mm2src/mm2_net/src/wasm_ws.rs +++ b/mm2src/mm2_net/src/wasm_ws.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use common::executor::SpawnFuture; use common::log::{debug, error}; -use common::state_machine::{LastState, State, StateExt, StateMachine, StateResult, TransitionFrom}; +use common::state_machine::{LastState, State, StateExt, StateMachineTrait, StateResult, TransitionFrom}; use common::stringify_js_error; use futures::channel::mpsc::{self, SendError, TrySendError}; use futures::channel::oneshot; @@ -206,7 +206,7 @@ fn spawn_ws_transport( let user_shutdown = into_one_shutdown(incoming_shutdown, outgoing_shutdown); let state_event_rx = StateEventListener::new(outgoing_rx, ws_transport_rx, user_shutdown); - let ws_ctx = WsContext { + let mut state_machine = WsStateMachine { idx, ws, event_tx: incoming_tx, @@ -214,8 +214,7 @@ fn spawn_ws_transport( }; let fut = async move { - let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ws_ctx); - state_machine.run(ConnectingState).await; + state_machine.run(Box::new(ConnectingState)).await; }; spawner.spawn(fut); @@ -367,7 +366,7 @@ impl Drop for WebSocketImpl { } } -struct WsContext { +struct WsStateMachine { idx: ConnIdx, ws: WebSocketImpl, /// The sender is used to send the transport events outside (to the userspace). @@ -376,7 +375,11 @@ struct WsContext { state_event_rx: StateEventListener, } -impl WsContext { +impl StateMachineTrait for WsStateMachine { + type Result = (); +} + +impl WsStateMachine { /// Send the `event` to the corresponding `WebSocketReceiver` instance. fn notify_listener(&mut self, event: WebSocketEvent) { if !self.event_tx.is_closed() { @@ -406,10 +409,10 @@ impl WsContext { } } -/// `WsContext` is not thread-safety `Send` because [`WebSocket::ws`] is not `Send` by default. -/// Although wasm is currently single-threaded, we can implement the `Send` trait for `WsContext`, +/// `WsStateMachine` is not thread-safety `Send` because [`WebSocket::ws`] is not `Send` by default. +/// Although wasm is currently single-threaded, we can implement the `Send` trait for `WsStateMachine`, /// but it won't be safe when wasm becomes multi-threaded. -unsafe impl Send for WsContext {} +unsafe impl Send for WsStateMachine {} struct StateEventListener { rx: Box + Unpin + Send>, @@ -488,10 +491,9 @@ impl TransitionFrom for ClosedState {} #[async_trait] impl LastState for ClosedState { - type Ctx = WsContext; - type Result = (); + type StateMachine = WsStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> Self::Result { + async fn on_changed(self: Box, ctx: &mut WsStateMachine) -> () { debug!("WebScoket idx={} => ClosedState", ctx.idx); // Notify the listener that the connection has been closed to prevent new outgoing messages. ctx.notify_listener(WebSocketEvent::Closed { @@ -499,16 +501,15 @@ impl LastState for ClosedState { }); // Please note that we don't need to close websocket via `ctx.ws.close_with_code()`. - // It will be closed on [`WsContext::drop`] right after the state machine is finished. + // It will be closed on [`WsStateMachine::drop`] right after the state machine is finished. } } #[async_trait] impl State for ConnectingState { - type Ctx = WsContext; - type Result = (); + type StateMachine = WsStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed(self: Box, ctx: &mut WsStateMachine) -> StateResult { debug!("WebSocket idx={} => ConnectingState", ctx.idx); while let Some(event) = ctx.state_event_rx.receive_one().await { match event { @@ -544,10 +545,9 @@ impl State for ConnectingState { #[async_trait] impl State for OpenState { - type Ctx = WsContext; - type Result = (); + type StateMachine = WsStateMachine; - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + async fn on_changed(self: Box, ctx: &mut WsStateMachine) -> StateResult { debug!("WebSocket idx={} => OpenState", ctx.idx); // notify the listener about the changed state ctx.notify_listener(WebSocketEvent::Establish); From 9d5ab1137c472930f01b6608e0643141c2c9dce0 Mon Sep 17 00:00:00 2001 From: Rozhkov Dmitrii Date: Thu, 24 Aug 2023 13:54:35 +0500 Subject: [PATCH 10/17] fix(cli): use the updated activation scheme (#1938) Activation scheme was changed hence related data types have to be fit for it. --- mm2src/adex_cli/src/rpc_data.rs | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/mm2src/adex_cli/src/rpc_data.rs b/mm2src/adex_cli/src/rpc_data.rs index 2f534e74c1..f8e1329453 100644 --- a/mm2src/adex_cli/src/rpc_data.rs +++ b/mm2src/adex_cli/src/rpc_data.rs @@ -4,8 +4,7 @@ //! use mm2_rpc::data::legacy::{ElectrumProtocol, GasStationPricePolicy, UtxoMergeParams}; -use serde::ser::SerializeSeq; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "method", rename_all = "lowercase")] @@ -17,8 +16,8 @@ pub(crate) enum ActivationRequest { #[derive(Debug, Deserialize, Serialize)] pub(crate) struct EnableRequest { coin: String, - #[serde(default, serialize_with = "serialize_urls", skip_serializing_if = "Vec::is_empty")] - urls: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + urls: Vec, #[serde(skip_serializing_if = "Option::is_none")] swap_contract_address: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -41,22 +40,6 @@ pub(crate) struct EnableRequest { contract_supports_watchers: Option, } -fn serialize_urls(urls: &Vec, s: S) -> Result -where - S: Serializer, -{ - let mut s_seq = s.serialize_seq(None)?; - for url in urls { - s_seq.serialize_element(url.url.as_str())?; - } - s_seq.end() -} - -#[derive(Debug, Deserialize)] -struct EnableUrl { - url: String, -} - #[derive(Debug, Deserialize, Serialize)] pub(crate) struct ElectrumRequest { coin: String, From 410eda2c6c7888a5ec6e449ed096236dc52e1dc8 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Thu, 24 Aug 2023 11:55:28 +0300 Subject: [PATCH 11/17] fix(kmd): use kmd rewards for fees if change + interest is below dust (#1944) --- mm2src/coins/qrc20.rs | 5 +++-- mm2src/coins/utxo.rs | 3 ++- mm2src/coins/utxo/bch.rs | 3 ++- mm2src/coins/utxo/qtum.rs | 3 ++- mm2src/coins/utxo/qtum_delegation.rs | 2 +- mm2src/coins/utxo/slp.rs | 3 ++- mm2src/coins/utxo/utxo_common.rs | 25 +++++++++++++++---------- mm2src/coins/utxo/utxo_standard.rs | 3 ++- mm2src/coins/utxo/utxo_tests.rs | 10 +++++----- mm2src/coins/utxo/utxo_withdraw.rs | 2 +- mm2src/coins/z_coin.rs | 5 +++-- 11 files changed, 38 insertions(+), 26 deletions(-) diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index d00139b56c..dcad8c47df 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -545,7 +545,7 @@ impl Qrc20Coin { self.utxo.conf.fork_id, )?; - let miner_fee = data.fee_amount + data.unused_change.unwrap_or_default(); + let miner_fee = data.fee_amount + data.unused_change; Ok(GenerateQrc20TxResult { signed, miner_fee, @@ -616,8 +616,9 @@ impl UtxoTxGenerationOps for Qrc20Coin { 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).await + utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await } } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 8c0459e63c..9ccf7c35f8 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -257,7 +257,7 @@ pub struct AdditionalTxData { pub received_by_me: u64, pub spent_by_me: u64, pub fee_amount: u64, - pub unused_change: Option, + pub unused_change: u64, pub kmd_rewards: Option, } @@ -839,6 +839,7 @@ pub trait UtxoTxGenerationOps { mut unsigned: TransactionInputSigner, mut data: AdditionalTxData, my_script_pub: Bytes, + dust: u64, ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)>; } diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 8d0592797d..e85f2a8d98 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -703,8 +703,9 @@ impl UtxoTxGenerationOps for BchCoin { 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).await + utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await } } diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index cfe3f2f4c1..8a4e8b4eae 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -341,8 +341,9 @@ impl UtxoTxGenerationOps for QtumCoin { 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).await + utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await } } diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index 65efadee7f..6cd4ee4336 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -299,7 +299,7 @@ impl QtumCoin { utxo.conf.fork_id, )?; - let miner_fee = data.fee_amount + data.unused_change.unwrap_or_default(); + let miner_fee = data.fee_amount + data.unused_change; let generated_tx = GenerateQrc20TxResult { signed, miner_fee, diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 8f996fdf0f..3e03eab7f2 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -1072,9 +1072,10 @@ impl UtxoTxGenerationOps for SlpToken { 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) + .calc_interest_if_required(unsigned, data, my_script_pub, dust) .await } } diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 33b705599c..053ab674b8 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1008,11 +1008,9 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } }); received_by_me += change; - None - } else if change > 0 { - Some(change) + 0 } else { - None + change }; let data = AdditionalTxData { @@ -1025,7 +1023,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { }; Ok(coin - .calc_interest_if_required(self.tx, data, change_script_pubkey) + .calc_interest_if_required(self.tx, data, change_script_pubkey, dust) .await?) } } @@ -1038,6 +1036,7 @@ pub async fn calc_interest_if_required( 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)); @@ -1069,11 +1068,17 @@ pub async fn calc_interest_if_required( match output_to_me { Some(ref mut output) => output.value += interest, None => { - let interest_output = TransactionOutput { - script_pubkey: my_script_pub, - value: interest, - }; - unsigned.outputs.push(interest_output); + 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 { diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 6a0ca49dac..431faaee86 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -111,8 +111,9 @@ impl UtxoTxGenerationOps for UtxoStandardCoin { 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).await + utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await } } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 6cc4337d75..e7e061a176 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -226,7 +226,7 @@ fn test_generate_transaction() { assert_eq!(generated.0.outputs.len(), 1); assert_eq!(generated.1.fee_amount, 1000); - assert_eq!(generated.1.unused_change, Some(999)); + assert_eq!(generated.1.unused_change, 999); assert_eq!(generated.1.received_by_me, 0); assert_eq!(generated.1.spent_by_me, 100000); @@ -251,7 +251,7 @@ fn test_generate_transaction() { assert_eq!(generated.0.outputs.len(), 1); assert_eq!(generated.1.fee_amount, 1000); - assert_eq!(generated.1.unused_change, None); + 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); @@ -1156,7 +1156,7 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower() { // 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, None); + assert_eq!(generated.1.unused_change, 0); assert_eq!(generated.1.received_by_me, 0); assert_eq!(generated.1.spent_by_me, 1000000000); assert!(unsafe { GET_RELAY_FEE_CALLED }); @@ -1201,7 +1201,7 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower_and_ded // 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, None); + assert_eq!(generated.1.unused_change, 0); assert_eq!(generated.1.received_by_me, 0); assert_eq!(generated.1.spent_by_me, 1000000000); assert!(unsafe { GET_RELAY_FEE_CALLED }); @@ -1248,7 +1248,7 @@ 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, None); + 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 }); diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index ea0da556c1..de5bad9f32 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -187,7 +187,7 @@ where // Finish by generating `TransactionDetails` from the signed transaction. self.on_finishing()?; - let fee_amount = data.fee_amount + data.unused_change.unwrap_or_default(); + 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), diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index ee6f6a23d3..c911bf4a7e 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -488,7 +488,7 @@ 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: None, + unused_change: 0, kmd_rewards: None, }; Ok((tx, additional_data, sync_guard)) @@ -1746,8 +1746,9 @@ impl UtxoTxGenerationOps for ZCoin { 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).await + utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await } } From e4b091b993fbab720e6fd948d2f3b8aa086b79ff Mon Sep 17 00:00:00 2001 From: Onur Date: Mon, 28 Aug 2023 13:55:16 +0300 Subject: [PATCH 12/17] chore(contact info update): replace old github username (#1949) Signed-off-by: ozkanonur --- docs/GIT_FLOW_AND_WORKING_PROCESS.md | 2 +- mm2src/coins/tendermint/iris/htlc.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/GIT_FLOW_AND_WORKING_PROCESS.md b/docs/GIT_FLOW_AND_WORKING_PROCESS.md index e9dc6a4821..2dd6ee1be2 100644 --- a/docs/GIT_FLOW_AND_WORKING_PROCESS.md +++ b/docs/GIT_FLOW_AND_WORKING_PROCESS.md @@ -31,5 +31,5 @@ Cons: [@artemii235](https://github.com/artemii235) [@sergeyboyko0791](https://github.com/sergeyboyko0791) [@shamardy](https://github.com/shamardy) -[@ozkanonur](https://github.com/ozkanonur) +[@onur-ozkan](https://github.com/onur-ozkan) diff --git a/mm2src/coins/tendermint/iris/htlc.rs b/mm2src/coins/tendermint/iris/htlc.rs index 226c1794fc..4e7b679481 100644 --- a/mm2src/coins/tendermint/iris/htlc.rs +++ b/mm2src/coins/tendermint/iris/htlc.rs @@ -11,7 +11,7 @@ // // Because we had limited time for the HTLC implementation, for now // we can use their unit tests in order to acquire IBC assets. -// For that, clone https://github.com/ozkanonur/irishub-sdk-js repository and check +// For that, clone https://github.com/onur-ozkan/irishub-sdk-js repository and check // dummy.test.ts file(change the asset, amount, target address if needed) // and then run the following commands: // - yarn From 51c44f67f91daec5e6f64d138c3a0abae49c9a2a Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Thu, 31 Aug 2023 16:45:53 +0300 Subject: [PATCH 13/17] fix(hd-wallet): enable/withdraw using any account'/change/address_index (#1933) Global enabling of an account'/change/address_index path for all coins using hd_account_id config parameter is replaced by enable_hd which is a bool that defaults to false. path_to_address parameter is added to coins activation requests to set the default account'/change/address_index path that will be used for swaps. If not provided, the default will be 0'/0/0. HD withdrawal from any account'/change/address_index path is implemented for UTXO, EVM and Tendermint coins for now, other coins will be added later. --- mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs | 19 +- mm2src/coins/eth.rs | 145 ++-- mm2src/coins/eth/v2_activation.rs | 63 +- mm2src/coins/lightning/ln_events.rs | 2 +- mm2src/coins/lightning/ln_utils.rs | 2 +- mm2src/coins/lp_coins.rs | 144 +++- mm2src/coins/nft.rs | 12 +- mm2src/coins/qrc20.rs | 2 +- .../rpc_command/tendermint/ibc_withdraw.rs | 3 +- mm2src/coins/solana.rs | 6 +- mm2src/coins/tendermint/tendermint_coin.rs | 248 ++++-- mm2src/coins/tendermint/tendermint_token.rs | 96 ++- mm2src/coins/utxo.rs | 18 +- mm2src/coins/utxo/qtum_delegation.rs | 4 +- mm2src/coins/utxo/slp.rs | 4 +- .../utxo/utxo_builder/utxo_coin_builder.rs | 48 +- mm2src/coins/utxo/utxo_common.rs | 37 +- mm2src/coins/utxo/utxo_common_tests.rs | 2 +- mm2src/coins/utxo/utxo_tests.rs | 4 +- mm2src/coins/utxo/utxo_withdraw.rs | 66 +- mm2src/coins/z_coin.rs | 31 +- mm2src/coins/z_coin/storage/walletdb/mod.rs | 23 +- mm2src/coins/z_coin/z_coin_native_tests.rs | 5 +- .../src/eth_with_token_activation.rs | 3 - .../src/tendermint_with_assets_activation.rs | 24 +- mm2src/crypto/src/crypto_ctx.rs | 19 +- mm2src/crypto/src/global_hd_ctx.rs | 65 +- mm2src/crypto/src/hw_ctx.rs | 5 +- mm2src/crypto/src/lib.rs | 14 +- mm2src/crypto/src/standard_hd_path.rs | 15 + mm2src/mm2_main/src/lp_native_dex.rs | 11 +- mm2src/mm2_main/src/lp_swap.rs | 2 + mm2src/mm2_main/src/wasm_tests.rs | 42 +- .../docker_tests/docker_ordermatch_tests.rs | 40 +- .../tests/docker_tests/docker_tests_common.rs | 16 +- .../tests/docker_tests/docker_tests_inner.rs | 240 +++--- .../tests/docker_tests/qrc20_tests.rs | 28 +- .../mm2_main/tests/docker_tests/slp_tests.rs | 7 +- .../tests/docker_tests/swap_watcher_tests.rs | 2 +- .../swaps_confs_settings_sync_tests.rs | 16 +- .../docker_tests/swaps_file_lock_tests.rs | 24 +- .../tests/integration_tests_common/mod.rs | 45 +- .../tests/mm2_tests/bch_and_slp_tests.rs | 67 +- .../tests/mm2_tests/best_orders_tests.rs | 18 +- mm2src/mm2_main/tests/mm2_tests/iris_swap.rs | 4 +- .../tests/mm2_tests/lightning_tests.rs | 20 +- .../tests/mm2_tests/mm2_tests_inner.rs | 765 ++++++++++++++---- .../tests/mm2_tests/orderbook_sync_tests.rs | 111 ++- .../tests/mm2_tests/tendermint_tests.rs | 178 +++- .../mm2_main/tests/mm2_tests/z_coin_tests.rs | 14 +- mm2src/mm2_test_helpers/src/for_tests.rs | 91 ++- 51 files changed, 2068 insertions(+), 802 deletions(-) diff --git a/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs b/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs index b21c3f292b..60b3fbe445 100644 --- a/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs +++ b/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs @@ -51,7 +51,7 @@ struct Mm2Cfg { #[serde(skip_serializing_if = "Vec::::is_empty")] seednodes: Vec, #[serde(skip_serializing_if = "Option::is_none")] - hd_account_id: Option, + enable_hd: Option, } impl Mm2Cfg { @@ -68,7 +68,7 @@ impl Mm2Cfg { rpc_local_only: None, i_am_seed: None, seednodes: Vec::::new(), - hd_account_id: None, + enable_hd: None, } } @@ -84,7 +84,7 @@ impl Mm2Cfg { self.inquire_rpc_local_only()?; self.inquire_i_am_a_seed()?; self.inquire_seednodes()?; - self.inquire_hd_account_id()?; + self.inquire_enable_hd()?; Ok(()) } @@ -311,13 +311,16 @@ impl Mm2Cfg { } #[inline] - fn inquire_hd_account_id(&mut self) -> Result<()> { - self.hd_account_id = CustomType::>::new("What is hd_account_id:") - .with_help_message(r#"Optional. If this value is set, the AtomicDEX-API will work in only the HD derivation mode, coins will need to have a coin derivation path entry in the coins file for activation. The hd_account_id value effectively takes its place in the full derivation as follows: m/44'/COIN_ID'/'/CHAIN/ADDRESS_ID"#) - .with_placeholder(DEFAULT_OPTION_PLACEHOLDER) + fn inquire_enable_hd(&mut self) -> Result<()> { + self.enable_hd = CustomType::>::new("What is enable_hd:") + .with_parser(OPTION_BOOL_PARSER) + .with_formatter(DEFAULT_OPTION_BOOL_FORMATTER) + .with_default_value_formatter(DEFAULT_DEFAULT_OPTION_BOOL_FORMATTER) + .with_default(InquireOption::None) + .with_help_message(r#"Optional. If this value is set, the Komodo DeFi API will work in HD wallet mode only, coins will need to have a coin derivation path entry in the coins file for activation. path_to_address `/account'/change/address_index` will have to be set in coins activation to change the default HD wallet address that is used in swaps for a coin in the full derivation path as follows: m/purpose'/coin_type/account'/change/address_index"#) .prompt() .map_err(|error| - error_anyhow!("Failed to get hd_account_id: {}", error) + error_anyhow!("Failed to get enable_hd: {}", error) )? .into(); Ok(()) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index b9cf3e3fad..cba9ece714 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -34,7 +34,7 @@ use common::{get_utc_timestamp, now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; #[cfg(target_arch = "wasm32")] use common::{now_ms, wait_until_ms}; use crypto::privkey::key_pair_from_secret; -use crypto::{CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; +use crypto::{CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy, StandardHDCoinAddress}; use derive_more::Display; use enum_from::EnumFromStringify; use ethabi::{Contract, Function, Token}; @@ -110,7 +110,7 @@ use crate::nft::{find_wallet_nft_amount, WithdrawNftResult}; use v2_activation::{build_address_and_priv_key_policy, EthActivationV2Error}; mod nonce; -use crate::TransactionResult; +use crate::{PrivKeyPolicy, TransactionResult, WithdrawFrom}; use nonce::ParityNonce; /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol @@ -171,6 +171,7 @@ lazy_static! { pub type Web3RpcFut = Box> + Send>; pub type Web3RpcResult = Result>; pub type GasStationResult = Result>; +type EthPrivKeyPolicy = PrivKeyPolicy; type GasDetails = (U256, U256); #[derive(Debug, Display)] @@ -410,35 +411,6 @@ impl TryFrom for EthPrivKeyBuildPolicy { } } -/// An alternative to `crate::PrivKeyPolicy`, typical only for ETH coin. -#[derive(Clone)] -pub enum EthPrivKeyPolicy { - KeyPair(KeyPair), - #[cfg(target_arch = "wasm32")] - Metamask(EthMetamaskPolicy), -} - -#[cfg(target_arch = "wasm32")] -#[derive(Clone)] -pub struct EthMetamaskPolicy { - pub(crate) public_key: H264, - pub(crate) public_key_uncompressed: H520, -} - -impl From for EthPrivKeyPolicy { - fn from(key_pair: KeyPair) -> Self { EthPrivKeyPolicy::KeyPair(key_pair) } -} - -impl EthPrivKeyPolicy { - pub fn key_pair_or_err(&self) -> MmResult<&KeyPair, PrivKeyPolicyNotAllowed> { - match self { - EthPrivKeyPolicy::KeyPair(key_pair) => Ok(key_pair), - #[cfg(target_arch = "wasm32")] - EthPrivKeyPolicy::Metamask(_) => MmError::err(PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), - } - } -} - /// pImpl idiom. pub struct EthCoinImpl { ticker: String, @@ -736,7 +708,28 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { let to_addr = coin .address_from_str(&req.to) .map_to_mm(WithdrawError::InvalidAddress)?; - let my_balance = coin.my_balance().compat().await?; + let (my_balance, my_address, key_pair) = match req.from { + Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + let raw_priv_key = coin + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(path_to_address)?; + let key_pair = KeyPair::from_secret_slice(raw_priv_key.as_slice()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + let address = key_pair.address(); + let balance = coin.address_balance(address).compat().await?; + (balance, address, key_pair) + }, + Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { + return MmError::err(WithdrawError::UnexpectedFromAddress( + "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for EVM!".to_string(), + )) + }, + None => ( + coin.my_balance().compat().await?, + coin.my_address, + coin.priv_key_policy.activated_key_or_err()?.clone(), + ), + }; let my_balance_dec = u256_to_big_decimal(my_balance, coin.decimals)?; let (mut wei_amount, dec_amount) = if req.max { @@ -779,9 +772,10 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { }; let (tx_hash, tx_hex) = match coin.priv_key_policy { - EthPrivKeyPolicy::KeyPair(ref key_pair) => { + EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { + // Todo: nonce_lock is still global for all addresses but this needs to be per address let _nonce_lock = coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce(coin.my_address, coin.web3_instances.clone()) + let (nonce, _) = get_addr_nonce(my_address, coin.web3_instances.clone()) .compat() .timeout_secs(30.) .await? @@ -801,6 +795,11 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { (signed.hash, BytesJson::from(bytes.to_vec())) }, + EthPrivKeyPolicy::Trezor => { + return MmError::err(WithdrawError::UnsupportedError( + "Trezor is not supported for EVM yet!".to_string(), + )) + }, #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => { if !req.broadcast { @@ -843,7 +842,7 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { let amount_decimal = u256_to_big_decimal(wei_amount, coin.decimals)?; let mut spent_by_me = amount_decimal.clone(); - let received_by_me = if to_addr == coin.my_address { + let received_by_me = if to_addr == my_address { amount_decimal.clone() } else { 0.into() @@ -852,10 +851,9 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { if coin.coin_type == EthCoinType::Eth { spent_by_me += &fee_details.total_fee; } - let my_address = coin.my_address()?; Ok(TransactionDetails { to: vec![checksum_address(&format!("{:#02x}", to_addr))], - from: vec![my_address], + from: vec![checksum_address(&format!("{:#02x}", my_address))], total_amount: amount_decimal, my_balance_change: &received_by_me - &spent_by_me, spent_by_me, @@ -952,7 +950,7 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit gas_price, }; - let secret = eth_coin.priv_key_policy.key_pair_or_err()?.secret(); + let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); let signed = tx.sign(secret, eth_coin.chain_id); let signed_bytes = rlp::encode(&signed); let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; @@ -1027,7 +1025,7 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd gas_price, }; - let secret = eth_coin.priv_key_policy.key_pair_or_err()?.secret(); + let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); let signed = tx.sign(secret, eth_coin.chain_id); let signed_bytes = rlp::encode(&signed); let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; @@ -1307,9 +1305,12 @@ impl SwapOps for EthCoin { #[inline] fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> keys::KeyPair { match self.priv_key_policy { - EthPrivKeyPolicy::KeyPair(ref key_pair) => { - key_pair_from_secret(key_pair.secret().as_bytes()).expect("valid key") - }, + EthPrivKeyPolicy::Iguana(ref key_pair) + | EthPrivKeyPolicy::HDWallet { + activated_key: ref key_pair, + .. + } => key_pair_from_secret(key_pair.secret().as_bytes()).expect("valid key"), + EthPrivKeyPolicy::Trezor => todo!(), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => todo!(), } @@ -1318,10 +1319,15 @@ impl SwapOps for EthCoin { #[inline] fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { match self.priv_key_policy { - EthPrivKeyPolicy::KeyPair(ref key_pair) => key_pair_from_secret(key_pair.secret().as_bytes()) + EthPrivKeyPolicy::Iguana(ref key_pair) + | EthPrivKeyPolicy::HDWallet { + activated_key: ref key_pair, + .. + } => key_pair_from_secret(key_pair.secret().as_bytes()) .expect("valid key") .public_slice() .to_vec(), + EthPrivKeyPolicy::Trezor => todo!(), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(ref metamask_policy) => metamask_policy.public_key.as_bytes().to_vec(), } @@ -1811,10 +1817,15 @@ impl MarketCoinOps for EthCoin { fn get_public_key(&self) -> Result> { match self.priv_key_policy { - EthPrivKeyPolicy::KeyPair(ref key_pair) => { + EthPrivKeyPolicy::Iguana(ref key_pair) + | EthPrivKeyPolicy::HDWallet { + activated_key: ref key_pair, + .. + } => { let uncompressed_without_prefix = hex::encode(key_pair.public()); Ok(format!("04{}", uncompressed_without_prefix)) }, + EthPrivKeyPolicy::Trezor => MmError::err(UnexpectedDerivationMethod::Trezor), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(ref metamask_policy) => { Ok(format!("{:02x}", metamask_policy.public_key_uncompressed)) @@ -1838,7 +1849,7 @@ impl MarketCoinOps for EthCoin { fn sign_message(&self, message: &str) -> SignatureResult { let message_hash = self.sign_message_hash(message).ok_or(SignatureError::PrefixNotFound)?; - let privkey = &self.priv_key_policy.key_pair_or_err()?.secret(); + let privkey = &self.priv_key_policy.activated_key_or_err()?.secret(); let signature = sign(privkey, &H256::from(message_hash))?; Ok(format!("0x{}", signature)) } @@ -2109,7 +2120,12 @@ impl MarketCoinOps for EthCoin { fn display_priv_key(&self) -> Result { match self.priv_key_policy { - EthPrivKeyPolicy::KeyPair(ref key_pair) => Ok(format!("{:#02x}", key_pair.secret())), + EthPrivKeyPolicy::Iguana(ref key_pair) + | EthPrivKeyPolicy::HDWallet { + activated_key: ref key_pair, + .. + } => Ok(format!("{:#02x}", key_pair.secret())), + EthPrivKeyPolicy::Trezor => ERR!("'display_priv_key' doesn't support Trezor yet!"), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support MetaMask"), } @@ -2974,9 +2990,12 @@ impl EthCoin { let coin = self.clone(); let fut = async move { match coin.priv_key_policy { - EthPrivKeyPolicy::KeyPair(ref key_pair) => { - sign_and_send_transaction_with_keypair(ctx, &coin, key_pair, value, action, data, gas).await - }, + EthPrivKeyPolicy::Iguana(ref key_pair) + | EthPrivKeyPolicy::HDWallet { + activated_key: ref key_pair, + .. + } => sign_and_send_transaction_with_keypair(ctx, &coin, key_pair, value, action, data, gas).await, + EthPrivKeyPolicy::Trezor => Err(TransactionErr::Plain(ERRL!("Trezor is not supported for EVM yet!"))), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => { sign_and_send_transaction_with_metamask(coin, value, action, data, gas).await @@ -3613,18 +3632,14 @@ impl EthCoin { } } - fn my_balance(&self) -> BalanceFut { + fn address_balance(&self, address: Address) -> BalanceFut { let coin = self.clone(); let fut = async move { match coin.coin_type { - EthCoinType::Eth => Ok(coin - .web3 - .eth() - .balance(coin.my_address, Some(BlockNumber::Latest)) - .await?), + EthCoinType::Eth => Ok(coin.web3.eth().balance(address, Some(BlockNumber::Latest)).await?), EthCoinType::Erc20 { ref token_addr, .. } => { let function = ERC20_CONTRACT.function("balanceOf")?; - let data = function.encode_input(&[Token::Address(coin.my_address)])?; + let data = function.encode_input(&[Token::Address(address)])?; let res = coin.call_request(*token_addr, None, Some(data.into())).await?; let decoded = function.decode_output(&res.0)?; @@ -3641,6 +3656,8 @@ impl EthCoin { Box::new(fut.boxed().compat()) } + fn my_balance(&self) -> BalanceFut { self.address_balance(self.my_address) } + pub async fn get_tokens_balance_list(&self) -> Result, MmError> { let coin = || self; let mut requests = Vec::new(); @@ -5140,7 +5157,12 @@ pub async fn eth_coin_from_conf_and_request( } let contract_supports_watchers = req["contract_supports_watchers"].as_bool().unwrap_or_default(); - let (my_address, key_pair) = try_s!(build_address_and_priv_key_policy(conf, priv_key_policy).await); + let path_to_address = try_s!(json::from_value::>( + req["path_to_address"].clone() + )) + .unwrap_or_default(); + let (my_address, key_pair) = + try_s!(build_address_and_priv_key_policy(conf, priv_key_policy, &path_to_address).await); let mut web3_instances = vec![]; let event_handlers = rpc_event_handlers_for_eth_transport(ctx, ticker.to_string()); @@ -5400,12 +5422,17 @@ impl From for GetEthAddressError { /// `get_eth_address` returns wallet address for coin with `ETH` protocol type. /// Note: result address has mixed-case checksum form. -pub async fn get_eth_address(ctx: &MmArc, ticker: &str) -> MmResult { +pub async fn get_eth_address( + ctx: &MmArc, + conf: &Json, + ticker: &str, + path_to_address: &StandardHDCoinAddress, +) -> MmResult { let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(ctx)?; // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. let priv_key_policy = EthPrivKeyBuildPolicy::try_from(priv_key_policy)?; - let (my_address, ..) = build_address_and_priv_key_policy(&ctx.conf, priv_key_policy).await?; + let (my_address, ..) = build_address_and_priv_key_policy(conf, priv_key_policy, path_to_address).await?; let wallet_address = checksum_address(&format!("{:#02x}", my_address)); Ok(MyWalletAddress { diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 856af882c9..fddf8da03f 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -1,6 +1,7 @@ use super::*; +#[cfg(target_arch = "wasm32")] use crate::EthMetamaskPolicy; use common::executor::AbortedError; -use crypto::{CryptoCtxError, StandardHDPathToCoin}; +use crypto::{CryptoCtxError, StandardHDCoinAddress}; use enum_from::EnumFromTrait; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] @@ -24,8 +25,6 @@ pub enum EthActivationV2Error { UnreachableNodes(String), #[display(fmt = "Enable request for ETH coin must have at least 1 node")] AtLeastOneNodeRequired, - #[display(fmt = "'derivation_path' field is not found in config")] - DerivationPathIsNotSet, #[display(fmt = "Error deserializing 'derivation_path': {}", _0)] ErrorDeserializingDerivationPath(String), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), @@ -100,6 +99,8 @@ pub struct EthActivationV2Request { pub required_confirmations: Option, #[serde(default)] pub priv_key_policy: EthPrivKeyActivationPolicy, + #[serde(default)] + pub path_to_address: StandardHDCoinAddress, } #[derive(Clone, Deserialize)] @@ -246,14 +247,25 @@ pub async fn eth_coin_from_conf_and_request_v2( } } - let (my_address, priv_key_policy) = build_address_and_priv_key_policy(conf, priv_key_policy).await?; + let (my_address, priv_key_policy) = + build_address_and_priv_key_policy(conf, priv_key_policy, &req.path_to_address).await?; let my_address_str = checksum_address(&format!("{:02x}", my_address)); let chain_id = conf["chain_id"].as_u64(); let (web3, web3_instances) = match (req.rpc_mode, &priv_key_policy) { - (EthRpcMode::Http, EthPrivKeyPolicy::KeyPair(key_pair)) => { - build_http_transport(ctx, ticker.clone(), my_address_str, key_pair, &req.nodes).await? + ( + EthRpcMode::Http, + EthPrivKeyPolicy::Iguana(key_pair) + | EthPrivKeyPolicy::HDWallet { + activated_key: key_pair, + .. + }, + ) => build_http_transport(ctx, ticker.clone(), my_address_str, key_pair, &req.nodes).await?, + (EthRpcMode::Http, EthPrivKeyPolicy::Trezor) => { + return MmError::err(EthActivationV2Error::PrivKeyPolicyNotAllowed( + PrivKeyPolicyNotAllowed::HardwareWalletNotSupported, + )); }, #[cfg(target_arch = "wasm32")] (EthRpcMode::Metamask, EthPrivKeyPolicy::Metamask(_)) => { @@ -263,7 +275,7 @@ pub async fn eth_coin_from_conf_and_request_v2( build_metamask_transport(ctx, ticker.clone(), chain_id).await? }, #[cfg(target_arch = "wasm32")] - (_, _) => { + (EthRpcMode::Http, EthPrivKeyPolicy::Metamask(_)) | (EthRpcMode::Metamask, _) => { let error = r#"priv_key_policy="Metamask" and rpc_mode="Metamask" should be used both"#.to_string(); return MmError::err(EthActivationV2Error::ActivationFailed { ticker, error }); }, @@ -322,37 +334,44 @@ pub async fn eth_coin_from_conf_and_request_v2( pub(crate) async fn build_address_and_priv_key_policy( conf: &Json, priv_key_policy: EthPrivKeyBuildPolicy, + path_to_address: &StandardHDCoinAddress, ) -> MmResult<(Address, EthPrivKeyPolicy), EthActivationV2Error> { - let raw_priv_key = match priv_key_policy { - EthPrivKeyBuildPolicy::IguanaPrivKey(iguana) => iguana, + match priv_key_policy { + EthPrivKeyBuildPolicy::IguanaPrivKey(iguana) => { + let key_pair = KeyPair::from_secret_slice(iguana.as_slice()) + .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; + Ok((key_pair.address(), EthPrivKeyPolicy::Iguana(key_pair))) + }, EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd_ctx) => { // Consider storing `derivation_path` at `EthCoinImpl`. - let derivation_path: Option = json::from_value(conf["derivation_path"].clone()) + let derivation_path = json::from_value(conf["derivation_path"].clone()) .map_to_mm(|e| EthActivationV2Error::ErrorDeserializingDerivationPath(e.to_string()))?; - let derivation_path = derivation_path.or_mm_err(|| EthActivationV2Error::DerivationPathIsNotSet)?; - global_hd_ctx - .derive_secp256k1_secret(&derivation_path) - .mm_err(|e| EthActivationV2Error::InternalError(e.to_string()))? + let raw_priv_key = global_hd_ctx + .derive_secp256k1_secret(&derivation_path, path_to_address) + .mm_err(|e| EthActivationV2Error::InternalError(e.to_string()))?; + let activated_key_pair = KeyPair::from_secret_slice(raw_priv_key.as_slice()) + .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; + let bip39_secp_priv_key = global_hd_ctx.root_priv_key().clone(); + Ok((activated_key_pair.address(), EthPrivKeyPolicy::HDWallet { + derivation_path, + activated_key: activated_key_pair, + bip39_secp_priv_key, + })) }, #[cfg(target_arch = "wasm32")] EthPrivKeyBuildPolicy::Metamask(metamask_ctx) => { let address = *metamask_ctx.check_active_eth_account().await?; let public_key_uncompressed = metamask_ctx.eth_account_pubkey_uncompressed(); let public_key = compress_public_key(public_key_uncompressed)?; - return Ok(( + Ok(( address, EthPrivKeyPolicy::Metamask(EthMetamaskPolicy { public_key, public_key_uncompressed, }), - )); + )) }, - }; - - let key_pair = KeyPair::from_secret_slice(raw_priv_key.as_slice()) - .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; - let address = key_pair.address(); - Ok((address, EthPrivKeyPolicy::KeyPair(key_pair))) + } } async fn build_http_transport( diff --git a/mm2src/coins/lightning/ln_events.rs b/mm2src/coins/lightning/ln_events.rs index cb66cf8c5e..a90b13be82 100644 --- a/mm2src/coins/lightning/ln_events.rs +++ b/mm2src/coins/lightning/ln_events.rs @@ -216,7 +216,7 @@ fn sign_funding_transaction( let key_pair = coin .as_ref() .priv_key_policy - .key_pair_or_err() + .activated_key_or_err() .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; let prev_script = Builder::build_p2pkh(&my_address.hash); diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index 8ab92629e7..88af1d68cc 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -92,7 +92,7 @@ pub fn init_keys_manager(platform: &Platform) -> EnableLightningResult for UnexpectedDerivationMethod { + fn from(e: PrivKeyPolicyNotAllowed) -> Self { + match e { + PrivKeyPolicyNotAllowed::HardwareWalletNotSupported => UnexpectedDerivationMethod::Trezor, + PrivKeyPolicyNotAllowed::UnsupportedMethod(method) => UnexpectedDerivationMethod::UnsupportedError(method), + PrivKeyPolicyNotAllowed::InternalError(e) => UnexpectedDerivationMethod::InternalError(e), + } + } } pub trait Transaction: fmt::Debug + 'static { @@ -1234,10 +1258,9 @@ pub trait GetWithdrawSenderAddress { ) -> MmResult, WithdrawError>; } -#[derive(Clone, Deserialize)] +#[derive(Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum WithdrawFrom { - // AccountId { account_id: u32 }, AddressId(HDAccountAddressId), /// 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". @@ -1245,6 +1268,7 @@ pub enum WithdrawFrom { DerivationPath { derivation_path: String, }, + HDWalletAddress(StandardHDCoinAddress), } #[derive(Clone, Deserialize)] @@ -2030,6 +2054,8 @@ pub enum WithdrawError { #[from_stringify("NumConversError", "UnexpectedDerivationMethod", "PrivKeyPolicyNotAllowed")] #[display(fmt = "Internal error: {}", _0)] InternalError(String), + #[display(fmt = "Unsupported error: {}", _0)] + UnsupportedError(String), #[display(fmt = "{} coin doesn't support NFT withdrawing", coin)] CoinDoesntSupportNftWithdraw { coin: String, @@ -2078,6 +2104,7 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::UnexpectedFromAddress(_) | WithdrawError::UnknownAccount { .. } | WithdrawError::UnexpectedUserAction { .. } + | WithdrawError::UnsupportedError(_) | WithdrawError::ActionNotAllowed(_) | WithdrawError::GetNftInfoError(_) | WithdrawError::AddressMismatchError { .. } @@ -2148,6 +2175,13 @@ impl From for WithdrawError { } } +impl From for WithdrawError { + fn from(e: Bip32Error) -> Self { + let error = format!("Error deriving key: {}", e); + WithdrawError::InternalError(error) + } +} + impl WithdrawError { /// Construct [`WithdrawError`] from [`GenerateTxError`] using additional `coin` and `decimals`. pub fn from_generate_tx_error(gen_tx_err: GenerateTxError, coin: String, decimals: u8) -> WithdrawError { @@ -2838,23 +2872,103 @@ impl Default for PrivKeyActivationPolicy { fn default() -> Self { PrivKeyActivationPolicy::ContextPrivKey } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum PrivKeyPolicy { - KeyPair(T), + Iguana(T), + HDWallet { + /// Derivation path of the coin. + /// This derivation path consists of `purpose` and `coin_type` only + /// where the full `BIP44` address has the following structure: + /// `m/purpose'/coin_type'/account'/change/address_index`. + derivation_path: StandardHDPathToCoin, + activated_key: T, + bip39_secp_priv_key: ExtendedPrivateKey, + }, Trezor, + #[cfg(target_arch = "wasm32")] + Metamask(EthMetamaskPolicy), +} + +#[cfg(target_arch = "wasm32")] +#[derive(Clone, Debug)] +pub struct EthMetamaskPolicy { + pub(crate) public_key: EthH264, + pub(crate) public_key_uncompressed: EthH520, +} + +impl From for PrivKeyPolicy { + fn from(key_pair: T) -> Self { PrivKeyPolicy::Iguana(key_pair) } } impl PrivKeyPolicy { - pub fn key_pair(&self) -> Option<&T> { + fn activated_key(&self) -> Option<&T> { match self { - PrivKeyPolicy::KeyPair(key_pair) => Some(key_pair), + PrivKeyPolicy::Iguana(key_pair) => Some(key_pair), + PrivKeyPolicy::HDWallet { + activated_key: activated_key_pair, + .. + } => Some(activated_key_pair), PrivKeyPolicy::Trezor => None, + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => None, } } - pub fn key_pair_or_err(&self) -> Result<&T, MmError> { - self.key_pair() - .or_mm_err(|| PrivKeyPolicyNotAllowed::HardwareWalletNotSupported) + fn activated_key_or_err(&self) -> Result<&T, MmError> { + self.activated_key().or_mm_err(|| { + PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`activated_key_or_err` is supported only for `PrivKeyPolicy::KeyPair` or `PrivKeyPolicy::HDWallet`" + .to_string(), + ) + }) + } + + fn bip39_secp_priv_key(&self) -> Option<&ExtendedPrivateKey> { + match self { + PrivKeyPolicy::HDWallet { + bip39_secp_priv_key, .. + } => Some(bip39_secp_priv_key), + PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor => None, + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => None, + } + } + + fn bip39_secp_priv_key_or_err( + &self, + ) -> Result<&ExtendedPrivateKey, MmError> { + self.bip39_secp_priv_key().or_mm_err(|| { + PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`bip39_secp_priv_key_or_err` is supported only for `PrivKeyPolicy::HDWallet`".to_string(), + ) + }) + } + + fn derivation_path(&self) -> Option<&StandardHDPathToCoin> { + match self { + PrivKeyPolicy::HDWallet { derivation_path, .. } => Some(derivation_path), + PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor => None, + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => None, + } + } + + fn derivation_path_or_err(&self) -> Result<&StandardHDPathToCoin, MmError> { + self.derivation_path().or_mm_err(|| { + PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`derivation_path_or_err` is supported only for `PrivKeyPolicy::HDWallet`".to_string(), + ) + }) + } + + fn hd_wallet_derived_priv_key_or_err( + &self, + path_to_address: &StandardHDCoinAddress, + ) -> Result> { + let bip39_secp_priv_key = self.bip39_secp_priv_key_or_err()?; + let derivation_path = self.derivation_path_or_err()?; + derive_secp256k1_secret(bip39_secp_priv_key.clone(), derivation_path, path_to_address) + .mm_err(|e| PrivKeyPolicyNotAllowed::InternalError(e.to_string())) } } @@ -4035,13 +4149,13 @@ pub trait RpcCommonOps { /// Currently supports only coins with `ETH` protocol type. pub async fn get_my_address(ctx: MmArc, req: MyAddressReq) -> MmResult { let ticker = req.coin.as_str(); - let coins_en = coin_conf(&ctx, ticker); - coins_conf_check(&ctx, &coins_en, ticker, None).map_to_mm(GetMyAddressError::CoinsConfCheckError)?; + let conf = coin_conf(&ctx, ticker); + coins_conf_check(&ctx, &conf, ticker, None).map_to_mm(GetMyAddressError::CoinsConfCheckError)?; - let protocol: CoinProtocol = json::from_value(coins_en["protocol"].clone())?; + let protocol: CoinProtocol = json::from_value(conf["protocol"].clone())?; let my_address = match protocol { - CoinProtocol::ETH => get_eth_address(&ctx, ticker).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/nft.rs b/mm2src/coins/nft.rs index e679947517..c84d0cbe24 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -8,7 +8,7 @@ pub(crate) mod storage; #[cfg(any(test, target_arch = "wasm32"))] mod nft_tests; -use crate::{get_my_address, MyAddressReq, WithdrawError}; +use crate::{coin_conf, get_my_address, MyAddressReq, WithdrawError}; use nft_errors::{GetInfoFromUriError, GetNftInfoError, UpdateNftError}; use nft_structs::{Chain, ContractType, ConvertChain, Nft, NftFromMoralis, NftList, NftListReq, NftMetadataReq, NftTransferHistory, NftTransferHistoryFromMoralis, NftTransfersReq, NftsTransferHistoryList, @@ -20,6 +20,7 @@ use crate::nft::nft_structs::{NftCommon, NftCtx, NftTransferCommon, RefreshMetad UriMeta}; use crate::nft::storage::{NftListStorageOps, NftStorageBuilder, NftTransferHistoryStorageOps}; use common::{parse_rfc3339_to_timestamp, APPLICATION_JSON}; +use crypto::StandardHDCoinAddress; use ethereum_types::Address; use http::header::ACCEPT; use mm2_err_handle::map_to_mm::MapToMmResult; @@ -213,7 +214,9 @@ pub async fn refresh_nft_metadata(ctx: MmArc, req: RefreshMetadataReq) -> MmResu async fn get_moralis_nft_list(ctx: &MmArc, chain: &Chain, url: &Url) -> MmResult, GetNftInfoError> { let mut res_list = Vec::new(); - let my_address = get_eth_address(ctx, &chain.to_ticker()).await?; + let ticker = chain.to_ticker(); + let conf = coin_conf(ctx, &ticker); + let my_address = get_eth_address(ctx, &conf, &ticker, &StandardHDCoinAddress::default()).await?; let mut uri_without_cursor = url.clone(); uri_without_cursor.set_path(MORALIS_API_ENDPOINT); @@ -265,7 +268,9 @@ async fn get_moralis_nft_transfers( url: &Url, ) -> MmResult, GetNftInfoError> { let mut res_list = Vec::new(); - let my_address = get_eth_address(ctx, &chain.to_ticker()).await?; + let ticker = chain.to_ticker(); + let conf = coin_conf(ctx, &ticker); + let my_address = get_eth_address(ctx, &conf, &ticker, &StandardHDCoinAddress::default()).await?; let mut uri_without_cursor = url.clone(); uri_without_cursor.set_path(MORALIS_API_ENDPOINT); @@ -501,6 +506,7 @@ async fn update_nft_list( let transfers = storage.get_transfers_from_block(chain, scan_from_block).await?; let req = MyAddressReq { coin: chain.to_ticker(), + path_to_address: StandardHDCoinAddress::default(), }; let my_address = get_my_address(ctx.clone(), req).await?.wallet_address.to_lowercase(); for transfer in transfers.into_iter() { diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index dcad8c47df..c99f2d47b7 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -534,7 +534,7 @@ impl Qrc20Coin { .await?; let my_address = self.utxo.derivation_method.single_addr_or_err()?; - let key_pair = self.utxo.priv_key_policy.key_pair_or_err()?; + let key_pair = self.utxo.priv_key_policy.activated_key_or_err()?; let prev_script = ScriptBuilder::build_p2pkh(&my_address.hash); let signed = sign_tx( diff --git a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs b/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs index eb5d29430f..037823ee66 100644 --- a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs +++ b/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs @@ -3,11 +3,12 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; use mm2_number::BigDecimal; -use crate::{lp_coinfind_or_err, MmCoinEnum, WithdrawError, WithdrawFee, WithdrawResult}; +use crate::{lp_coinfind_or_err, MmCoinEnum, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawResult}; #[derive(Clone, Deserialize)] pub struct IBCWithdrawRequest { pub(crate) ibc_source_channel: String, + pub(crate) from: Option, pub(crate) coin: String, pub(crate) to: String, #[serde(default)] diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs index 12ca7379e0..1c4964f3a0 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -19,7 +19,7 @@ use base58::ToBase58; use bincode::{deserialize, serialize}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}; use common::{async_blocking, now_sec}; -use crypto::StandardHDPathToCoin; +use crypto::{StandardHDCoinAddress, StandardHDPathToCoin}; use derive_more::Display; use futures::{FutureExt, TryFutureExt}; use futures01::Future; @@ -139,6 +139,8 @@ impl From for WithdrawError { pub struct SolanaActivationParams { confirmation_commitment: CommitmentLevel, client_url: String, + #[serde(default)] + path_to_address: StandardHDCoinAddress, } #[derive(Debug, Display)] @@ -187,7 +189,7 @@ pub async fn solana_coin_with_policy( PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => priv_key, PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { let derivation_path: StandardHDPathToCoin = try_s!(json::from_value(conf["derivation_path"].clone())); - try_s!(global_hd.derive_secp256k1_secret(&derivation_path)) + try_s!(global_hd.derive_secp256k1_secret(&derivation_path, ¶ms.path_to_address)) }, PrivKeyBuildPolicy::Trezor => return ERR!("{}", PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), }; diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 09758242b9..33e48d1885 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -17,7 +17,7 @@ use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, - PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, RefundError, RefundPaymentArgs, RefundResult, RpcCommonOps, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, SignatureResult, SpendPaymentArgs, @@ -27,8 +27,8 @@ use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, - WithdrawRequest}; + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFrom, + WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; @@ -50,7 +50,8 @@ use cosmrs::tendermint::chain::Id as ChainId; use cosmrs::tendermint::PublicKey; use cosmrs::tx::{self, Fee, Msg, Raw, SignDoc, SignerInfo}; use cosmrs::{AccountId, Any, Coin, Denom, ErrorReport}; -use crypto::{privkey::key_pair_from_secret, Secp256k1Secret, StandardHDPathToCoin}; +use crypto::privkey::key_pair_from_secret; +use crypto::{Secp256k1Secret, StandardHDCoinAddress, StandardHDPathToCoin}; use derive_more::Display; use futures::future::try_join_all; use futures::lock::Mutex as AsyncMutex; @@ -104,6 +105,8 @@ const MIN_TIME_LOCK: i64 = 50; const ACCOUNT_SEQUENCE_ERR: &str = "incorrect account sequence"; +type TendermintPrivKeyPolicy = PrivKeyPolicy; + #[async_trait] pub trait TendermintCommons { fn platform_denom(&self) -> &Denom; @@ -222,7 +225,7 @@ pub struct TendermintCoinImpl { /// My address pub account_id: AccountId, pub(super) account_prefix: String, - priv_key: Vec, + pub(super) priv_key_policy: TendermintPrivKeyPolicy, pub(crate) decimals: u8, pub(super) denom: Denom, chain_id: ChainId, @@ -262,8 +265,14 @@ pub enum TendermintInitErrorKind { InvalidDenom(String), #[display(fmt = "'derivation_path' field is not found in config")] DerivationPathIsNotSet, + #[display(fmt = "'account' field is not found in config")] + AccountIsNotSet, + #[display(fmt = "'address_index' field is not found in config")] + AddressIndexIsNotSet, #[display(fmt = "Error deserializing 'derivation_path': {}", _0)] ErrorDeserializingDerivationPath(String), + #[display(fmt = "Error deserializing 'path_to_address': {}", _0)] + ErrorDeserializingPathToAddress(String), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), RpcError(String), #[display(fmt = "avg_blocktime is missing in coin configuration")] @@ -285,6 +294,10 @@ impl From for TendermintCoinRpcError { fn from(err: DecodeError) -> Self { TendermintCoinRpcError::Prost(err) } } +impl From for TendermintCoinRpcError { + fn from(err: PrivKeyPolicyNotAllowed) -> Self { TendermintCoinRpcError::InternalError(err.to_string()) } +} + impl From for WithdrawError { fn from(err: TendermintCoinRpcError) -> Self { WithdrawError::Transport(err.to_string()) } } @@ -346,7 +359,7 @@ impl crate::Transaction for CosmosTransaction { } } -fn account_id_from_privkey(priv_key: &[u8], prefix: &str) -> MmResult { +pub(crate) fn account_id_from_privkey(priv_key: &[u8], prefix: &str) -> MmResult { let signing_key = SigningKey::from_bytes(priv_key).map_to_mm(|e| TendermintInitErrorKind::InvalidPrivKey(e.to_string()))?; @@ -421,7 +434,9 @@ impl TendermintCommons for TendermintCoin { } async fn all_balances(&self) -> MmResult { - let platform_balance_denom = self.balance_for_denom(self.denom.to_string()).await?; + let platform_balance_denom = self + .account_balance_for_denom(&self.account_id, self.denom.to_string()) + .await?; let platform_balance = big_decimal_from_sat_unsigned(platform_balance_denom, self.decimals); let ibc_assets_info = self.tokens_info.lock().clone(); @@ -429,7 +444,7 @@ impl TendermintCommons for TendermintCoin { for (ticker, info) in ibc_assets_info { let fut = async move { let balance_denom = self - .balance_for_denom(info.denom.to_string()) + .account_balance_for_denom(&self.account_id, info.denom.to_string()) .await .map_err(|e| e.into_inner())?; let balance_decimal = big_decimal_from_sat_unsigned(balance_denom, info.decimals); @@ -459,7 +474,7 @@ impl TendermintCoin { protocol_info: TendermintProtocolInfo, rpc_urls: Vec, tx_history: bool, - priv_key_policy: PrivKeyBuildPolicy, + priv_key_policy: TendermintPrivKeyPolicy, ) -> MmResult { if rpc_urls.is_empty() { return MmError::err(TendermintInitError { @@ -468,7 +483,10 @@ impl TendermintCoin { }); } - let priv_key = secret_from_priv_key_policy(&conf, &ticker, priv_key_policy)?; + let priv_key = priv_key_policy.activated_key_or_err().mm_err(|e| TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::Internal(e.to_string()), + })?; let account_id = account_id_from_privkey(priv_key.as_slice(), &protocol_info.account_prefix).mm_err(|kind| { @@ -515,7 +533,7 @@ impl TendermintCoin { ticker, account_id, account_prefix: protocol_info.account_prefix, - priv_key: priv_key.to_vec(), + priv_key_policy, decimals: protocol_info.decimals, denom, chain_id, @@ -535,8 +553,26 @@ impl TendermintCoin { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + let (account_id, priv_key) = match req.from { + Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + let priv_key = coin + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(path_to_address)?; + let account_id = account_id_from_privkey(priv_key.as_slice(), &coin.account_prefix) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + (account_id, priv_key) + }, + Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { + return MmError::err(WithdrawError::UnexpectedFromAddress( + "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" + .to_string(), + )) + }, + None => (coin.account_id.clone(), *coin.priv_key_policy.activated_key_or_err()?), + }; + let (balance_denom, balance_dec) = coin - .get_balance_as_unsigned_and_decimal(&coin.denom, coin.decimals()) + .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) .await?; // << BEGIN TX SIMULATION FOR FEE CALCULATION @@ -554,7 +590,7 @@ impl TendermintCoin { }); } - let received_by_me = if to_address == coin.account_id { + let received_by_me = if to_address == account_id { amount_dec } else { BigDecimal::default() @@ -564,7 +600,7 @@ impl TendermintCoin { let msg_transfer = MsgTransfer::new_with_default_timeout( req.ibc_source_channel.clone(), - coin.account_id.clone(), + account_id.clone(), to_address.clone(), Coin { denom: coin.denom.clone(), @@ -586,7 +622,14 @@ impl TendermintCoin { let (_, gas_limit) = coin.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT); let fee_amount_u64 = coin - .calculate_fee_amount_as_u64(msg_transfer.clone(), timeout_height, memo.clone(), req.fee) + .calculate_account_fee_amount_as_u64( + &account_id, + &priv_key, + msg_transfer.clone(), + timeout_height, + memo.clone(), + req.fee, + ) .await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); @@ -622,7 +665,7 @@ impl TendermintCoin { let msg_transfer = MsgTransfer::new_with_default_timeout( req.ibc_source_channel.clone(), - coin.account_id.clone(), + account_id.clone(), to_address.clone(), Coin { denom: coin.denom.clone(), @@ -632,9 +675,9 @@ impl TendermintCoin { .to_any() .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let account_info = coin.my_account_info().await?; + let account_info = coin.account_info(&account_id).await?; let tx_raw = coin - .any_to_signed_raw_tx(account_info, msg_transfer, fee, timeout_height, memo.clone()) + .any_to_signed_raw_tx(&priv_key, account_info, msg_transfer, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let tx_bytes = tx_raw @@ -645,7 +688,7 @@ impl TendermintCoin { Ok(TransactionDetails { tx_hash: hex::encode_upper(hash.as_slice()), tx_hex: tx_bytes.into(), - from: vec![coin.account_id.to_string()], + from: vec![account_id.to_string()], to: vec![req.to], my_balance_change: &received_by_me - &total_amount, spent_by_me: total_amount.clone(), @@ -794,6 +837,7 @@ impl TendermintCoin { pub(super) fn gen_simulated_tx( &self, account_info: BaseAccount, + priv_key: &Secp256k1Secret, tx_payload: Any, timeout_height: u64, memo: String, @@ -805,7 +849,7 @@ impl TendermintCoin { let fee = Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT); - let signkey = SigningKey::from_bytes(&self.priv_key)?; + let signkey = SigningKey::from_bytes(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)?; @@ -850,7 +894,8 @@ impl TendermintCoin { ) -> Result<(String, Raw), TransactionErr> { let (tx_id, tx_raw) = loop { let tx_raw = try_tx_s!(self.any_to_signed_raw_tx( - try_tx_s!(self.my_account_info().await), + try_tx_s!(self.priv_key_policy.activated_key_or_err()), + try_tx_s!(self.account_info(&self.account_id).await), tx_payload.clone(), fee.clone(), timeout_height, @@ -884,9 +929,16 @@ impl TendermintCoin { let path = AbciPath::from_str(ABCI_SIMULATE_TX_PATH).expect("valid path"); let (response, raw_response) = loop { - let account_info = self.my_account_info().await?; + let account_info = self.account_info(&self.account_id).await?; + let activated_priv_key = self.priv_key_policy.activated_key_or_err()?; let tx_bytes = self - .gen_simulated_tx(account_info, msg.clone(), timeout_height, memo.clone()) + .gen_simulated_tx( + account_info, + activated_priv_key, + msg.clone(), + timeout_height, + memo.clone(), + ) .map_to_mm(|e| TendermintCoinRpcError::InternalError(format!("{}", e)))?; let request = AbciRequest::new( @@ -939,8 +991,10 @@ impl TendermintCoin { } #[allow(deprecated)] - pub(super) async fn calculate_fee_amount_as_u64( + pub(super) async fn calculate_account_fee_amount_as_u64( &self, + account_id: &AccountId, + priv_key: &Secp256k1Secret, msg: Any, timeout_height: u64, memo: String, @@ -949,9 +1003,9 @@ impl TendermintCoin { let path = AbciPath::from_str(ABCI_SIMULATE_TX_PATH).expect("valid path"); let (response, raw_response) = loop { - let account_info = self.my_account_info().await?; + let account_info = self.account_info(account_id).await?; let tx_bytes = self - .gen_simulated_tx(account_info, msg.clone(), timeout_height, memo.clone()) + .gen_simulated_tx(account_info, priv_key, msg.clone(), timeout_height, memo.clone()) .map_to_mm(|e| TendermintCoinRpcError::InternalError(format!("{}", e)))?; let request = AbciRequest::new( @@ -996,10 +1050,10 @@ impl TendermintCoin { Ok(((gas.gas_used as f64 * 1.5) * gas_price).ceil() as u64) } - pub(super) async fn my_account_info(&self) -> MmResult { + pub(super) async fn account_info(&self, account_id: &AccountId) -> MmResult { let path = AbciPath::from_str(ABCI_QUERY_ACCOUNT_PATH).expect("valid path"); let request = QueryAccountRequest { - address: self.account_id.to_string(), + address: account_id.to_string(), }; let request = AbciRequest::new( Some(path), @@ -1031,10 +1085,14 @@ impl TendermintCoin { Ok(base_account) } - pub(super) async fn balance_for_denom(&self, denom: String) -> MmResult { + pub(super) async fn account_balance_for_denom( + &self, + account_id: &AccountId, + denom: String, + ) -> MmResult { let path = AbciPath::from_str(ABCI_QUERY_BALANCE_PATH).expect("valid path"); let request = QueryBalanceRequest { - address: self.account_id.to_string(), + address: account_id.to_string(), denom, }; let request = AbciRequest::new( @@ -1103,13 +1161,14 @@ impl TendermintCoin { pub(super) fn any_to_signed_raw_tx( &self, + priv_key: &Secp256k1Secret, account_info: BaseAccount, tx_payload: Any, fee: Fee, timeout_height: u64, memo: String, ) -> cosmrs::Result { - let signkey = SigningKey::from_bytes(&self.priv_key)?; + let signkey = SigningKey::from_bytes(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)?; @@ -1524,7 +1583,11 @@ impl TendermintCoin { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; let fee_uamount = self - .calculate_fee_amount_as_u64( + .calculate_account_fee_amount_as_u64( + &self.account_id, + self.priv_key_policy + .activated_key_or_err() + .mm_err(|e| TradePreimageError::InternalError(e.to_string()))?, create_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned(), @@ -1573,7 +1636,16 @@ impl TendermintCoin { .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let fee_uamount = self - .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned(), None) + .calculate_account_fee_amount_as_u64( + &self.account_id, + self.priv_key_policy + .activated_key_or_err() + .mm_err(|e| TradePreimageError::InternalError(e.to_string()))?, + msg_send, + timeout_height, + TX_DEFAULT_MEMO.to_owned(), + None, + ) .await?; let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, decimals); @@ -1586,10 +1658,11 @@ impl TendermintCoin { pub(super) async fn get_balance_as_unsigned_and_decimal( &self, + account_id: &AccountId, denom: &Denom, decimals: u8, ) -> MmResult<(u64, BigDecimal), TendermintCoinRpcError> { - let denom_ubalance = self.balance_for_denom(denom.to_string()).await?; + let denom_ubalance = self.account_balance_for_denom(account_id, denom.to_string()).await?; let denom_balance_dec = big_decimal_from_sat_unsigned(denom_ubalance, decimals); Ok((denom_ubalance, denom_balance_dec)) @@ -1832,8 +1905,26 @@ impl MmCoin for TendermintCoin { ))); } + let (account_id, priv_key) = match req.from { + Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + let priv_key = coin + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(path_to_address)?; + let account_id = account_id_from_privkey(priv_key.as_slice(), &coin.account_prefix) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + (account_id, priv_key) + }, + Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { + return MmError::err(WithdrawError::UnexpectedFromAddress( + "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" + .to_string(), + )) + }, + None => (coin.account_id.clone(), *coin.priv_key_policy.activated_key_or_err()?), + }; + let (balance_denom, balance_dec) = coin - .get_balance_as_unsigned_and_decimal(&coin.denom, coin.decimals()) + .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) .await?; // << BEGIN TX SIMULATION FOR FEE CALCULATION @@ -1853,14 +1944,14 @@ impl MmCoin for TendermintCoin { }); } - let received_by_me = if to_address == coin.account_id { + let received_by_me = if to_address == account_id { amount_dec } else { BigDecimal::default() }; let msg_send = MsgSend { - from_address: coin.account_id.clone(), + from_address: account_id.clone(), to_address: to_address.clone(), amount: vec![Coin { denom: coin.denom.clone(), @@ -1883,7 +1974,14 @@ impl MmCoin for TendermintCoin { let (_, gas_limit) = coin.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); let fee_amount_u64 = coin - .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, memo.clone(), req.fee) + .calculate_account_fee_amount_as_u64( + &account_id, + &priv_key, + msg_send, + timeout_height, + memo.clone(), + req.fee, + ) .await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); @@ -1918,7 +2016,7 @@ impl MmCoin for TendermintCoin { }; let msg_send = MsgSend { - from_address: coin.account_id.clone(), + from_address: account_id.clone(), to_address, amount: vec![Coin { denom: coin.denom.clone(), @@ -1928,9 +2026,9 @@ impl MmCoin for TendermintCoin { .to_any() .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let account_info = coin.my_account_info().await?; + let account_info = coin.account_info(&account_id).await?; let tx_raw = coin - .any_to_signed_raw_tx(account_info, msg_send, fee, timeout_height, memo.clone()) + .any_to_signed_raw_tx(&priv_key, account_info, msg_send, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let tx_bytes = tx_raw @@ -1942,7 +2040,7 @@ impl MmCoin for TendermintCoin { Ok(TransactionDetails { tx_hash: hex::encode_upper(hash.as_slice()), tx_hex: tx_bytes.into(), - from: vec![coin.account_id.to_string()], + from: vec![account_id.to_string()], to: vec![req.to], my_balance_change: &received_by_me - &total_amount, spent_by_me: total_amount.clone(), @@ -2105,7 +2203,8 @@ impl MarketCoinOps for TendermintCoin { fn my_address(&self) -> MmResult { Ok(self.account_id.to_string()) } fn get_public_key(&self) -> Result> { - let key = SigningKey::from_bytes(&self.priv_key).expect("privkey validity is checked on coin creation"); + let key = SigningKey::from_bytes(self.priv_key_policy.activated_key_or_err()?.as_slice()) + .expect("privkey validity is checked on coin creation"); Ok(key.public_key().to_string()) } @@ -2127,7 +2226,9 @@ impl MarketCoinOps for TendermintCoin { fn my_balance(&self) -> BalanceFut { let coin = self.clone(); let fut = async move { - let balance_denom = coin.balance_for_denom(coin.denom.to_string()).await?; + let balance_denom = coin + .account_balance_for_denom(&coin.account_id, coin.denom.to_string()) + .await?; Ok(CoinBalance { spendable: big_decimal_from_sat_unsigned(balance_denom, coin.decimals), unspendable: BigDecimal::default(), @@ -2285,7 +2386,13 @@ impl MarketCoinOps for TendermintCoin { Box::new(fut.boxed().compat()) } - fn display_priv_key(&self) -> Result { Ok(hex::encode(&self.priv_key)) } + fn display_priv_key(&self) -> Result { + Ok(self + .priv_key_policy + .activated_key_or_err() + .map_err(|e| e.to_string())? + .to_string()) + } fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat(MIN_TX_SATOSHIS, self.decimals) } @@ -2524,7 +2631,13 @@ impl SwapOps for TendermintCoin { #[inline] fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { - key_pair_from_secret(&self.priv_key).expect("valid priv key") + key_pair_from_secret( + self.priv_key_policy + .activated_key_or_err() + .expect("valid priv key") + .as_ref(), + ) + .expect("valid priv key") } #[inline] @@ -2652,27 +2765,34 @@ impl WatcherOps for TendermintCoin { } } -/// Processes the given `priv_key_policy` and returns corresponding `Secp256k1Secret`. +/// Processes the given `priv_key_build_policy` and returns corresponding `TendermintPrivKeyPolicy`. /// This function expects either [`PrivKeyBuildPolicy::IguanaPrivKey`] /// or [`PrivKeyBuildPolicy::GlobalHDAccount`], otherwise returns `PrivKeyPolicyNotAllowed` error. -pub(crate) fn secret_from_priv_key_policy( +pub fn tendermint_priv_key_policy( conf: &TendermintConf, ticker: &str, - priv_key_policy: PrivKeyBuildPolicy, -) -> MmResult { - match priv_key_policy { - PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(iguana), + priv_key_build_policy: PrivKeyBuildPolicy, + path_to_address: StandardHDCoinAddress, +) -> MmResult { + match priv_key_build_policy { + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(TendermintPrivKeyPolicy::Iguana(iguana)), PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { let derivation_path = conf.derivation_path.as_ref().or_mm_err(|| TendermintInitError { ticker: ticker.to_string(), kind: TendermintInitErrorKind::DerivationPathIsNotSet, })?; - global_hd - .derive_secp256k1_secret(derivation_path) + let activated_priv_key = global_hd + .derive_secp256k1_secret(derivation_path, &path_to_address) .mm_err(|e| TendermintInitError { ticker: ticker.to_string(), kind: TendermintInitErrorKind::InvalidPrivKey(e.to_string()), - }) + })?; + let bip39_secp_priv_key = global_hd.root_priv_key().clone(); + Ok(TendermintPrivKeyPolicy::HDWallet { + derivation_path: derivation_path.clone(), + activated_key: activated_priv_key, + bip39_secp_priv_key, + }) }, PrivKeyBuildPolicy::Trezor => { let kind = @@ -2777,7 +2897,7 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); let coin = block_on(TendermintCoin::init( &ctx, @@ -2889,7 +3009,7 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); let coin = block_on(TendermintCoin::init( &ctx, @@ -2947,7 +3067,7 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); let coin = block_on(TendermintCoin::init( &ctx, @@ -3019,7 +3139,7 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); let coin = block_on(TendermintCoin::init( &ctx, @@ -3213,7 +3333,7 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); let coin = block_on(TendermintCoin::init( &ctx, @@ -3297,7 +3417,7 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); let coin = block_on(TendermintCoin::init( &ctx, @@ -3371,7 +3491,7 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); let coin = block_on(TendermintCoin::init( &ctx, @@ -3441,7 +3561,7 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); let coin = common::block_on(TendermintCoin::init( &ctx, @@ -3493,7 +3613,7 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); let coin = common::block_on(TendermintCoin::init( &ctx, diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 09417c210f..f985ccec3c 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -5,6 +5,7 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATOSHIS, TIMEOUT_HEIGHT_DELTA, TX_DEFAULT_MEMO}; use crate::rpc_command::tendermint::IBCWithdrawRequest; +use crate::tendermint::account_id_from_privkey; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, @@ -18,7 +19,7 @@ use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFu ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, - WithdrawError, WithdrawFut, WithdrawRequest}; + WithdrawError, WithdrawFrom, WithdrawFut, WithdrawRequest}; use crate::{MmCoinEnum, PaymentInstructionArgs, WatcherReward, WatcherRewardError}; use async_trait::async_trait; use bitcrypto::sha256; @@ -110,12 +111,33 @@ impl TendermintToken { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + let (account_id, priv_key) = match req.from { + Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + let priv_key = platform + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(path_to_address)?; + let account_id = account_id_from_privkey(priv_key.as_slice(), &platform.account_prefix) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + (account_id, priv_key) + }, + Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { + return MmError::err(WithdrawError::UnexpectedFromAddress( + "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" + .to_string(), + )) + }, + None => ( + platform.account_id.clone(), + *platform.priv_key_policy.activated_key_or_err()?, + ), + }; + let (base_denom_balance, base_denom_balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&platform.denom, token.decimals()) + .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) .await?; let (balance_denom, balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&token.denom, token.decimals()) + .get_balance_as_unsigned_and_decimal(&account_id, &token.denom, token.decimals()) .await?; let (amount_denom, amount_dec, total_amount) = if req.max { @@ -147,7 +169,7 @@ impl TendermintToken { }); } - let received_by_me = if to_address == platform.account_id { + let received_by_me = if to_address == account_id { amount_dec } else { BigDecimal::default() @@ -157,7 +179,7 @@ impl TendermintToken { let msg_transfer = MsgTransfer::new_with_default_timeout( req.ibc_source_channel.clone(), - platform.account_id.clone(), + account_id.clone(), to_address.clone(), Coin { denom: token.denom.clone(), @@ -178,7 +200,14 @@ impl TendermintToken { let (_, gas_limit) = platform.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT); let fee_amount_u64 = platform - .calculate_fee_amount_as_u64(msg_transfer.clone(), timeout_height, memo.clone(), req.fee) + .calculate_account_fee_amount_as_u64( + &account_id, + &priv_key, + msg_transfer.clone(), + timeout_height, + memo.clone(), + req.fee, + ) .await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); @@ -198,9 +227,9 @@ impl TendermintToken { let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); - let account_info = platform.my_account_info().await?; + let account_info = platform.account_info(&account_id).await?; let tx_raw = platform - .any_to_signed_raw_tx(account_info, msg_transfer, fee, timeout_height, memo.clone()) + .any_to_signed_raw_tx(&priv_key, account_info, msg_transfer, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let tx_bytes = tx_raw @@ -211,7 +240,7 @@ impl TendermintToken { Ok(TransactionDetails { tx_hash: hex::encode_upper(hash.as_slice()), tx_hex: tx_bytes.into(), - from: vec![platform.account_id.to_string()], + from: vec![account_id.to_string()], to: vec![req.to], my_balance_change: &received_by_me - &total_amount, spent_by_me: total_amount.clone(), @@ -517,7 +546,10 @@ impl MarketCoinOps for TendermintToken { fn my_balance(&self) -> BalanceFut { let coin = self.clone(); let fut = async move { - let balance_denom = coin.platform_coin.balance_for_denom(coin.denom.to_string()).await?; + let balance_denom = coin + .platform_coin + .account_balance_for_denom(&coin.platform_coin.account_id, coin.denom.to_string()) + .await?; Ok(CoinBalance { spendable: big_decimal_from_sat_unsigned(balance_denom, coin.decimals), unspendable: BigDecimal::default(), @@ -588,12 +620,33 @@ impl MmCoin for TendermintToken { ))); } + let (account_id, priv_key) = match req.from { + Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + let priv_key = platform + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(path_to_address)?; + let account_id = account_id_from_privkey(priv_key.as_slice(), &platform.account_prefix) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + (account_id, priv_key) + }, + Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { + return MmError::err(WithdrawError::UnexpectedFromAddress( + "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" + .to_string(), + )) + }, + None => ( + platform.account_id.clone(), + *platform.priv_key_policy.activated_key_or_err()?, + ), + }; + let (base_denom_balance, base_denom_balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&platform.denom, token.decimals()) + .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) .await?; let (balance_denom, balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&token.denom, token.decimals()) + .get_balance_as_unsigned_and_decimal(&account_id, &token.denom, token.decimals()) .await?; let (amount_denom, amount_dec, total_amount) = if req.max { @@ -625,14 +678,14 @@ impl MmCoin for TendermintToken { }); } - let received_by_me = if to_address == platform.account_id { + let received_by_me = if to_address == account_id { amount_dec } else { BigDecimal::default() }; let msg_send = MsgSend { - from_address: platform.account_id.clone(), + from_address: account_id.clone(), to_address, amount: vec![Coin { denom: token.denom.clone(), @@ -654,7 +707,14 @@ impl MmCoin for TendermintToken { let (_, gas_limit) = platform.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); let fee_amount_u64 = platform - .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, memo.clone(), req.fee) + .calculate_account_fee_amount_as_u64( + &account_id, + &priv_key, + msg_send.clone(), + timeout_height, + memo.clone(), + req.fee, + ) .await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); @@ -674,9 +734,9 @@ impl MmCoin for TendermintToken { let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); - let account_info = platform.my_account_info().await?; + let account_info = platform.account_info(&account_id).await?; let tx_raw = platform - .any_to_signed_raw_tx(account_info, msg_send, fee, timeout_height, memo.clone()) + .any_to_signed_raw_tx(&priv_key, account_info, msg_send, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let tx_bytes = tx_raw @@ -687,7 +747,7 @@ impl MmCoin for TendermintToken { Ok(TransactionDetails { tx_hash: hex::encode_upper(hash.as_slice()), tx_hex: tx_bytes.into(), - from: vec![platform.account_id.to_string()], + from: vec![account_id.to_string()], to: vec![req.to], my_balance_change: &received_by_me - &total_amount, spent_by_me: total_amount.clone(), diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 9ccf7c35f8..ab1fee5bde 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -52,7 +52,7 @@ use common::jsonrpc_client::JsonRpcError; use common::log::LogOnError; use common::{now_sec, now_sec_u32}; use crypto::{Bip32DerPathOps, Bip32Error, Bip44Chain, ChildNumber, DerivationPath, Secp256k1ExtendedPublicKey, - StandardHDPathError, StandardHDPathToAccount, StandardHDPathToCoin}; + StandardHDCoinAddress, StandardHDPathError, StandardHDPathToAccount, StandardHDPathToCoin}; use derive_more::Display; #[cfg(not(target_arch = "wasm32"))] use dirs::home_dir; use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender, UnboundedSender}; @@ -946,7 +946,7 @@ pub trait UtxoCommonOps: fn denominate_satoshis(&self, satoshi: i64) -> f64; - /// Get a public key that matches [`PrivKeyPolicy::KeyPair`]. + /// Get a public key that matches [`PrivKeyPolicy::Iguana`]. /// /// # Fail /// @@ -1369,6 +1369,10 @@ pub struct UtxoActivationParams { /// The flag determines whether to use mature unspent outputs *only* to generate transactions. /// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 pub check_utxo_maturity: Option, + /// This determines which Address of the HD account to be used for swaps for this UTXO coin. + /// If not specified, the first non-change address for the first account is used. + #[serde(default)] + pub path_to_address: StandardHDCoinAddress, } #[derive(Debug, Display)] @@ -1384,6 +1388,8 @@ pub enum UtxoFromLegacyReqErr { InvalidScanPolicy(json::Error), InvalidMinAddressesNumber(json::Error), InvalidPrivKeyPolicy(json::Error), + InvalidAccount(json::Error), + InvalidAddressIndex(json::Error), } impl UtxoActivationParams { @@ -1421,6 +1427,9 @@ impl UtxoActivationParams { let priv_key_policy = json::from_value::>(req["priv_key_policy"].clone()) .map_to_mm(UtxoFromLegacyReqErr::InvalidPrivKeyPolicy)? .unwrap_or(PrivKeyActivationPolicy::ContextPrivKey); + let path_to_address = json::from_value::>(req["path_to_address"].clone()) + .map_to_mm(UtxoFromLegacyReqErr::InvalidAddressIndex)? + .unwrap_or_default(); Ok(UtxoActivationParams { mode, @@ -1433,6 +1442,7 @@ impl UtxoActivationParams { enable_params, priv_key_policy, check_utxo_maturity, + path_to_address, }) } } @@ -1790,7 +1800,7 @@ where T: AsRef + UtxoTxGenerationOps + UtxoTxBroadcastOps, { let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()); - let key_pair = try_tx_s!(coin.as_ref().priv_key_policy.key_pair_or_err()); + let key_pair = try_tx_s!(coin.as_ref().priv_key_policy.activated_key_or_err()); let mut builder = UtxoTxBuilder::new(coin) .add_available_inputs(unspents) @@ -1862,6 +1872,8 @@ pub fn address_by_conf_and_pubkey_str( enable_params: EnabledCoinBalanceParams::default(), priv_key_policy: PrivKeyActivationPolicy::ContextPrivKey, check_utxo_maturity: None, + // This will not be used since the pubkey from orderbook/etc.. will be used to generate the address + path_to_address: StandardHDCoinAddress::default(), }; let conf_builder = UtxoConfBuilder::new(conf, ¶ms, coin); let utxo_conf = try_s!(conf_builder.build()); diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index 6cd4ee4336..231d230c51 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -105,7 +105,7 @@ impl QtumDelegationOps for QtumCoin { let mut buffer = b"\x15Qtum Signed Message:\n\x28".to_vec(); buffer.append(&mut addr_hash.to_string().into_bytes()); let hashed = dhash256(&buffer); - let key_pair = self.as_ref().priv_key_policy.key_pair_or_err()?; + let key_pair = self.as_ref().priv_key_policy.activated_key_or_err()?; let signature = key_pair .private() .sign_compact(&hashed) @@ -268,7 +268,7 @@ impl QtumCoin { ) -> DelegationResult { let utxo = self.as_ref(); - let key_pair = utxo.priv_key_policy.key_pair_or_err()?; + let key_pair = utxo.priv_key_policy.activated_key_or_err()?; let my_address = utxo.derivation_method.single_addr_or_err()?; let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 3e03eab7f2..340b5059aa 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -646,7 +646,7 @@ impl SlpToken { unsigned.lock_time = tx_locktime; unsigned.inputs[0].sequence = input_sequence; - let my_key_pair = self.platform_coin.as_ref().priv_key_policy.key_pair_or_err()?; + let my_key_pair = self.platform_coin.as_ref().priv_key_policy.activated_key_or_err()?; let signed_p2sh_input = p2sh_spend( &unsigned, 0, @@ -1596,7 +1596,7 @@ impl MmCoin for SlpToken { let coin = self.clone(); let fut = async move { let my_address = coin.platform_coin.as_ref().derivation_method.single_addr_or_err()?; - let key_pair = coin.platform_coin.as_ref().priv_key_policy.key_pair_or_err()?; + let key_pair = coin.platform_coin.as_ref().priv_key_policy.activated_key_or_err()?; let address = CashAddress::decode(&req.to).map_to_mm(WithdrawError::InvalidAddress)?; if address.prefix != *coin.slp_prefix() { diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index af9ba59f8a..7a8f89b029 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -17,8 +17,8 @@ use common::executor::{abortable_queue::AbortableQueue, AbortSettings, Abortable Timer}; use common::log::{error, info, LogOnError}; use common::small_rng; -use crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, Secp256k1Secret, - StandardHDPathError, StandardHDPathToCoin}; +use crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, StandardHDPathError, + StandardHDPathToCoin}; use derive_more::Display; use futures::channel::mpsc::{channel, unbounded, Receiver as AsyncReceiver, UnboundedReceiver}; use futures::compat::Future01CompatExt; @@ -112,6 +112,10 @@ impl From for UtxoCoinBuildError { fn from(e: AbortedError) -> Self { UtxoCoinBuildError::Internal(e.to_string()) } } +impl From for UtxoCoinBuildError { + fn from(e: PrivKeyPolicyNotAllowed) -> Self { UtxoCoinBuildError::PrivKeyPolicyNotAllowed(e) } +} + #[async_trait] pub trait UtxoCoinBuilder: UtxoFieldsWithIguanaSecretBuilder + UtxoFieldsWithGlobalHDBuilder + UtxoFieldsWithHardwareWalletBuilder @@ -141,7 +145,15 @@ pub trait UtxoFieldsWithIguanaSecretBuilder: UtxoCoinBuilderCommonOps { priv_key: IguanaPrivKey, ) -> UtxoCoinBuildResult { let conf = UtxoConfBuilder::new(self.conf(), self.activation_params(), self.ticker()).build()?; - build_utxo_coin_fields_with_conf_and_secret(self, conf, priv_key).await + let private = Private { + prefix: conf.wif_prefix, + secret: priv_key, + compressed: true, + checksum_type: conf.checksum_type, + }; + let key_pair = KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; + let priv_key_policy = PrivKeyPolicy::Iguana(key_pair); + build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy).await } } @@ -158,27 +170,34 @@ pub trait UtxoFieldsWithGlobalHDBuilder: UtxoCoinBuilderCommonOps { .as_ref() .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet)?; let secret = global_hd_ctx - .derive_secp256k1_secret(derivation_path) + .derive_secp256k1_secret(derivation_path, &self.activation_params().path_to_address) .mm_err(|e| UtxoCoinBuildError::Internal(e.to_string()))?; - build_utxo_coin_fields_with_conf_and_secret(self, conf, secret).await + let private = Private { + prefix: conf.wif_prefix, + secret, + compressed: true, + checksum_type: conf.checksum_type, + }; + let activated_key_pair = + KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; + let priv_key_policy = PrivKeyPolicy::HDWallet { + derivation_path: derivation_path.clone(), + activated_key: activated_key_pair, + bip39_secp_priv_key: global_hd_ctx.root_priv_key().clone(), + }; + build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy).await } } -async fn build_utxo_coin_fields_with_conf_and_secret( +async fn build_utxo_coin_fields_with_conf_and_policy( builder: &Builder, conf: UtxoCoinConf, - secret: Secp256k1Secret, + priv_key_policy: PrivKeyPolicy, ) -> UtxoCoinBuildResult where Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, { - let private = Private { - prefix: conf.wif_prefix, - secret, - compressed: true, - checksum_type: conf.checksum_type, - }; - let key_pair = KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; + let key_pair = priv_key_policy.activated_key_or_err()?; let addr_format = builder.address_format()?; let my_address = Address { prefix: conf.pub_addr_prefix, @@ -191,7 +210,6 @@ where let my_script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let derivation_method = DerivationMethod::SingleAddress(my_address); - let priv_key_policy = PrivKeyPolicy::KeyPair(key_pair); // 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 053ab674b8..1a4c8e6c69 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -698,9 +698,17 @@ pub fn address_from_str_unchecked(coin: &UtxoCoinFields, address: &str) -> MmRes pub fn my_public_key(coin: &UtxoCoinFields) -> Result<&Public, MmError> { match coin.priv_key_policy { - PrivKeyPolicy::KeyPair(ref key_pair) => Ok(key_pair.public()), - // Hardware Wallets requires BIP39/BIP44 derivation path to extract a public key. - PrivKeyPolicy::Trezor => MmError::err(UnexpectedDerivationMethod::ExpectedSingleAddress), + PrivKeyPolicy::Iguana(ref key_pair) => Ok(key_pair.public()), + PrivKeyPolicy::HDWallet { + activated_key: ref activated_key_pair, + .. + } => Ok(activated_key_pair.public()), + // Hardware Wallets requires BIP32/BIP44 derivation path to extract a public key. + PrivKeyPolicy::Trezor => MmError::err(UnexpectedDerivationMethod::Trezor), + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => MmError::err(UnexpectedDerivationMethod::UnsupportedError( + "`PrivKeyPolicy::Metamask` is not supported in this context".to_string(), + )), } } @@ -2548,7 +2556,7 @@ pub fn sign_message_hash(coin: &UtxoCoinFields, message: &str) -> Option<[u8; 32 pub fn sign_message(coin: &UtxoCoinFields, message: &str) -> SignatureResult { let message_hash = sign_message_hash(coin, message).ok_or(SignatureError::PrefixNotFound)?; - let private_key = coin.priv_key_policy.key_pair_or_err()?.private(); + let private_key = coin.priv_key_policy.activated_key_or_err()?.private(); let signature = private_key.sign_compact(&H256::from(message_hash))?; Ok(base64::encode(&*signature)) } @@ -2689,8 +2697,14 @@ pub fn current_block(coin: &UtxoCoinFields) -> Box Result { match coin.priv_key_policy { - PrivKeyPolicy::KeyPair(ref key_pair) => Ok(key_pair.private().to_string()), + PrivKeyPolicy::Iguana(ref key_pair) => Ok(key_pair.private().to_string()), + PrivKeyPolicy::HDWallet { + activated_key: ref activated_key_pair, + .. + } => Ok(activated_key_pair.private().to_string()), PrivKeyPolicy::Trezor => ERR!("'display_priv_key' doesn't support Hardware Wallets"), + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support Metamask"), } } @@ -2816,6 +2830,11 @@ where } HDAccountAddressId::from(derivation_path) }, + WithdrawFrom::HDWalletAddress(_) => { + return MmError::err(WithdrawError::UnsupportedError( + "`WithdrawFrom::HDWalletAddress` is not supported for `get_withdraw_hd_sender`".to_string(), + )) + }, }; let hd_account = hd_wallet @@ -4525,8 +4544,14 @@ where #[inline] pub fn derive_htlc_key_pair(coin: &UtxoCoinFields, _swap_unique_data: &[u8]) -> KeyPair { match coin.priv_key_policy { - PrivKeyPolicy::KeyPair(k) => k, + PrivKeyPolicy::Iguana(k) => k, + PrivKeyPolicy::HDWallet { + activated_key: activated_key_pair, + .. + } => activated_key_pair, PrivKeyPolicy::Trezor => todo!(), + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => panic!("`PrivKeyPolicy::Metamask` is not supported for UTXO coins"), } } diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index 45dbbb84bf..246cadcbdb 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -80,7 +80,7 @@ pub(super) fn utxo_coin_fields_for_test( }; let my_script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); - let priv_key_policy = PrivKeyPolicy::KeyPair(key_pair); + let priv_key_policy = PrivKeyPolicy::Iguana(key_pair); let derivation_method = DerivationMethod::SingleAddress(my_address); let bech32_hrp = if is_segwit_coin { diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index e7e061a176..be56824f6a 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -546,7 +546,7 @@ fn test_search_for_swap_tx_spend_electrum_was_refunded() { let search_input = SearchForSwapTxSpendInput { time_lock: 1591933469, - other_pub: coin.as_ref().priv_key_policy.key_pair_or_err().unwrap().public(), + other_pub: coin.as_ref().priv_key_policy.activated_key_or_err().unwrap().public(), secret_hash: &secret_hash, tx: &payment_tx_bytes, search_from_block: 0, @@ -3360,7 +3360,7 @@ fn test_split_qtum() { let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).unwrap(); let p2pkh_address = coin.as_ref().derivation_method.unwrap_single_addr(); let script: Script = output_script(p2pkh_address, ScriptType::P2PKH); - let key_pair = coin.as_ref().priv_key_policy.key_pair_or_err().unwrap(); + let key_pair = coin.as_ref().priv_key_policy.activated_key_or_err().unwrap(); let (unspents, _) = block_on(coin.get_mature_unspent_ordered_list(p2pkh_address)).expect("Unspent list is empty"); log!("Mature unspents vec = {:?}", unspents.mature); let outputs = vec![ diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index de5bad9f32..f51726c6c4 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -3,14 +3,14 @@ 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::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionDetails, WithdrawError, - WithdrawFee, WithdrawRequest, WithdrawResult}; + WithdrawFee, WithdrawFrom, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use chain::TransactionOutput; use common::log::info; use common::now_sec; use crypto::trezor::{TrezorError, TrezorProcessingError}; use crypto::{from_hw_error, CryptoCtx, CryptoCtxError, DerivationPath, HwError, HwProcessingError, HwRpcError}; -use keys::{Public as PublicKey, Type as ScriptType}; +use keys::{AddressHashEnum, KeyPair, Private, Public as PublicKey, Type as ScriptType}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc::v1::types::ToTxHash; @@ -310,11 +310,22 @@ where .or_mm_err(|| WithdrawError::HwError(HwRpcError::NoTrezorDeviceAvailable))?; let sign_policy = match self.coin.as_ref().priv_key_policy { - PrivKeyPolicy::KeyPair(ref key_pair) => SignPolicy::WithKeyPair(key_pair), + PrivKeyPolicy::Iguana(ref key_pair) => SignPolicy::WithKeyPair(key_pair), + // InitUtxoWithdraw works only for hardware wallets so it's ok to use signing with activated keypair here as a placeholder. + PrivKeyPolicy::HDWallet { + activated_key: ref activated_key_pair, + .. + } => SignPolicy::WithKeyPair(activated_key_pair), PrivKeyPolicy::Trezor => { let trezor_session = hw_ctx.trezor().await?; SignPolicy::WithTrezor(trezor_session) }, + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => { + return MmError::err(WithdrawError::UnsupportedError( + "`PrivKeyPolicy::Metamask` is not supported for UTXO coins!".to_string(), + )) + }, }; self.task_handle @@ -365,6 +376,7 @@ impl<'a, Coin> InitUtxoWithdraw<'a, Coin> { pub struct StandardUtxoWithdraw { coin: Coin, req: WithdrawRequest, + key_pair: KeyPair, my_address: Address, my_address_string: String, } @@ -387,10 +399,9 @@ where fn on_finishing(&self) -> Result<(), MmError> { Ok(()) } async fn sign_tx(&self, unsigned_tx: TransactionInputSigner) -> Result> { - let key_pair = self.coin.as_ref().priv_key_policy.key_pair_or_err()?; Ok(with_key_pair::sign_tx( unsigned_tx, - key_pair, + &self.key_pair, self.prev_script(), self.signature_version(), self.coin.as_ref().conf.fork_id, @@ -404,11 +415,52 @@ where { #[allow(clippy::result_large_err)] pub fn new(coin: Coin, req: WithdrawRequest) -> Result> { - let my_address = coin.as_ref().derivation_method.single_addr_or_err()?.clone(); - let my_address_string = coin.my_address()?; + let (key_pair, my_address) = match req.from { + Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + let secret = coin + .as_ref() + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(path_to_address)?; + let private = Private { + prefix: coin.as_ref().conf.wif_prefix, + secret, + compressed: true, + checksum_type: coin.as_ref().conf.checksum_type, + }; + let key_pair = + KeyPair::from_private(private).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + let addr_format = coin + .as_ref() + .derivation_method + .single_addr_or_err()? + .clone() + .addr_format; + let my_address = Address { + prefix: coin.as_ref().conf.pub_addr_prefix, + t_addr_prefix: coin.as_ref().conf.pub_t_addr_prefix, + hash: AddressHashEnum::AddressHash(key_pair.public().address_hash()), + checksum_type: coin.as_ref().conf.checksum_type, + hrp: coin.as_ref().conf.bech32_hrp.clone(), + addr_format, + }; + (key_pair, my_address) + }, + Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { + return MmError::err(WithdrawError::UnsupportedError( + "Only `WithdrawFrom::HDWalletAddress` is supported for `StandardUtxoWithdraw`".to_string(), + )) + }, + None => { + let key_pair = coin.as_ref().priv_key_policy.activated_key_or_err()?; + let my_address = coin.as_ref().derivation_method.single_addr_or_err()?.clone(); + (*key_pair, my_address) + }, + }; + let my_address_string = my_address.display_address().map_to_mm(WithdrawError::InternalError)?; Ok(StandardUtxoWithdraw { coin, req, + key_pair, my_address, my_address_string, }) diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index c911bf4a7e..9d0b042297 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -34,8 +34,8 @@ use common::executor::{AbortableSystem, AbortedError}; use common::sha256_digest; use common::{log, one_thousand_u32}; use crypto::privkey::{key_pair_from_secret, secp_privkey_from_hash}; -use crypto::StandardHDPathToCoin; use crypto::{Bip32DerPathOps, GlobalHDAccountArc}; +use crypto::{StandardHDCoinAddress, StandardHDPathToCoin}; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; @@ -324,7 +324,7 @@ impl ZCoin { fn secp_keypair(&self) -> &KeyPair { self.utxo_arc .priv_key_policy - .key_pair() + .activated_key() .expect("Zcoin doesn't support HW wallets") } @@ -776,6 +776,8 @@ pub struct ZcoinActivationParams { pub scan_blocks_per_iteration: u32, #[serde(default)] pub scan_interval_ms: u64, + #[serde(default)] + pub account: u32, } #[cfg(not(target_arch = "wasm32"))] @@ -867,7 +869,11 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { 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)?, + 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 @@ -888,7 +894,7 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { ); let blocks_db = self.blocks_db().await?; - let wallet_db = WalletDbShared::new(&self) + let wallet_db = WalletDbShared::new(&self, &z_spending_key) .await .map_err(|err| ZCoinBuildError::ZcashDBError(err.to_string()))?; @@ -973,6 +979,8 @@ impl<'a> ZCoinBuilder<'a> { enable_params: Default::default(), priv_key_policy: PrivKeyActivationPolicy::ContextPrivKey, check_utxo_maturity: None, + // This is not used for Zcoin so we just provide a default value + path_to_address: StandardHDCoinAddress::default(), }; ZCoinBuilder { ctx, @@ -1901,11 +1909,17 @@ impl InitWithdrawCoin for ZCoin { task_handle: &WithdrawTaskHandle, ) -> Result> { if req.fee.is_some() { - return MmError::err(WithdrawError::InternalError( + return MmError::err(WithdrawError::UnsupportedError( "Setting a custom withdraw fee is not supported for ZCoin yet".to_owned(), )); } + if req.from.is_some() { + return MmError::err(WithdrawError::UnsupportedError( + "Withdraw from a specific address is not supported for ZCoin yet".to_owned(), + )); + } + let to_addr = decode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &req.to) .map_to_mm(|e| WithdrawError::InvalidAddress(format!("{}", e)))? .or_mm_err(|| WithdrawError::InvalidAddress(format!("Address {} decoded to None", req.to)))?; @@ -1985,11 +1999,12 @@ pub fn interpret_memo_string(memo_str: &str) -> MmResult MmResult { match priv_key_policy { PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(ExtendedSpendingKey::master(iguana.as_slice())), PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { - extended_spending_key_from_global_hd_account(protocol_info, global_hd) + extended_spending_key_from_global_hd_account(protocol_info, global_hd, account) }, PrivKeyBuildPolicy::Trezor => { let priv_key_err = PrivKeyPolicyNotAllowed::HardwareWalletNotSupported; @@ -2003,12 +2018,12 @@ fn extended_spending_key_from_protocol_info_and_policy( fn extended_spending_key_from_global_hd_account( protocol_info: &ZcoinProtocolInfo, global_hd: &GlobalHDAccountArc, + account: u32, ) -> MmResult { let path_to_coin = protocol_info .z_derivation_path .clone() .or_mm_err(|| ZCoinBuildError::ZDerivationPathNotSet)?; - let path_to_account = path_to_coin .to_derivation_path() .into_iter() @@ -2016,7 +2031,7 @@ fn extended_spending_key_from_global_hd_account( .map(|child| Zip32Child::from_index(child.0)) // Push the hardened `account` index, so the derivation path looks like: // `m/purpose'/coin'/account'`. - .chain(iter::once(Zip32Child::Hardened(global_hd.account_id()))); + .chain(iter::once(Zip32Child::Hardened(account))); let mut spending_key = ExtendedSpendingKey::master(global_hd.root_seed_bytes()); for zip32_child in path_to_account { diff --git a/mm2src/coins/z_coin/storage/walletdb/mod.rs b/mm2src/coins/z_coin/storage/walletdb/mod.rs index c7773df4ab..623ff042dc 100644 --- a/mm2src/coins/z_coin/storage/walletdb/mod.rs +++ b/mm2src/coins/z_coin/storage/walletdb/mod.rs @@ -1,8 +1,9 @@ use crate::z_coin::{ZCoinBuilder, ZcoinClientInitError}; use mm2_err_handle::prelude::*; +use zcash_primitives::zip32::ExtendedSpendingKey; cfg_native!( - use crate::z_coin::{extended_spending_key_from_protocol_info_and_policy, ZcoinConsensusParams}; + use crate::z_coin::{ZcoinConsensusParams}; use crate::z_coin::z_rpc::create_wallet_db; use parking_lot::Mutex; @@ -35,22 +36,17 @@ pub struct WalletDbShared { #[cfg(not(target_arch = "wasm32"))] impl<'a> WalletDbShared { - pub async fn new(zcoin_builder: &ZCoinBuilder<'a>) -> MmResult { - let z_spending_key = match zcoin_builder.z_spending_key { - Some(ref z_spending_key) => z_spending_key.clone(), - None => extended_spending_key_from_protocol_info_and_policy( - &zcoin_builder.protocol_info, - &zcoin_builder.priv_key_policy, - ) - .map_err(|err| WalletDbError::ZCoinBuildError(err.to_string()))?, - }; + pub async fn new( + zcoin_builder: &ZCoinBuilder<'a>, + z_spending_key: &ExtendedSpendingKey, + ) -> MmResult { let wallet_db = create_wallet_db( zcoin_builder .db_dir_path .join(format!("{}_wallet.db", zcoin_builder.ticker)), zcoin_builder.protocol_info.consensus_params.clone(), zcoin_builder.protocol_info.check_point_block.clone(), - ExtendedFullViewingKey::from(&z_spending_key), + ExtendedFullViewingKey::from(z_spending_key), ) .await .map_err(|err| MmError::new(WalletDbError::ZcoinClientInitError(err.into_inner())))?; @@ -69,7 +65,10 @@ cfg_wasm32!( pub type WalletDbInnerLocked<'a> = DbLocked<'a, WalletDbInner>; impl<'a> WalletDbShared { - pub async fn new(zcoin_builder: &ZCoinBuilder<'a>) -> MmResult { + pub async fn new( + zcoin_builder: &ZCoinBuilder<'a>, + _z_spending_key: &ExtendedSpendingKey, + ) -> MmResult { Ok(Self { db: ConstructibleDb::new(zcoin_builder.ctx).into_shared(), ticker: zcoin_builder.ticker.to_string(), diff --git a/mm2src/coins/z_coin/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs index 00df86e612..efe4079ea2 100644 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ b/mm2src/coins/z_coin/z_coin_native_tests.rs @@ -38,7 +38,7 @@ fn zombie_coin_send_and_refund_maker_payment() { .unwrap(); let time_lock = now_sec_u32() - 3600; - let taker_pub = coin.utxo_arc.priv_key_policy.key_pair_or_err().unwrap().public(); + let taker_pub = coin.utxo_arc.priv_key_policy.activated_key_or_err().unwrap().public(); let secret_hash = [0; 20]; let args = SendPaymentArgs { @@ -95,7 +95,7 @@ fn zombie_coin_send_and_spend_maker_payment() { .unwrap(); let lock_time = now_sec_u32() - 1000; - let taker_pub = coin.utxo_arc.priv_key_policy.key_pair_or_err().unwrap().public(); + let taker_pub = coin.utxo_arc.priv_key_policy.activated_key_or_err().unwrap().public(); let secret = [0; 32]; let secret_hash = dhash160(&secret); @@ -289,5 +289,6 @@ fn default_zcoin_activation_params() -> ZcoinActivationParams { zcash_params_path: None, scan_blocks_per_iteration: 0, scan_interval_ms: 0, + account: 0, } } diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index abbe91a106..0f7c8d8455 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -43,9 +43,6 @@ impl From for EnablePlatformCoinWithTokensError { EthActivationV2Error::CouldNotFetchBalance(e) | EthActivationV2Error::UnreachableNodes(e) => { EnablePlatformCoinWithTokensError::Transport(e) }, - EthActivationV2Error::DerivationPathIsNotSet => EnablePlatformCoinWithTokensError::InvalidPayload( - "'derivation_path' field is not found in config".to_string(), - ), EthActivationV2Error::ErrorDeserializingDerivationPath(e) => { EnablePlatformCoinWithTokensError::InvalidPayload(e) }, diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index fa0b5f9c47..006f7993d3 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -6,12 +6,13 @@ use crate::prelude::*; use async_trait::async_trait; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tendermint::tendermint_tx_history_v2::tendermint_history_loop; -use coins::tendermint::{TendermintCoin, TendermintCommons, TendermintConf, TendermintInitError, - TendermintInitErrorKind, TendermintProtocolInfo, TendermintToken, +use coins::tendermint::{tendermint_priv_key_policy, TendermintCoin, TendermintCommons, TendermintConf, + TendermintInitError, TendermintInitErrorKind, TendermintProtocolInfo, TendermintToken, TendermintTokenActivationParams, TendermintTokenInitError, TendermintTokenProtocolInfo}; use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum, PrivKeyBuildPolicy}; use common::executor::{AbortSettings, SpawnAbortable}; use common::{true_f, Future01CompatExt}; +use crypto::StandardHDCoinAddress; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; @@ -37,6 +38,9 @@ pub struct TendermintActivationParams { tx_history: bool, #[serde(default = "true_f")] pub get_balances: bool, + /// /account'/change/address_index`. + #[serde(default)] + pub path_to_address: StandardHDCoinAddress, } impl TxHistory for TendermintActivationParams { @@ -168,10 +172,18 @@ impl PlatformWithTokensActivationOps for TendermintCoin { ) -> Result> { let conf = TendermintConf::try_from_json(&ticker, &coin_conf)?; - let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(&ctx).mm_err(|e| TendermintInitError { - ticker: ticker.clone(), - kind: TendermintInitErrorKind::Internal(e.to_string()), - })?; + let priv_key_build_policy = + PrivKeyBuildPolicy::detect_priv_key_policy(&ctx).mm_err(|e| TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::Internal(e.to_string()), + })?; + + let priv_key_policy = tendermint_priv_key_policy( + &conf, + &ticker, + priv_key_build_policy, + activation_request.path_to_address, + )?; TendermintCoin::init( &ctx, diff --git a/mm2src/crypto/src/crypto_ctx.rs b/mm2src/crypto/src/crypto_ctx.rs index 4e2391e362..4f91448af4 100644 --- a/mm2src/crypto/src/crypto_ctx.rs +++ b/mm2src/crypto/src/crypto_ctx.rs @@ -29,11 +29,6 @@ pub enum CryptoInitError { EmptyPassphrase, #[display(fmt = "Invalid passphrase: '{}'", _0)] InvalidPassphrase(PrivKeyError), - #[display(fmt = "Invalid 'hd_account_id' = {}: {}", hd_account_id, error)] - InvalidHdAccount { - hd_account_id: u64, - error: String, - }, Internal(String), } @@ -223,12 +218,8 @@ impl CryptoCtx { Self::init_crypto_ctx_with_policy_builder(ctx, passphrase, KeyPairPolicyBuilder::Iguana) } - pub fn init_with_global_hd_account( - ctx: MmArc, - passphrase: &str, - hd_account_id: u64, - ) -> CryptoInitResult> { - let builder = KeyPairPolicyBuilder::GlobalHDAccount { hd_account_id }; + pub fn init_with_global_hd_account(ctx: MmArc, passphrase: &str) -> CryptoInitResult> { + let builder = KeyPairPolicyBuilder::GlobalHDAccount; Self::init_crypto_ctx_with_policy_builder(ctx, passphrase, builder) } @@ -338,7 +329,7 @@ impl CryptoCtx { enum KeyPairPolicyBuilder { Iguana, - GlobalHDAccount { hd_account_id: u64 }, + GlobalHDAccount, } impl KeyPairPolicyBuilder { @@ -349,8 +340,8 @@ impl KeyPairPolicyBuilder { let secp256k1_key_pair = key_pair_from_seed(passphrase)?; Ok((secp256k1_key_pair, KeyPairPolicy::Iguana)) }, - KeyPairPolicyBuilder::GlobalHDAccount { hd_account_id } => { - let (mm2_internal_key_pair, global_hd_ctx) = GlobalHDAccountCtx::new(passphrase, hd_account_id)?; + KeyPairPolicyBuilder::GlobalHDAccount => { + let (mm2_internal_key_pair, global_hd_ctx) = GlobalHDAccountCtx::new(passphrase)?; let key_pair_policy = KeyPairPolicy::GlobalHDAccount(global_hd_ctx.into_arc()); Ok((mm2_internal_key_pair, key_pair_policy)) }, diff --git a/mm2src/crypto/src/global_hd_ctx.rs b/mm2src/crypto/src/global_hd_ctx.rs index 13b2736f41..79326e9889 100644 --- a/mm2src/crypto/src/global_hd_ctx.rs +++ b/mm2src/crypto/src/global_hd_ctx.rs @@ -1,11 +1,11 @@ use crate::privkey::{bip39_seed_from_passphrase, key_pair_from_secret, PrivKeyError}; +use crate::standard_hd_path::StandardHDCoinAddress; use crate::{mm2_internal_der_path, Bip32DerPathOps, Bip32Error, CryptoInitError, CryptoInitResult, StandardHDPathToCoin}; use bip32::{ChildNumber, ExtendedPrivateKey}; +use common::drop_mutability; use keys::{KeyPair, Secret as Secp256k1Secret}; use mm2_err_handle::prelude::*; -use std::convert::TryInto; -use std::num::TryFromIntError; use std::ops::Deref; use std::sync::Arc; @@ -26,31 +26,16 @@ impl Deref for GlobalHDAccountArc { pub struct GlobalHDAccountCtx { bip39_seed: bip39::Seed, bip39_secp_priv_key: ExtendedPrivateKey, - /// This account is set globally for every activated coin. - hd_account: ChildNumber, } impl GlobalHDAccountCtx { - pub fn new(passphrase: &str, hd_account_id: u64) -> CryptoInitResult<(Mm2InternalKeyPair, GlobalHDAccountCtx)> { + pub fn new(passphrase: &str) -> CryptoInitResult<(Mm2InternalKeyPair, GlobalHDAccountCtx)> { let bip39_seed = bip39_seed_from_passphrase(passphrase)?; let bip39_secp_priv_key: ExtendedPrivateKey = ExtendedPrivateKey::new(bip39_seed.as_bytes()) .map_to_mm(|e| PrivKeyError::InvalidPrivKey(e.to_string()))?; - let hd_account_id = - hd_account_id - .try_into() - .map_to_mm(|e: TryFromIntError| CryptoInitError::InvalidHdAccount { - hd_account_id, - error: e.to_string(), - })?; - let hd_account = - ChildNumber::new(hd_account_id, NON_HARDENED).map_to_mm(|e| CryptoInitError::InvalidHdAccount { - hd_account_id: hd_account_id as u64, - error: e.to_string(), - })?; - - let derivation_path = mm2_internal_der_path(Some(hd_account)); + let derivation_path = mm2_internal_der_path(); let mut internal_priv_key = bip39_secp_priv_key.clone(); for child in derivation_path { @@ -64,7 +49,6 @@ impl GlobalHDAccountCtx { let global_hd_ctx = GlobalHDAccountCtx { bip39_seed, bip39_secp_priv_key, - hd_account, }; Ok((mm2_internal_key_pair, global_hd_ctx)) } @@ -72,15 +56,15 @@ impl GlobalHDAccountCtx { #[inline] pub fn into_arc(self) -> GlobalHDAccountArc { GlobalHDAccountArc(Arc::new(self)) } - /// Returns an identifier of the selected HD account. - pub fn account_id(&self) -> u32 { self.hd_account.index() } - /// Returns the root BIP39 seed. pub fn root_seed(&self) -> &bip39::Seed { &self.bip39_seed } /// Returns the root BIP39 seed as bytes. pub fn root_seed_bytes(&self) -> &[u8] { self.bip39_seed.as_bytes() } + /// Returns the root BIP39 private key. + pub fn root_priv_key(&self) -> &ExtendedPrivateKey { &self.bip39_secp_priv_key } + /// Derives a `secp256k1::SecretKey` from [`HDAccountCtx::bip39_secp_priv_key`] /// at the given `m/purpose'/coin_type'/account_id'/chain/address_id` derivation path, /// where: @@ -92,21 +76,28 @@ impl GlobalHDAccountCtx { pub fn derive_secp256k1_secret( &self, derivation_path: &StandardHDPathToCoin, + path_to_address: &StandardHDCoinAddress, ) -> MmResult { - const ACCOUNT_ID: u32 = 0; - const CHAIN_ID: u32 = 0; - - let mut account_der_path = derivation_path.to_derivation_path(); - account_der_path.push(ChildNumber::new(ACCOUNT_ID, HARDENED).unwrap()); - account_der_path.push(ChildNumber::new(CHAIN_ID, NON_HARDENED).unwrap()); - account_der_path.push(self.hd_account); - - let mut priv_key = self.bip39_secp_priv_key.clone(); - for child in account_der_path { - priv_key = priv_key.derive_child(child)?; - } + derive_secp256k1_secret(self.bip39_secp_priv_key.clone(), derivation_path, path_to_address) + } +} - let secret = *priv_key.private_key().as_ref(); - Ok(Secp256k1Secret::from(secret)) +pub fn derive_secp256k1_secret( + bip39_secp_priv_key: ExtendedPrivateKey, + derivation_path: &StandardHDPathToCoin, + path_to_address: &StandardHDCoinAddress, +) -> MmResult { + let mut account_der_path = derivation_path.to_derivation_path(); + account_der_path.push(ChildNumber::new(path_to_address.account, HARDENED).unwrap()); + account_der_path.push(ChildNumber::new(path_to_address.is_change as u32, NON_HARDENED).unwrap()); + account_der_path.push(ChildNumber::new(path_to_address.address_index, NON_HARDENED).unwrap()); + + let mut priv_key = bip39_secp_priv_key; + for child in account_der_path { + priv_key = priv_key.derive_child(child)?; } + drop_mutability!(priv_key); + + let secret = *priv_key.private_key().as_ref(); + Ok(Secp256k1Secret::from(secret)) } diff --git a/mm2src/crypto/src/hw_ctx.rs b/mm2src/crypto/src/hw_ctx.rs index 50a8c6a6a9..f5dc984407 100644 --- a/mm2src/crypto/src/hw_ctx.rs +++ b/mm2src/crypto/src/hw_ctx.rs @@ -2,7 +2,6 @@ use crate::hw_client::{HwClient, HwConnectionStatus, HwDeviceInfo, HwProcessingE use crate::hw_error::HwError; use crate::trezor::TrezorSession; use crate::{mm2_internal_der_path, HwWalletType}; -use bip32::ChildNumber; use bitcrypto::dhash160; use common::log::warn; use hw_common::primitives::{EcdsaCurve, Secp256k1ExtendedPublicKey}; @@ -119,9 +118,7 @@ impl HardwareWalletCtx { where Processor: TrezorRequestProcessor + Sync, { - const ADDRESS_INDEX: Option = None; - - let path = mm2_internal_der_path(ADDRESS_INDEX); + let path = mm2_internal_der_path(); let mm2_internal_xpub = trezor .get_public_key( path, diff --git a/mm2src/crypto/src/lib.rs b/mm2src/crypto/src/lib.rs index 65be730906..da9ff99a29 100644 --- a/mm2src/crypto/src/lib.rs +++ b/mm2src/crypto/src/lib.rs @@ -18,7 +18,7 @@ mod xpub; pub use bip32_child::{Bip32Child, Bip32DerPathError, Bip32DerPathOps, Bip44Tail}; pub use crypto_ctx::{CryptoCtx, CryptoCtxError, CryptoInitError, CryptoInitResult, HwCtxInitError, KeyPairPolicy}; -pub use global_hd_ctx::GlobalHDAccountArc; +pub use global_hd_ctx::{derive_secp256k1_secret, GlobalHDAccountArc}; pub use hw_client::{HwClient, HwConnectionStatus, HwDeviceInfo, HwProcessingError, HwPubkey, HwWalletType, TrezorConnectProcessor}; pub use hw_common::primitives::{Bip32Error, ChildNumber, DerivationPath, EcdsaCurve, ExtendedPublicKey, @@ -26,8 +26,8 @@ pub use hw_common::primitives::{Bip32Error, ChildNumber, DerivationPath, EcdsaCu pub use hw_ctx::{HardwareWalletArc, HardwareWalletCtx}; pub use hw_error::{from_hw_error, HwError, HwResult, HwRpcError, WithHwRpcError}; pub use keys::Secret as Secp256k1Secret; -pub use standard_hd_path::{Bip44Chain, StandardHDPath, StandardHDPathError, StandardHDPathToAccount, - StandardHDPathToCoin, UnknownChainError}; +pub use standard_hd_path::{Bip44Chain, StandardHDCoinAddress, StandardHDPath, StandardHDPathError, + StandardHDPathToAccount, StandardHDPathToCoin, UnknownChainError}; pub use trezor; pub use xpub::{XPubConverter, XpubError}; @@ -48,11 +48,9 @@ use std::str::FromStr; /// * `account = (2 ^ 31 - 1) = 2147483647` - latest available account index. /// This number is chosen so that it does not cross with real accounts; /// * `change = 0` - nothing special. -/// * `address_index` - is ether specified by the config or default `0`. -pub(crate) fn mm2_internal_der_path(address_index: Option) -> DerivationPath { - let mut der_path = DerivationPath::from_str("m/44'/141'/2147483647/0").expect("valid derivation path"); - der_path.push(address_index.unwrap_or_default()); - der_path +/// * `address_index = 0`. +pub(crate) fn mm2_internal_der_path() -> DerivationPath { + DerivationPath::from_str("m/44'/141'/2147483647/0/0").expect("valid derivation path") } #[derive(Clone, Debug, PartialEq)] diff --git a/mm2src/crypto/src/standard_hd_path.rs b/mm2src/crypto/src/standard_hd_path.rs index 4d13456b1a..f7d29f2e8b 100644 --- a/mm2src/crypto/src/standard_hd_path.rs +++ b/mm2src/crypto/src/standard_hd_path.rs @@ -122,6 +122,21 @@ impl From for Bip32DerPathError { } } +/// A struct that represents a standard HD path from account to address. +/// +/// This is used in coins activation to specify the default address that will be used for swaps. +/// +/// # Attributes +/// * `account`: The account number of the address. +/// * `is_change`: A flag that indicates whether the address is a change address or not. +/// * `address_index`: The index of the address within the account. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct StandardHDCoinAddress { + pub account: u32, + pub is_change: bool, + pub address_index: u32, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq, Primitive)] pub enum StandardHDIndex { Purpose = 0, diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index d11a47a2ca..8401f2a0fd 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -208,10 +208,6 @@ impl From for MmInitError { }, CryptoInitError::EmptyPassphrase => MmInitError::EmptyPassphrase, CryptoInitError::InvalidPassphrase(pass) => MmInitError::InvalidPassphrase(pass.to_string()), - CryptoInitError::InvalidHdAccount { error, .. } => MmInitError::FieldWrongValueInConfig { - field: "hd_account".to_string(), - error, - }, CryptoInitError::Internal(internal) => MmInitError::Internal(internal), } } @@ -430,9 +426,10 @@ pub async fn lp_init(ctx: MmArc, version: String, datetime: String) -> MmInitRes error: e.to_string(), })?; - match ctx.conf["hd_account_id"].as_u64() { - Some(hd_account_id) => CryptoCtx::init_with_global_hd_account(ctx.clone(), &passphrase, hd_account_id)?, - None => CryptoCtx::init_with_iguana_passphrase(ctx.clone(), &passphrase)?, + // This defaults to false to maintain backward compatibility. + match ctx.conf["enable_hd"].as_bool().unwrap_or(false) { + 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/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index f2d6b91e4b..1507d97771 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -1425,6 +1425,7 @@ mod lp_swap_tests { use coins::MarketCoinOps; use coins::PrivKeyActivationPolicy; use common::{block_on, new_uuid}; + use crypto::StandardHDCoinAddress; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_test_helpers::for_tests::{morty_conf, rick_conf, MORTY_ELECTRUM_ADDRS, RICK_ELECTRUM_ADDRS}; @@ -1802,6 +1803,7 @@ mod lp_swap_tests { enable_params: Default::default(), priv_key_policy: PrivKeyActivationPolicy::ContextPrivKey, check_utxo_maturity: None, + path_to_address: StandardHDCoinAddress::default(), } } diff --git a/mm2src/mm2_main/src/wasm_tests.rs b/mm2src/mm2_main/src/wasm_tests.rs index 6a29a497c2..680ecb3667 100644 --- a/mm2src/mm2_main/src/wasm_tests.rs +++ b/mm2src/mm2_main/src/wasm_tests.rs @@ -1,6 +1,7 @@ use crate::mm2::lp_init; use common::executor::{spawn, Timer}; use common::log::wasm_log::register_wasm_log; +use crypto::StandardHDCoinAddress; use mm2_core::mm_ctx::MmArc; use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::electrums::{morty_electrums, rick_electrums}; @@ -46,17 +47,17 @@ async fn test_mm2_stops_impl( let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); // Enable coins on Bob side. Print the replies in case we need the address. - let rc = enable_electrum_json(&mm_bob, RICK, true, rick_electrums()).await; + let rc = enable_electrum_json(&mm_bob, RICK, true, rick_electrums(), None).await; log!("enable RICK (bob): {:?}", rc); - let rc = enable_electrum_json(&mm_bob, MORTY, true, morty_electrums()).await; + let rc = enable_electrum_json(&mm_bob, MORTY, true, morty_electrums(), None).await; log!("enable MORTY (bob): {:?}", rc); // Enable coins on Alice side. Print the replies in case we need the address. - let rc = enable_electrum_json(&mm_alice, RICK, true, rick_electrums()).await; + let rc = enable_electrum_json(&mm_alice, RICK, true, rick_electrums(), None).await; log!("enable RICK (bob): {:?}", rc); - let rc = enable_electrum_json(&mm_alice, MORTY, true, morty_electrums()).await; + let rc = enable_electrum_json(&mm_alice, MORTY, true, morty_electrums(), None).await; log!("enable MORTY (bob): {:?}", rc); start_swaps(&mut mm_bob, &mut mm_alice, pairs, maker_price, taker_price, volume).await; @@ -84,6 +85,8 @@ async fn test_qrc20_tx_history() { test_qrc20_history_impl(Some(wasm_start)).awa async fn trade_base_rel_electrum( bob_priv_key_policy: Mm2InitPrivKeyPolicy, alice_priv_key_policy: Mm2InitPrivKeyPolicy, + bob_path_to_address: Option, + alice_path_to_address: Option, pairs: &[(&'static str, &'static str)], maker_price: f64, taker_price: f64, @@ -91,7 +94,7 @@ async fn trade_base_rel_electrum( ) { let coins = json!([rick_conf(), morty_conf(),]); - let bob_conf = Mm2TestConfForSwap::bob_conf_with_policy(bob_priv_key_policy, &coins); + let bob_conf = Mm2TestConfForSwap::bob_conf_with_policy(&bob_priv_key_policy, &coins); let mut mm_bob = MarketMakerIt::start_async(bob_conf.conf, bob_conf.rpc_password, Some(wasm_start)) .await .unwrap(); @@ -99,7 +102,7 @@ async fn trade_base_rel_electrum( let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); Timer::sleep(1.).await; - let alice_conf = Mm2TestConfForSwap::alice_conf_with_policy(alice_priv_key_policy, &coins, &mm_bob.my_seed_addr()); + let alice_conf = Mm2TestConfForSwap::alice_conf_with_policy(&alice_priv_key_policy, &coins, &mm_bob.my_seed_addr()); let mut mm_alice = MarketMakerIt::start_async(alice_conf.conf, alice_conf.rpc_password, Some(wasm_start)) .await .unwrap(); @@ -107,17 +110,17 @@ async fn trade_base_rel_electrum( let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); // Enable coins on Bob side. Print the replies in case we need the address. - let rc = enable_electrum_json(&mm_bob, RICK, true, rick_electrums()).await; + let rc = enable_electrum_json(&mm_bob, RICK, true, rick_electrums(), bob_path_to_address.clone()).await; log!("enable RICK (bob): {:?}", rc); - let rc = enable_electrum_json(&mm_bob, MORTY, true, morty_electrums()).await; + let rc = enable_electrum_json(&mm_bob, MORTY, true, morty_electrums(), bob_path_to_address).await; log!("enable MORTY (bob): {:?}", rc); // Enable coins on Alice side. Print the replies in case we need the address. - let rc = enable_electrum_json(&mm_alice, RICK, true, rick_electrums()).await; + let rc = enable_electrum_json(&mm_alice, RICK, true, rick_electrums(), alice_path_to_address.clone()).await; log!("enable RICK (bob): {:?}", rc); - let rc = enable_electrum_json(&mm_alice, MORTY, true, morty_electrums()).await; + let rc = enable_electrum_json(&mm_alice, MORTY, true, morty_electrums(), alice_path_to_address).await; log!("enable MORTY (bob): {:?}", rc); let uuids = start_swaps(&mut mm_bob, &mut mm_alice, pairs, maker_price, taker_price, volume).await; @@ -160,7 +163,22 @@ async fn trade_base_rel_electrum( #[wasm_bindgen_test] async fn trade_test_rick_and_morty() { let bob_policy = Mm2InitPrivKeyPolicy::Iguana; - let alice_policy = Mm2InitPrivKeyPolicy::GlobalHDAccount(0); + let alice_policy = Mm2InitPrivKeyPolicy::GlobalHDAccount; + let alice_path_to_address = StandardHDCoinAddress { + account: 0, + is_change: false, + address_index: 0, + }; let pairs: &[_] = &[("RICK", "MORTY")]; - trade_base_rel_electrum(bob_policy, alice_policy, pairs, 1., 1., 0.0001).await; + trade_base_rel_electrum( + bob_policy, + alice_policy, + None, + Some(alice_path_to_address), + pairs, + 1., + 1., + 0.0001, + ) + .await; } 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 10f90abdb8..2a08f18efc 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs @@ -217,10 +217,10 @@ fn test_ordermatch_custom_orderbook_ticker_both_on_maker() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN-Custom", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1-Custom", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN-Custom", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1-Custom", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json! ({ "userpass": mm_bob.userpass, "method": "setprice", @@ -352,10 +352,10 @@ fn test_ordermatch_custom_orderbook_ticker_both_on_taker() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN-Custom", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1-Custom", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN-Custom", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1-Custom", &[], None))); let rc = block_on(mm_bob.rpc(&json! ({ "userpass": mm_bob.userpass, "method": "setprice", @@ -485,10 +485,10 @@ fn test_ordermatch_custom_orderbook_ticker_mixed_case_one() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN-Custom", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1-Custom", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN-Custom", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1-Custom", &[], None))); let rc = block_on(mm_bob.rpc(&json! ({ "userpass": mm_bob.userpass, "method": "setprice", @@ -626,10 +626,10 @@ fn test_ordermatch_custom_orderbook_ticker_mixed_case_two() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1-Custom", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN-Custom", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1-Custom", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN-Custom", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json! ({ "userpass": mm_bob.userpass, "method": "setprice", @@ -758,8 +758,8 @@ fn test_zombie_order_after_balance_reduce_and_mm_restart() { let mm_maker = MarketMakerIt::start(conf.clone(), "pass".to_string(), None).unwrap(); let (_dump_log, _dump_dashboard) = mm_dump(&mm_maker.log_path); - log!("{:?}", block_on(enable_native(&mm_maker, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_maker, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_maker, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_maker, "MYCOIN1", &[], None))); let rc = block_on(mm_maker.rpc(&json! ({ "userpass": mm_maker.userpass, @@ -858,8 +858,8 @@ fn test_zombie_order_after_balance_reduce_and_mm_restart() { ); // activate coins to kickstart our order - log!("{:?}", block_on(enable_native(&mm_maker_dup, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_maker_dup, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_maker_dup, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_maker_dup, "MYCOIN1", &[], None))); thread::sleep(Duration::from_secs(5)); 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 2079a76141..39dbcbe6e6 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -840,19 +840,19 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { log!("{:?}", block_on(enable_qrc20_native(&mm_bob, "QICK"))); log!("{:?}", block_on(enable_qrc20_native(&mm_bob, "QORTY"))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "QTUM", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "QTUM", &[], None))); log!("{:?}", block_on(enable_native_bch(&mm_bob, "FORSLP", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "ADEXSLP", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "ADEXSLP", &[], None))); log!("{:?}", block_on(enable_qrc20_native(&mm_alice, "QICK"))); log!("{:?}", block_on(enable_qrc20_native(&mm_alice, "QORTY"))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "QTUM", &[]))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "QTUM", &[], None))); log!("{:?}", block_on(enable_native_bch(&mm_alice, "FORSLP", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "ADEXSLP", &[]))); + log!("{:?}", block_on(enable_native(&mm_alice, "ADEXSLP", &[], None))); let rc = block_on(mm_bob.rpc(&json! ({ "userpass": mm_bob.userpass, "method": "setprice", 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 4df7bf911a..922bff0623 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -395,8 +395,8 @@ fn order_should_be_cancelled_when_entire_balance_is_withdrawn() { ) .unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, "method": "setprice", @@ -528,10 +528,10 @@ fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_after_upda .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, @@ -661,10 +661,10 @@ fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_before_upd .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, @@ -808,10 +808,10 @@ fn test_order_should_be_updated_when_matched_partially() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, @@ -913,10 +913,10 @@ fn test_match_and_trade_setprice_max() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, "method": "setprice", @@ -1016,10 +1016,10 @@ fn test_max_taker_vol_swap() { let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let price = MmNumber::from((100, 1620)); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, @@ -1132,10 +1132,10 @@ fn test_buy_when_coins_locked_by_other_swap() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, "method": "setprice", @@ -1225,10 +1225,10 @@ fn test_sell_when_coins_locked_by_other_swap() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, "method": "setprice", @@ -1301,8 +1301,8 @@ fn test_buy_max() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_alice.rpc(&json!({ "userpass": mm_alice.userpass, "method": "buy", @@ -1365,8 +1365,8 @@ fn test_maker_trade_preimage() { .unwrap(); let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); let rc = block_on(mm.rpc(&json!({ "userpass": mm.userpass, @@ -1502,8 +1502,8 @@ fn test_taker_trade_preimage() { .unwrap(); let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); // `max` field is not supported for `buy/sell` swap methods let rc = block_on(mm.rpc(&json!({ @@ -1643,8 +1643,8 @@ fn test_trade_preimage_not_sufficient_balance() { .unwrap(); let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); fill_balance_functor(MmNumber::from("0.000015").to_decimal()); // Try sell the max amount with the zero balance. @@ -1762,8 +1762,8 @@ fn test_trade_preimage_additional_validation() { .unwrap(); let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); // Price is too low let rc = block_on(mm.rpc(&json!({ @@ -1902,8 +1902,8 @@ fn test_trade_preimage_legacy() { .unwrap(); let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); let rc = block_on(mm.rpc(&json!({ "userpass": mm.userpass, @@ -1972,8 +1972,8 @@ fn test_get_max_taker_vol() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); let rc = block_on(mm_alice.rpc(&json!({ "userpass": mm_alice.userpass, "method": "max_taker_vol", @@ -2024,8 +2024,8 @@ fn test_get_max_taker_vol_dex_fee_threshold() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); let rc = block_on(mm_alice.rpc(&json!({ "userpass": mm_alice.userpass, "method": "max_taker_vol", @@ -2085,8 +2085,8 @@ fn test_get_max_taker_vol_dust_threshold() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm.log_path); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); let rc = block_on(mm.rpc(&json!({ "userpass": mm.userpass, @@ -2136,13 +2136,19 @@ fn test_get_max_taker_vol_with_kmd() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - let electrum = block_on(enable_electrum(&mm_alice, "KMD", false, &[ - "electrum1.cipig.net:10001", - "electrum2.cipig.net:10001", - "electrum3.cipig.net:10001", - ])); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + let electrum = block_on(enable_electrum( + &mm_alice, + "KMD", + false, + &[ + "electrum1.cipig.net:10001", + "electrum2.cipig.net:10001", + "electrum3.cipig.net:10001", + ], + None, + )); log!("{:?}", electrum); let rc = block_on(mm_alice.rpc(&json!({ "userpass": mm_alice.userpass, @@ -2182,8 +2188,8 @@ fn test_get_max_maker_vol() { let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); // 1 - tx_fee let expected_volume = MmNumber::from("0.99999"); @@ -2208,7 +2214,7 @@ fn test_get_max_maker_vol_error() { let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); let actual_error = block_on(max_maker_vol(&mm, "MYCOIN")).unwrap_err::(); let expected_error = max_maker_vol_error::NotSufficientBalance { @@ -2242,8 +2248,8 @@ fn test_set_price_max() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_alice.rpc(&json!({ "userpass": mm_alice.userpass, "method": "setprice", @@ -2313,10 +2319,10 @@ fn swaps_should_stop_on_stop_rpc() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, "method": "setprice", @@ -2384,8 +2390,8 @@ fn test_maker_order_should_kick_start_and_appear_in_orderbook_on_restart() { 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); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, "method": "setprice", @@ -2404,8 +2410,8 @@ fn test_maker_order_should_kick_start_and_appear_in_orderbook_on_restart() { let mm_bob_dup = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); let (_bob_dup_dump_log, _bob_dup_dump_dashboard) = mm_dump(&mm_bob_dup.log_path); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[], None))); thread::sleep(Duration::from_secs(2)); @@ -2441,8 +2447,8 @@ fn test_maker_order_should_not_kick_start_and_appear_in_orderbook_if_balance_is_ 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); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, "method": "setprice", @@ -2480,8 +2486,8 @@ fn test_maker_order_should_not_kick_start_and_appear_in_orderbook_if_balance_is_ let mm_bob_dup = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); let (_bob_dup_dump_log, _bob_dup_dump_dashboard) = mm_dump(&mm_bob_dup.log_path); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[], None))); thread::sleep(Duration::from_secs(2)); @@ -2567,10 +2573,10 @@ fn test_maker_order_kick_start_should_trigger_subscription_and_match() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, @@ -2590,8 +2596,8 @@ fn test_maker_order_kick_start_should_trigger_subscription_and_match() { let mut mm_bob_dup = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); let (_bob_dup_dump_log, _bob_dup_dump_dashboard) = mm_dump(&mm_bob_dup.log_path); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[], None))); log!("Give restarted Bob 2 seconds to kickstart the order"); thread::sleep(Duration::from_secs(2)); @@ -2664,12 +2670,12 @@ fn test_orders_should_match_on_both_nodes_with_same_priv() { .unwrap(); let (_alice_2_dump_log, _alice_2_dump_dashboard) = mm_dump(&mm_alice_2.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice_1, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice_1, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice_2, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice_2, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice_1, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice_1, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice_2, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice_2, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, @@ -2751,10 +2757,10 @@ fn test_maker_and_taker_order_created_with_same_priv_should_not_match() { .unwrap(); let (_alice_1_dump_log, _alice_1_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, @@ -2822,10 +2828,10 @@ fn test_taker_order_converted_to_maker_should_cancel_properly_when_matched() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, "method": "sell", @@ -3029,7 +3035,7 @@ fn test_withdraw_not_sufficient_balance() { ) .unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm.log_path); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); // balance = 0, but amount = 1 let amount = BigDecimal::from(1); @@ -3148,12 +3154,12 @@ fn test_taker_should_match_with_best_price_buy() { .unwrap(); let (_eve_dump_log, _eve_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, @@ -3281,12 +3287,12 @@ fn test_taker_should_match_with_best_price_sell() { .unwrap(); let (_eve_dump_log, _eve_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, @@ -3403,10 +3409,10 @@ fn test_match_utxo_with_eth_taker_sell() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - block_on(enable_native(&mm_bob, "ETH", ETH_DEV_NODES)); - block_on(enable_native(&mm_alice, "ETH", ETH_DEV_NODES)); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + block_on(enable_native(&mm_bob, "ETH", ETH_DEV_NODES, None)); + block_on(enable_native(&mm_alice, "ETH", ETH_DEV_NODES, None)); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, @@ -3479,11 +3485,11 @@ fn test_match_utxo_with_eth_taker_buy() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - block_on(enable_native(&mm_bob, "ETH", ETH_DEV_NODES)); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + block_on(enable_native(&mm_bob, "ETH", ETH_DEV_NODES, None)); - block_on(enable_native(&mm_alice, "ETH", ETH_DEV_NODES)); + block_on(enable_native(&mm_alice, "ETH", ETH_DEV_NODES, None)); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, @@ -3529,10 +3535,10 @@ fn test_locked_amount() { let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); block_on(start_swaps( &mut mm_bob, diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index 8a08865490..dc6f915948 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -934,7 +934,7 @@ fn test_check_balance_on_order_post_base_coin_locked() { let (_dump_log, _dump_dashboard) = mm_dump(&mm_bob.log_path); log!("Log path: {}", mm_bob.log_path.display()); block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - block_on(enable_native(&mm_bob, "MYCOIN", &[])); + block_on(enable_native(&mm_bob, "MYCOIN", &[], None)); block_on(enable_qrc20_native(&mm_bob, "QICK")); // start alice @@ -957,7 +957,7 @@ fn test_check_balance_on_order_post_base_coin_locked() { let (_dump_log, _dump_dashboard) = mm_dump(&mm_alice.log_path); log!("Log path: {}", mm_alice.log_path.display()); block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - block_on(enable_native(&mm_alice, "MYCOIN", &[])); + block_on(enable_native(&mm_alice, "MYCOIN", &[], None)); block_on(enable_qrc20_native(&mm_alice, "QICK")); let rc = block_on(mm_alice.rpc(&json! ({ @@ -1033,8 +1033,8 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm.log_path); block_on(mm.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm, "QTUM", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "QTUM", &[], None))); let qtum_balance = coin.my_spendable_balance().wait().expect("!my_balance"); let qtum_dex_fee_threshold = MmNumber::from("0.000728"); @@ -1219,8 +1219,8 @@ fn test_trade_preimage_not_sufficient_base_coin_balance_for_ticker() { let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm.log_path); block_on(mm.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm, "QICK", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "QICK", &[], None))); // txfee > 0, amount = 0.005 => required = txfee + amount > 0.005, // but balance = 0.005 @@ -1278,8 +1278,8 @@ fn test_trade_preimage_dynamic_fee_not_sufficient_balance() { let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm.log_path); block_on(mm.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm, "QTUM", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "QTUM", &[], None))); // txfee > 0, amount = 0.5 => required = txfee + amount > 0.5, // but balance = 0.5 @@ -1339,8 +1339,8 @@ fn test_trade_preimage_deduct_fee_from_output_failed() { let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm.log_path); block_on(mm.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm, "QTUM", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "QTUM", &[], None))); let rc = block_on(mm.rpc(&json!({ "userpass": mm.userpass, @@ -1398,7 +1398,7 @@ fn test_segwit_native_balance() { let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm.log_path); block_on(mm.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - let enable_res = block_on(enable_native(&mm, "QTUM", &[])); + let enable_res = block_on(enable_native(&mm, "QTUM", &[], None)); let expected_balance: BigDecimal = "0.5".parse().unwrap(); assert_eq!(enable_res.balance, expected_balance); @@ -1444,7 +1444,7 @@ fn test_withdraw_and_send_from_segwit() { let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm.log_path); block_on(mm.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - log!("{:?}", block_on(enable_native(&mm, "QTUM", &[]))); + log!("{:?}", block_on(enable_native(&mm, "QTUM", &[], None))); // Send from Segwit Address to Segwit Address withdraw_and_send(&mm, "QTUM", "qcrt1q6pwxl4na4a363mgmrw8tjyppdcwuyfmat836dd", 0.2); @@ -1492,7 +1492,7 @@ fn test_withdraw_and_send_legacy_to_segwit() { let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm.log_path); block_on(mm.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - log!("{:?}", block_on(enable_native(&mm, "QTUM", &[]))); + log!("{:?}", block_on(enable_native(&mm, "QTUM", &[], None))); // Send from Legacy Address to Segwit Address withdraw_and_send(&mm, "QTUM", "qcrt1q6pwxl4na4a363mgmrw8tjyppdcwuyfmat836dd", 0.2); @@ -1694,7 +1694,7 @@ fn segwit_address_in_the_orderbook() { fill_address(&coin, &segwit_addr, 1000.into(), 30); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[]))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); let rc = block_on(mm.rpc(&json! ({ "userpass": mm.userpass, diff --git a/mm2src/mm2_main/tests/docker_tests/slp_tests.rs b/mm2src/mm2_main/tests/docker_tests/slp_tests.rs index b1fe7b6b9f..e5ce537b7b 100644 --- a/mm2src/mm2_main/tests/docker_tests/slp_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/slp_tests.rs @@ -70,7 +70,7 @@ fn test_bch_and_slp_balance() { assert_eq!(expected_spendable, bch_balance.balance); assert_eq!(expected_unspendable, bch_balance.unspendable_balance); - let enable_slp = block_on(enable_native(&mm, "ADEXSLP", &[])); + let enable_slp = block_on(enable_native(&mm, "ADEXSLP", &[], None)); let expected_spendable = BigDecimal::from(1000); assert_eq!(expected_spendable, enable_slp.balance); @@ -132,6 +132,7 @@ fn test_bch_and_slp_balance_enable_bch_with_tokens_v2() { &["ADEXSLP"], UtxoRpcMode::Native, tx_history, + None, )); let enable_bch_with_tokens: RpcV2Response = json::from_value(enable_bch_with_tokens).unwrap(); @@ -210,7 +211,7 @@ fn test_withdraw_bch_max_must_not_spend_slp() { let mm = slp_supplied_node(); block_on(enable_native_bch(&mm, "FORSLP", &[])); - block_on(enable_native(&mm, "ADEXSLP", &[])); + block_on(enable_native(&mm, "ADEXSLP", &[], None)); withdraw_max_and_send_v1(&mm, "FORSLP", &utxo_burn_address().to_string()); thread::sleep(Duration::from_secs(1)); @@ -237,6 +238,7 @@ fn test_disable_platform_coin_with_tokens() { &["ADEXSLP"], UtxoRpcMode::Native, false, + None, )); // Try to disable ADEXSLP token. let res = block_on(disable_coin(&mm, "ADEXSLP", false)); @@ -256,6 +258,7 @@ fn test_disable_platform_coin_with_tokens() { &["ADEXSLP"], UtxoRpcMode::Native, false, + None, )); // Try to force disable platform coin, FORSLP. diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs index 264bbc4f9b..383ebf0879 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -50,7 +50,7 @@ struct BalanceResult { fn enable_coin(mm_node: &MarketMakerIt, coin: &str) { if coin == "MYCOIN" { - log!("{:?}", block_on(enable_native(mm_node, coin, &[]))); + log!("{:?}", block_on(enable_native(mm_node, coin, &[], None))); } else { enable_eth(mm_node, coin); } 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 0886e3e83b..13c35cb0be 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 @@ -60,10 +60,10 @@ fn test_confirmation_settings_sync_correctly_on_buy( .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json! ({ "userpass": mm_bob.userpass, "method": "setprice", @@ -225,10 +225,10 @@ fn test_confirmation_settings_sync_correctly_on_sell( .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json! ({ "userpass": mm_bob.userpass, "method": "setprice", 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 e69733cbff..8ca965e784 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 @@ -48,8 +48,8 @@ fn swap_file_lock_prevents_double_swap_start_on_kick_start(swap_json: &str) { }); 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); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); block_on(mm_bob.wait_for_log(22., |log| { log.contains("Kick starting the swap 5acb0e63-8b26-469e-81df-7dd9e4a9ad15") })) @@ -104,10 +104,10 @@ fn test_swaps_should_kick_start_if_process_was_killed() { let mut mm_alice = MarketMakerIt::start(alice_conf.clone(), "pass".to_string(), None).unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); let rc = block_on(mm_bob.rpc(&json! ({ "userpass": mm_bob.userpass, "method": "setprice", @@ -153,8 +153,8 @@ fn test_swaps_should_kick_start_if_process_was_killed() { drop(mm_bob); let mut mm_bob_dup = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); let (_bob_dup_dump_log, _bob_dup_dump_dashboard) = mm_dump(&mm_bob_dup.log_path); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[], None))); block_on(mm_bob_dup.wait_for_log(50., |log| log.contains(&format!("Swap {} kick started.", uuid)))).unwrap(); @@ -167,8 +167,8 @@ fn test_swaps_should_kick_start_if_process_was_killed() { alice_conf["skip_seednodes_check"] = true.into(); let mut mm_alice_dup = MarketMakerIt::start(alice_conf, "pass".to_string(), None).unwrap(); let (_alice_dup_dump_log, _alice_dup_dump_dashboard) = mm_dump(&mm_alice_dup.log_path); - log!("{:?}", block_on(enable_native(&mm_alice_dup, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice_dup, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_alice_dup, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice_dup, "MYCOIN1", &[], None))); block_on(mm_alice_dup.wait_for_log(50., |log| log.contains(&format!("Swap {} kick started.", uuid)))).unwrap(); } @@ -214,8 +214,8 @@ fn swap_should_not_kick_start_if_finished_during_waiting_for_file_lock( }); 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); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[]))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); block_on(mm_bob.wait_for_log(22., |log| { log.contains("Kick starting the swap 5acb0e63-8b26-469e-81df-7dd9e4a9ad15") })) diff --git a/mm2src/mm2_main/tests/integration_tests_common/mod.rs b/mm2src/mm2_main/tests/integration_tests_common/mod.rs index 75d95e8af5..327c7bbc8d 100644 --- a/mm2src/mm2_main/tests/integration_tests_common/mod.rs +++ b/mm2src/mm2_main/tests/integration_tests_common/mod.rs @@ -2,6 +2,7 @@ use common::executor::Timer; use common::log::LogLevel; use common::{block_on, log, now_ms, wait_until_ms}; use crypto::privkey::key_pair_from_seed; +use crypto::StandardHDCoinAddress; use mm2_main::mm2::{lp_main, LpMainParams}; use mm2_rpc::data::legacy::CoinInitResponse; use mm2_test_helpers::electrums::{morty_electrums, rick_electrums}; @@ -33,10 +34,16 @@ pub fn test_mm_start_impl() { } /// Ideally, this function should be replaced everywhere with `enable_electrum_json`. -pub async fn enable_electrum(mm: &MarketMakerIt, coin: &str, tx_history: bool, urls: &[&str]) -> CoinInitResponse { +pub async fn enable_electrum( + mm: &MarketMakerIt, + coin: &str, + tx_history: bool, + urls: &[&str], + path_to_address: Option, +) -> CoinInitResponse { use mm2_test_helpers::for_tests::enable_electrum as enable_electrum_impl; - let value = enable_electrum_impl(mm, coin, tx_history, urls).await; + let value = enable_electrum_impl(mm, coin, tx_history, urls, path_to_address).await; json::from_value(value).unwrap() } @@ -45,24 +52,33 @@ pub async fn enable_electrum_json( coin: &str, tx_history: bool, servers: Vec, + path_to_address: Option, ) -> CoinInitResponse { use mm2_test_helpers::for_tests::enable_electrum_json as enable_electrum_impl; - let value = enable_electrum_impl(mm, coin, tx_history, servers).await; + let value = enable_electrum_impl(mm, coin, tx_history, servers, path_to_address).await; json::from_value(value).unwrap() } -pub async fn enable_native(mm: &MarketMakerIt, coin: &str, urls: &[&str]) -> CoinInitResponse { - let value = enable_native_impl(mm, coin, urls).await; +pub async fn enable_native( + mm: &MarketMakerIt, + coin: &str, + urls: &[&str], + path_to_address: Option, +) -> CoinInitResponse { + let value = enable_native_impl(mm, coin, urls, path_to_address).await; json::from_value(value).unwrap() } pub async fn enable_coins_rick_morty_electrum(mm: &MarketMakerIt) -> HashMap<&'static str, CoinInitResponse> { let mut replies = HashMap::new(); - replies.insert("RICK", enable_electrum_json(mm, "RICK", false, rick_electrums()).await); + replies.insert( + "RICK", + enable_electrum_json(mm, "RICK", false, rick_electrums(), None).await, + ); replies.insert( "MORTY", - enable_electrum_json(mm, "MORTY", false, morty_electrums()).await, + enable_electrum_json(mm, "MORTY", false, morty_electrums(), None).await, ); replies } @@ -72,8 +88,9 @@ pub async fn enable_z_coin_light( coin: &str, electrums: &[&str], lightwalletd_urls: &[&str], + account: Option, ) -> CoinActivationResult { - let init = init_z_coin_light(mm, coin, electrums, lightwalletd_urls).await; + let init = init_z_coin_light(mm, coin, electrums, lightwalletd_urls, account).await; let init: RpcV2Response = json::from_value(init).unwrap(); let timeout = wait_until_ms(12000000); @@ -122,15 +139,19 @@ pub async fn enable_utxo_v2_electrum( pub async fn enable_coins_eth_electrum( mm: &MarketMakerIt, eth_urls: &[&str], + path_to_address: Option, ) -> HashMap<&'static str, CoinInitResponse> { let mut replies = HashMap::new(); - replies.insert("RICK", enable_electrum_json(mm, "RICK", false, rick_electrums()).await); + replies.insert( + "RICK", + enable_electrum_json(mm, "RICK", false, rick_electrums(), path_to_address.clone()).await, + ); replies.insert( "MORTY", - enable_electrum_json(mm, "MORTY", false, morty_electrums()).await, + enable_electrum_json(mm, "MORTY", false, morty_electrums(), path_to_address.clone()).await, ); - replies.insert("ETH", enable_native(mm, "ETH", eth_urls).await); - replies.insert("JST", enable_native(mm, "JST", eth_urls).await); + replies.insert("ETH", enable_native(mm, "ETH", eth_urls, path_to_address.clone()).await); + replies.insert("JST", enable_native(mm, "JST", eth_urls, path_to_address).await); replies } 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 cf66cb92ad..f4790b4b35 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 @@ -1,5 +1,6 @@ use common::custom_futures::repeatable::{Ready, Retry}; use common::{block_on, log, repeatable}; +use crypto::StandardHDCoinAddress; use http::StatusCode; use itertools::Itertools; use mm2_test_helpers::for_tests::{disable_coin, enable_bch_with_tokens, enable_slp, my_tx_history_v2, sign_message, @@ -427,7 +428,7 @@ async fn test_bch_and_slp_testnet_history_impl() { let rpc_mode = UtxoRpcMode::electrum(T_BCH_ELECTRUMS); let tx_history = true; - let enable_bch = enable_bch_with_tokens(&mm, "tBCH", &[], rpc_mode, tx_history).await; + let enable_bch = enable_bch_with_tokens(&mm, "tBCH", &[], rpc_mode, tx_history, None).await; log!("enable_bch: {:?}", enable_bch); let history = wait_till_history_has_records(&mm, 4, "tBCH", None, TIMEOUT_S).await; log!("bch history: {:?}", history); @@ -596,7 +597,7 @@ fn test_sign_verify_message_slp() { log!("log path: {}", mm.log_path.display()); let rpc_mode = UtxoRpcMode::electrum(T_BCH_ELECTRUMS); - let enable_bch = block_on(enable_bch_with_tokens(&mm, "tBCH", &[], rpc_mode, false)); + let enable_bch = block_on(enable_bch_with_tokens(&mm, "tBCH", &[], rpc_mode, false, None)); log!("enable_bch: {:?}", enable_bch); let enable_usdf = block_on(enable_slp(&mm, "USDF")); @@ -626,15 +627,18 @@ fn test_sign_verify_message_slp() { /// Tested via [Electron-Cash-SLP](https://github.com/simpleledger/Electron-Cash-SLP). #[test] #[cfg(not(target_arch = "wasm32"))] -fn test_bch_and_slp_with_hd_account_id() { +fn test_bch_and_slp_with_enable_hd() { const TX_HISTORY: bool = false; let coins = json!([tbch_for_slp_conf(), tbch_usdf_conf()]); - // HD account 0 - - let hd_account_id = 0; - let conf_0 = Mm2TestConf::seednode_with_hd_account(BIP39_PASSPHRASE, hd_account_id, &coins); + // HD account 0 and change 0 and address_index 0 + let path_to_address = StandardHDCoinAddress { + account: 0, + is_change: false, + address_index: 0, + }; + let conf_0 = Mm2TestConf::seednode_with_hd_account(BIP39_PASSPHRASE, &coins); let mm_hd_0 = MarketMakerIt::start(conf_0.conf, conf_0.rpc_password, None).unwrap(); let rpc_mode = UtxoRpcMode::electrum(T_BCH_ELECTRUMS); @@ -644,6 +648,7 @@ fn test_bch_and_slp_with_hd_account_id() { &["USDF"], rpc_mode, TX_HISTORY, + Some(path_to_address), )); let activation_result: RpcV2Response = json::from_value(activation_result).unwrap(); @@ -663,10 +668,13 @@ fn test_bch_and_slp_with_hd_account_id() { .unwrap(); assert_eq!(slp_addr, "slptest:qpylzql7gzh6yctm7uslsz5qufl44gk2tsfnl7m9uj"); - // HD account 1 - - let hd_account_id = 1; - let conf_1 = Mm2TestConf::seednode_with_hd_account(BIP39_PASSPHRASE, hd_account_id, &coins); + // HD account 0 and change 0 and address_index 1 + let path_to_address = StandardHDCoinAddress { + account: 0, + is_change: false, + address_index: 1, + }; + let conf_1 = Mm2TestConf::seednode_with_hd_account(BIP39_PASSPHRASE, &coins); let mm_hd_1 = MarketMakerIt::start(conf_1.conf, conf_1.rpc_password, None).unwrap(); let rpc_mode = UtxoRpcMode::electrum(T_BCH_ELECTRUMS); @@ -676,6 +684,7 @@ fn test_bch_and_slp_with_hd_account_id() { &["USDF"], rpc_mode, TX_HISTORY, + Some(path_to_address), )); let activation_result: RpcV2Response = json::from_value(activation_result).unwrap(); @@ -694,4 +703,40 @@ fn test_bch_and_slp_with_hd_account_id() { .exactly_one() .unwrap(); assert_eq!(slp_addr, "slptest:qpyhwc7shd5hlul8zg0snmaptaa9q9yc4q9uzddky0"); + + // HD account 7 and change 1 and address_index 77 + let path_to_address = StandardHDCoinAddress { + account: 7, + is_change: true, + address_index: 77, + }; + let conf_1 = Mm2TestConf::seednode_with_hd_account(BIP39_PASSPHRASE, &coins); + let mm_hd_1 = MarketMakerIt::start(conf_1.conf, conf_1.rpc_password, None).unwrap(); + + let rpc_mode = UtxoRpcMode::electrum(T_BCH_ELECTRUMS); + let activation_result = block_on(enable_bch_with_tokens( + &mm_hd_1, + "tBCH", + &["USDF"], + rpc_mode, + TX_HISTORY, + Some(path_to_address), + )); + + let activation_result: RpcV2Response = json::from_value(activation_result).unwrap(); + let (bch_addr, _) = activation_result + .result + .bch_addresses_infos + .into_iter() + .exactly_one() + .unwrap(); + assert_eq!(bch_addr, "bchtest:qzghm7m4v2jyn3dz4qcfdmzg9xnhqvlgeqlx6ru72p"); + + let (slp_addr, _) = activation_result + .result + .slp_addresses_infos + .into_iter() + .exactly_one() + .unwrap(); + assert_eq!(slp_addr, "slptest:qzghm7m4v2jyn3dz4qcfdmzg9xnhqvlgeqyjacxfcu"); } 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 41a510c63c..fcf6629642 100644 --- a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs @@ -42,7 +42,7 @@ fn test_best_orders() { log!("Bob log path: {}", mm_bob.log_path.display()); // Enable coins on Bob side. Print the replies in case we need the "address". - let bob_coins = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES)); + let bob_coins = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)); log!("enable_coins (bob): {:?}", bob_coins); // issue sell request on Bob side by setting base/rel price log!("Issue bob sell requests"); @@ -207,7 +207,7 @@ fn test_best_orders_v2_by_number() { log!("Bob log path: {:?}", [mm_bob.log_path.display()]); // Enable coins on Bob side. Print the replies in case we need the "address". - let bob_coins = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES)); + let bob_coins = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)); log!("enable_coins (bob) {:?}", [bob_coins]); // issue sell request on Bob side by setting base/rel price log!("Issue bob sell requests"); @@ -330,7 +330,7 @@ fn test_best_orders_v2_by_volume() { log!("Bob log path: {:?}", [mm_bob.log_path.display()]); // Enable coins on Bob side. Print the replies in case we need the "address". - let bob_coins = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES)); + let bob_coins = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)); log!("enable_coins (bob): {:?}", [bob_coins]); // issue sell request on Bob side by setting base/rel price log!("Issue bob sell requests"); @@ -444,7 +444,7 @@ fn test_best_orders_v2_exclude_mine() { ) .unwrap(); - let _ = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES)); + let _ = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)); let bob_orders = [ ("RICK", "MORTY", "0.9", "0.9", None), ("RICK", "MORTY", "0.8", "0.9", None), @@ -485,7 +485,7 @@ fn test_best_orders_v2_exclude_mine() { ) .unwrap(); - let _ = block_on(enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES)); + let _ = block_on(enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES, None)); let alice_orders = [("RICK", "MORTY", "0.85", "1", None)]; let mut alice_order_ids = BTreeSet::::new(); for (base, rel, price, volume, min_volume) in alice_orders.iter() { @@ -703,7 +703,7 @@ fn test_best_orders_filter_response() { log!("Bob log path: {}", mm_bob.log_path.display()); // Enable coins on Bob side. Print the replies in case we need the "address". - let bob_coins = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES)); + let bob_coins = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)); log!("enable_coins (bob): {:?}", bob_coins); // issue sell request on Bob side by setting base/rel price log!("Issue bob sell requests"); @@ -992,10 +992,10 @@ fn best_orders_must_return_duplicate_for_orderbook_tickers() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - let t_btc_bob = block_on(enable_electrum(&mm_bob, "tBTC", false, TBTC_ELECTRUMS)); + let t_btc_bob = block_on(enable_electrum(&mm_bob, "tBTC", false, TBTC_ELECTRUMS, None)); log!("Bob enable tBTC: {:?}", t_btc_bob); - let rick_bob = block_on(enable_electrum(&mm_bob, "RICK", false, RICK_ELECTRUM_ADDRS)); + let rick_bob = block_on(enable_electrum(&mm_bob, "RICK", false, RICK_ELECTRUM_ADDRS, None)); log!("Bob enable RICK: {:?}", rick_bob); // issue sell request on Bob side by setting base/rel price @@ -1146,7 +1146,7 @@ fn zhtlc_best_orders() { log!("bob_zombie_cache_path {}", bob_zombie_cache_path.display()); std::fs::copy("./mm2src/coins/for_tests/ZOMBIE_CACHE.db", bob_zombie_cache_path).unwrap(); - block_on(enable_electrum_json(&mm_bob, "RICK", false, rick_electrums())); + block_on(enable_electrum_json(&mm_bob, "RICK", false, rick_electrums(), None)); block_on(enable_z_coin(&mm_bob, "ZOMBIE")); let set_price_json = json!({ diff --git a/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs b/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs index 9bbc37f056..561e97774d 100644 --- a/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs +++ b/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs @@ -94,7 +94,7 @@ pub async fn trade_base_rel_iris( ) .await ); - dbg!(enable_electrum(&mm_bob, "RICK", false, RICK_ELECTRUM_ADDRS).await); + dbg!(enable_electrum(&mm_bob, "RICK", false, RICK_ELECTRUM_ADDRS, None).await); dbg!( enable_tendermint( @@ -106,7 +106,7 @@ pub async fn trade_base_rel_iris( ) .await ); - dbg!(enable_electrum(&mm_alice, "RICK", false, RICK_ELECTRUM_ADDRS).await); + dbg!(enable_electrum(&mm_alice, "RICK", false, RICK_ELECTRUM_ADDRS, None).await); dbg!(enable_eth_coin(&mm_bob, "tBNB", TBNB_URLS, TBNB_SWAP_CONTRACT, None, false).await); dbg!(enable_eth_coin(&mm_alice, "tBNB", TBNB_URLS, TBNB_SWAP_CONTRACT, None, false).await); diff --git a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs index 4fde665bd6..5e69a89771 100644 --- a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs @@ -120,7 +120,13 @@ fn start_lightning_nodes(enable_0_confs: bool) -> (MarketMakerIt, MarketMakerIt, let (_dump_log, _dump_dashboard) = mm_node_1.mm_dump(); log!("Node 1 log path: {}", mm_node_1.log_path.display()); - let electrum = block_on(enable_electrum(&mm_node_1, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); + let electrum = block_on(enable_electrum( + &mm_node_1, + "tBTC-TEST-segwit", + false, + T_BTC_ELECTRUMS, + None, + )); log!("Node 1 tBTC address: {}", electrum.address); let enable_lightning_1 = block_on(enable_lightning(&mm_node_1, "tBTC-TEST-lightning", 600)); @@ -144,7 +150,13 @@ fn start_lightning_nodes(enable_0_confs: bool) -> (MarketMakerIt, MarketMakerIt, let (_dump_log, _dump_dashboard) = mm_node_2.mm_dump(); log!("Node 2 log path: {}", mm_node_2.log_path.display()); - let electrum = block_on(enable_electrum(&mm_node_2, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); + let electrum = block_on(enable_electrum( + &mm_node_2, + "tBTC-TEST-segwit", + false, + T_BTC_ELECTRUMS, + None, + )); log!("Node 2 tBTC address: {}", electrum.address); let enable_lightning_2 = block_on(enable_lightning(&mm_node_2, "tBTC-TEST-lightning", 600)); @@ -376,7 +388,7 @@ fn test_enable_lightning() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - let _electrum = block_on(enable_electrum(&mm, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); + let _electrum = block_on(enable_electrum(&mm, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS, None)); let enable_lightning_coin = block_on(enable_lightning(&mm, "tBTC-TEST-lightning", 600)); assert_eq!(&enable_lightning_coin.platform_coin, "tBTC-TEST-segwit"); @@ -1063,7 +1075,7 @@ fn test_sign_verify_message_lightning() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - block_on(enable_electrum(&mm, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); + block_on(enable_electrum(&mm, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS, None)); block_on(enable_lightning(&mm, "tBTC-TEST-lightning", 600)); let response = block_on(sign_message(&mm, "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 02e0b53b04..2bfff0f3b6 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -15,14 +15,16 @@ use mm2_test_helpers::for_tests::check_stats_swap_status; use mm2_test_helpers::for_tests::{btc_segwit_conf, btc_with_spv_conf, btc_with_sync_starting_header, check_recent_swaps, enable_eth_coin, enable_qrc20, eth_jst_testnet_conf, eth_testnet_conf, find_metrics_in_json, from_env_file, get_shared_db_id, mm_spat, - morty_conf, rick_conf, sign_message, start_swaps, tbtc_with_spv_conf, - test_qrc20_history_impl, tqrc20_conf, verify_message, + morty_conf, rick_conf, sign_message, start_swaps, tbtc_segwit_conf, + tbtc_with_spv_conf, test_qrc20_history_impl, tqrc20_conf, verify_message, wait_for_swap_contract_negotiation, wait_for_swap_negotiation_failure, wait_for_swaps_finish_and_check_status, wait_till_history_has_records, MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, RaiiDump, ETH_DEV_NODES, ETH_DEV_SWAP_CONTRACT, ETH_DEV_TOKEN_CONTRACT, ETH_MAINNET_NODE, - ETH_MAINNET_SWAP_CONTRACT, MORTY, QRC20_ELECTRUMS, RICK, RICK_ELECTRUM_ADDRS}; + ETH_MAINNET_SWAP_CONTRACT, MORTY, QRC20_ELECTRUMS, RICK, RICK_ELECTRUM_ADDRS, + TBTC_ELECTRUMS}; +use crypto::StandardHDCoinAddress; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::*; use serde_json::{self as json, json, Value as Json}; @@ -230,11 +232,17 @@ fn test_my_balance() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); // Enable RICK. - let json = block_on(enable_electrum(&mm, "RICK", false, &[ - "electrum1.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum3.cipig.net:10017", - ])); + let json = block_on(enable_electrum( + &mm, + "RICK", + false, + &[ + "electrum1.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum3.cipig.net:10017", + ], + None, + )); assert_eq!(json.balance, "7.777".parse().unwrap()); let my_balance = block_on(mm.rpc(&json! ({ @@ -416,9 +424,11 @@ fn test_check_balance_on_order_post() { // Enable coins. Print the replies in case we need the "address". log!( "enable_coins (bob): {:?}", - block_on(enable_coins_eth_electrum(&mm, &[ - "https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b" - ])) + block_on(enable_coins_eth_electrum( + &mm, + &["https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b"], + None + )), ); // issue sell request by setting base/rel price @@ -597,11 +607,17 @@ fn test_mmrpc_v2() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - let _electrum = block_on(enable_electrum(&mm, "RICK", false, &[ - "electrum3.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum1.cipig.net:10017", - ])); + let _electrum = block_on(enable_electrum( + &mm, + "RICK", + false, + &[ + "electrum3.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum1.cipig.net:10017", + ], + None, + )); // no `userpass` let withdraw = block_on(mm.rpc(&json! ({ @@ -725,9 +741,12 @@ fn test_rpc_password_from_json_no_userpass() { /// Trades few pairs concurrently to speed up the process and also act like "load" test /// /// Please note that it +#[allow(clippy::too_many_arguments)] async fn trade_base_rel_electrum( bob_priv_key_policy: Mm2InitPrivKeyPolicy, alice_priv_key_policy: Mm2InitPrivKeyPolicy, + bob_path_to_address: Option, + alice_path_to_address: Option, pairs: &[(&'static str, &'static str)], maker_price: f64, taker_price: f64, @@ -741,7 +760,7 @@ async fn trade_base_rel_electrum( {"coin":"ZOMBIE","asset":"ZOMBIE","fname":"ZOMBIE (TESTCOIN)","txversion":4,"overwintered":1,"mm2":1,"protocol":{"type":"ZHTLC"},"required_confirmations":0}, ]); - let bob_conf = Mm2TestConfForSwap::bob_conf_with_policy(bob_priv_key_policy, &coins); + let bob_conf = Mm2TestConfForSwap::bob_conf_with_policy(&bob_priv_key_policy, &coins); let mut mm_bob = MarketMakerIt::start_async(bob_conf.conf, bob_conf.rpc_password, None) .await .unwrap(); @@ -754,7 +773,7 @@ async fn trade_base_rel_electrum( Timer::sleep(1.).await; - let alice_conf = Mm2TestConfForSwap::alice_conf_with_policy(alice_priv_key_policy, &coins, &mm_bob.my_seed_addr()); + let alice_conf = Mm2TestConfForSwap::alice_conf_with_policy(&alice_priv_key_policy, &coins, &mm_bob.my_seed_addr()); let mut mm_alice = MarketMakerIt::start_async(alice_conf.conf, alice_conf.rpc_password, None) .await .unwrap(); @@ -791,11 +810,11 @@ async fn trade_base_rel_electrum( log!("enable ZOMBIE alice {:?}", zombie_alice); } // Enable coins on Bob side. Print the replies in case we need the address. - let rc = enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES).await; + let rc = enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, bob_path_to_address).await; log!("enable_coins (bob): {:?}", rc); // Enable coins on Alice side. Print the replies in case we need the address. - let rc = enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES).await; + let rc = enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES, alice_path_to_address).await; log!("enable_coins (alice): {:?}", rc); let uuids = start_swaps(&mut mm_bob, &mut mm_alice, pairs, maker_price, taker_price, volume).await; @@ -873,11 +892,18 @@ async fn trade_base_rel_electrum( #[cfg(not(target_arch = "wasm32"))] fn trade_test_electrum_and_eth_coins() { let bob_policy = Mm2InitPrivKeyPolicy::Iguana; - let alice_policy = Mm2InitPrivKeyPolicy::GlobalHDAccount(0); + let alice_policy = Mm2InitPrivKeyPolicy::GlobalHDAccount; + let alice_path_to_address = StandardHDCoinAddress { + account: 0, + is_change: false, + address_index: 0, + }; let pairs = &[("ETH", "JST")]; block_on(trade_base_rel_electrum( bob_policy, alice_policy, + None, + Some(alice_path_to_address), pairs, 1., 2., @@ -891,13 +917,23 @@ fn trade_test_electrum_rick_zombie() { let bob_policy = Mm2InitPrivKeyPolicy::Iguana; let alice_policy = Mm2InitPrivKeyPolicy::Iguana; let pairs = &[("RICK", "ZOMBIE")]; - block_on(trade_base_rel_electrum(bob_policy, alice_policy, pairs, 1., 2., 0.1)); + block_on(trade_base_rel_electrum( + bob_policy, + alice_policy, + None, + None, + pairs, + 1., + 2., + 0.1, + )); } #[cfg(not(target_arch = "wasm32"))] fn withdraw_and_send( mm: &MarketMakerIt, coin: &str, + from: Option, to: &str, enable_res: &HashMap<&'static str, CoinInitResponse>, expected_bal_change: &str, @@ -905,14 +941,16 @@ fn withdraw_and_send( ) { use std::ops::Sub; - use coins::TxFeeDetails; + use coins::{TxFeeDetails, WithdrawFrom}; + let from = from.map(WithdrawFrom::HDWalletAddress); let withdraw = block_on(mm.rpc(&json! ({ "mmrpc": "2.0", "userpass": mm.userpass, "method": "withdraw", "params": { "coin": coin, + "from": from, "to": to, "amount": amount, }, @@ -925,7 +963,7 @@ fn withdraw_and_send( json::from_str(&withdraw.1).expect("Expected 'RpcSuccessResponse'"); let tx_details = res.result; - let from = addr_from_enable(enable_res, coin).to_owned(); + let from_str = addr_from_enable(enable_res, coin).to_owned(); let mut expected_bal_change = BigDecimal::from_str(expected_bal_change).expect("!BigDecimal::from_str"); let fee_details: TxFeeDetails = json::from_value(tx_details.fee_details).unwrap(); @@ -938,7 +976,10 @@ fn withdraw_and_send( assert_eq!(tx_details.to, vec![to.to_owned()]); assert_eq!(tx_details.my_balance_change, expected_bal_change); - assert_eq!(tx_details.from, vec![from]); + // Todo: Should check the from address for withdraws from another HD wallet address when there is an RPC method for addresses + if from.is_none() { + assert_eq!(tx_details.from, vec![from_str]); + } let send = block_on(mm.rpc(&json! ({ "userpass": mm.userpass, @@ -987,20 +1028,27 @@ fn test_withdraw_and_send() { // wait until RPC API is active // Enable coins. Print the replies in case we need the address. - let mut enable_res = block_on(enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES)); + let mut enable_res = block_on(enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES, None)); enable_res.insert( "MORTY_SEGWIT", - block_on(enable_electrum(&mm_alice, "MORTY_SEGWIT", false, &[ - "electrum1.cipig.net:10018", - "electrum2.cipig.net:10018", - "electrum3.cipig.net:10018", - ])), + block_on(enable_electrum( + &mm_alice, + "MORTY_SEGWIT", + false, + &[ + "electrum1.cipig.net:10018", + "electrum2.cipig.net:10018", + "electrum3.cipig.net:10018", + ], + None, + )), ); log!("enable_coins (alice): {:?}", enable_res); withdraw_and_send( &mm_alice, "MORTY", + None, "RJTYiYeJ8eVvJ53n2YbrVmxWNNMVZjDGLh", &enable_res, "-0.00101", @@ -1009,6 +1057,7 @@ fn test_withdraw_and_send() { withdraw_and_send( &mm_alice, "ETH", + None, "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9", &enable_res, "-0.001", @@ -1019,6 +1068,7 @@ fn test_withdraw_and_send() { withdraw_and_send( &mm_alice, "JST", + None, "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9", &enable_res, "-0.001", @@ -1104,6 +1154,89 @@ fn test_withdraw_and_send() { block_on(mm_alice.stop()).unwrap(); } +// This test is ignored because it requires refilling addresses with coins +#[test] +#[ignore] +#[cfg(not(target_arch = "wasm32"))] +fn test_withdraw_and_send_hd() { + const TX_HISTORY: bool = false; + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let coins = json!([rick_conf(), tbtc_segwit_conf(), eth_testnet_conf()]); + + 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()); + + let rick = block_on(enable_electrum(&mm_hd, "RICK", TX_HISTORY, RICK_ELECTRUM_ADDRS, None)); + assert_eq!(rick.address, "RXNtAyDSsY3DS3VxTpJegzoHU9bUX54j56"); + let mut rick_enable_res = HashMap::new(); + rick_enable_res.insert("RICK", rick); + + let tbtc_segwit = block_on(enable_electrum(&mm_hd, "tBTC-Segwit", TX_HISTORY, TBTC_ELECTRUMS, None)); + assert_eq!(tbtc_segwit.address, "tb1q7z9vzf8wpp9cks0l4nj5v28zf7jt56kuekegh5"); + let mut tbtc_segwit_enable_res = HashMap::new(); + tbtc_segwit_enable_res.insert("tBTC-Segwit", tbtc_segwit); + + // Enable ETH with HD account 0, change address 0, index 1 to try to withdraw from index 0 which has funds + let eth = block_on(enable_native( + &mm_hd, + "ETH", + ETH_DEV_NODES, + Some(StandardHDCoinAddress { + account: 0, + is_change: false, + address_index: 1, + }), + )); + assert_eq!(eth.address, "0xDe841899aB4A22E23dB21634e54920aDec402397"); + let mut eth_enable_res = HashMap::new(); + eth_enable_res.insert("ETH", eth); + + // Withdraw from HD account 0, change address 0, index 1 + let mut from_account_address = StandardHDCoinAddress { + account: 0, + is_change: false, + address_index: 1, + }; + + withdraw_and_send( + &mm_hd, + "RICK", + Some(from_account_address.clone()), + "RJTYiYeJ8eVvJ53n2YbrVmxWNNMVZjDGLh", + &rick_enable_res, + "-0.00101", + 0.001, + ); + + withdraw_and_send( + &mm_hd, + "tBTC-Segwit", + Some(from_account_address.clone()), + "tb1q7z9vzf8wpp9cks0l4nj5v28zf7jt56kuekegh5", + &tbtc_segwit_enable_res, + "-0.00100144", + 0.001, + ); + + from_account_address.address_index = 0; + withdraw_and_send( + &mm_hd, + "ETH", + Some(from_account_address), + "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9", + ð_enable_res, + "-0.001", + 0.001, + ); + log!("Wait for the ETH payment to be sent"); + thread::sleep(Duration::from_secs(15)); + + block_on(mm_hd.stop()).unwrap(); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_tbtc_withdraw_to_cashaddresses_should_fail() { @@ -1228,11 +1361,17 @@ fn test_withdraw_legacy() { let mut enable_res = block_on(enable_coins_rick_morty_electrum(&mm_alice)); enable_res.insert( "MORTY_SEGWIT", - block_on(enable_electrum(&mm_alice, "MORTY_SEGWIT", false, &[ - "electrum1.cipig.net:10018", - "electrum2.cipig.net:10018", - "electrum3.cipig.net:10018", - ])), + block_on(enable_electrum( + &mm_alice, + "MORTY_SEGWIT", + false, + &[ + "electrum1.cipig.net:10018", + "electrum2.cipig.net:10018", + "electrum3.cipig.net:10018", + ], + None, + )), ); log!("enable_coins (alice): {:?}", enable_res); @@ -1462,11 +1601,17 @@ fn test_order_errors_when_base_equal_rel() { .unwrap(); let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - block_on(enable_electrum(&mm, "RICK", false, &[ - "electrum3.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum1.cipig.net:10017", - ])); + block_on(enable_electrum( + &mm, + "RICK", + false, + &[ + "electrum3.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum1.cipig.net:10017", + ], + None, + )); let rc = block_on(mm.rpc(&json! ({ "userpass": mm.userpass, @@ -1528,7 +1673,7 @@ fn startup_passphrase(passphrase: &str, expected_address: &str) { { log!("Log path: {}", mm.log_path.display()) } - let enable = block_on(enable_electrum(&mm, "KMD", false, &["electrum1.cipig.net:10001"])); + let enable = block_on(enable_electrum(&mm, "KMD", false, &["electrum1.cipig.net:10001"], None)); assert_eq!(expected_address, enable.address); block_on(mm.stop()).unwrap(); } @@ -1884,21 +2029,33 @@ fn test_electrum_enable_conn_errors() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); // Using working servers and few else with random ports to trigger "connection refused" - block_on(enable_electrum(&mm_bob, "RICK", false, &[ - "electrum3.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum1.cipig.net:10017", - "electrum1.cipig.net:60017", - "electrum1.cipig.net:60018", - ])); + block_on(enable_electrum( + &mm_bob, + "RICK", + false, + &[ + "electrum3.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum1.cipig.net:10017", + "electrum1.cipig.net:60017", + "electrum1.cipig.net:60018", + ], + None, + )); // use random domain name to trigger name is not resolved - block_on(enable_electrum(&mm_bob, "MORTY", false, &[ - "electrum3.cipig.net:10018", - "electrum2.cipig.net:10018", - "electrum1.cipig.net:10018", - "random-electrum-domain-name1.net:60017", - "random-electrum-domain-name2.net:60017", - ])); + block_on(enable_electrum( + &mm_bob, + "MORTY", + false, + &[ + "electrum3.cipig.net:10018", + "electrum2.cipig.net:10018", + "electrum1.cipig.net:10018", + "random-electrum-domain-name1.net:60017", + "random-electrum-domain-name2.net:60017", + ], + None, + )); } #[test] @@ -1930,18 +2087,30 @@ fn test_order_should_not_be_displayed_when_node_is_down() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - let electrum_rick = block_on(enable_electrum(&mm_bob, "RICK", false, &[ - "electrum3.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum1.cipig.net:10017", - ])); + let electrum_rick = block_on(enable_electrum( + &mm_bob, + "RICK", + false, + &[ + "electrum3.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum1.cipig.net:10017", + ], + None, + )); log!("Bob enable RICK {:?}", electrum_rick); - let electrum_morty = block_on(enable_electrum(&mm_bob, "MORTY", false, &[ - "electrum3.cipig.net:10018", - "electrum2.cipig.net:10018", - "electrum1.cipig.net:10018", - ])); + let electrum_morty = block_on(enable_electrum( + &mm_bob, + "MORTY", + false, + &[ + "electrum3.cipig.net:10018", + "electrum2.cipig.net:10018", + "electrum1.cipig.net:10018", + ], + None, + )); log!("Bob enable MORTY {:?}", electrum_morty); let mm_alice = MarketMakerIt::start( @@ -1964,18 +2133,30 @@ fn test_order_should_not_be_displayed_when_node_is_down() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - let electrum_rick = block_on(enable_electrum(&mm_alice, "RICK", false, &[ - "electrum3.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum1.cipig.net:10017", - ])); + let electrum_rick = block_on(enable_electrum( + &mm_alice, + "RICK", + false, + &[ + "electrum3.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum1.cipig.net:10017", + ], + None, + )); log!("Alice enable RICK {:?}", electrum_rick); - let electrum_morty = block_on(enable_electrum(&mm_alice, "MORTY", false, &[ - "electrum3.cipig.net:10018", - "electrum2.cipig.net:10018", - "electrum1.cipig.net:10018", - ])); + let electrum_morty = block_on(enable_electrum( + &mm_alice, + "MORTY", + false, + &[ + "electrum3.cipig.net:10018", + "electrum2.cipig.net:10018", + "electrum1.cipig.net:10018", + ], + None, + )); log!("Alice enable MORTY {:?}", electrum_morty); // issue sell request on Bob side by setting base/rel price @@ -2058,18 +2239,30 @@ fn test_own_orders_should_not_be_removed_from_orderbook() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - let electrum_rick = block_on(enable_electrum(&mm_bob, "RICK", false, &[ - "electrum3.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum1.cipig.net:10017", - ])); + let electrum_rick = block_on(enable_electrum( + &mm_bob, + "RICK", + false, + &[ + "electrum3.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum1.cipig.net:10017", + ], + None, + )); log!("Bob enable RICK {:?}", electrum_rick); - let electrum_morty = block_on(enable_electrum(&mm_bob, "MORTY", false, &[ - "electrum3.cipig.net:10018", - "electrum2.cipig.net:10018", - "electrum1.cipig.net:10018", - ])); + let electrum_morty = block_on(enable_electrum( + &mm_bob, + "MORTY", + false, + &[ + "electrum3.cipig.net:10018", + "electrum2.cipig.net:10018", + "electrum1.cipig.net:10018", + ], + None, + )); log!("Bob enable MORTY {:?}", electrum_morty); // issue sell request on Bob side by setting base/rel price @@ -2144,7 +2337,7 @@ fn test_show_priv_key() { log!("Log path: {}", mm.log_path.display()); log!( "enable_coins: {:?}", - block_on(enable_coins_eth_electrum(&mm, ETH_DEV_NODES)) + block_on(enable_coins_eth_electrum(&mm, ETH_DEV_NODES, None)) ); check_priv_key(&mm, "RICK", "UvCjJf4dKSs2vFGVtCnUTAhR5FTZGdg43DDRa9s7s5DV1sSDX14g"); @@ -2331,7 +2524,7 @@ fn setprice_buy_sell_too_low_volume() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - let enable = block_on(enable_coins_eth_electrum(&mm, ETH_DEV_NODES)); + let enable = block_on(enable_coins_eth_electrum(&mm, ETH_DEV_NODES, None)); log!("{:?}", enable); check_too_low_volume_order_creation_fails(&mm, "MORTY", "ETH"); @@ -2367,7 +2560,10 @@ fn test_fill_or_kill_taker_order_should_not_transform_to_maker() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); log!("Issue bob ETH/JST sell request"); let rc = block_on(mm_bob.rpc(&json! ({ @@ -2431,7 +2627,10 @@ fn test_gtc_taker_order_should_transform_to_maker() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); log!("Issue bob ETH/JST sell request"); let rc = block_on(mm_bob.rpc(&json! ({ @@ -2501,7 +2700,10 @@ fn test_set_price_must_save_order_to_db() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); log!("Issue bob ETH/JST sell request"); let rc = block_on(mm_bob.rpc(&json! ({ @@ -2551,7 +2753,10 @@ fn test_set_price_response_format() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); log!("Issue bob ETH/JST sell request"); let rc = block_on(mm_bob.rpc(&json! ({ @@ -2804,11 +3009,17 @@ fn test_metrics_method() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - let _electrum = block_on(enable_electrum(&mm, "RICK", false, &[ - "electrum1.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum3.cipig.net:10017", - ])); + let _electrum = block_on(enable_electrum( + &mm, + "RICK", + false, + &[ + "electrum1.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum3.cipig.net:10017", + ], + None, + )); let metrics = request_metrics(&mm); assert!(!metrics.metrics.is_empty()); @@ -2861,11 +3072,17 @@ fn test_electrum_tx_history() { log!("log path: {}", mm.log_path.display()); // Enable RICK electrum client with tx_history loop. - let electrum = block_on(enable_electrum(&mm, "RICK", true, &[ - "electrum1.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum3.cipig.net:10017", - ])); + let electrum = block_on(enable_electrum( + &mm, + "RICK", + true, + &[ + "electrum1.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum3.cipig.net:10017", + ], + None, + )); // Wait till tx_history will not be loaded block_on(mm.wait_for_log(500., |log| log.contains("history has been loaded successfully"))).unwrap(); @@ -2884,6 +3101,7 @@ fn test_electrum_tx_history() { withdraw_and_send( &mm, "RICK", + None, "RRYmiZSDo3UdHHqj1rLKf8cbJroyv9NxXw", &enable_res, "-0.00001", @@ -2954,13 +3172,19 @@ fn test_convert_utxo_address() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - let _electrum = block_on(enable_electrum(&mm, "BCH", false, &[ - "electroncash.de:50003", - "tbch.loping.net:60001", - "blackie.c3-soft.com:60001", - "bch0.kister.net:51001", - "testnet.imaginary.cash:50001", - ])); + let _electrum = block_on(enable_electrum( + &mm, + "BCH", + false, + &[ + "electroncash.de:50003", + "tbch.loping.net:60001", + "blackie.c3-soft.com:60001", + "bch0.kister.net:51001", + "testnet.imaginary.cash:50001", + ], + None, + )); // test standard to cashaddress let rc = block_on(mm.rpc(&json! ({ @@ -3091,11 +3315,17 @@ fn test_convert_segwit_address() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - let _electrum = block_on(enable_electrum(&mm, "tBTC", false, &[ - "electrum1.cipig.net:10068", - "electrum2.cipig.net:10068", - "electrum3.cipig.net:10068", - ])); + let _electrum = block_on(enable_electrum( + &mm, + "tBTC", + false, + &[ + "electrum1.cipig.net:10068", + "electrum2.cipig.net:10068", + "electrum3.cipig.net:10068", + ], + None, + )); // test standard to segwit let rc = block_on(mm.rpc(&json! ({ @@ -3206,7 +3436,7 @@ fn test_convert_eth_address() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - block_on(enable_native(&mm, "ETH", ETH_DEV_NODES)); + block_on(enable_native(&mm, "ETH", ETH_DEV_NODES, None)); // test single-case to mixed-case let rc = block_on(mm.rpc(&json! ({ @@ -3310,11 +3540,17 @@ fn test_add_delegation_qtum() { ) .unwrap(); - let json = block_on(enable_electrum(&mm, "tQTUM", false, &[ - "electrum1.cipig.net:10071", - "electrum2.cipig.net:10071", - "electrum3.cipig.net:10071", - ])); + let json = block_on(enable_electrum( + &mm, + "tQTUM", + false, + &[ + "electrum1.cipig.net:10071", + "electrum2.cipig.net:10071", + "electrum3.cipig.net:10071", + ], + None, + )); println!("{}", json.balance); let rc = block_on(mm.rpc(&json!({ @@ -3395,11 +3631,17 @@ fn test_remove_delegation_qtum() { ) .unwrap(); - let json = block_on(enable_electrum(&mm, "tQTUM", false, &[ - "electrum1.cipig.net:10071", - "electrum2.cipig.net:10071", - "electrum3.cipig.net:10071", - ])); + let json = block_on(enable_electrum( + &mm, + "tQTUM", + false, + &[ + "electrum1.cipig.net:10071", + "electrum2.cipig.net:10071", + "electrum3.cipig.net:10071", + ], + None, + )); println!("{}", json.balance); let rc = block_on(mm.rpc(&json!({ @@ -3455,11 +3697,17 @@ fn test_get_staking_infos_qtum() { ) .unwrap(); - let json = block_on(enable_electrum(&mm, "tQTUM", false, &[ - "electrum1.cipig.net:10071", - "electrum2.cipig.net:10071", - "electrum3.cipig.net:10071", - ])); + let json = block_on(enable_electrum( + &mm, + "tQTUM", + false, + &[ + "electrum1.cipig.net:10071", + "electrum2.cipig.net:10071", + "electrum3.cipig.net:10071", + ], + None, + )); println!("{}", json.balance); let rc = block_on(mm.rpc(&json!({ @@ -3516,6 +3764,7 @@ fn test_convert_qrc20_address() { "electrum3.cipig.net:10071", ], "0xba8b71f3544b93e2f681f996da519a98ace0107a", + None, )); // test wallet to contract @@ -3652,7 +3901,7 @@ fn test_validateaddress() { .unwrap(); let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm, ETH_DEV_NODES))); + log!("{:?}", block_on(enable_coins_eth_electrum(&mm, ETH_DEV_NODES, None))); // test valid RICK address @@ -3933,6 +4182,7 @@ fn qrc20_activate_electrum() { "electrum3.cipig.net:10071", ], "0xba8b71f3544b93e2f681f996da519a98ace0107a", + None, )); assert_eq!( electrum_json["address"].as_str(), @@ -3981,6 +4231,7 @@ fn test_qrc20_withdraw() { "electrum3.cipig.net:10071", ], "0xba8b71f3544b93e2f681f996da519a98ace0107a", + None, )); assert_eq!( electrum_json["address"].as_str(), @@ -4061,6 +4312,7 @@ fn test_qrc20_withdraw_error() { "electrum3.cipig.net:10071", ], "0xba8b71f3544b93e2f681f996da519a98ace0107a", + None, )); let balance = electrum_json["balance"].as_str().unwrap(); assert_eq!(balance, "10"); @@ -4134,11 +4386,17 @@ fn test_get_raw_transaction() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); // RICK - let _electrum = block_on(enable_electrum(&mm, "RICK", false, &[ - "electrum3.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum1.cipig.net:10017", - ])); + let _electrum = block_on(enable_electrum( + &mm, + "RICK", + false, + &[ + "electrum3.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum1.cipig.net:10017", + ], + None, + )); let raw = block_on(mm.rpc(&json! ({ "mmrpc": "2.0", "userpass": mm.userpass, @@ -4602,7 +4860,10 @@ fn test_buy_conf_settings() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); log!("Issue bob buy request"); let rc = block_on(mm_bob.rpc(&json! ({ @@ -4671,7 +4932,10 @@ fn test_buy_response_format() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); log!("Issue bob buy request"); let rc = block_on(mm_bob.rpc(&json! ({ @@ -4718,7 +4982,10 @@ fn test_sell_response_format() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); log!("Issue bob sell request"); let rc = block_on(mm_bob.rpc(&json! ({ @@ -4765,7 +5032,10 @@ fn test_my_orders_response_format() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); log!("Issue bob buy request"); let rc = block_on(mm_bob.rpc(&json! ({ @@ -4851,10 +5121,10 @@ fn test_my_orders_after_matched() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); // Enable coins on Bob side. Print the replies in case we need the address. - let rc = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES)); + let rc = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)); log!("enable_coins (bob): {:?}", rc); // Enable coins on Alice side. Print the replies in case we need the address. - let rc = block_on(enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES)); + let rc = block_on(enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES, None)); log!("enable_coins (alice): {:?}", rc); let rc = block_on(mm_bob.rpc(&json! ({ @@ -4923,7 +5193,10 @@ fn test_sell_conf_settings() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); log!("Issue bob sell request"); let rc = block_on(mm_bob.rpc(&json! ({ @@ -4993,7 +5266,10 @@ fn test_set_price_conf_settings() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); log!("Issue bob sell request"); let rc = block_on(mm_bob.rpc(&json! ({ @@ -5418,10 +5694,10 @@ fn test_update_maker_order_after_matched() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); // Enable coins on Bob side. Print the replies in case we need the address. - let rc = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES)); + let rc = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)); log!("enable_coins (bob): {:?}", rc); // Enable coins on Alice side. Print the replies in case we need the address. - let rc = block_on(enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES)); + let rc = block_on(enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES, None)); log!("enable_coins (alice): {:?}", rc); let rc = block_on(mm_bob.rpc(&json! ({ @@ -5735,7 +6011,10 @@ fn test_sell_min_volume() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); let min_volume: BigDecimal = "0.1".parse().unwrap(); log!("Issue bob ETH/JST sell request"); @@ -5904,7 +6183,10 @@ fn test_buy_min_volume() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES))); + log!( + "{:?}", + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) + ); let min_volume: BigDecimal = "0.1".parse().unwrap(); log!("Issue bob ETH/JST sell request"); @@ -6011,7 +6293,7 @@ fn test_orderbook_depth() { log!("Bob log path: {}", mm_bob.log_path.display()); // Enable coins on Bob side. Print the replies in case we need the "address". - let bob_coins = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES)); + let bob_coins = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)); log!("enable_coins (bob): {:?}", bob_coins); // issue sell request on Bob side by setting base/rel price log!("Issue bob sell requests"); @@ -6134,11 +6416,17 @@ fn test_get_current_mtp() { let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let (_dump_log, _dump_dashboard) = mm.mm_dump(); - let electrum = block_on(enable_electrum(&mm, "KMD", false, &[ - "electrum1.cipig.net:10001", - "electrum2.cipig.net:10001", - "electrum3.cipig.net:10001", - ])); + let electrum = block_on(enable_electrum( + &mm, + "KMD", + false, + &[ + "electrum1.cipig.net:10001", + "electrum2.cipig.net:10001", + "electrum3.cipig.net:10001", + ], + None, + )); log!("{:?}", electrum); let rc = block_on(mm.rpc(&json!({ @@ -6237,6 +6525,36 @@ fn test_get_public_key_hash() { assert_eq!(v.result.public_key_hash, "b506088aa2a3b4bb1da3a29bf00ce1a550ea1df9") } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_get_my_address_hd() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let coins = json!([eth_testnet_conf()]); + + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("log path: {}", mm.log_path.display()); + + let resp = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "get_my_address", + "params": { + "coin": "ETH", + } + }))) + .unwrap(); + + assert_eq!(resp.0, StatusCode::OK); + let my_wallet_address: Json = json::from_str(&resp.1).unwrap(); + assert_eq!( + my_wallet_address["result"]["wallet_address"], + "0x1737F1FaB40c6Fd3dc729B51C0F97DB3297CCA93" + ) +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_get_orderbook_with_same_orderbook_ticker() { @@ -6731,7 +7049,7 @@ fn test_sign_verify_message_eth() { // Enable coins on Bob side. Print the replies in case we need the "address". log!( "enable_coins (bob): {:?}", - block_on(enable_native(&mm_bob, "ETH", ETH_DEV_NODES)) + block_on(enable_native(&mm_bob, "ETH", ETH_DEV_NODES, None)) ); let response = block_on(sign_message(&mm_bob, "ETH")); @@ -6767,8 +7085,8 @@ fn test_no_login() { let (_dump_log, _dump_dashboard) = no_login_node.mm_dump(); log!("log path: {}", no_login_node.log_path.display()); - block_on(enable_electrum_json(&seednode, RICK, false, rick_electrums())); - block_on(enable_electrum_json(&seednode, MORTY, false, morty_electrums())); + block_on(enable_electrum_json(&seednode, RICK, false, rick_electrums(), None)); + block_on(enable_electrum_json(&seednode, MORTY, false, morty_electrums(), None)); let orders = [ // (base, rel, price, volume, min_volume) @@ -7244,7 +7562,7 @@ fn test_tbtc_block_header_sync() { #[test] #[cfg(not(target_arch = "wasm32"))] -fn test_enable_coins_with_hd_account_id() { +fn test_enable_coins_with_enable_hd() { const TX_HISTORY: bool = false; const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; @@ -7256,49 +7574,152 @@ fn test_enable_coins_with_hd_account_id() { btc_segwit_conf(), ]); - let hd_account_id = 0; - let conf_0 = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, hd_account_id, &coins); + let path_to_address = StandardHDCoinAddress { + account: 0, + is_change: false, + address_index: 0, + }; + 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 eth = block_on(enable_native(&mm_hd_0, "ETH", ETH_DEV_NODES)); + let eth = block_on(enable_native( + &mm_hd_0, + "ETH", + ETH_DEV_NODES, + Some(path_to_address.clone()), + )); assert_eq!(eth.address, "0x1737F1FaB40c6Fd3dc729B51C0F97DB3297CCA93"); - let jst = block_on(enable_native(&mm_hd_0, "JST", ETH_DEV_NODES)); + let jst = block_on(enable_native( + &mm_hd_0, + "JST", + ETH_DEV_NODES, + Some(path_to_address.clone()), + )); assert_eq!(jst.address, "0x1737F1FaB40c6Fd3dc729B51C0F97DB3297CCA93"); - let rick = block_on(enable_electrum(&mm_hd_0, "RICK", TX_HISTORY, RICK_ELECTRUM_ADDRS)); + let rick = block_on(enable_electrum( + &mm_hd_0, + "RICK", + TX_HISTORY, + RICK_ELECTRUM_ADDRS, + Some(path_to_address.clone()), + )); assert_eq!(rick.address, "RXNtAyDSsY3DS3VxTpJegzoHU9bUX54j56"); let qrc20 = block_on(enable_qrc20( &mm_hd_0, "QRC20", QRC20_ELECTRUMS, "0xd362e096e873eb7907e205fadc6175c6fec7bc44", + Some(path_to_address.clone()), )); assert_eq!(qrc20["address"].as_str(), Some("qRtCTiPHW9e6zH9NcRhjMVfq7sG37SvgrL")); - let btc_segwit = block_on(enable_electrum(&mm_hd_0, "BTC-segwit", TX_HISTORY, RICK_ELECTRUM_ADDRS)); + let btc_segwit = block_on(enable_electrum( + &mm_hd_0, + "BTC-segwit", + TX_HISTORY, + RICK_ELECTRUM_ADDRS, + Some(path_to_address), + )); assert_eq!(btc_segwit.address, "bc1q6vyur5hjul2m0979aadd6u7ptuj9ac4gt0ha0c"); - let hd_account_id = 1; - let conf_1 = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, hd_account_id, &coins); + let path_to_address = StandardHDCoinAddress { + account: 0, + is_change: false, + address_index: 1, + }; + let conf_1 = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); let mm_hd_1 = MarketMakerIt::start(conf_1.conf, conf_1.rpc_password, None).unwrap(); let (_dump_log, _dump_dashboard) = mm_hd_1.mm_dump(); log!("log path: {}", mm_hd_1.log_path.display()); - let eth = block_on(enable_native(&mm_hd_1, "ETH", ETH_DEV_NODES)); + let eth = block_on(enable_native( + &mm_hd_1, + "ETH", + ETH_DEV_NODES, + Some(path_to_address.clone()), + )); assert_eq!(eth.address, "0xDe841899aB4A22E23dB21634e54920aDec402397"); - let jst = block_on(enable_native(&mm_hd_1, "JST", ETH_DEV_NODES)); + let jst = block_on(enable_native( + &mm_hd_1, + "JST", + ETH_DEV_NODES, + Some(path_to_address.clone()), + )); assert_eq!(jst.address, "0xDe841899aB4A22E23dB21634e54920aDec402397"); - let rick = block_on(enable_electrum(&mm_hd_1, "RICK", TX_HISTORY, RICK_ELECTRUM_ADDRS)); + let rick = block_on(enable_electrum( + &mm_hd_1, + "RICK", + TX_HISTORY, + RICK_ELECTRUM_ADDRS, + Some(path_to_address.clone()), + )); assert_eq!(rick.address, "RVyndZp3ZrhGKSwHryyM3Kcz9aq2EJrW1z"); let qrc20 = block_on(enable_qrc20( &mm_hd_1, "QRC20", QRC20_ELECTRUMS, "0xd362e096e873eb7907e205fadc6175c6fec7bc44", + Some(path_to_address.clone()), )); assert_eq!(qrc20["address"].as_str(), Some("qY8FNq2ZDUh52BjNvaroFoeHdr3AAhqsxW")); - let btc_segwit = block_on(enable_electrum(&mm_hd_1, "BTC-segwit", TX_HISTORY, RICK_ELECTRUM_ADDRS)); + let btc_segwit = block_on(enable_electrum( + &mm_hd_1, + "BTC-segwit", + TX_HISTORY, + RICK_ELECTRUM_ADDRS, + Some(path_to_address), + )); assert_eq!(btc_segwit.address, "bc1q6kxcwcrsm5z8pe940xxu294q7588mqvarttxcx"); + + let path_to_address = StandardHDCoinAddress { + account: 77, + is_change: false, + address_index: 7, + }; + let conf_1 = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd_1 = MarketMakerIt::start(conf_1.conf, conf_1.rpc_password, None).unwrap(); + let (_dump_log, _dump_dashboard) = mm_hd_1.mm_dump(); + log!("log path: {}", mm_hd_1.log_path.display()); + + let eth = block_on(enable_native( + &mm_hd_1, + "ETH", + ETH_DEV_NODES, + Some(path_to_address.clone()), + )); + assert_eq!(eth.address, "0xa420a4DBd8C50e6240014Db4587d2ec8D0cE0e6B"); + let jst = block_on(enable_native( + &mm_hd_1, + "JST", + ETH_DEV_NODES, + Some(path_to_address.clone()), + )); + assert_eq!(jst.address, "0xa420a4DBd8C50e6240014Db4587d2ec8D0cE0e6B"); + let rick = block_on(enable_electrum( + &mm_hd_1, + "RICK", + TX_HISTORY, + RICK_ELECTRUM_ADDRS, + Some(path_to_address.clone()), + )); + assert_eq!(rick.address, "RLNu8gszQ8ENUrY3VSyBS2714CNVwn1f7P"); + let qrc20 = block_on(enable_qrc20( + &mm_hd_1, + "QRC20", + QRC20_ELECTRUMS, + "0xd362e096e873eb7907e205fadc6175c6fec7bc44", + Some(path_to_address.clone()), + )); + assert_eq!(qrc20["address"].as_str(), Some("qREuDjyn7dzUPgnCkxPvALz9Szgy7diB5w")); + let btc_segwit = block_on(enable_electrum( + &mm_hd_1, + "BTC-segwit", + TX_HISTORY, + RICK_ELECTRUM_ADDRS, + Some(path_to_address), + )); + assert_eq!(btc_segwit.address, "bc1q0dxnd7afj997a40j86a8a6dq3xs3dwm7rkzams"); } /// `shared_db_id` must be the same for Iguana and all HD accounts derived from the same passphrase. @@ -7311,8 +7732,8 @@ fn test_get_shared_db_id() { let coins = json!([rick_conf()]); let confs = vec![ Mm2TestConf::seednode(PASSPHRASE, &coins), - Mm2TestConf::seednode_with_hd_account(PASSPHRASE, 0, &coins), - Mm2TestConf::seednode_with_hd_account(PASSPHRASE, 1, &coins), + Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins), + Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins), ]; let mut shared_db_id = None; 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 8accb1beba..b221a264c1 100644 --- a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs @@ -897,16 +897,28 @@ fn orderbook_extended_data() { .unwrap(); let (_dump_log, _dump_dashboard) = &mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - block_on(enable_electrum(&mm, "RICK", false, &[ - "electrum3.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum1.cipig.net:10017", - ])); - block_on(enable_electrum(&mm, "MORTY", false, &[ - "electrum3.cipig.net:10018", - "electrum2.cipig.net:10018", - "electrum1.cipig.net:10018", - ])); + block_on(enable_electrum( + &mm, + "RICK", + false, + &[ + "electrum3.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum1.cipig.net:10017", + ], + None, + )); + block_on(enable_electrum( + &mm, + "MORTY", + false, + &[ + "electrum3.cipig.net:10018", + "electrum2.cipig.net:10018", + "electrum1.cipig.net:10018", + ], + None, + )); let bob_orders = &[ // (base, rel, price, volume) @@ -1017,16 +1029,28 @@ fn orderbook_should_display_base_rel_volumes() { .unwrap(); let (_dump_log, _dump_dashboard) = &mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - block_on(enable_electrum(&mm, "RICK", false, &[ - "electrum3.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum1.cipig.net:10017", - ])); - block_on(enable_electrum(&mm, "MORTY", false, &[ - "electrum3.cipig.net:10018", - "electrum2.cipig.net:10018", - "electrum1.cipig.net:10018", - ])); + block_on(enable_electrum( + &mm, + "RICK", + false, + &[ + "electrum3.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum1.cipig.net:10017", + ], + None, + )); + block_on(enable_electrum( + &mm, + "MORTY", + false, + &[ + "electrum3.cipig.net:10018", + "electrum2.cipig.net:10018", + "electrum1.cipig.net:10018", + ], + None, + )); let price = BigRational::new(2.into(), 1.into()); let volume = BigRational::new(1.into(), 1.into()); @@ -1147,7 +1171,7 @@ fn orderbook_should_work_without_coins_activation() { log!( "enable_coins (bob): {:?}", - block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES)) + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) ); let rc = block_on(mm_bob.rpc(&json!({ @@ -1204,16 +1228,28 @@ fn test_all_orders_per_pair_per_node_must_be_displayed_in_orderbook() { .unwrap(); let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - block_on(enable_electrum(&mm, "RICK", false, &[ - "electrum3.cipig.net:10017", - "electrum2.cipig.net:10017", - "electrum1.cipig.net:10017", - ])); - block_on(enable_electrum(&mm, "MORTY", false, &[ - "electrum3.cipig.net:10018", - "electrum2.cipig.net:10018", - "electrum1.cipig.net:10018", - ])); + block_on(enable_electrum( + &mm, + "RICK", + false, + &[ + "electrum3.cipig.net:10017", + "electrum2.cipig.net:10017", + "electrum1.cipig.net:10017", + ], + None, + )); + block_on(enable_electrum( + &mm, + "MORTY", + false, + &[ + "electrum3.cipig.net:10018", + "electrum2.cipig.net:10018", + "electrum1.cipig.net:10018", + ], + None, + )); // set 2 orders with different prices let rc = block_on(mm.rpc(&json!({ @@ -1307,11 +1343,11 @@ fn setprice_min_volume_should_be_displayed_in_orderbook() { log!( "enable_coins (bob): {:?}", - block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES)) + block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES, None)) ); log!( "enable_coins (alice): {:?}", - block_on(enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES)) + block_on(enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES, None)) ); // issue orderbook call on Alice side to trigger subscription to a topic @@ -1394,12 +1430,13 @@ fn zhtlc_orders_sync_alice_connected_before_creation() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - block_on(enable_electrum_json(&mm_bob, RICK, false, rick_electrums())); + block_on(enable_electrum_json(&mm_bob, RICK, false, rick_electrums(), None)); block_on(enable_z_coin_light( &mm_bob, ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, )); let set_price_json = json!({ @@ -1456,12 +1493,13 @@ fn zhtlc_orders_sync_alice_connected_after_creation() { let (_dump_log, _dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - block_on(enable_electrum_json(&mm_bob, "RICK", false, rick_electrums())); + block_on(enable_electrum_json(&mm_bob, "RICK", false, rick_electrums(), None)); block_on(enable_z_coin_light( &mm_bob, ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, )); let set_price_json = json!({ @@ -1484,12 +1522,13 @@ fn zhtlc_orders_sync_alice_connected_after_creation() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - block_on(enable_electrum_json(&mm_alice, RICK, false, rick_electrums())); + block_on(enable_electrum_json(&mm_alice, RICK, false, rick_electrums(), None)); block_on(enable_z_coin_light( &mm_alice, ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, )); let set_price_json = json!({ diff --git a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs index 05e89f74fb..07fad8599f 100644 --- a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs @@ -1,4 +1,5 @@ use common::block_on; +use crypto::StandardHDCoinAddress; use mm2_number::BigDecimal; use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_err, enable_tendermint, enable_tendermint_token, enable_tendermint_without_balance, @@ -16,6 +17,9 @@ const ATOM_TENDERMINT_RPC_URLS: &[&str] = &["https://rpc.sentry-02.theta-testnet const IRIS_TEST_SEED: &str = "iris test seed"; const IRIS_TESTNET_RPC_URLS: &[&str] = &["http://34.80.202.172:26657"]; +const TENDERMINT_TEST_BIP39_SEED: &str = + "emerge canoe salmon dolphin glow priority random become gasp sell blade argue"; + #[test] fn test_tendermint_activation_and_balance() { let coins = json!([atom_testnet_conf()]); @@ -66,6 +70,27 @@ fn test_tendermint_activation_without_balance() { assert!(result.result.tokens_tickers.unwrap().is_empty()); } +#[test] +fn test_tendermint_hd_address() { + let coins = json!([atom_testnet_conf()]); + // Default address m/44'/118'/0'/0/0 when no path_to_address is specified in activation request + let expected_address = "cosmos1nv4mqaky7n7rqjhch7829kgypx5s8fh62wdtr8"; + + let conf = Mm2TestConf::seednode_with_hd_account(TENDERMINT_TEST_BIP39_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_result = block_on(enable_tendermint( + &mm, + ATOM_TICKER, + &[], + ATOM_TENDERMINT_RPC_URLS, + false, + )); + + let result: RpcV2Response = json::from_value(activation_result).unwrap(); + assert_eq!(result.result.address, expected_address); +} + #[test] fn test_tendermint_withdraw() { let coins = json!([atom_testnet_conf()]); @@ -88,6 +113,7 @@ fn test_tendermint_withdraw() { ATOM_TICKER, "cosmos1svaw0aqc4584x825ju7ua03g5xtxwd0ahl86hz", "0.1", + None, )); println!("Withdraw to other {}", json::to_string(&tx_details).unwrap()); // TODO how to check it if the fee is dynamic? @@ -111,6 +137,7 @@ fn test_tendermint_withdraw() { ATOM_TICKER, "cosmos1w5h6wud7a8zpa539rc99ehgl9gwkad3wjsjq8v", "0.1", + None, )); println!("Withdraw to self {}", json::to_string(&tx_details).unwrap()); @@ -137,11 +164,93 @@ fn test_tendermint_withdraw() { ATOM_TICKER, "cosmos1w5h6wud7a8zpa539rc99ehgl9gwkad3wjsjq8v", "0.1", + None, )); let send_raw_tx = block_on(send_raw_transaction(&mm, ATOM_TICKER, &tx_details.tx_hex)); println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); } +#[test] +fn test_tendermint_withdraw_hd() { + let coins = json!([iris_testnet_conf()]); + let coin = coins[0]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode_with_hd_account(TENDERMINT_TEST_BIP39_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_tendermint(&mm, coin, &[], IRIS_TESTNET_RPC_URLS, false)); + println!("Activation with assets {}", json::to_string(&activation_res).unwrap()); + + // We will withdraw from HD account 0 and change 0 and address_index 1 + let path_to_address = StandardHDCoinAddress { + account: 0, + is_change: false, + address_index: 1, + }; + + // just call withdraw without sending to check response correctness + let tx_details = block_on(withdraw_v1( + &mm, + coin, + "iaa1llp0f6qxemgh4g4m5ewk0ew0hxj76avuz8kwd5", + "0.1", + Some(path_to_address.clone()), + )); + println!("Withdraw to other {}", json::to_string(&tx_details).unwrap()); + // TODO how to check it if the fee is dynamic? + /* + let expected_total: BigDecimal = "0.15".parse().unwrap(); + assert_eq!(tx_details.total_amount, expected_total); + assert_eq!(tx_details.spent_by_me, expected_total); + assert_eq!(tx_details.my_balance_change, expected_total * BigDecimal::from(-1)); + */ + assert_eq!(tx_details.received_by_me, BigDecimal::default()); + assert_eq!(tx_details.to, vec![ + "iaa1llp0f6qxemgh4g4m5ewk0ew0hxj76avuz8kwd5".to_owned() + ]); + assert_eq!(tx_details.from, vec![ + "iaa1tpd0um0r3z0y88p3gkv3y38dq8lmqc2xs9u0pv".to_owned() + ]); + + // withdraw and send transaction to ourselves + let tx_details = block_on(withdraw_v1( + &mm, + coin, + "iaa1tpd0um0r3z0y88p3gkv3y38dq8lmqc2xs9u0pv", + "0.1", + Some(path_to_address.clone()), + )); + println!("Withdraw to self {}", json::to_string(&tx_details).unwrap()); + + // TODO how to check it if the fee is dynamic? + /* + let expected_total: BigDecimal = "0.15".parse().unwrap(); + let expected_balance_change: BigDecimal = "-0.05".parse().unwrap(); + assert_eq!(tx_details.total_amount, expected_total); + assert_eq!(tx_details.spent_by_me, expected_total); + assert_eq!(tx_details.my_balance_change, expected_balance_change); + */ + let expected_received: BigDecimal = "0.1".parse().unwrap(); + assert_eq!(tx_details.received_by_me, expected_received); + + assert_eq!(tx_details.to, vec![ + "iaa1tpd0um0r3z0y88p3gkv3y38dq8lmqc2xs9u0pv".to_owned() + ]); + assert_eq!(tx_details.from, vec![ + "iaa1tpd0um0r3z0y88p3gkv3y38dq8lmqc2xs9u0pv".to_owned() + ]); + + let tx_details = block_on(withdraw_v1( + &mm, + coin, + "iaa1tpd0um0r3z0y88p3gkv3y38dq8lmqc2xs9u0pv", + "0.1", + Some(path_to_address), + )); + let send_raw_tx = block_on(send_raw_transaction(&mm, coin, &tx_details.tx_hex)); + println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); +} + #[test] fn test_custom_gas_limit_on_tendermint_withdraw() { let coins = json!([atom_testnet_conf()]); @@ -198,7 +307,14 @@ fn test_tendermint_ibc_withdraw() { let activation_res = block_on(enable_tendermint_token(&mm, token)); println!("Token activation {}", json::to_string(&activation_res).unwrap()); - let tx_details = block_on(ibc_withdraw(&mm, IBC_SOURCE_CHANNEL, token, IBC_TARGET_ADDRESS, "0.1")); + let tx_details = block_on(ibc_withdraw( + &mm, + IBC_SOURCE_CHANNEL, + token, + IBC_TARGET_ADDRESS, + "0.1", + None, + )); println!("IBC transfer to atom address {}", json::to_string(&tx_details).unwrap()); let expected_spent: BigDecimal = "0.1".parse().unwrap(); @@ -207,10 +323,65 @@ fn test_tendermint_ibc_withdraw() { assert_eq!(tx_details.to, vec![IBC_TARGET_ADDRESS.to_owned()]); assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); - let tx_details = block_on(ibc_withdraw(&mm, IBC_SOURCE_CHANNEL, token, IBC_TARGET_ADDRESS, "0.1")); + let tx_details = block_on(ibc_withdraw( + &mm, + IBC_SOURCE_CHANNEL, + token, + IBC_TARGET_ADDRESS, + "0.1", + None, + )); let send_raw_tx = block_on(send_raw_transaction(&mm, token, &tx_details.tx_hex)); println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); } +#[test] +fn test_tendermint_ibc_withdraw_hd() { + // visit `{rpc_url}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels + const IBC_SOURCE_CHANNEL: &str = "channel-93"; + + const IBC_TARGET_ADDRESS: &str = "cosmos1r5v5srda7xfth3hn2s26txvrcrntldjumt8mhl"; + const MY_ADDRESS: &str = "iaa1tpd0um0r3z0y88p3gkv3y38dq8lmqc2xs9u0pv"; + + let coins = json!([iris_testnet_conf()]); + let coin = coins[0]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode_with_hd_account(TENDERMINT_TEST_BIP39_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_tendermint(&mm, coin, &[], IRIS_TESTNET_RPC_URLS, false)); + println!("Activation with assets {}", json::to_string(&activation_res).unwrap()); + + // We will withdraw from HD account 0 and change 0 and address_index 1 + let path_to_address = StandardHDCoinAddress { + account: 0, + is_change: false, + address_index: 1, + }; + + let tx_details = block_on(ibc_withdraw( + &mm, + IBC_SOURCE_CHANNEL, + coin, + IBC_TARGET_ADDRESS, + "0.1", + Some(path_to_address.clone()), + )); + println!("IBC transfer to atom address {}", json::to_string(&tx_details).unwrap()); + + assert_eq!(tx_details.to, vec![IBC_TARGET_ADDRESS.to_owned()]); + assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); + + let tx_details = block_on(ibc_withdraw( + &mm, + IBC_SOURCE_CHANNEL, + coin, + IBC_TARGET_ADDRESS, + "0.1", + Some(path_to_address), + )); + let send_raw_tx = block_on(send_raw_transaction(&mm, coin, &tx_details.tx_hex)); + println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); +} #[test] fn test_tendermint_token_activation_and_withdraw() { @@ -233,6 +404,7 @@ fn test_tendermint_token_activation_and_withdraw() { token, "iaa1llp0f6qxemgh4g4m5ewk0ew0hxj76avuz8kwd5", "0.1", + None, )); println!("Withdraw to other {}", json::to_string(&tx_details).unwrap()); @@ -263,6 +435,7 @@ fn test_tendermint_token_activation_and_withdraw() { token, "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2", "0.1", + None, )); println!("Withdraw to self {}", json::to_string(&tx_details).unwrap()); @@ -292,6 +465,7 @@ fn test_tendermint_token_activation_and_withdraw() { token, "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2", "0.1", + None, )); let send_raw_tx = block_on(send_raw_transaction(&mm, token, &tx_details.tx_hex)); println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); diff --git a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs index c2884dd875..3c7836108a 100644 --- a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs @@ -60,6 +60,7 @@ fn activate_z_coin_light() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, )); let balance = match activation_result.wallet_balance { @@ -74,8 +75,7 @@ fn activate_z_coin_light() { fn activate_z_coin_with_hd_account() { let coins = json!([zombie_conf()]); - let hd_account_id = 0; - let conf = Mm2TestConf::seednode_with_hd_account(ZOMBIE_TEST_BIP39_ACTIVATION_SEED, hd_account_id, &coins); + let conf = Mm2TestConf::seednode_with_hd_account(ZOMBIE_TEST_BIP39_ACTIVATION_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let activation_result = block_on(enable_z_coin_light( @@ -83,6 +83,7 @@ fn activate_z_coin_with_hd_account() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + Some(0), )); let actual = match activation_result.wallet_balance { @@ -109,6 +110,7 @@ fn test_z_coin_tx_history() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, )); let tx_history = block_on(z_coin_tx_history(&mm, ZOMBIE_TICKER, 5, None)); @@ -352,6 +354,7 @@ fn withdraw_z_coin_light() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, )); println!("{:?}", activation_result); @@ -393,11 +396,12 @@ fn trade_rick_zombie_light() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, )); println!("Bob ZOMBIE activation {:?}", zombie_activation); - let rick_activation = block_on(enable_electrum_json(&mm_bob, RICK, false, rick_electrums())); + let rick_activation = block_on(enable_electrum_json(&mm_bob, RICK, false, rick_electrums(), None)); println!("Bob RICK activation {:?}", rick_activation); @@ -425,11 +429,12 @@ fn trade_rick_zombie_light() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, )); println!("Alice ZOMBIE activation {:?}", zombie_activation); - let rick_activation = block_on(enable_electrum_json(&mm_alice, RICK, false, rick_electrums())); + let rick_activation = block_on(enable_electrum_json(&mm_alice, RICK, false, rick_electrums(), None)); println!("Alice RICK activation {:?}", rick_activation); @@ -481,6 +486,7 @@ fn activate_pirate_light() { ARRR, PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, + None, )); let balance = match activation_result.wallet_balance { diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 4004cde892..8376afa8a8 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -7,7 +7,7 @@ use common::executor::Timer; use common::log::debug; use common::{cfg_native, now_float, now_ms, repeatable, wait_until_ms, PagingOptionsEnum}; use common::{get_utc_timestamp, log}; -use crypto::CryptoCtx; +use crypto::{CryptoCtx, StandardHDCoinAddress}; use gstuff::{try_s, ERR, ERRL}; use http::{HeaderMap, StatusCode}; use lazy_static::lazy_static; @@ -191,7 +191,7 @@ impl Mm2TestConf { } } - pub fn seednode_with_hd_account(passphrase: &str, hd_account_id: u32, coins: &Json) -> Self { + pub fn seednode_with_hd_account(passphrase: &str, coins: &Json) -> Self { Mm2TestConf { conf: json!({ "gui": "nogui", @@ -200,7 +200,7 @@ impl Mm2TestConf { "coins": coins, "rpc_password": DEFAULT_RPC_PASSWORD, "i_am_seed": true, - "hd_account_id": hd_account_id, + "enable_hd": true, }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -236,7 +236,7 @@ impl Mm2TestConf { } } - pub fn light_node_with_hd_account(passphrase: &str, hd_account_id: u32, coins: &Json, seednodes: &[&str]) -> Self { + pub fn light_node_with_hd_account(passphrase: &str, coins: &Json, seednodes: &[&str]) -> Self { Mm2TestConf { conf: json!({ "gui": "nogui", @@ -245,7 +245,7 @@ impl Mm2TestConf { "coins": coins, "rpc_password": DEFAULT_RPC_PASSWORD, "seednodes": seednodes, - "hd_account_id": hd_account_id, + "enable_hd": true, }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -275,26 +275,26 @@ impl Mm2TestConfForSwap { const ALICE_HD_PASSPHRASE: &'static str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; - pub fn bob_conf_with_policy(priv_key_policy: Mm2InitPrivKeyPolicy, coins: &Json) -> Mm2TestConf { + pub fn bob_conf_with_policy(priv_key_policy: &Mm2InitPrivKeyPolicy, coins: &Json) -> Mm2TestConf { match priv_key_policy { Mm2InitPrivKeyPolicy::Iguana => { let bob_passphrase = crate::get_passphrase!(".env.seed", "BOB_PASSPHRASE").unwrap(); Mm2TestConf::seednode(&bob_passphrase, coins) }, - Mm2InitPrivKeyPolicy::GlobalHDAccount(hd_account_id) => { - Mm2TestConf::seednode_with_hd_account(Self::BOB_HD_PASSPHRASE, hd_account_id, coins) + Mm2InitPrivKeyPolicy::GlobalHDAccount => { + Mm2TestConf::seednode_with_hd_account(Self::BOB_HD_PASSPHRASE, coins) }, } } - pub fn alice_conf_with_policy(priv_key_policy: Mm2InitPrivKeyPolicy, coins: &Json, bob_ip: &str) -> Mm2TestConf { + pub fn alice_conf_with_policy(priv_key_policy: &Mm2InitPrivKeyPolicy, coins: &Json, bob_ip: &str) -> Mm2TestConf { match priv_key_policy { Mm2InitPrivKeyPolicy::Iguana => { let alice_passphrase = crate::get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); Mm2TestConf::light_node(&alice_passphrase, coins, &[bob_ip]) }, - Mm2InitPrivKeyPolicy::GlobalHDAccount(hd_account_id) => { - Mm2TestConf::light_node_with_hd_account(Self::ALICE_HD_PASSPHRASE, hd_account_id, coins, &[bob_ip]) + Mm2InitPrivKeyPolicy::GlobalHDAccount => { + Mm2TestConf::light_node_with_hd_account(Self::ALICE_HD_PASSPHRASE, coins, &[bob_ip]) }, } } @@ -302,7 +302,7 @@ impl Mm2TestConfForSwap { pub enum Mm2InitPrivKeyPolicy { Iguana, - GlobalHDAccount(u32), + GlobalHDAccount, } pub fn zombie_conf() -> Json { @@ -455,7 +455,8 @@ pub fn atom_testnet_conf() -> Json { "account_prefix": "cosmos", "chain_id": "theta-testnet-001", }, - } + }, + "derivation_path": "m/44'/118'", }) } @@ -579,6 +580,7 @@ pub fn tbtc_segwit_conf() -> Json { "txfee": 0, "estimate_fee_mode": "ECONOMICAL", "required_confirmations": 0, + "derivation_path": "m/84'/1'", "address_format": { "format": "segwit" }, @@ -625,6 +627,7 @@ pub fn eth_testnet_conf() -> Json { json!({ "coin": "ETH", "name": "ethereum", + "mm2": 1, "derivation_path": "m/44'/60'", "protocol": { "type": "ETH" @@ -1466,14 +1469,26 @@ pub fn mm_spat() -> (&'static str, MarketMakerIt, RaiiDump, RaiiDump) { /// Asks MM to enable the given currency in electrum mode /// fresh list of servers at https://github.com/jl777/coins/blob/master/electrums/. -pub async fn enable_electrum(mm: &MarketMakerIt, coin: &str, tx_history: bool, urls: &[&str]) -> Json { +pub async fn enable_electrum( + mm: &MarketMakerIt, + coin: &str, + tx_history: bool, + urls: &[&str], + path_to_address: Option, +) -> Json { let servers = urls.iter().map(|url| json!({ "url": url })).collect(); - enable_electrum_json(mm, coin, tx_history, servers).await + enable_electrum_json(mm, coin, tx_history, servers, path_to_address).await } /// Asks MM to enable the given currency in electrum mode /// fresh list of servers at https://github.com/jl777/coins/blob/master/electrums/. -pub async fn enable_electrum_json(mm: &MarketMakerIt, coin: &str, tx_history: bool, servers: Vec) -> Json { +pub async fn enable_electrum_json( + mm: &MarketMakerIt, + coin: &str, + tx_history: bool, + servers: Vec, + path_to_address: Option, +) -> Json { let electrum = mm .rpc(&json!({ "userpass": mm.userpass, @@ -1482,6 +1497,7 @@ pub async fn enable_electrum_json(mm: &MarketMakerIt, coin: &str, tx_history: bo "servers": servers, "mm2": 1, "tx_history": tx_history, + "path_to_address": path_to_address.unwrap_or_default(), })) .await .unwrap(); @@ -1495,7 +1511,13 @@ pub async fn enable_electrum_json(mm: &MarketMakerIt, coin: &str, tx_history: bo json::from_str(&electrum.1).unwrap() } -pub async fn enable_qrc20(mm: &MarketMakerIt, coin: &str, urls: &[&str], swap_contract_address: &str) -> Json { +pub async fn enable_qrc20( + mm: &MarketMakerIt, + coin: &str, + urls: &[&str], + swap_contract_address: &str, + path_to_address: Option, +) -> Json { let servers: Vec<_> = urls.iter().map(|url| json!({ "url": url })).collect(); let electrum = mm .rpc(&json!({ @@ -1505,6 +1527,7 @@ pub async fn enable_qrc20(mm: &MarketMakerIt, coin: &str, urls: &[&str], swap_co "servers": servers, "mm2": 1, "swap_contract_address": swap_contract_address, + "path_to_address": path_to_address.unwrap_or_default(), })) .await .unwrap(); @@ -1573,7 +1596,12 @@ pub fn get_passphrase(path: &dyn AsRef, env: &str) -> Result Json { +pub async fn enable_native( + mm: &MarketMakerIt, + coin: &str, + urls: &[&str], + path_to_address: Option, +) -> Json { let native = mm .rpc(&json!({ "userpass": mm.userpass, @@ -1582,6 +1610,7 @@ pub async fn enable_native(mm: &MarketMakerIt, coin: &str, urls: &[&str]) -> Jso "urls": urls, // Dev chain swap contract address "swap_contract_address": ETH_DEV_SWAP_CONTRACT, + "path_to_address": path_to_address.unwrap_or_default(), "mm2": 1, })) .await @@ -1697,6 +1726,7 @@ pub async fn enable_bch_with_tokens( tokens: &[&str], mode: UtxoRpcMode, tx_history: bool, + path_to_address: Option, ) -> Json { let slp_requests: Vec<_> = tokens.iter().map(|ticker| json!({ "ticker": ticker })).collect(); @@ -1712,6 +1742,7 @@ pub async fn enable_bch_with_tokens( "mode": mode, "tx_history": tx_history, "slp_tokens_requests": slp_requests, + "path_to_address": path_to_address.unwrap_or_default(), } })) .await @@ -2200,7 +2231,13 @@ pub async fn init_withdraw(mm: &MarketMakerIt, coin: &str, to: &str, amount: &st json::from_str(&request.1).unwrap() } -pub async fn withdraw_v1(mm: &MarketMakerIt, coin: &str, to: &str, amount: &str) -> TransactionDetails { +pub async fn withdraw_v1( + mm: &MarketMakerIt, + coin: &str, + to: &str, + amount: &str, + from: Option, +) -> TransactionDetails { let request = mm .rpc(&json!({ "userpass": mm.userpass, @@ -2208,6 +2245,7 @@ pub async fn withdraw_v1(mm: &MarketMakerIt, coin: &str, to: &str, amount: &str) "coin": coin, "to": to, "amount": amount, + "from": from, })) .await .unwrap(); @@ -2221,6 +2259,7 @@ pub async fn ibc_withdraw( coin: &str, to: &str, amount: &str, + from: Option, ) -> TransactionDetails { let request = mm .rpc(&json!({ @@ -2231,7 +2270,8 @@ pub async fn ibc_withdraw( "ibc_source_channel": source_channel, "coin": coin, "to": to, - "amount": amount + "amount": amount, + "from": from, } })) .await @@ -2289,7 +2329,13 @@ pub async fn init_z_coin_native(mm: &MarketMakerIt, coin: &str) -> Json { json::from_str(&request.1).unwrap() } -pub async fn init_z_coin_light(mm: &MarketMakerIt, coin: &str, electrums: &[&str], lightwalletd_urls: &[&str]) -> Json { +pub async fn init_z_coin_light( + mm: &MarketMakerIt, + coin: &str, + electrums: &[&str], + lightwalletd_urls: &[&str], + account: Option, +) -> Json { let request = mm .rpc(&json!({ "userpass": mm.userpass, @@ -2304,7 +2350,8 @@ pub async fn init_z_coin_light(mm: &MarketMakerIt, coin: &str, electrums: &[&str "electrum_servers": electrum_servers_rpc(electrums), "light_wallet_d_servers": lightwalletd_urls, }, - } + }, + "account": account.unwrap_or_default(), }, } })) From 9a7174443f0e677036c2d84eae9438b61ef93d0c Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Mon, 4 Sep 2023 11:13:41 +0300 Subject: [PATCH 14/17] fix(tests): ignore failing tests due to BCHD (#1955) --- mm2src/coins/utxo/bchd_grpc.rs | 37 ++++++++++++++++++++-------------- mm2src/coins/utxo/slp.rs | 3 +++ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/mm2src/coins/utxo/bchd_grpc.rs b/mm2src/coins/utxo/bchd_grpc.rs index d07abd09ad..6017f3b9c0 100644 --- a/mm2src/coins/utxo/bchd_grpc.rs +++ b/mm2src/coins/utxo/bchd_grpc.rs @@ -247,6 +247,7 @@ mod bchd_grpc_tests { use mm2_test_helpers::for_tests::BCHD_TESTNET_URLS; #[test] + #[ignore] fn test_validate_slp_utxos_valid() { let tx_hash = H256::from_reversed_str("0ba1b91abbfceaa0777424165edb2928dace87d59669c913989950da31968032"); @@ -280,6 +281,7 @@ mod bchd_grpc_tests { } #[test] + #[ignore] fn test_validate_slp_utxos_non_slp_input() { let tx_hash = H256::from_reversed_str("0ba1b91abbfceaa0777424165edb2928dace87d59669c913989950da31968032"); @@ -328,6 +330,7 @@ mod bchd_grpc_tests { } #[test] + #[ignore] fn test_validate_slp_utxos_invalid_amount() { let tx_hash = H256::from_reversed_str("0ba1b91abbfceaa0777424165edb2928dace87d59669c913989950da31968032"); let invalid_utxo = SlpUnspent { @@ -370,6 +373,7 @@ mod bchd_grpc_tests { } #[test] + #[ignore] fn test_validate_slp_utxos_unexpected_token_id() { let tx_hash = H256::from_reversed_str("0ba1b91abbfceaa0777424165edb2928dace87d59669c913989950da31968032"); @@ -411,6 +415,7 @@ mod bchd_grpc_tests { } #[test] + #[ignore] fn test_check_slp_transaction_valid() { // https://testnet.simpleledger.info/tx/c5f46ccc5431687154335d5b6526f1b9cfa961c44b97956b7bec77f884f56c73 let tx = hex::decode("010000000232809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b020000006a473044022057c88d815fa563eda8ef7d0dd5c522f4501ffa6110df455b151b31609f149c22022048fecfc9b16e983fbfd05b0d2b7c011c3dbec542577fa00cd9bd192b81961f8e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff32809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b030000006a4730440220539e1204d2805c0474111a1f233ff82c0ab06e6e2bfc0cbe4975eacae64a0b1f02200ec83d32c2180f5567d0f760e85f1efc99d9341cfebd86c9a334310f6d4381494121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000002326e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f694801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8983d460").unwrap(); @@ -418,6 +423,7 @@ mod bchd_grpc_tests { } #[test] + #[ignore] fn test_check_slp_transaction_invalid() { // https://www.blockchain.com/bch-testnet/tx/d76723c092b64bc598d5d2ceafd6f0db37dce4032db569d6f26afb35491789a7 let tx = hex::decode("010000000190e35c09c83b5818b441c18a2d5ec54734851e5581fb21bde7936e77c6c3dca8030000006b483045022100e6b1415cbd81f2d04360597fba65965bc77ab5a972f5b8f8d5c0f1b1912923c402206a63f305f03e9c49ffba6c71c7a76ef60631f67dce7631f673a0e8485b86898d4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff020000000000000000376a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb70800000000000003e82500ae00000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac62715161").unwrap(); @@ -431,18 +437,19 @@ mod bchd_grpc_tests { } } -#[cfg(target_arch = "wasm32")] -mod wasm_tests { - use super::*; - use mm2_test_helpers::for_tests::BCHD_TESTNET_URLS; - use wasm_bindgen_test::*; - - wasm_bindgen_test_configure!(run_in_browser); - - #[wasm_bindgen_test] - async fn test_check_slp_transaction_valid() { - // https://testnet.simpleledger.info/tx/c5f46ccc5431687154335d5b6526f1b9cfa961c44b97956b7bec77f884f56c73 - let tx = hex::decode("010000000232809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b020000006a473044022057c88d815fa563eda8ef7d0dd5c522f4501ffa6110df455b151b31609f149c22022048fecfc9b16e983fbfd05b0d2b7c011c3dbec542577fa00cd9bd192b81961f8e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff32809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b030000006a4730440220539e1204d2805c0474111a1f233ff82c0ab06e6e2bfc0cbe4975eacae64a0b1f02200ec83d32c2180f5567d0f760e85f1efc99d9341cfebd86c9a334310f6d4381494121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000002326e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f694801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8983d460").unwrap(); - check_slp_transaction(BCHD_TESTNET_URLS, tx).await.unwrap(); - } -} +// Todo: once BCHD issues are solved or we use another node implementation for SLP validation, we can re-enable this test. +// #[cfg(target_arch = "wasm32")] +// mod wasm_tests { +// use super::*; +// use mm2_test_helpers::for_tests::BCHD_TESTNET_URLS; +// use wasm_bindgen_test::*; +// +// wasm_bindgen_test_configure!(run_in_browser); +// +// #[wasm_bindgen_test] +// async fn test_check_slp_transaction_valid() { +// // https://testnet.simpleledger.info/tx/c5f46ccc5431687154335d5b6526f1b9cfa961c44b97956b7bec77f884f56c73 +// let tx = hex::decode("010000000232809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b020000006a473044022057c88d815fa563eda8ef7d0dd5c522f4501ffa6110df455b151b31609f149c22022048fecfc9b16e983fbfd05b0d2b7c011c3dbec542577fa00cd9bd192b81961f8e4121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff32809631da50999813c96996d587ceda2829db5e16247477a0eafcbb1ab9a10b030000006a4730440220539e1204d2805c0474111a1f233ff82c0ab06e6e2bfc0cbe4975eacae64a0b1f02200ec83d32c2180f5567d0f760e85f1efc99d9341cfebd86c9a334310f6d4381494121036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202cffffffff040000000000000000406a04534c500001010453454e4420bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7080000000000000001080000000000002326e8030000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ace8030000000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac9f694801000000001976a9148cfffc2409d063437d6aa8b75a009b9ba51b71fc88ac8983d460").unwrap(); +// check_slp_transaction(BCHD_TESTNET_URLS, tx).await.unwrap(); +// } +// } diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 340b5059aa..24ccf72448 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -2082,6 +2082,7 @@ mod slp_tests { } #[test] + #[ignore] fn test_validate_htlc_valid() { let (_ctx, bch) = tbch_coin_for_test(); let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); @@ -2119,6 +2120,7 @@ mod slp_tests { } #[test] + #[ignore] fn construct_and_send_invalid_slp_htlc_should_fail() { let (_ctx, bch) = tbch_coin_for_test(); let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); @@ -2204,6 +2206,7 @@ mod slp_tests { } #[test] + #[ignore] fn test_validate_htlc_invalid_slp_utxo() { let (_ctx, bch) = tbch_coin_for_test(); let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); From 96a53ce57cf7561755c61bc265946586a81a1ef5 Mon Sep 17 00:00:00 2001 From: Onur Date: Mon, 4 Sep 2023 11:24:17 +0300 Subject: [PATCH 15/17] chore(mm2 binary): remove debug info from release binary Binary report comparison from old to new mm2 builds shows reduction in file size after this commit. --------------------- Signed-off-by: onur-ozkan --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7a92ac1426..0004c9dadd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ resolver = "2" debug = 0 debug-assertions = false opt-level = 3 -# strip = true +strip = true codegen-units = 1 # lto = true panic = "abort" From 15385641bc2bc72326b93b7b5089e2dc0ab9c299 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Thu, 7 Sep 2023 09:00:36 +0000 Subject: [PATCH 16/17] feat(zcoin): allow ARRR to sync using a start date (#1922) Improve ARRR synchronization based on a user-selected date. This feature will enable users to specify a specific date as the starting point for synchronization as a substitute for the checkpoint block from config or syncing from the first block. --------- Signed-off-by: borngraced --- .github/workflows/test.yml | 9 +- mm2src/coins/utxo/utxo_builder/mod.rs | 2 +- .../utxo/utxo_builder/utxo_coin_builder.rs | 44 ++- mm2src/coins/z_coin.rs | 57 ++-- mm2src/coins/z_coin/storage/blockdb/mod.rs | 11 + mm2src/coins/z_coin/storage/walletdb/mod.rs | 7 +- mm2src/coins/z_coin/z_coin_errors.rs | 12 + mm2src/coins/z_coin/z_rpc.rs | 270 ++++++++++++++---- .../coins_activation/src/z_coin_activation.rs | 43 ++- .../tests/integration_tests_common/mod.rs | 11 +- mm2src/mm2_main/tests/mm2_tests/mod.rs | 4 +- .../tests/mm2_tests/orderbook_sync_tests.rs | 3 + .../mm2_main/tests/mm2_tests/z_coin_tests.rs | 86 +++++- mm2src/mm2_test_helpers/src/for_tests.rs | 24 +- mm2src/mm2_test_helpers/src/structs.rs | 47 ++- 15 files changed, 521 insertions(+), 109 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a939b19b8..382fd15bd8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -117,7 +117,9 @@ jobs: uses: ./.github/actions/cargo-cache - name: Test - run: cargo test --test 'mm2_tests_main' --no-fail-fast + run: | + wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.sh | bash + cargo test --test 'mm2_tests_main' --no-fail-fast # https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits # https://github.com/KomodoPlatform/atomicDEX-API/actions/runs/4419618128/jobs/7748266141#step:4:1790 @@ -161,7 +163,10 @@ jobs: uses: ./.github/actions/cargo-cache - name: Test - run: cargo test --test 'mm2_tests_main' --no-fail-fast + run: | + Invoke-WebRequest -Uri https://github.com/KomodoPlatform/komodo/raw/d456be35acd1f8584e1e4f971aea27bd0644d5c5/zcutil/wget64.exe -OutFile \wget64.exe + Invoke-WebRequest -Uri https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.bat -OutFile \cmd.bat && \cmd.bat + cargo test --test 'mm2_tests_main' --no-fail-fast docker-tests: timeout-minutes: 90 diff --git a/mm2src/coins/utxo/utxo_builder/mod.rs b/mm2src/coins/utxo/utxo_builder/mod.rs index b25503d486..205c36ee71 100644 --- a/mm2src/coins/utxo/utxo_builder/mod.rs +++ b/mm2src/coins/utxo/utxo_builder/mod.rs @@ -5,7 +5,7 @@ mod utxo_conf_builder; pub use utxo_arc_builder::{MergeUtxoArcOps, UtxoArcBuilder}; pub use utxo_coin_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, - UtxoFieldsWithIguanaSecretBuilder}; + UtxoFieldsWithIguanaSecretBuilder, DAY_IN_SECONDS}; pub use utxo_conf_builder::{UtxoConfBuilder, UtxoConfError, UtxoConfResult}; #[cfg(test)] diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 7a8f89b029..16f778d5ff 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -16,7 +16,7 @@ use common::custom_futures::repeatable::{Ready, Retry}; use common::executor::{abortable_queue::AbortableQueue, AbortSettings, AbortableSystem, AbortedError, SpawnAbortable, Timer}; use common::log::{error, info, LogOnError}; -use common::small_rng; +use common::{now_sec, small_rng}; use crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, StandardHDPathError, StandardHDPathToCoin}; use derive_more::Display; @@ -44,6 +44,9 @@ cfg_native! { use std::path::{Path, PathBuf}; } +/// Number of seconds in a day (24 hours * 60 * 60) +pub const DAY_IN_SECONDS: u64 = 86400; + pub type UtxoCoinBuildResult = Result>; #[derive(Debug, Display)] @@ -85,6 +88,7 @@ pub enum UtxoCoinBuildError { Internal(String), #[display(fmt = "SPV params verificaiton failed. Error: {_0}")] SPVError(SPVError), + ErrorCalculatingStartingHeight(String), } impl From for UtxoCoinBuildError { @@ -685,6 +689,44 @@ pub trait UtxoCoinBuilderCommonOps { (None, None) } + + /// Calculates the starting block height based on a given date and the current block height. + /// + /// # Arguments + /// * `date`: The date in seconds representing the desired starting date. + /// * `current_block_height`: The current block height at the time of calculation. + /// + fn calculate_starting_height_from_date( + &self, + date_s: u64, + current_block_height: u64, + ) -> UtxoCoinBuildResult> { + let avg_blocktime = self.conf()["avg_blocktime"] + .as_u64() + .ok_or_else(|| format!("avg_blocktime not specified in {} coin config", self.ticker())) + .map_to_mm(UtxoCoinBuildError::ErrorCalculatingStartingHeight)?; + let blocks_per_day = DAY_IN_SECONDS / avg_blocktime; + let current_time_s = now_sec(); + + if current_time_s < date_s { + return MmError::err(UtxoCoinBuildError::ErrorCalculatingStartingHeight(format!( + "{} sync date must be earlier then current date", + self.ticker() + ))); + }; + + let secs_since_date = current_time_s - date_s; + let days_since_date = (secs_since_date / DAY_IN_SECONDS) - 1; + let blocks_to_sync = (days_since_date * blocks_per_day) + blocks_per_day; + + if current_block_height < blocks_to_sync { + return Ok(None); + } + + let block_to_sync_from = current_block_height - blocks_to_sync; + + Ok(Some(block_to_sync_from)) + } } /// Attempts to parse native daemon conf file and return rpcport, rpcuser and rpcpassword diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 9d0b042297..d838db26f6 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -76,7 +76,7 @@ use z_htlc::{z_p2sh_spend, z_send_dex_fee, z_send_htlc}; mod z_rpc; use z_rpc::init_light_client; -pub use z_rpc::SyncStatus; +pub use z_rpc::{FirstSyncBlock, SyncStatus}; cfg_native!( use crate::{NumConversError, TransactionDetails, TxFeeDetails}; @@ -305,9 +305,15 @@ impl ZCoin { #[inline] pub fn consensus_params_ref(&self) -> &ZcoinConsensusParams { &self.z_fields.consensus_params } + /// Asynchronously checks the synchronization status and returns `true` if + /// the Sapling state has finished synchronizing, meaning that the block number is available. + /// Otherwise, it returns `false`. #[inline] pub async fn is_sapling_state_synced(&self) -> bool { - matches!(self.sync_status().await, Ok(SyncStatus::Finished { block_number: _ })) + matches!( + self.sync_status().await, + Ok(SyncStatus::Finished { block_number: _, .. }) + ) } #[inline] @@ -755,14 +761,34 @@ impl AsRef for ZCoin { fn as_ref(&self) -> &UtxoCoinFields { &self.utxo_arc } } +/// SyncStartPoint represents the starting point for synchronizing a wallet's blocks and transaction history. +/// This can be specified as a date, a block height, or starting from the earliest available data. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum SyncStartPoint { + /// Synchronize from a specific date (in Unix timestamp format). + Date(u64), + /// Synchronize from a specific block height. + Height(u64), + /// Synchronize from the earliest available data(`sapling_activation_height` from coin config). + Earliest, +} + +// ZcoinRpcMode reprs available RPC modes for interacting with the Zcoin network. It includes +/// modes for both native and light client, each with their own configuration options. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "rpc", content = "rpc_data")] pub enum ZcoinRpcMode { #[cfg(not(target_arch = "wasm32"))] Native, + #[serde(alias = "Electrum")] Light { + #[serde(alias = "servers")] electrum_servers: Vec, light_wallet_d_servers: Vec, + /// Specifies the parameters for synchronizing the wallet from a specific block. This overrides the + /// `CheckPointBlockInfo` configuration in the coin settings. + sync_params: Option, }, } @@ -894,36 +920,23 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { ); let blocks_db = self.blocks_db().await?; - let wallet_db = WalletDbShared::new(&self, &z_spending_key) - .await - .map_err(|err| ZCoinBuildError::ZcashDBError(err.to_string()))?; - let (sync_state_connector, light_wallet_db) = match &self.z_coin_params.mode { #[cfg(not(target_arch = "wasm32"))] ZcoinRpcMode::Native => { let native_client = self.native_client()?; - init_native_client( - self.ticker.into(), - native_client, - blocks_db, - wallet_db, - self.protocol_info.consensus_params.clone(), - self.z_coin_params.scan_blocks_per_iteration, - self.z_coin_params.scan_interval_ms, - ) - .await? + init_native_client(&self, native_client, blocks_db, &z_spending_key).await? }, ZcoinRpcMode::Light { - light_wallet_d_servers, .. + light_wallet_d_servers, + sync_params, + .. } => { init_light_client( - self.ticker.into(), + &self, light_wallet_d_servers.clone(), blocks_db, - wallet_db, - self.protocol_info.consensus_params.clone(), - self.z_coin_params.scan_blocks_per_iteration, - self.z_coin_params.scan_interval_ms, + sync_params, + &z_spending_key, ) .await? }, diff --git a/mm2src/coins/z_coin/storage/blockdb/mod.rs b/mm2src/coins/z_coin/storage/blockdb/mod.rs index 3067162008..cd9aceb6ef 100644 --- a/mm2src/coins/z_coin/storage/blockdb/mod.rs +++ b/mm2src/coins/z_coin/storage/blockdb/mod.rs @@ -142,6 +142,17 @@ impl BlockDbImpl { Ok(()) } + + pub(crate) async fn get_earliest_block(&self) -> Result { + Ok(query_single_row( + &self.db.lock().unwrap(), + "SELECT MIN(height) from compactblocks", + [], + |row| row.get::<_, Option>(0), + )? + .flatten() + .unwrap_or(0)) + } } #[cfg(not(target_arch = "wasm32"))] diff --git a/mm2src/coins/z_coin/storage/walletdb/mod.rs b/mm2src/coins/z_coin/storage/walletdb/mod.rs index 623ff042dc..698a54a60b 100644 --- a/mm2src/coins/z_coin/storage/walletdb/mod.rs +++ b/mm2src/coins/z_coin/storage/walletdb/mod.rs @@ -3,7 +3,7 @@ use mm2_err_handle::prelude::*; use zcash_primitives::zip32::ExtendedSpendingKey; cfg_native!( - use crate::z_coin::{ZcoinConsensusParams}; + use crate::z_coin::{CheckPointBlockInfo, ZcoinConsensusParams}; use crate::z_coin::z_rpc::create_wallet_db; use parking_lot::Mutex; @@ -38,6 +38,7 @@ pub struct WalletDbShared { impl<'a> WalletDbShared { pub async fn new( zcoin_builder: &ZCoinBuilder<'a>, + checkpoint_block: Option, z_spending_key: &ExtendedSpendingKey, ) -> MmResult { let wallet_db = create_wallet_db( @@ -45,11 +46,11 @@ impl<'a> WalletDbShared { .db_dir_path .join(format!("{}_wallet.db", zcoin_builder.ticker)), zcoin_builder.protocol_info.consensus_params.clone(), - zcoin_builder.protocol_info.check_point_block.clone(), + checkpoint_block, ExtendedFullViewingKey::from(z_spending_key), ) .await - .map_err(|err| MmError::new(WalletDbError::ZcoinClientInitError(err.into_inner())))?; + .mm_err(WalletDbError::ZcoinClientInitError)?; Ok(Self { db: Arc::new(Mutex::new(wallet_db)), diff --git a/mm2src/coins/z_coin/z_coin_errors.rs b/mm2src/coins/z_coin/z_coin_errors.rs index b8e3875e81..2a78aedc3f 100644 --- a/mm2src/coins/z_coin/z_coin_errors.rs +++ b/mm2src/coins/z_coin/z_coin_errors.rs @@ -17,6 +17,7 @@ use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; use zcash_client_sqlite::error::SqliteClientError; use zcash_primitives::transaction::builder::Error as ZTxBuilderError; +/// Represents possible errors that might occur while interacting with Zcoin rpc. #[derive(Debug, Display)] #[non_exhaustive] pub enum UpdateBlocksCacheErr { @@ -27,6 +28,7 @@ pub enum UpdateBlocksCacheErr { JsonRpcError(JsonRpcError), GetLiveLightClientError(String), ZcashDBError(String), + DecodeError(String), } #[cfg(not(target_arch = "wasm32"))] @@ -52,6 +54,10 @@ impl From for UpdateBlocksCacheErr { fn from(err: JsonRpcError) -> Self { UpdateBlocksCacheErr::JsonRpcError(err) } } +/// This enum encompasses various error scenarios that may arise +/// when configuring and activating a Zcoin, such as invalid +/// configuration settings, network connectivity issues, or other +/// initialization failures. #[derive(Debug, Display)] #[non_exhaustive] pub enum ZcoinClientInitError { @@ -59,6 +65,12 @@ pub enum ZcoinClientInitError { EmptyLightwalletdUris, #[display(fmt = "Fail to init clients while iterating lightwalletd urls {:?}", _0)] UrlIterFailure(Vec), + UpdateBlocksCacheErr(UpdateBlocksCacheErr), + UtxoCoinBuildError(UtxoCoinBuildError), +} + +impl From for ZcoinClientInitError { + fn from(err: UpdateBlocksCacheErr) -> Self { ZcoinClientInitError::UpdateBlocksCacheErr(err) } } #[cfg(not(target_arch = "wasm32"))] diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index 129abab700..6615847287 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -1,6 +1,6 @@ -use super::{z_coin_errors::*, ZcoinConsensusParams}; +use super::{z_coin_errors::*, BlockDbImpl, WalletDbShared, ZCoinBuilder, ZcoinConsensusParams}; use crate::utxo::rpc_clients::NativeClient; -use crate::z_coin::storage::{BlockDbImpl, WalletDbShared}; +use crate::z_coin::SyncStartPoint; use async_trait::async_trait; use common::executor::{spawn_abortable, AbortOnDropHandle}; use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender}; @@ -12,24 +12,27 @@ use parking_lot::Mutex; use std::sync::Arc; use zcash_primitives::consensus::BlockHeight; use zcash_primitives::transaction::TxId; +use zcash_primitives::zip32::ExtendedSpendingKey; cfg_native!( - use super::CheckPointBlockInfo; use crate::{RpcCommonOps, ZTransaction}; use crate::utxo::rpc_clients::{UtxoRpcClientOps, NO_TX_ERROR_CODE}; + use crate::utxo::utxo_builder::{UtxoCoinBuilderCommonOps, DAY_IN_SECONDS}; use crate::z_coin::storage::BlockDbError; + use crate::z_coin::CheckPointBlockInfo; use db_common::sqlite::rusqlite::Connection; use db_common::sqlite::{query_single_row, run_optimization_pragmas}; - use common::async_blocking; + use common::{async_blocking, now_sec}; use common::executor::Timer; use common::log::{debug, error, info, LogOnError}; use common::Future01CompatExt; use futures::channel::mpsc::channel; use group::GroupEncoding; + use hex::{FromHex, FromHexError}; use http::Uri; use prost::Message; - use rpc::v1::types::H256 as H256Json; + use rpc::v1::types::{Bytes, H256 as H256Json}; use std::path::PathBuf; use std::pin::Pin; use std::str::FromStr; @@ -48,7 +51,7 @@ cfg_native!( mod z_coin_grpc { tonic::include_proto!("cash.z.wallet.sdk.rpc"); } - + use z_coin_grpc::TreeState; use z_coin_grpc::compact_tx_streamer_client::CompactTxStreamerClient; use z_coin_grpc::{BlockId, BlockRange, ChainSpec, CompactBlock as TonicCompactBlock, CompactOutput as TonicCompactOutput, CompactSpend as TonicCompactSpend, CompactTx as TonicCompactTx, @@ -62,10 +65,22 @@ pub type OnCompactBlockFn<'a> = dyn FnMut(TonicCompactBlock) -> Result<(), MmErr #[allow(unused)] pub type OnCompactBlockFn<'a> = dyn FnMut(String) -> Result<(), MmError> + Send + 'a; +/// ZRpcOps trait provides asynchronous methods for performing various operations related to +/// Zcoin blockchain and wallet synchronization. #[async_trait] pub trait ZRpcOps { + /// Asynchronously retrieve the current block height from the Zcoin network. async fn get_block_height(&mut self) -> Result>; + /// Asynchronously retrieve the tree state for a specific block height from the Zcoin network. + #[cfg(not(target_arch = "wasm32"))] + async fn get_tree_state(&mut self, height: u64) -> Result>; + + /// Asynchronously scan and process blocks within a specified block height range. + /// + /// This method allows for scanning and processing blocks starting from `start_block` up to + /// and including `last_block`. It invokes the provided `on_block` function for each compact + /// block within the specified range. async fn scan_blocks( &mut self, start_block: u64, @@ -74,6 +89,17 @@ pub trait ZRpcOps { ) -> Result<(), MmError>; async fn check_tx_existence(&mut self, tx_id: TxId) -> bool; + + /// Retrieves checkpoint block information from the database at a specific height. + /// + /// checkpoint_block_from_height retrieves tree state information from rpc corresponding to the given + /// height and constructs a `CheckPointBlockInfo` struct containing some needed details such as + /// block height, hash, time, and sapling tree. + #[cfg(not(target_arch = "wasm32"))] + async fn checkpoint_block_from_height( + &mut self, + height: u64, + ) -> MmResult, UpdateBlocksCacheErr>; } #[cfg(not(target_arch = "wasm32"))] @@ -119,6 +145,19 @@ impl ZRpcOps for LightRpcClient { Ok(block.height) } + #[cfg(not(target_arch = "wasm32"))] + async fn get_tree_state(&mut self, height: u64) -> Result> { + let request = tonic::Request::new(BlockId { height, hash: vec![] }); + + Ok(self + .get_live_client() + .await? + .get_tree_state(request) + .await + .map_to_mm(UpdateBlocksCacheErr::GrpcError)? + .into_inner()) + } + async fn scan_blocks( &mut self, start_block: u64, @@ -176,6 +215,28 @@ impl ZRpcOps for LightRpcClient { } true } + + #[cfg(not(target_arch = "wasm32"))] + async fn checkpoint_block_from_height( + &mut self, + height: u64, + ) -> MmResult, UpdateBlocksCacheErr> { + let tree_state = self.get_tree_state(height).await?; + let hash = H256Json::from_str(&tree_state.hash) + .map_err(|err| UpdateBlocksCacheErr::DecodeError(err.to_string()))? + .reversed(); + let sapling_tree = Bytes::new( + FromHex::from_hex(&tree_state.tree) + .map_err(|err: FromHexError| UpdateBlocksCacheErr::DecodeError(err.to_string()))?, + ); + + Ok(Some(CheckPointBlockInfo { + height: tree_state.height as u32, + hash, + time: tree_state.time, + sapling_tree, + })) + } } #[cfg(not(target_arch = "wasm32"))] @@ -185,6 +246,9 @@ impl ZRpcOps for NativeClient { Ok(self.get_block_count().compat().await?) } + #[cfg(not(target_arch = "wasm32"))] + async fn get_tree_state(&mut self, _height: u64) -> Result> { todo!() } + async fn scan_blocks( &mut self, start_block: u64, @@ -281,34 +345,61 @@ impl ZRpcOps for NativeClient { } true } + + #[cfg(not(target_arch = "wasm32"))] + async fn checkpoint_block_from_height( + &mut self, + _height: u64, + ) -> MmResult, UpdateBlocksCacheErr> { + todo!() + } } +/// `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 +/// potentially rewinding the database to a specified synchronization height, performing optimizations, and +/// setting up the initial state of the wallet database. #[cfg(not(target_arch = "wasm32"))] pub async fn create_wallet_db( wallet_db_path: PathBuf, consensus_params: ZcoinConsensusParams, - check_point_block: Option, + checkpoint_block: Option, evk: ExtendedFullViewingKey, ) -> Result, MmError> { async_blocking({ move || -> Result, MmError> { let db = WalletDb::for_path(wallet_db_path, consensus_params) .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; + let extrema = db.block_height_extrema()?; + let min_sync_height = extrema.map(|(min, _)| u32::from(min)); + let init_block_height = checkpoint_block.clone().map(|block| block.height); + run_optimization_pragmas(db.sql_conn()) .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; init_wallet_db(&db).map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; - if db.get_extended_full_viewing_keys()?.is_empty() { - init_accounts_table(&db, &[evk])?; - if let Some(check_point) = check_point_block { + + // Check if the initial block height is less than the previous synchronization height and + // Rewind walletdb to the minimum possible height. + if db.get_extended_full_viewing_keys()?.is_empty() || init_block_height != min_sync_height { + info!("Older/Newer sync height detected!, rewinding walletdb to new height: {init_block_height:?}"); + let mut wallet_ops = db.get_update_ops().expect("get_update_ops always returns Ok"); + wallet_ops + .rewind_to_height(u32::MIN.into()) + .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; + if let Some(block) = checkpoint_block.clone() { init_blocks_table( &db, - BlockHeight::from_u32(check_point.height), - BlockHash(check_point.hash.0), - check_point.time, - &check_point.sapling_tree.0, + BlockHeight::from_u32(block.height), + BlockHash(block.hash.0), + block.time, + &block.sapling_tree.0, )?; } } + + if db.get_extended_full_viewing_keys()?.is_empty() { + init_accounts_table(&db, &[evk])?; + } Ok(db) } }) @@ -316,15 +407,14 @@ pub async fn create_wallet_db( } #[cfg(not(target_arch = "wasm32"))] -pub(super) async fn init_light_client( - coin: String, +pub(super) async fn init_light_client<'a>( + builder: &ZCoinBuilder<'a>, lightwalletd_urls: Vec, blocks_db: BlockDbImpl, - wallet_db: WalletDbShared, - consensus_params: ZcoinConsensusParams, - scan_blocks_per_iteration: u32, - scan_interval_ms: u64, + sync_params: &Option, + z_spending_key: &ExtendedSpendingKey, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { + let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); let (on_tx_gen_notifier, on_tx_gen_watcher) = channel(1); let mut rpc_clients = Vec::new(); @@ -363,22 +453,61 @@ pub(super) async fn init_light_client( return MmError::err(ZcoinClientInitError::UrlIterFailure(errors)); } + let mut light_rpc_clients = LightRpcClient { + rpc_clients: AsyncMutex::new(rpc_clients), + }; + + let current_block_height = light_rpc_clients + .get_block_height() + .await + .mm_err(ZcoinClientInitError::UpdateBlocksCacheErr)?; + let sapling_activation_height = builder.protocol_info.consensus_params.sapling_activation_height as u64; + let sync_height = match sync_params { + Some(SyncStartPoint::Date(date)) => builder + .calculate_starting_height_from_date(*date, current_block_height) + .mm_err(ZcoinClientInitError::UtxoCoinBuildError)? + .unwrap_or(sapling_activation_height), + Some(SyncStartPoint::Height(height)) => *height, + Some(SyncStartPoint::Earliest) => sapling_activation_height, + None => builder + .calculate_starting_height_from_date(now_sec() - DAY_IN_SECONDS, current_block_height) + .mm_err(ZcoinClientInitError::UtxoCoinBuildError)? + .unwrap_or(sapling_activation_height), + }; + let maybe_checkpoint_block = light_rpc_clients + .checkpoint_block_from_height(sync_height.max(sapling_activation_height)) + .await?; + + let wallet_db = WalletDbShared::new(builder, maybe_checkpoint_block, z_spending_key) + .await + .mm_err(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; + + // Get min_height in blocks_db and rewind blocks_db to 0 if sync_height != min_height + let min_height = blocks_db.get_earliest_block().await?; + if sync_height != min_height as u64 { + blocks_db + .rewind_to_height(u32::MIN) + .map_err(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; + }; + let sync_handle = SaplingSyncLoopHandle { coin, current_block: BlockHeight::from_u32(0), blocks_db, wallet_db: wallet_db.clone(), - consensus_params, + consensus_params: builder.protocol_info.consensus_params.clone(), sync_status_notifier, on_tx_gen_watcher, watch_for_tx: None, - scan_blocks_per_iteration, - scan_interval_ms, + scan_blocks_per_iteration: builder.z_coin_params.scan_blocks_per_iteration, + scan_interval_ms: builder.z_coin_params.scan_interval_ms, + first_sync_block: FirstSyncBlock { + requested: sync_height, + is_pre_sapling: sync_height < sapling_activation_height, + actual: sync_height.max(sapling_activation_height), + }, }; - let light_rpc_clients = LightRpcClient { - rpc_clients: AsyncMutex::new(rpc_clients), - }; let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(light_rpc_clients))); Ok(( @@ -389,42 +518,50 @@ pub(super) async fn init_light_client( #[cfg(target_arch = "wasm32")] #[allow(unused)] -pub(super) async fn init_light_client( - _coin: String, +pub(super) async fn init_light_client<'a>( + _builder: &ZCoinBuilder<'a>, _lightwalletd_urls: Vec, _blocks_db: BlockDbImpl, - _wallet_db: WalletDbShared, - _consensus_params: ZcoinConsensusParams, - _scan_blocks_per_iteration: u32, - _scan_interval_ms: u64, + _sync_params: &Option, + z_spending_key: &ExtendedSpendingKey, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { todo!() } #[cfg(not(target_arch = "wasm32"))] -pub(super) async fn init_native_client( - coin: String, +pub(super) async fn init_native_client<'a>( + builder: &ZCoinBuilder<'a>, native_client: NativeClient, blocks_db: BlockDbImpl, - wallet_db: WalletDbShared, - consensus_params: ZcoinConsensusParams, - scan_blocks_per_iteration: u32, - scan_interval_ms: u64, + z_spending_key: &ExtendedSpendingKey, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { + let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); let (on_tx_gen_notifier, on_tx_gen_watcher) = channel(1); + let checkpoint_block = builder.protocol_info.check_point_block.clone(); + let sapling_height = builder.protocol_info.consensus_params.sapling_activation_height; + let checkpoint_height = checkpoint_block.clone().map(|b| b.height).unwrap_or(sapling_height) as u64; + let first_sync_block = FirstSyncBlock { + requested: checkpoint_height, + is_pre_sapling: false, + actual: checkpoint_height, + }; + let wallet_db = WalletDbShared::new(builder, checkpoint_block, z_spending_key) + .await + .mm_err(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; let sync_handle = SaplingSyncLoopHandle { coin, current_block: BlockHeight::from_u32(0), blocks_db, wallet_db: wallet_db.clone(), - consensus_params, + consensus_params: builder.protocol_info.consensus_params.clone(), sync_status_notifier, on_tx_gen_watcher, watch_for_tx: None, - scan_blocks_per_iteration, - scan_interval_ms, + scan_blocks_per_iteration: builder.z_coin_params.scan_blocks_per_iteration, + scan_interval_ms: builder.z_coin_params.scan_interval_ms, + first_sync_block, }; let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(native_client))); @@ -435,14 +572,12 @@ pub(super) async fn init_native_client( } #[cfg(target_arch = "wasm32")] -pub(super) async fn _init_native_client( - _coin: String, - _native_client: NativeClient, +pub(super) async fn _init_native_client<'a>( + _builder: &ZCoinBuilder<'a>, + mut _native_client: NativeClient, _blocks_db: BlockDbImpl, - _consensus_params: ZcoinConsensusParams, - _scan_blocks_per_iteration: u32, - _scan_interval_ms: u64, -) -> Result<(AsyncMutex, String), MmError> { + _z_spending_key: &ExtendedSpendingKey, +) -> Result<(AsyncMutex, WalletDbShared), MmError> { todo!() } @@ -486,21 +621,53 @@ impl SaplingSyncRespawnGuard { } } +/// `SyncStatus` enumerates different states that may occur during the execution of +/// Zcoin-related operations during block sync. +/// +/// - `UpdatingBlocksCache`: Represents the state of updating the blocks cache, with associated data +/// about the first synchronization block, the current scanned block, and the latest block. +/// - `BuildingWalletDb`: Denotes the state of building the wallet db, with associated data about +/// the first synchronization block, the current scanned block, and the latest block. +/// - `TemporaryError(String)`: Represents a temporary error state, with an associated error message +/// providing details about the error. +/// - `RequestingWalletBalance`: Indicates the process of requesting the wallet balance. +/// - `Finishing`: Represents the finishing state of an operation. pub enum SyncStatus { UpdatingBlocksCache { + first_sync_block: FirstSyncBlock, current_scanned_block: u64, latest_block: u64, }, BuildingWalletDb { + first_sync_block: FirstSyncBlock, current_scanned_block: u64, latest_block: u64, }, TemporaryError(String), Finished { + first_sync_block: FirstSyncBlock, block_number: u64, }, } +/// The `FirstSyncBlock` struct contains details about the block block that is used to start the synchronization +/// process. +/// It includes information about the requested block height, whether it predates the Sapling activation, and the +/// actual starting block height used during synchronization. +/// +/// - `requested`: The requested block height during synchronization. +/// - `is_pre_sapling`: Indicates whether the block predates the Sapling activation. +/// - `actual`: The actual block height used for synchronization(may be altered). +#[derive(Clone, Serialize)] +#[serde(deny_unknown_fields)] +pub struct FirstSyncBlock { + pub requested: u64, + pub is_pre_sapling: bool, + pub actual: u64, +} + +/// The `SaplingSyncLoopHandle` struct is used to manage and control Zcoin synchronization loop. +/// It includes information about the coin being synchronized, the current block height, database access, etc. #[allow(unused)] pub struct SaplingSyncLoopHandle { coin: String, @@ -516,15 +683,19 @@ pub struct SaplingSyncLoopHandle { watch_for_tx: Option, scan_blocks_per_iteration: u32, scan_interval_ms: u64, + first_sync_block: FirstSyncBlock, } #[cfg(not(target_arch = "wasm32"))] impl SaplingSyncLoopHandle { + fn first_sync_block(&self) -> FirstSyncBlock { self.first_sync_block.clone() } + fn notify_blocks_cache_status(&mut self, current_scanned_block: u64, latest_block: u64) { self.sync_status_notifier .try_send(SyncStatus::UpdatingBlocksCache { current_scanned_block, latest_block, + first_sync_block: self.first_sync_block(), }) .debug_log_with_msg("No one seems interested in SyncStatus"); } @@ -534,6 +705,7 @@ impl SaplingSyncLoopHandle { .try_send(SyncStatus::BuildingWalletDb { current_scanned_block, latest_block, + first_sync_block: self.first_sync_block(), }) .debug_log_with_msg("No one seems interested in SyncStatus"); } @@ -548,6 +720,7 @@ impl SaplingSyncLoopHandle { self.sync_status_notifier .try_send(SyncStatus::Finished { block_number: self.current_block.into(), + first_sync_block: self.first_sync_block(), }) .debug_log_with_msg("No one seems interested in SyncStatus"); } @@ -571,6 +744,7 @@ impl SaplingSyncLoopHandle { if let Some((_, max_in_wallet)) = extrema { from_block = from_block.max(max_in_wallet.into()); } + if current_block >= from_block { rpc.scan_blocks(from_block, current_block, &mut |block: TonicCompactBlock| { block_in_place(|| self.blocks_db.insert_block(block.height as u32, block.encode_to_vec())) diff --git a/mm2src/coins_activation/src/z_coin_activation.rs b/mm2src/coins_activation/src/z_coin_activation.rs index 77b397d887..ba1997b1a3 100644 --- a/mm2src/coins_activation/src/z_coin_activation.rs +++ b/mm2src/coins_activation/src/z_coin_activation.rs @@ -7,8 +7,8 @@ use async_trait::async_trait; use coins::coin_balance::{CoinBalanceReport, IguanaWalletBalance}; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tx_history_storage::CreateTxHistoryStorageError; -use coins::z_coin::{z_coin_from_conf_and_params, BlockchainScanStopped, SyncStatus, ZCoin, ZCoinBuildError, - ZcoinActivationParams, ZcoinProtocolInfo}; +use coins::z_coin::{z_coin_from_conf_and_params, BlockchainScanStopped, FirstSyncBlock, SyncStatus, ZCoin, + ZCoinBuildError, ZcoinActivationParams, ZcoinProtocolInfo}; use coins::{BalanceError, CoinProtocol, MarketCoinOps, PrivKeyBuildPolicy, RegisterCoinError}; use crypto::hw_rpc_task::{HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use crypto::CryptoCtxError; @@ -30,11 +30,21 @@ pub type ZcoinRpcTaskHandle = InitStandaloneCoinTaskHandle; pub type ZcoinAwaitingStatus = HwRpcTaskAwaitingStatus; pub type ZcoinUserAction = HwRpcTaskUserAction; +/// `ZCoinActivationResult` provides information/data for Zcoin activation. It includes +/// details such as the ticker, the current block height, the wallet balance, and the result +/// of the first synchronization block (if applicable). +/// +/// - `ticker`: A string representing the ticker of the Zcoin. +/// - `current_block`: The current block height at the time of this activation result. +/// - `wallet_balance`: Information about the wallet's coin balance and status. +/// - `first_sync_block`: An optional field containing details about the first synchronization block +/// during the activation process. It may be `None` if no first synchronization block is available. #[derive(Clone, Serialize)] pub struct ZcoinActivationResult { pub ticker: String, pub current_block: u64, pub wallet_balance: CoinBalanceReport, + pub first_sync_block: Option, } impl CurrentBlock for ZcoinActivationResult { @@ -47,15 +57,32 @@ impl GetAddressesBalances for ZcoinActivationResult { } } +/// `ZcoinInProgressStatus` enumerates different states that may occur during the execution of +/// Zcoin-related operations during coin activation. +/// +/// - `ActivatingCoin`: Indicates that Zcoin is in the process of activating. +/// - `UpdatingBlocksCache`: Represents the state of updating the blocks cache, with associated data +/// about the first synchronization block, the current scanned block, and the latest block. +/// - `BuildingWalletDb`: Denotes the state of building the wallet db, with associated data about +/// the first synchronization block, the current scanned block, and the latest block. +/// - `TemporaryError(String)`: Represents a temporary error state, with an associated error message +/// providing details about the error. +/// - `RequestingWalletBalance`: Indicates the process of requesting the wallet balance. +/// - `Finishing`: Represents the finishing state of an operation. +/// - `WaitingForTrezorToConnect`: Denotes a state where Zcoin is waiting for a Trezor device to connect. +/// - `WaitingForUserToConfirmPubkey`: Represents a state where Zcoin is waiting for the user to confirm +/// or decline an address on their device, without requiring explicit user action. #[derive(Clone, Serialize)] #[non_exhaustive] pub enum ZcoinInProgressStatus { ActivatingCoin, UpdatingBlocksCache { + first_sync_block: FirstSyncBlock, current_scanned_block: u64, latest_block: u64, }, BuildingWalletDb { + first_sync_block: FirstSyncBlock, current_scanned_block: u64, latest_block: u64, }, @@ -220,16 +247,20 @@ impl InitStandaloneCoinActivationOps for ZCoin { loop { let in_progress_status = match coin.sync_status().await? { SyncStatus::UpdatingBlocksCache { + first_sync_block, current_scanned_block, latest_block, } => ZcoinInProgressStatus::UpdatingBlocksCache { + first_sync_block, current_scanned_block, latest_block, }, SyncStatus::BuildingWalletDb { + first_sync_block, current_scanned_block, latest_block, } => ZcoinInProgressStatus::BuildingWalletDb { + first_sync_block, current_scanned_block, latest_block, }, @@ -256,6 +287,13 @@ impl InitStandaloneCoinActivationOps for ZCoin { .map_to_mm(ZcoinInitError::CouldNotGetBlockCount)?; let balance = self.my_balance().compat().await?; + let first_sync_block = match self.sync_status().await? { + SyncStatus::Finished { first_sync_block, .. } + | SyncStatus::BuildingWalletDb { first_sync_block, .. } + | SyncStatus::UpdatingBlocksCache { first_sync_block, .. } => Some(first_sync_block), + _ => None, + }; + Ok(ZcoinActivationResult { ticker: self.ticker().into(), current_block, @@ -263,6 +301,7 @@ impl InitStandaloneCoinActivationOps for ZCoin { address: self.my_z_address_encoded(), balance, }), + first_sync_block, }) } diff --git a/mm2src/mm2_main/tests/integration_tests_common/mod.rs b/mm2src/mm2_main/tests/integration_tests_common/mod.rs index 327c7bbc8d..3568b610ad 100644 --- a/mm2src/mm2_main/tests/integration_tests_common/mod.rs +++ b/mm2src/mm2_main/tests/integration_tests_common/mod.rs @@ -8,8 +8,8 @@ use mm2_rpc::data::legacy::CoinInitResponse; use mm2_test_helpers::electrums::{morty_electrums, rick_electrums}; use mm2_test_helpers::for_tests::{enable_native as enable_native_impl, init_utxo_electrum, init_utxo_status, init_z_coin_light, init_z_coin_status, MarketMakerIt}; -use mm2_test_helpers::structs::{CoinActivationResult, InitTaskResult, InitUtxoStatus, InitZcoinStatus, RpcV2Response, - UtxoStandardActivationResult}; +use mm2_test_helpers::structs::{InitTaskResult, InitUtxoStatus, InitZcoinStatus, RpcV2Response, + UtxoStandardActivationResult, ZCoinActivationResult}; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; use std::env::var; @@ -88,11 +88,12 @@ pub async fn enable_z_coin_light( coin: &str, electrums: &[&str], lightwalletd_urls: &[&str], + starting_date: Option, account: Option, -) -> CoinActivationResult { - let init = init_z_coin_light(mm, coin, electrums, lightwalletd_urls, account).await; +) -> ZCoinActivationResult { + let init = init_z_coin_light(mm, coin, electrums, lightwalletd_urls, starting_date, account).await; let init: RpcV2Response = json::from_value(init).unwrap(); - let timeout = wait_until_ms(12000000); + let timeout = wait_until_ms(60000); loop { if now_ms() > timeout { diff --git a/mm2src/mm2_main/tests/mm2_tests/mod.rs b/mm2src/mm2_main/tests/mm2_tests/mod.rs index 10c01f6e13..e5953479bc 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mod.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mod.rs @@ -18,11 +18,13 @@ mod zhtlc_native_reexport { pub use mm2_test_helpers::structs::{CoinActivationResult, InitTaskResult, InitZcoinStatus, RpcV2Response}; } +#[cfg(all(feature = "zhtlc-native-tests", not(target_arch = "wasm32")))] +use mm2_test_helpers::structs::ZCoinActivationResult; #[cfg(all(feature = "zhtlc-native-tests", not(target_arch = "wasm32")))] use zhtlc_native_reexport::*; #[cfg(all(feature = "zhtlc-native-tests", not(target_arch = "wasm32")))] -async fn enable_z_coin(mm: &MarketMakerIt, coin: &str) -> CoinActivationResult { +async fn enable_z_coin(mm: &MarketMakerIt, coin: &str) -> ZCoinActivationResult { let init = init_z_coin_native(mm, coin).await; let init: RpcV2Response = serde_json::from_value(init).unwrap(); let timeout = wait_until_ms(120000); 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 b221a264c1..1a5c0c8d4c 100644 --- a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs @@ -1437,6 +1437,7 @@ fn zhtlc_orders_sync_alice_connected_before_creation() { ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, None, + None, )); let set_price_json = json!({ @@ -1500,6 +1501,7 @@ fn zhtlc_orders_sync_alice_connected_after_creation() { ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, None, + None, )); let set_price_json = json!({ @@ -1529,6 +1531,7 @@ fn zhtlc_orders_sync_alice_connected_after_creation() { ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, None, + None, )); let set_price_json = json!({ diff --git a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs index 3c7836108a..1d2b58362c 100644 --- a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs @@ -1,12 +1,12 @@ use crate::integration_tests_common::*; use common::executor::Timer; -use common::{block_on, log, now_ms, wait_until_ms}; +use common::{block_on, log, now_ms, now_sec, wait_until_ms}; use mm2_number::BigDecimal; use mm2_test_helpers::electrums::rick_electrums; -use mm2_test_helpers::for_tests::{init_withdraw, pirate_conf, rick_conf, send_raw_transaction, withdraw_status, - z_coin_tx_history, zombie_conf, MarketMakerIt, Mm2TestConf, ARRR, PIRATE_ELECTRUMS, - PIRATE_LIGHTWALLETD_URLS, RICK, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, - ZOMBIE_TICKER}; +use mm2_test_helpers::for_tests::{disable_coin, init_withdraw, pirate_conf, rick_conf, send_raw_transaction, + withdraw_status, z_coin_tx_history, zombie_conf, MarketMakerIt, Mm2TestConf, ARRR, + PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, RICK, ZOMBIE_ELECTRUMS, + ZOMBIE_LIGHTWALLETD_URLS, ZOMBIE_TICKER}; use mm2_test_helpers::structs::{EnableCoinBalance, InitTaskResult, RpcV2Response, TransactionDetails, WithdrawStatus, ZcoinHistoryRes}; use serde_json::{self as json, json, Value as Json}; @@ -46,9 +46,7 @@ async fn withdraw(mm: &MarketMakerIt, coin: &str, to: &str, amount: &str) -> Tra } } -// ignored because it requires a long-running Zcoin initialization process #[test] -#[ignore] fn activate_z_coin_light() { let coins = json!([zombie_conf()]); @@ -61,18 +59,18 @@ fn activate_z_coin_light() { ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, None, + None, )); let balance = match activation_result.wallet_balance { EnableCoinBalance::Iguana(iguana) => iguana, _ => panic!("Expected EnableCoinBalance::Iguana"), }; - assert_eq!(balance.balance.spendable, BigDecimal::from_str("3.1").unwrap()); + assert_eq!(balance.balance.spendable, BigDecimal::default()); } #[test] -#[ignore] -fn activate_z_coin_with_hd_account() { +fn activate_z_coin_light_with_changing_height() { let coins = json!([zombie_conf()]); let conf = Mm2TestConf::seednode_with_hd_account(ZOMBIE_TEST_BIP39_ACTIVATION_SEED, &coins); @@ -83,9 +81,72 @@ fn activate_z_coin_with_hd_account() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, Some(0), )); + let old_first_sync_block = activation_result.first_sync_block; + let balance = match activation_result.wallet_balance { + EnableCoinBalance::Iguana(iguana) => iguana, + _ => panic!("Expected EnableCoinBalance::Iguana"), + }; + assert_eq!(balance.balance.spendable, BigDecimal::default()); + + // disable coin + block_on(disable_coin(&mm, ZOMBIE_TICKER, true)); + + // Perform activation with changed height + // Calculate timestamp for 2 days ago + let two_day_seconds = 2 * 24 * 60 * 60; + let two_days_ago = now_sec() - two_day_seconds; + log!( + "Re-running enable_z_coin_light_with_changing_height with new starting date {}", + two_days_ago + ); + + let activation_result = block_on(enable_z_coin_light( + &mm, + ZOMBIE_TICKER, + ZOMBIE_ELECTRUMS, + ZOMBIE_LIGHTWALLETD_URLS, + Some(two_days_ago), + None, + )); + + let new_first_sync_block = activation_result.first_sync_block; + let balance = match activation_result.wallet_balance { + EnableCoinBalance::Iguana(iguana) => iguana, + _ => panic!("Expected EnableCoinBalance::Iguana"), + }; + assert_eq!(balance.balance.spendable, BigDecimal::default()); + + // let's check to make sure first activation starting height is different from current one + assert_ne!( + old_first_sync_block.as_ref().unwrap().actual, + new_first_sync_block.as_ref().unwrap().actual + ); + // let's check to make sure first activation starting height is greater than current one since we used date later + // than current date + assert!(old_first_sync_block.as_ref().unwrap().actual > new_first_sync_block.as_ref().unwrap().actual); +} + +#[test] +fn activate_z_coin_with_hd_account() { + let coins = json!([zombie_conf()]); + + let hd_account_id = 0; + let conf = Mm2TestConf::seednode_with_hd_account(ZOMBIE_TEST_BIP39_ACTIVATION_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_result = block_on(enable_z_coin_light( + &mm, + ZOMBIE_TICKER, + ZOMBIE_ELECTRUMS, + ZOMBIE_LIGHTWALLETD_URLS, + None, + Some(hd_account_id), + )); + let actual = match activation_result.wallet_balance { EnableCoinBalance::Iguana(iguana) => iguana.address, EnableCoinBalance::HD(_) => panic!("Expected 'Iguana' wallet balance, found HD"), @@ -111,6 +172,7 @@ fn test_z_coin_tx_history() { ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, None, + None, )); let tx_history = block_on(z_coin_tx_history(&mm, ZOMBIE_TICKER, 5, None)); @@ -355,6 +417,7 @@ fn withdraw_z_coin_light() { ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, None, + None, )); println!("{:?}", activation_result); @@ -397,6 +460,7 @@ fn trade_rick_zombie_light() { ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, None, + None, )); println!("Bob ZOMBIE activation {:?}", zombie_activation); @@ -430,6 +494,7 @@ fn trade_rick_zombie_light() { ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, None, + None, )); println!("Alice ZOMBIE activation {:?}", zombie_activation); @@ -487,6 +552,7 @@ fn activate_pirate_light() { PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, None, + None, )); let balance = match activation_result.wallet_balance { diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 8376afa8a8..d6f1d75fbd 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -5,7 +5,7 @@ use crate::structs::*; use common::custom_futures::repeatable::{Ready, Retry}; use common::executor::Timer; use common::log::debug; -use common::{cfg_native, now_float, now_ms, repeatable, wait_until_ms, PagingOptionsEnum}; +use common::{cfg_native, now_float, now_ms, now_sec, repeatable, wait_until_ms, PagingOptionsEnum}; use common::{get_utc_timestamp, log}; use crypto::{CryptoCtx, StandardHDCoinAddress}; use gstuff::{try_s, ERR, ERRL}; @@ -312,6 +312,7 @@ pub fn zombie_conf() -> Json { "txversion":4, "overwintered":1, "mm2":1, + "avg_blocktime": 60, "protocol":{ "type":"ZHTLC", "protocol_data": { @@ -328,12 +329,6 @@ pub fn zombie_conf() -> Json { "b58_pubkey_address_prefix": [ 28, 184 ], "b58_script_address_prefix": [ 28, 189 ] }, - "check_point_block": { - "height": 290000, - "time": 1664200629, - "hash": "106BAA72C53E7FA52E30E6D3D15B37001207E3CF3B9FCE9BAB6C6D4AF9ED9200", - "sapling_tree": "017797D05B070D29A47EFEBE3FAD3F29345D31BE608C46A5131CD55D201A631C13000D000119CE6220D0CB0F82AD6466B677828A0B4C2983662DAB181A86F913F7E9FB9C28000139C4399E4CA741CBABBDDAEB6DCC3541BA902343E394160EEECCDF20C289BA65011823D28B592E9612A6C3CF4778F174E10B1B714B4FF85E6E58EE19DD4A0D5734016FA4682B0007E61B63A0442B85E0B8C0CE2409E665F219013B5E24E385F6066B00000001A325043E11CD6A431A0BD99141C4C6E9632A156185EB9B0DBEF665EEC803DD6F00000103C11FCCC90C2EC1A126635F708311EDEF9B93D3E752E053D3AA9EFA0AF9D526" - }, "z_derivation_path": "m/32'/133'", } }, @@ -349,6 +344,7 @@ pub fn pirate_conf() -> Json { "txversion":4, "overwintered":1, "mm2":1, + "avg_blocktime": 60, "protocol":{ "type":"ZHTLC", "protocol_data": { @@ -365,12 +361,6 @@ pub fn pirate_conf() -> Json { "b58_pubkey_address_prefix": [ 28, 184 ], "b58_script_address_prefix": [ 28, 189 ] }, - "check_point_block": { - "height": 2010000, - "time": 1659288887, - "hash": "b9024dc7a9b1bb0fe5c3f84175be91c572706d3268f1dc74bbe5121a00000000", - "sapling_tree": "019b6df2c6f4f42d867ac881f9f728e7954ed2e6922801a60c492b4ff85ef5f0400135609f387dd5034be026b7223ec5a85037b6a9d58f9afac8b7373e2606a5be6f150104bdb768da2504afb2f1963c340668490873885c22a06a7a9ac866320f10030d010e2e8e76a1e8bf8a1146661b48296ff36d5ce2082284193346b8ee7e4ec0e33600012f7f502f94a659eee3425e22a66ab4656fb7791ca0a2ac8a558a5fca7e4fbf630000019a9f3d2f985e10841c71bd70b84bb209a51a15680943842fa232e8ad4dc0b8590001b289928cbcc0c951720f14d35db6ea6383e67e62cebbf6d60103f4b0faadbd63000001ff9a30c61d63bb599e967dcac24ab6fedc0e8daa618a0efbd0314366fa05994200014d7ad3b2f0ee6ec7fa8cb4a0e9fadbaac4a3ed7d07f5643171a8eba26f93fc5a0001efa2884b5610614c3676441010863dc4fa6959f4df7a8018e74b93bb26b9d412014b705396332773077e58f73fe5a4c2c1d7ad2a1058bd5856670fb9529d1cd61c01e29681aab902f9876726a6fada746879e13af24d3675ae53fcc20629a85e196b010b5fd8e7610754075f936463780e85841f3ab8ca2978f9afdf7c2c250f16a75f01db56bc66eb1cd54ec6861e5cf24af2f4a17991556a52ca781007569e95b9842401c03877ecdd98378b321250640a1885604d675aaa50380e49da8cfa6ff7deaf15" - }, } }, "required_confirmations":0 @@ -2334,8 +2324,13 @@ pub async fn init_z_coin_light( coin: &str, electrums: &[&str], lightwalletd_urls: &[&str], + starting_date: Option, account: Option, ) -> Json { + // Number of seconds in a day + let one_day_seconds = 24 * 60 * 60; + let starting_date = starting_date.unwrap_or(now_sec() - one_day_seconds); + let request = mm .rpc(&json!({ "userpass": mm.userpass, @@ -2349,6 +2344,9 @@ pub async fn init_z_coin_light( "rpc_data": { "electrum_servers": electrum_servers_rpc(electrums), "light_wallet_d_servers": lightwalletd_urls, + "sync_params": { + "date": starting_date + } }, }, "account": account.unwrap_or_default(), diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index 81fe48e5b1..bd9f3e3975 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -536,6 +536,39 @@ pub enum EnableCoinBalance { HD(HDWalletBalance), } +/// The `FirstSyncBlock` struct contains details about the block block that is used to start the synchronization +/// process. +/// It includes information about the requested block height, whether it predates the Sapling activation, and the +/// actual starting block height used during synchronization. +/// +/// - `requested`: The requested block height during synchronization. +/// - `is_pre_sapling`: Indicates whether the block predates the Sapling activation. +/// - `actual`: The actual block height used for synchronization(may be altered). +#[derive(Debug, Deserialize)] +pub struct FirstSyncBlock { + pub requested: u64, + pub is_pre_sapling: bool, + pub actual: u64, +} + +/// `ZCoinActivationResult` provides information/data for Zcoin activation. It includes +/// details such as the ticker, the current block height, the wallet balance, and the result +/// of the first synchronization block (if applicable). +/// +/// - `ticker`: A string representing the ticker of the Zcoin. +/// - `current_block`: The current block height at the time of this activation result. +/// - `wallet_balance`: Information about the wallet's coin balance and status. +/// - `first_sync_block`: An optional field containing details about the first synchronization block +/// during the activation process. It may be `None` if no first synchronization block is available. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ZCoinActivationResult { + pub ticker: String, + pub current_block: u64, + pub wallet_balance: EnableCoinBalance, + pub first_sync_block: Option, +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct CoinActivationResult { @@ -573,10 +606,22 @@ pub enum MmRpcResult { Err(Json), } +/// `InitZcoinStatus` encapsulates different states that may occur during the initialization of Zcoin, +/// These states include successful initialization, error conditions, ongoing +/// progress, and situations where user action is required. +/// +/// - `Ok(ZCoinActivationResult)`: Indicates that Zcoin initialization was successful, with an associated +/// `ZCoinActivationResult` containing activation and status information. +/// - `Error(Json)`: Represents an error state during initialization, with an associated JSON object (`Json`) +/// providing details about the error. +/// - `InProgress(Json)`: Indicates that initialization is in progress, with an associated JSON object (`Json`) +/// containing information about the ongoing process. +/// - `UserActionRequired(Json)`: Denotes a state where user action is required for initialization to proceed, +/// with an associated JSON object (`Json`) providing instructions or requirements. #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, tag = "status", content = "details")] pub enum InitZcoinStatus { - Ok(CoinActivationResult), + Ok(ZCoinActivationResult), Error(Json), InProgress(Json), UserActionRequired(Json), From 1b10a0666226fb5ff3758020dce2f67c255b9787 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Thu, 7 Sep 2023 12:52:39 +0300 Subject: [PATCH 17/17] chore(release): add changelog entries for v1.0.7-beta (#1961) --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c06f7c875..eded4bcbc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +## v1.0.7-beta - 2023-09-08 +**Features:** +- Trading Protocol Upgrade [#1895](https://github.com/KomodoPlatform/atomicDEX-API/issues/1895) + - SwapOpsV2 trait was added containing methods of the new protocol (WIP) in [#1927](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1927) + - SwapOpsV2 was implemented for UtxoStandardCoin in [#1927](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1927) + - Dockerized integration tests added, sending and spending/refunding "dex fee + premium" UTXO in [#1927](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1927) +- HD Wallet [#1838](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1838) + - Global enabling of an account'/change/address_index path for all coins using hd_account_id config parameter was replaced by enable_hd which is a bool that defaults to false in [#1933](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1933) + - path_to_address parameter was added to coins activation requests to set the default account'/change/address_index path that will be used for swaps. If not provided, the default will be 0'/0/0 in [#1933](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1933) + - HD withdrawal from any account'/change/address_index path was implemented for UTXO, EVM and Tendermint coins in [#1933](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1933) +- Pirate Integration [#927](https://github.com/KomodoPlatform/komodo-defi-framework/issues/927) + - ARRR synchronization now supports using a specific start date. This allows users to specify a specific date as the starting point for synchronization as a substitute for the checkpoint block from config or syncing from the first block [#1922](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1922) + +**Enhancements/Fixes:** +- Adex-CLI [#1682](https://github.com/KomodoPlatform/atomicDEX-API/issues/1682) + - The file permissions of the cli config file is now set to 660 in unix to disallow reading by other users [#1913](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1913) + - Activation types have been introduced to prevent malicious substitution of them in the activation scheme file [#1912](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1912) + - HTTPS connection support was added in [#1910](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1910) + - Activation scheme was changed so the related data types were refactored to be fit for it in [#1938](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1938) +- PoSV coins withdrawal issue was fixed. The issue was a missing n_time field in the generated transaction. The fix now correctly considers when n_time is required, and the rawtransaction can be broadcasted [#1925](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1925) +- Latest relayer channel is now used for tendermint test [#1929](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1929) +- Price urls were updated in [#1928](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1928) +- NFT transactions that transfer multiple NFT tokens were fixed in db, log_index is now used as part of the transfers history table primary key [#1926](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1926) +- State machine was refactored as a preparation step for StorableStateMachine pattern extension in [#1927](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1927) +- A fix was introduced to use kmd rewards for fees if change + interest is below dust threshold in [#1944](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1944) +- Debug info was removed from release binary to reduce the file size in [#1954](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1954) +- Failing tests due to BCHD were ignored in [#1955](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1955) + + ## v1.0.6-beta - 2023-07-24 **Features:**