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/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:** 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/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" 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/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/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_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/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/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(()) } 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..f8e1329453 --- /dev/null +++ b/mm2src/adex_cli/src/rpc_data.rs @@ -0,0 +1,71 @@ +//! 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::{Deserialize, Serialize}; + +#[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, 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, +} + +#[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/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/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/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/coins/eth.rs b/mm2src/coins/eth.rs index 59c5736358..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}; @@ -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; @@ -109,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::{PrivKeyPolicy, TransactionResult, WithdrawFrom}; use nonce::ParityNonce; /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol @@ -169,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)] @@ -408,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, @@ -734,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 { @@ -777,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? @@ -799,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 { @@ -841,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() @@ -850,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, @@ -950,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)?; @@ -1025,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)?; @@ -1094,18 +1094,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<()> { @@ -1305,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!(), } @@ -1316,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(), } @@ -1809,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)) @@ -1836,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)) } @@ -2107,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"), } @@ -2972,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 @@ -3611,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)?; @@ -3639,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(); @@ -4247,7 +4266,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 +5019,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 { @@ -5153,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()); @@ -5413,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/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/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index f3eda4d4ff..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)] @@ -204,7 +205,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()), @@ -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.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/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 + Send>; +pub type TransactionResult = Result; pub type BalanceResult = Result>; pub type BalanceFut = Box> + Send>; pub type NonZeroBalanceFut = Box> + Send>; @@ -310,6 +313,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; @@ -407,6 +413,8 @@ pub struct RawTransactionRes { #[derive(Debug, Deserialize)] pub struct MyAddressReq { coin: String, + #[serde(default)] + path_to_address: StandardHDCoinAddress, } #[derive(Debug, Serialize)] @@ -437,6 +445,10 @@ pub enum TxHistoryError { pub enum PrivKeyPolicyNotAllowed { #[display(fmt = "Hardware Wallet is not supported")] HardwareWalletNotSupported, + #[display(fmt = "Unsupported method: {}", _0)] + UnsupportedMethod(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), } impl Serialize for PrivKeyPolicyNotAllowed { @@ -454,6 +466,22 @@ pub enum UnexpectedDerivationMethod { ExpectedSingleAddress, #[display(fmt = "Expected 'HDWallet' derivationMethod")] ExpectedHDWallet, + #[display(fmt = "Trezor derivation method is not supported yet!")] + Trezor, + #[display(fmt = "Unsupported error: {}", _0)] + UnsupportedError(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl From 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 { @@ -821,9 +849,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 +1017,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 { @@ -1103,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". @@ -1114,6 +1268,7 @@ pub enum WithdrawFrom { DerivationPath { derivation_path: String, }, + HDWalletAddress(StandardHDCoinAddress), } #[derive(Clone, Deserialize)] @@ -1899,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, @@ -1947,6 +2104,7 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::UnexpectedFromAddress(_) | WithdrawError::UnknownAccount { .. } | WithdrawError::UnexpectedUserAction { .. } + | WithdrawError::UnsupportedError(_) | WithdrawError::ActionNotAllowed(_) | WithdrawError::GetNftInfoError(_) | WithdrawError::AddressMismatchError { .. } @@ -2017,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 { @@ -2707,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, + } + } + + 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, } } - pub fn key_pair_or_err(&self) -> Result<&T, MmError> { - self.key_pair() - .or_mm_err(|| PrivKeyPolicyNotAllowed::HardwareWalletNotSupported) + 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())) } } @@ -3904,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/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/coins/nft.rs b/mm2src/coins/nft.rs index 3d84103f3f..c84d0cbe24 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -8,18 +8,19 @@ 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, 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 crypto::StandardHDCoinAddress; use ethereum_types::Address; use http::header::ACCEPT; use mm2_err_handle::map_to_mm::MapToMmResult; @@ -91,16 +92,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 +167,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,14 +205,18 @@ 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(()) } 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); @@ -263,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); @@ -292,12 +299,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 +485,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 +496,99 @@ 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(), + path_to_address: StandardHDCoinAddress::default(), }; 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 +598,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 +608,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 +701,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 +713,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 +728,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 +770,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 +786,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 +841,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())) } diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index d46371ed6f..c99f2d47b7 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; @@ -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( @@ -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 } } @@ -832,33 +833,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/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 90e914711c..1c4964f3a0 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -8,18 +8,18 @@ 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}; 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), }; @@ -480,11 +482,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/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 diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 5279e4fbef..33e48d1885 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -17,17 +17,18 @@ 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, 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, WithdrawFrom, + WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; @@ -49,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; @@ -103,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; @@ -221,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, @@ -261,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")] @@ -284,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()) } } @@ -345,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()))?; @@ -420,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(); @@ -428,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); @@ -458,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 { @@ -467,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| { @@ -514,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, @@ -534,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 @@ -553,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() @@ -563,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(), @@ -585,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()); @@ -621,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(), @@ -631,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 @@ -644,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(), @@ -793,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, @@ -804,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)?; @@ -849,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, @@ -883,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( @@ -938,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, @@ -948,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( @@ -995,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), @@ -1030,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( @@ -1102,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)?; @@ -1523,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(), @@ -1572,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); @@ -1585,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)) @@ -1831,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 @@ -1852,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(), @@ -1882,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()); @@ -1917,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(), @@ -1927,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 @@ -1941,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(), @@ -2104,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()) } @@ -2126,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(), @@ -2284,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) } @@ -2426,16 +2534,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<()> { @@ -2523,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] @@ -2651,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 = @@ -2776,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, @@ -2888,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, @@ -2946,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, @@ -3018,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, @@ -3212,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, @@ -3296,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, @@ -3370,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, @@ -3440,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, @@ -3492,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 f4c128114d..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, @@ -13,12 +14,12 @@ 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, - 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(), @@ -276,16 +305,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<()> { @@ -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/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 90faca52dd..ab1fee5bde 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; @@ -51,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}; @@ -67,6 +68,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}; @@ -255,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, } @@ -777,7 +779,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, @@ -833,6 +839,7 @@ pub trait UtxoTxGenerationOps { mut unsigned: TransactionInputSigner, mut data: AdditionalTxData, my_script_pub: Bytes, + dust: u64, ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)>; } @@ -939,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 /// @@ -968,6 +975,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. @@ -1341,15 +1350,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, @@ -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/bch.rs b/mm2src/coins/utxo/bch.rs index b089af276d..e85f2a8d98 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; @@ -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 } } @@ -864,13 +865,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/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/qtum.rs b/mm2src/coins/utxo/qtum.rs index 9640e66184..8a4e8b4eae 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, @@ -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 } } @@ -550,13 +551,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/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index 65efadee7f..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?; @@ -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/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/slp.rs b/mm2src/coins/utxo/slp.rs index 9d8d968b53..24ccf72448 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, @@ -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, @@ -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 } } @@ -1285,40 +1286,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<()> { @@ -1603,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() { @@ -2089,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"); @@ -2126,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"); @@ -2211,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"); 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_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 af9ba59f8a..16f778d5ff 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -16,9 +16,9 @@ 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 crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, Secp256k1Secret, - StandardHDPathError, StandardHDPathToCoin}; +use common::{now_sec, small_rng}; +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; @@ -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 { @@ -112,6 +116,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 +149,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 +174,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 +214,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. @@ -667,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/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 5d359dad9a..1a4c8e6c69 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}; @@ -32,7 +35,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}; @@ -56,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; @@ -86,15 +88,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 { @@ -705,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(), + )), } } @@ -1015,11 +1016,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 { @@ -1032,7 +1031,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?) } } @@ -1045,6 +1044,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)); @@ -1076,11 +1076,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 { @@ -1102,11 +1108,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 { @@ -1118,21 +1138,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![], @@ -1144,7 +1164,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, @@ -1177,6 +1210,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, @@ -1211,7 +1472,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)), @@ -1247,7 +1509,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 { @@ -1552,61 +1815,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( @@ -1628,59 +1903,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 @@ -2328,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)) } @@ -2469,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"), } } @@ -2596,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 @@ -3481,9 +3720,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) @@ -3962,6 +4208,11 @@ struct SwapPaymentOutputsResult { outputs: Vec, } +enum SwapPaymentType { + TakerOrMakerPayment, + DexFeeWithPremium, +} + fn generate_swap_payment_outputs( coin: T, time_lock: u32, @@ -3969,17 +4220,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 { @@ -4291,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"), } } @@ -4327,6 +4586,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_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_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 8c03e66a8c..431faaee86 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}; @@ -109,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 } } @@ -315,13 +318,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 +582,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_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 6cc4337d75..be56824f6a 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); @@ -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, @@ -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 }); @@ -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_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/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index ea0da556c1..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; @@ -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), @@ -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/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..d838db26f6 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; @@ -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}; @@ -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] @@ -324,7 +330,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") } @@ -488,7 +494,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)) @@ -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, }, } @@ -776,6 +802,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 +895,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,36 +920,23 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { ); let blocks_db = self.blocks_db().await?; - let wallet_db = WalletDbShared::new(&self) - .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? }, @@ -973,6 +992,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, @@ -1277,60 +1298,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<()> { @@ -1753,8 +1767,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 } } @@ -1907,11 +1922,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)))?; @@ -1991,11 +2012,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; @@ -2009,12 +2031,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() @@ -2022,7 +2044,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/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 c7773df4ab..698a54a60b 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::{CheckPointBlockInfo, ZcoinConsensusParams}; use crate::z_coin::z_rpc::create_wallet_db; use parking_lot::Mutex; @@ -35,25 +36,21 @@ 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>, + checkpoint_block: Option, + 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), + 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)), @@ -69,7 +66,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_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_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs index ef4929a21a..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 { @@ -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)); } @@ -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/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/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/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/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/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_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" 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_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; 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/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/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 6c5db29d00..39dbcbe6e6 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; @@ -837,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 94f1a8f6c2..922bff0623 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(), @@ -401,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", @@ -534,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, @@ -667,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, @@ -814,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, @@ -919,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", @@ -1022,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, @@ -1138,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", @@ -1231,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", @@ -1307,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", @@ -1371,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, @@ -1508,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!({ @@ -1649,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. @@ -1768,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!({ @@ -1908,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, @@ -1978,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", @@ -2030,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", @@ -2091,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, @@ -2142,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, @@ -2188,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"); @@ -2214,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 { @@ -2248,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", @@ -2319,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", @@ -2390,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", @@ -2410,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)); @@ -2447,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", @@ -2486,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)); @@ -2573,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, @@ -2596,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)); @@ -2670,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, @@ -2757,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, @@ -2828,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", @@ -3035,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); @@ -3154,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, @@ -3287,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, @@ -3409,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, @@ -3485,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, @@ -3535,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/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..dc6f915948 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); @@ -943,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 @@ -966,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! ({ @@ -1042,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"); @@ -1228,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 @@ -1287,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 @@ -1348,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, @@ -1407,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); @@ -1453,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); @@ -1501,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); @@ -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(), @@ -1709,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_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_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..3568b610ad 100644 --- a/mm2src/mm2_main/tests/integration_tests_common/mod.rs +++ b/mm2src/mm2_main/tests/integration_tests_common/mod.rs @@ -2,13 +2,14 @@ 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}; 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; @@ -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,10 +88,12 @@ pub async fn enable_z_coin_light( coin: &str, electrums: &[&str], lightwalletd_urls: &[&str], -) -> CoinActivationResult { - let init = init_z_coin_light(mm, coin, electrums, lightwalletd_urls).await; + starting_date: Option, + account: Option, +) -> 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 { @@ -122,15 +140,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/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 8accb1beba..1a5c0c8d4c 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,14 @@ 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, + None, )); let set_price_json = json!({ @@ -1456,12 +1494,14 @@ 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, + None, )); let set_price_json = json!({ @@ -1484,12 +1524,14 @@ 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, + 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 7b5e33fe8b..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()]); @@ -34,7 +38,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)); @@ -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()]); @@ -179,7 +288,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"; @@ -196,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(); @@ -205,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() { @@ -231,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()); @@ -261,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()); @@ -290,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..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()]); @@ -60,22 +58,84 @@ fn activate_z_coin_light() { ZOMBIE_TICKER, 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] +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); + 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(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] -#[ignore] 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 +143,8 @@ fn activate_z_coin_with_hd_account() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, + Some(hd_account_id), )); let actual = match activation_result.wallet_balance { @@ -109,6 +171,8 @@ fn test_z_coin_tx_history() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, + None, )); let tx_history = block_on(z_coin_tx_history(&mm, ZOMBIE_TICKER, 5, None)); @@ -352,6 +416,8 @@ fn withdraw_z_coin_light() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, + None, )); println!("{:?}", activation_result); @@ -393,11 +459,13 @@ fn trade_rick_zombie_light() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, + 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 +493,13 @@ fn trade_rick_zombie_light() { ZOMBIE_TICKER, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, + None, + 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 +551,8 @@ fn activate_pirate_light() { ARRR, PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, + None, + None, )); let balance = match activation_result.wallet_balance { 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; 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); 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..d6f1d75fbd 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -5,16 +5,16 @@ 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; +use crypto::{CryptoCtx, StandardHDCoinAddress}; use gstuff::{try_s, ERR, ERRL}; use http::{HeaderMap, StatusCode}; 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}; @@ -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 { @@ -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 @@ -455,7 +445,8 @@ pub fn atom_testnet_conf() -> Json { "account_prefix": "cosmos", "chain_id": "theta-testnet-001", }, - } + }, + "derivation_path": "m/44'/118'", }) } @@ -579,6 +570,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 +617,7 @@ pub fn eth_testnet_conf() -> Json { json!({ "coin": "ETH", "name": "ethereum", + "mm2": 1, "derivation_path": "m/44'/60'", "protocol": { "type": "ETH" @@ -1466,14 +1459,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 +1487,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 +1501,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 +1517,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 +1586,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 +1600,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 @@ -1647,19 +1666,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, @@ -1710,6 +1716,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(); @@ -1725,6 +1732,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 @@ -2213,7 +2221,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, @@ -2221,6 +2235,7 @@ pub async fn withdraw_v1(mm: &MarketMakerIt, coin: &str, to: &str, amount: &str) "coin": coin, "to": to, "amount": amount, + "from": from, })) .await .unwrap(); @@ -2234,6 +2249,7 @@ pub async fn ibc_withdraw( coin: &str, to: &str, amount: &str, + from: Option, ) -> TransactionDetails { let request = mm .rpc(&json!({ @@ -2244,7 +2260,8 @@ pub async fn ibc_withdraw( "ibc_source_channel": source_channel, "coin": coin, "to": to, - "amount": amount + "amount": amount, + "from": from, } })) .await @@ -2302,7 +2319,18 @@ 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], + 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, @@ -2316,8 +2344,12 @@ pub async fn init_z_coin_light(mm: &MarketMakerIt, coin: &str, electrums: &[&str "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),