From 45c91189cd7f71766a167ad042c1a51ddd5de631 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:39:38 +0200 Subject: [PATCH 1/6] fix(hd-wallet): correctly display evm addr in `get_new_address` response (#2264) --- mm2src/coins/rpc_command/get_new_address.rs | 7 ++--- .../tests/docker_tests/docker_tests_inner.rs | 27 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/rpc_command/get_new_address.rs b/mm2src/coins/rpc_command/get_new_address.rs index e37efb730f..35796de9c2 100644 --- a/mm2src/coins/rpc_command/get_new_address.rs +++ b/mm2src/coins/rpc_command/get_new_address.rs @@ -472,7 +472,7 @@ pub(crate) mod common_impl { Ok(GetNewAddressResponse { new_address: HDAddressBalance { - address: address.to_string(), + address: coin.address_formatter()(&address), derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, balance, @@ -510,13 +510,14 @@ pub(crate) mod common_impl { let address = hd_address.address(); let balance = coin.known_address_balance(&address).await?; - coin.prepare_addresses_for_balance_stream_if_enabled(HashSet::from([address.to_string()])) + let formatted_address = coin.address_formatter()(&address); + coin.prepare_addresses_for_balance_stream_if_enabled(HashSet::from([formatted_address.clone()])) .await .map_err(|e| GetNewAddressRpcError::FailedScripthashSubscription(e.to_string()))?; Ok(GetNewAddressResponse { new_address: HDAddressBalance { - address: address.to_string(), + address: formatted_address, derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, balance, 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 d89b456874..adc2da8574 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -17,11 +17,11 @@ use crypto::privkey::key_pair_from_seed; use crypto::{CryptoCtx, DerivationPath, KeyPairPolicy}; use http::StatusCode; use mm2_number::{BigDecimal, BigRational, MmNumber}; -use mm2_test_helpers::for_tests::{check_my_swap_status_amounts, disable_coin, disable_coin_err, enable_eth_coin, - enable_eth_with_tokens_v2, erc20_dev_conf, eth_dev_conf, get_locked_amount, - kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, mycoin_conf, set_price, start_swaps, - wait_for_swap_contract_negotiation, wait_for_swap_negotiation_failure, - MarketMakerIt, Mm2TestConf}; +use mm2_test_helpers::for_tests::{account_balance, check_my_swap_status_amounts, disable_coin, disable_coin_err, + enable_eth_coin, enable_eth_with_tokens_v2, erc20_dev_conf, eth_dev_conf, + get_locked_amount, get_new_address, kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, + mycoin_conf, set_price, start_swaps, wait_for_swap_contract_negotiation, + wait_for_swap_negotiation_failure, MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::{get_passphrase, structs::*}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -5258,7 +5258,7 @@ fn test_sell_min_volume_dust() { } #[test] -fn test_enable_eth_erc20_coins_with_enable_hd() { +fn test_eth_erc20_hd() { const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); @@ -5336,6 +5336,21 @@ fn test_enable_eth_erc20_coins_with_enable_hd() { assert!(account.addresses[0].balance.contains_key("ETH")); assert!(account.addresses[0].balance.contains_key("ERC20DEV")); + let get_new_address = block_on(get_new_address(&mm_hd, "ETH", 0, Some(Bip44Chain::External))); + assert!(get_new_address.new_address.balance.contains_key("ETH")); + // Make sure balance is returned for any token enabled with ETH as platform coin + assert!(get_new_address.new_address.balance.contains_key("ERC20DEV")); + assert_eq!( + get_new_address.new_address.address, + "0x4249E165a68E4FF9C41B1C3C3b4245c30ecB43CC" + ); + // Make sure that the address is also added to tokens + let account_balance = block_on(account_balance(&mm_hd, "ERC20DEV", 0, Bip44Chain::External)); + assert_eq!( + account_balance.addresses[2].address, + "0x4249E165a68E4FF9C41B1C3C3b4245c30ecB43CC" + ); + block_on(mm_hd.stop()).unwrap(); // Enable HD account 77, change address 0, index 7 From 4d0f812ac5089364de51f896e282c419019ac4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 14 Nov 2024 10:44:33 +0300 Subject: [PATCH 2/6] use safer subtraction on healthcheck expiration check (#2272) Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 5e9db51111..820a5ad619 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -114,7 +114,7 @@ impl HealthcheckMessage { let now_secs = u64::try_from(Utc::now().timestamp()) .map_err(|e| SignValidationError::Internal { reason: e.to_string() })?; - let remaining_expiration_secs = self.data.expires_at_secs - now_secs; + let remaining_expiration_secs = self.data.expires_at_secs.saturating_sub(now_secs); if remaining_expiration_secs == 0 { return Err(SignValidationError::Expired { From 22719c0cebf4f9fbb89c3f9d3e0760a32c91c0b8 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Mon, 18 Nov 2024 20:12:11 +0200 Subject: [PATCH 3/6] feat(tokens): custom token activation for evm (#2141) Adds support for enabling custom EVM (ERC20, PLG20, etc..) tokens without requiring them to be in the coins config. This allows users to interact with any ERC20 token by providing the contract address. --- mm2src/coins/eth.rs | 37 +-- mm2src/coins/eth/erc20.rs | 107 +++++++ mm2src/coins/eth/eth_balance_events.rs | 8 +- mm2src/coins/eth/v2_activation.rs | 42 ++- mm2src/coins/lp_coins.rs | 111 ++++++- mm2src/coins/tendermint/tendermint_coin.rs | 4 + mm2src/coins/tendermint/tendermint_token.rs | 4 + mm2src/coins/z_coin.rs | 6 +- .../src/erc20_token_activation.rs | 12 +- .../src/eth_with_token_activation.rs | 14 +- .../src/init_erc20_token_activation.rs | 21 +- mm2src/coins_activation/src/init_token.rs | 24 +- mm2src/coins_activation/src/l2/init_l2.rs | 2 +- .../coins_activation/src/l2/init_l2_error.rs | 9 +- .../src/platform_coin_with_tokens.rs | 39 ++- mm2src/coins_activation/src/prelude.rs | 67 +++- .../src/slp_token_activation.rs | 3 + .../standalone_coin/init_standalone_coin.rs | 2 +- .../init_standalone_coin_error.rs | 10 +- .../src/tendermint_token_activation.rs | 3 + mm2src/coins_activation/src/token.rs | 30 +- mm2src/mm2_bin_lib/src/mm2_native_lib.rs | 2 +- mm2src/mm2_main/src/rpc.rs | 4 +- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 12 +- .../src/rpc/dispatcher/dispatcher_legacy.rs | 2 +- mm2src/mm2_main/src/rpc/lp_commands/db_id.rs | 18 ++ .../{lp_commands_legacy.rs => legacy.rs} | 0 mm2src/mm2_main/src/rpc/lp_commands/mod.rs | 5 + mm2src/mm2_main/src/rpc/lp_commands/pubkey.rs | 48 +++ mm2src/mm2_main/src/rpc/lp_commands/tokens.rs | 95 ++++++ .../lp_commands/{lp_commands.rs => trezor.rs} | 58 +--- .../tests/docker_tests/docker_tests_inner.rs | 146 +-------- .../tests/docker_tests/eth_docker_tests.rs | 288 +++++++++++++++++- mm2src/mm2_test_helpers/src/for_tests.rs | 97 ++++++ mm2src/mm2_test_helpers/src/structs.rs | 42 +++ 35 files changed, 1064 insertions(+), 308 deletions(-) create mode 100644 mm2src/coins/eth/erc20.rs create mode 100644 mm2src/mm2_main/src/rpc/lp_commands/db_id.rs rename mm2src/mm2_main/src/rpc/lp_commands/{lp_commands_legacy.rs => legacy.rs} (100%) create mode 100644 mm2src/mm2_main/src/rpc/lp_commands/mod.rs create mode 100644 mm2src/mm2_main/src/rpc/lp_commands/pubkey.rs create mode 100644 mm2src/mm2_main/src/rpc/lp_commands/tokens.rs rename mm2src/mm2_main/src/rpc/lp_commands/{lp_commands.rs => trezor.rs} (52%) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 53e3b472a0..7934bdf485 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -158,6 +158,9 @@ pub(crate) use eip1559_gas_fee::FeePerGasEstimated; use eip1559_gas_fee::{BlocknativeGasApiCaller, FeePerGasSimpleEstimator, GasApiConfig, GasApiProvider, InfuraGasApiCaller}; +pub mod erc20; +use erc20::get_token_decimals; + pub(crate) mod eth_swap_v2; use eth_swap_v2::{EthPaymentType, PaymentMethod}; @@ -883,7 +886,7 @@ pub struct EthCoinImpl { /// and unlocked once the transaction is confirmed. This prevents nonce conflicts when multiple transactions /// are initiated concurrently from the same address. address_nonce_locks: Arc>>>>, - erc20_tokens_infos: Arc>>, + erc20_tokens_infos: Arc>>, /// Stores information about NFTs owned by the user. Each entry in the HashMap is uniquely identified by a composite key /// consisting of the token address and token ID, separated by a comma. This field is essential for tracking the NFT assets /// information (chain & contract type, amount etc.), where ownership and amount, in ERC1155 case, might change over time. @@ -907,7 +910,7 @@ pub struct Web3Instance { /// Information about a token that follows the ERC20 protocol on an EVM-based network. #[derive(Clone, Debug)] -pub struct Erc20TokenInfo { +pub struct Erc20TokenDetails { /// The contract address of the token on the EVM-based network. pub token_address: Address, /// The number of decimal places the token uses. @@ -1068,14 +1071,14 @@ impl EthCoinImpl { } } - pub fn add_erc_token_info(&self, ticker: String, info: Erc20TokenInfo) { + pub fn add_erc_token_info(&self, ticker: String, info: Erc20TokenDetails) { self.erc20_tokens_infos.lock().unwrap().insert(ticker, info); } /// # Warning /// Be very careful using this function since it returns dereferenced clone /// of value behind the MutexGuard and makes it non-thread-safe. - pub fn get_erc_tokens_infos(&self) -> HashMap { + pub fn get_erc_tokens_infos(&self) -> HashMap { let guard = self.erc20_tokens_infos.lock().unwrap(); (*guard).clone() } @@ -6318,32 +6321,6 @@ fn signed_tx_from_web3_tx(transaction: Web3Transaction) -> Result, token_addr: Address) -> Result { - let function = try_s!(ERC20_CONTRACT.function("decimals")); - let data = try_s!(function.encode_input(&[])); - let request = CallRequest { - from: Some(Address::default()), - to: Some(token_addr), - gas: None, - gas_price: None, - value: Some(0.into()), - data: Some(data.into()), - ..CallRequest::default() - }; - - let res = web3 - .eth() - .call(request, Some(BlockId::Number(BlockNumber::Latest))) - .map_err(|e| ERRL!("{}", e)) - .await?; - let tokens = try_s!(function.decode_output(&res.0)); - let decimals = match tokens[0] { - Token::Uint(dec) => dec.as_u64(), - _ => return ERR!("Invalid decimals type {:?}", tokens), - }; - Ok(decimals as u8) -} - pub fn valid_addr_from_str(addr_str: &str) -> Result { let addr = try_s!(addr_from_str(addr_str)); if !is_valid_checksum_addr(addr_str) { diff --git a/mm2src/coins/eth/erc20.rs b/mm2src/coins/eth/erc20.rs new file mode 100644 index 0000000000..75f7033fda --- /dev/null +++ b/mm2src/coins/eth/erc20.rs @@ -0,0 +1,107 @@ +use crate::eth::web3_transport::Web3Transport; +use crate::eth::{EthCoin, ERC20_CONTRACT}; +use crate::{CoinsContext, MmCoinEnum}; +use ethabi::Token; +use ethereum_types::Address; +use futures_util::TryFutureExt; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::mm_error::MmResult; +use web3::types::{BlockId, BlockNumber, CallRequest}; +use web3::{Transport, Web3}; + +async fn call_erc20_function( + web3: &Web3, + token_addr: Address, + function_name: &str, +) -> Result, String> { + let function = try_s!(ERC20_CONTRACT.function(function_name)); + let data = try_s!(function.encode_input(&[])); + let request = CallRequest { + from: Some(Address::default()), + to: Some(token_addr), + gas: None, + gas_price: None, + value: Some(0.into()), + data: Some(data.into()), + ..CallRequest::default() + }; + + let res = web3 + .eth() + .call(request, Some(BlockId::Number(BlockNumber::Latest))) + .map_err(|e| ERRL!("{}", e)) + .await?; + function.decode_output(&res.0).map_err(|e| ERRL!("{}", e)) +} + +pub(crate) async fn get_token_decimals(web3: &Web3, token_addr: Address) -> Result { + let tokens = call_erc20_function(web3, token_addr, "decimals").await?; + let Some(token) = tokens.into_iter().next() else { + return ERR!("No value returned from decimals() call"); + }; + let Token::Uint(dec) = token else { + return ERR!("Expected Uint token for decimals, got {:?}", token); + }; + Ok(dec.as_u64() as u8) +} + +async fn get_token_symbol(coin: &EthCoin, token_addr: Address) -> Result { + let web3 = try_s!(coin.web3().await); + let tokens = call_erc20_function(&web3, token_addr, "symbol").await?; + let Some(token) = tokens.into_iter().next() else { + return ERR!("No value returned from symbol() call"); + }; + let Token::String(symbol) = token else { + return ERR!("Expected String token for symbol, got {:?}", token); + }; + Ok(symbol) +} + +#[derive(Serialize)] +pub struct Erc20TokenInfo { + pub symbol: String, + pub decimals: u8, +} + +pub async fn get_erc20_token_info(coin: &EthCoin, token_addr: Address) -> Result { + let symbol = get_token_symbol(coin, token_addr).await?; + let web3 = try_s!(coin.web3().await); + let decimals = get_token_decimals(&web3, token_addr).await?; + Ok(Erc20TokenInfo { symbol, decimals }) +} + +/// Finds if an ERC20 token is in coins config by its contract address and returns its ticker. +pub fn get_erc20_ticker_by_contract_address(ctx: &MmArc, platform: &str, contract_address: &str) -> Option { + ctx.conf["coins"].as_array()?.iter().find_map(|coin| { + let protocol = coin.get("protocol")?; + let protocol_type = protocol.get("type")?.as_str()?; + if protocol_type != "ERC20" { + return None; + } + let protocol_data = protocol.get("protocol_data")?; + let coin_platform = protocol_data.get("platform")?.as_str()?; + let coin_contract_address = protocol_data.get("contract_address")?.as_str()?; + + if coin_platform == platform && coin_contract_address == contract_address { + coin.get("coin")?.as_str().map(|s| s.to_string()) + } else { + None + } + }) +} + +/// Finds an enabled ERC20 token by its contract address and returns it as `MmCoinEnum`. +pub async fn get_enabled_erc20_by_contract( + ctx: &MmArc, + contract_address: Address, +) -> MmResult, String> { + let cctx = CoinsContext::from_ctx(ctx)?; + let coins = cctx.coins.lock().await; + + Ok(coins.values().find_map(|coin| match &coin.inner { + MmCoinEnum::EthCoin(eth_coin) if eth_coin.erc20_token_address() == Some(contract_address) => { + Some(coin.inner.clone()) + }, + _ => None, + })) +} diff --git a/mm2src/coins/eth/eth_balance_events.rs b/mm2src/coins/eth/eth_balance_events.rs index 231aa68507..0cc798ad7e 100644 --- a/mm2src/coins/eth/eth_balance_events.rs +++ b/mm2src/coins/eth/eth_balance_events.rs @@ -14,7 +14,7 @@ use mm2_number::BigDecimal; use std::collections::{HashMap, HashSet}; use super::EthCoin; -use crate::{eth::{u256_to_big_decimal, Erc20TokenInfo}, +use crate::{eth::{u256_to_big_decimal, Erc20TokenDetails}, BalanceError, CoinWithDerivationMethod, MmCoin}; struct BalanceData { @@ -40,9 +40,9 @@ async fn get_all_balance_results_concurrently(coin: &EthCoin, addresses: HashSet // // Unlike tokens, the platform coin length is constant (=1). Instead of creating a generic // type and mapping the platform coin and the entire token list (which can grow at any time), we map - // the platform coin to Erc20TokenInfo so that we can use the token list right away without + // the platform coin to Erc20TokenDetails so that we can use the token list right away without // additional mapping. - tokens.insert(coin.ticker.clone(), Erc20TokenInfo { + tokens.insert(coin.ticker.clone(), Erc20TokenDetails { // This is a dummy value, since there is no token address for the platform coin. // In the fetch_balance function, we check if the token_ticker is equal to this // coin's ticker to avoid using token_address to fetch the balance @@ -72,7 +72,7 @@ async fn fetch_balance( coin: &EthCoin, address: Address, token_ticker: String, - info: &Erc20TokenInfo, + info: &Erc20TokenDetails, ) -> Result { let (balance_as_u256, decimals) = if token_ticker == coin.ticker { ( diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index d96d11a95c..cee2313ba2 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -1,4 +1,5 @@ use super::*; +use crate::eth::erc20::{get_enabled_erc20_by_contract, get_token_decimals}; use crate::eth::web3_transport::http_transport::HttpTransport; use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDPathAccountToAddressId, HDWalletCoinStorage, HDWalletStorageError, DEFAULT_GAP_LIMIT}; @@ -62,6 +63,8 @@ pub enum EthActivationV2Error { HwError(HwRpcError), #[display(fmt = "Hardware wallet must be called within rpc task framework")] InvalidHardwareWalletCall, + #[display(fmt = "Custom token error: {}", _0)] + CustomTokenError(CustomTokenError), } impl From for EthActivationV2Error { @@ -93,6 +96,7 @@ impl From for EthActivationV2Error { EthActivationV2Error::UnexpectedDerivationMethod(err) }, EthTokenActivationError::PrivKeyPolicyNotAllowed(e) => EthActivationV2Error::PrivKeyPolicyNotAllowed(e), + EthTokenActivationError::CustomTokenError(e) => EthActivationV2Error::CustomTokenError(e), } } } @@ -211,6 +215,7 @@ pub enum EthTokenActivationError { Transport(String), UnexpectedDerivationMethod(UnexpectedDerivationMethod), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + CustomTokenError(CustomTokenError), } impl From for EthTokenActivationError { @@ -376,9 +381,11 @@ pub struct NftProtocol { impl EthCoin { pub async fn initialize_erc20_token( &self, + ticker: String, activation_params: Erc20TokenActivationRequest, + token_conf: Json, protocol: Erc20Protocol, - ticker: String, + is_custom: bool, ) -> MmResult { // TODO // Check if ctx is required. @@ -387,9 +394,24 @@ impl EthCoin { .ok_or_else(|| String::from("No context")) .map_err(EthTokenActivationError::InternalError)?; - let conf = coin_conf(&ctx, &ticker); + // Todo: when custom token config storage is added, this might not be needed + // `is_custom` was added to avoid this unnecessary check for non-custom tokens + if is_custom { + match get_enabled_erc20_by_contract(&ctx, protocol.token_addr).await { + Ok(Some(token)) => { + return MmError::err(EthTokenActivationError::CustomTokenError( + CustomTokenError::TokenWithSameContractAlreadyActivated { + ticker: token.ticker().to_string(), + contract_address: display_eth_address(&protocol.token_addr), + }, + )); + }, + Ok(None) => {}, + Err(e) => return MmError::err(EthTokenActivationError::InternalError(e.to_string())), + } + } - let decimals = match conf["decimals"].as_u64() { + let decimals = match token_conf["decimals"].as_u64() { None | Some(0) => get_token_decimals( &self .web3() @@ -404,7 +426,11 @@ impl EthCoin { let required_confirmations = activation_params .required_confirmations - .unwrap_or_else(|| conf["required_confirmations"].as_u64().unwrap_or(1)) + .unwrap_or_else(|| { + token_conf["required_confirmations"] + .as_u64() + .unwrap_or(self.required_confirmations()) + }) .into(); // Create an abortable system linked to the `MmCtx` so if the app is stopped on `MmArc::stop`, @@ -415,11 +441,11 @@ impl EthCoin { platform: protocol.platform, token_addr: protocol.token_addr, }; - let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &conf, &coin_type).await?; - let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &conf, &coin_type).await?; - let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&conf) + let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &token_conf, &coin_type).await?; + let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &token_conf, &coin_type).await?; + let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&token_conf) .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; - let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(&conf) + let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(&token_conf) .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; let token = EthCoinImpl { diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 9b0e43fc2e..5e40eb9221 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -54,7 +54,7 @@ use crypto::{derive_secp256k1_secret, Bip32Error, Bip44Chain, CryptoCtx, CryptoC Secp256k1ExtendedPublicKey, Secp256k1Secret, WithHwRpcError}; use derive_more::Display; use enum_derives::{EnumFromStringify, EnumFromTrait}; -use ethereum_types::H256; +use ethereum_types::{H256, U256}; use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures::{FutureExt, TryFutureExt}; @@ -218,11 +218,11 @@ use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut, Vali pub mod coins_tests; pub mod eth; +use eth::erc20::get_erc20_ticker_by_contract_address; use eth::eth_swap_v2::{PaymentStatusErr, PrepareTxDataError, ValidatePaymentV2Err}; use eth::GetValidEthWithdrawAddError; use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetailsErr, EthTxFeeDetails, GetEthAddressError, SignedEthTx}; -use ethereum_types::U256; pub mod hd_wallet; use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, @@ -3277,6 +3277,10 @@ pub trait MmCoin: /// The coin can be initialized, but it cannot participate in the swaps. fn wallet_only(&self, ctx: &MmArc) -> bool { let coin_conf = coin_conf(ctx, self.ticker()); + // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only + if coin_conf.is_null() { + return true; + } coin_conf["wallet_only"].as_bool().unwrap_or(false) } @@ -4253,6 +4257,97 @@ pub enum CoinProtocol { }, } +#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] +pub enum CustomTokenError { + #[display( + fmt = "Token with the same ticker already exists in coins configs, ticker in config: {}", + ticker_in_config + )] + DuplicateTickerInConfig { ticker_in_config: String }, + #[display( + fmt = "Token with the same contract address already exists in coins configs, ticker in config: {}", + ticker_in_config + )] + DuplicateContractInConfig { ticker_in_config: String }, + #[display( + fmt = "Token is already activated, ticker: {}, contract address: {}", + ticker, + contract_address + )] + TokenWithSameContractAlreadyActivated { ticker: String, contract_address: String }, +} + +impl CoinProtocol { + /// Returns the platform coin associated with the coin protocol, if any. + pub fn platform(&self) -> Option<&str> { + match self { + CoinProtocol::QRC20 { platform, .. } + | CoinProtocol::ERC20 { platform, .. } + | CoinProtocol::SLPTOKEN { platform, .. } + | CoinProtocol::NFT { platform, .. } => Some(platform), + CoinProtocol::TENDERMINTTOKEN(info) => Some(&info.platform), + #[cfg(not(target_arch = "wasm32"))] + CoinProtocol::LIGHTNING { platform, .. } => Some(platform), + CoinProtocol::UTXO + | CoinProtocol::QTUM + | CoinProtocol::ETH + | CoinProtocol::BCH { .. } + | CoinProtocol::TENDERMINT(_) + | CoinProtocol::ZHTLC(_) => None, + #[cfg(feature = "enable-sia")] + CoinProtocol::SIA => None, + } + } + + /// Returns the contract address associated with the coin, if any. + pub fn contract_address(&self) -> Option<&str> { + match self { + CoinProtocol::QRC20 { contract_address, .. } | CoinProtocol::ERC20 { contract_address, .. } => { + Some(contract_address) + }, + CoinProtocol::SLPTOKEN { .. } + | CoinProtocol::UTXO + | CoinProtocol::QTUM + | CoinProtocol::ETH + | CoinProtocol::BCH { .. } + | CoinProtocol::TENDERMINT(_) + | CoinProtocol::TENDERMINTTOKEN(_) + | CoinProtocol::ZHTLC(_) + | CoinProtocol::NFT { .. } => None, + #[cfg(not(target_arch = "wasm32"))] + CoinProtocol::LIGHTNING { .. } => None, + #[cfg(feature = "enable-sia")] + CoinProtocol::SIA => None, + } + } + + /// Several checks to be preformed when a custom token is being activated to check uniqueness among other things. + #[allow(clippy::result_large_err)] + pub fn custom_token_validations(&self, ctx: &MmArc) -> MmResult<(), CustomTokenError> { + let CoinProtocol::ERC20 { + platform, + contract_address, + } = self + else { + return Ok(()); + }; + + // Check if there is a token with the same contract address in the config. + // If there is, return an error as the user should use this token instead of activating a custom one. + // This is necessary as we will create an orderbook for this custom token using the contract address, + // if it is duplicated in config, we will have two orderbooks one using the ticker and one using the contract address. + // Todo: We should use the contract address for orderbook topics instead of the ticker once we make custom tokens non-wallet only. + // If a coin is added to the config later, users who added it as a custom token and did not update will not see the orderbook. + if let Some(existing_ticker) = get_erc20_ticker_by_contract_address(ctx, platform, contract_address) { + return Err(MmError::new(CustomTokenError::DuplicateContractInConfig { + ticker_in_config: existing_ticker, + })); + } + + Ok(()) + } +} + /// Common methods to handle the connection events. /// /// Note that the handler methods are sync and shouldn't take long time executing, otherwise it will hurt the performance. @@ -4425,10 +4520,20 @@ pub fn coin_conf(ctx: &MmArc, ticker: &str) -> Json { } } -pub fn is_wallet_only_conf(conf: &Json) -> bool { conf["wallet_only"].as_bool().unwrap_or(false) } +pub fn is_wallet_only_conf(conf: &Json) -> bool { + // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only + if conf.is_null() { + return true; + } + conf["wallet_only"].as_bool().unwrap_or(false) +} pub fn is_wallet_only_ticker(ctx: &MmArc, ticker: &str) -> bool { let coin_conf = coin_conf(ctx, ticker); + // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only + if coin_conf.is_null() { + return true; + } coin_conf["wallet_only"].as_bool().unwrap_or(false) } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index fd1396724f..323637599d 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2162,6 +2162,10 @@ impl MmCoin for TendermintCoin { fn wallet_only(&self, ctx: &MmArc) -> bool { let coin_conf = crate::coin_conf(ctx, self.ticker()); + // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only + if coin_conf.is_null() { + return true; + } let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); wallet_only_conf || self.is_keplr_from_ledger diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 3ddee75ebc..e5cc90f895 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -491,6 +491,10 @@ impl MmCoin for TendermintToken { fn wallet_only(&self, ctx: &MmArc) -> bool { let coin_conf = crate::coin_conf(ctx, self.ticker()); + // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only + if coin_conf.is_null() { + return true; + } let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); wallet_only_conf || self.platform_coin.is_keplr_from_ledger diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index d8082ea73f..b8c7c8c944 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -139,7 +139,7 @@ cfg_native!( const SAPLING_OUTPUT_EXPECTED_HASH: &str = "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"; ); -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ZcoinConsensusParams { // we don't support coins without overwinter and sapling active so these are mandatory overwinter_activation_height: u32, @@ -156,7 +156,7 @@ pub struct ZcoinConsensusParams { b58_script_address_prefix: [u8; 2], } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct CheckPointBlockInfo { height: u32, hash: H256Json, @@ -164,7 +164,7 @@ pub struct CheckPointBlockInfo { sapling_tree: BytesJson, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ZcoinProtocolInfo { consensus_params: ZcoinConsensusParams, check_point_block: Option, diff --git a/mm2src/coins_activation/src/erc20_token_activation.rs b/mm2src/coins_activation/src/erc20_token_activation.rs index 0f7f4edbd9..77970284b4 100644 --- a/mm2src/coins_activation/src/erc20_token_activation.rs +++ b/mm2src/coins_activation/src/erc20_token_activation.rs @@ -10,6 +10,7 @@ use coins::{eth::{v2_activation::{Erc20Protocol, EthTokenActivationError}, use common::Future01CompatExt; use mm2_err_handle::prelude::*; use serde::Serialize; +use serde_json::Value as Json; use std::collections::HashMap; #[derive(Debug, Serialize)] @@ -43,6 +44,7 @@ impl From for EnableTokenError { EthTokenActivationError::InvalidPayload(e) => EnableTokenError::InvalidPayload(e), EthTokenActivationError::UnexpectedDerivationMethod(e) => EnableTokenError::UnexpectedDerivationMethod(e), EthTokenActivationError::PrivKeyPolicyNotAllowed(e) => EnableTokenError::PrivKeyPolicyNotAllowed(e), + EthTokenActivationError::CustomTokenError(e) => EnableTokenError::CustomTokenError(e), } } } @@ -133,13 +135,21 @@ impl TokenActivationOps for EthCoin { ticker: String, platform_coin: Self::PlatformCoin, activation_params: Self::ActivationParams, + token_conf: Json, protocol_conf: Self::ProtocolInfo, + is_custom: bool, ) -> Result<(Self, Self::ActivationResult), MmError> { match activation_params { EthTokenActivationParams::Erc20(erc20_init_params) => match protocol_conf { EthTokenProtocol::Erc20(erc20_protocol) => { let token = platform_coin - .initialize_erc20_token(erc20_init_params, erc20_protocol, ticker.clone()) + .initialize_erc20_token( + ticker.clone(), + erc20_init_params, + token_conf, + erc20_protocol, + is_custom, + ) .await?; let address = display_eth_address(&token.derivation_method().single_addr_or_err().await?); diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 62e8fe4c4c..7bc62b444a 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -12,7 +12,7 @@ use coins::coin_balance::{CoinBalanceReport, EnableCoinBalanceOps}; use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationRequest, EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy}; use coins::eth::v2_activation::{EthTokenActivationError, NftActivationRequest, NftProviderEnum}; -use coins::eth::{display_eth_address, Erc20TokenInfo, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; +use coins::eth::{display_eth_address, Erc20TokenDetails, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; use coins::hd_wallet::RpcTaskXPubExtractor; use coins::my_tx_history_v2::TxHistoryStorage; use coins::nft::nft_structs::NftInfo; @@ -85,6 +85,7 @@ impl From for EnablePlatformCoinWithTokensError { EthActivationV2Error::InvalidHardwareWalletCall => EnablePlatformCoinWithTokensError::Internal( "Hardware wallet must be used within rpc task manager".to_string(), ), + EthActivationV2Error::CustomTokenError(e) => EnablePlatformCoinWithTokensError::CustomTokenError(e), } } } @@ -118,6 +119,7 @@ impl From for InitTokensAsMmCoinsError { InitTokensAsMmCoinsError::UnexpectedDerivationMethod(e) }, EthTokenActivationError::PrivKeyPolicyNotAllowed(e) => InitTokensAsMmCoinsError::Internal(e.to_string()), + EthTokenActivationError::CustomTokenError(e) => InitTokensAsMmCoinsError::CustomTokenError(e), } } } @@ -143,7 +145,13 @@ impl TokenInitializer for Erc20Initializer { for param in activation_params { let token: EthCoin = self .platform_coin - .initialize_erc20_token(param.activation_request, param.protocol, param.ticker) + .initialize_erc20_token( + param.ticker, + param.activation_request, + param.conf, + param.protocol, + param.is_custom, + ) .await?; tokens.push(token); } @@ -183,7 +191,7 @@ impl RegisterTokenInfo for EthCoin { return; } - self.add_erc_token_info(token.ticker().to_string(), Erc20TokenInfo { + self.add_erc_token_info(token.ticker().to_string(), Erc20TokenDetails { token_address: token.erc20_token_address().unwrap(), decimals: token.decimals(), }); diff --git a/mm2src/coins_activation/src/init_erc20_token_activation.rs b/mm2src/coins_activation/src/init_erc20_token_activation.rs index de322c9ee5..f162cb1754 100644 --- a/mm2src/coins_activation/src/init_erc20_token_activation.rs +++ b/mm2src/coins_activation/src/init_erc20_token_activation.rs @@ -7,7 +7,7 @@ use coins::coin_balance::{EnableCoinBalanceError, EnableCoinBalanceOps}; use coins::eth::v2_activation::{Erc20Protocol, EthTokenActivationError, InitErc20TokenActivationRequest}; use coins::eth::EthCoin; use coins::hd_wallet::RpcTaskXPubExtractor; -use coins::{MarketCoinOps, MmCoin, RegisterCoinError}; +use coins::{CustomTokenError, MarketCoinOps, MmCoin, RegisterCoinError}; use common::Future01CompatExt; use crypto::HwRpcError; use derive_more::Display; @@ -17,6 +17,7 @@ use mm2_err_handle::prelude::*; use rpc_task::RpcTaskError; use ser_error_derive::SerializeErrorType; use serde_derive::Serialize; +use serde_json::Value as Json; use std::time::Duration; pub type Erc20TokenTaskManagerShared = InitTokenTaskManagerShared; @@ -38,6 +39,8 @@ pub enum InitErc20Error { Transport(String), #[display(fmt = "Internal error: {}", _0)] Internal(String), + #[display(fmt = "Custom token error: {}", _0)] + CustomTokenError(CustomTokenError), } impl From for InitTokenError { @@ -45,13 +48,16 @@ impl From for InitTokenError { match e { InitErc20Error::HwError(hw) => InitTokenError::HwError(hw), InitErc20Error::TaskTimedOut { duration } => InitTokenError::TaskTimedOut { duration }, - InitErc20Error::TokenIsAlreadyActivated { ticker } => InitTokenError::TokenIsAlreadyActivated { ticker }, + InitErc20Error::TokenIsAlreadyActivated { ticker, .. } => { + InitTokenError::TokenIsAlreadyActivated { ticker } + }, InitErc20Error::TokenCreationError { ticker, error } => { InitTokenError::TokenCreationError { ticker, error } }, InitErc20Error::CouldNotFetchBalance(error) => InitTokenError::CouldNotFetchBalance(error), InitErc20Error::Transport(transport) => InitTokenError::Transport(transport), InitErc20Error::Internal(internal) => InitTokenError::Internal(internal), + InitErc20Error::CustomTokenError(error) => InitTokenError::CustomTokenError(error), } } } @@ -66,6 +72,7 @@ impl From for InitErc20Error { | EthTokenActivationError::CouldNotFetchBalance(_) | EthTokenActivationError::InvalidPayload(_) | EthTokenActivationError::Transport(_) => InitErc20Error::Transport(e.to_string()), + EthTokenActivationError::CustomTokenError(e) => InitErc20Error::CustomTokenError(e), } } } @@ -118,11 +125,19 @@ impl InitTokenActivationOps for EthCoin { ticker: String, platform_coin: Self::PlatformCoin, activation_request: &Self::ActivationRequest, + token_conf: Json, protocol_conf: Self::ProtocolInfo, _task_handle: InitTokenTaskHandleShared, + is_custom: bool, ) -> Result> { let token = platform_coin - .initialize_erc20_token(activation_request.clone().into(), protocol_conf, ticker) + .initialize_erc20_token( + ticker, + activation_request.clone().into(), + token_conf, + protocol_conf, + is_custom, + ) .await?; Ok(token) diff --git a/mm2src/coins_activation/src/init_token.rs b/mm2src/coins_activation/src/init_token.rs index dbc03b1754..01d47b3656 100644 --- a/mm2src/coins_activation/src/init_token.rs +++ b/mm2src/coins_activation/src/init_token.rs @@ -5,7 +5,8 @@ use crate::prelude::{coin_conf_with_protocol, CoinConfWithProtocolError, Current use crate::token::TokenProtocolParams; use async_trait::async_trait; use coins::coin_balance::CoinBalanceReport; -use coins::{lp_coinfind, lp_coinfind_or_err, CoinBalanceMap, CoinProtocol, CoinsContext, MmCoinEnum, RegisterCoinError}; +use coins::{lp_coinfind, lp_coinfind_or_err, CoinBalanceMap, CoinProtocol, CoinsContext, CustomTokenError, MmCoinEnum, + RegisterCoinError}; use common::{log, HttpStatusCode, StatusCode, SuccessResponse}; use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use crypto::HwRpcError; @@ -19,6 +20,7 @@ use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTa RpcTaskTypes, TaskId}; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; +use serde_json::Value as Json; use std::time::Duration; pub type InitTokenResponse = InitRpcTaskResponse; @@ -37,6 +39,7 @@ pub type CancelInitTokenError = CancelRpcTaskError; #[derive(Debug, Deserialize, Clone)] pub struct InitTokenReq { ticker: String, + protocol: Option, activation_params: T, } @@ -65,8 +68,10 @@ pub trait InitTokenActivationOps: Into + TokenOf + Clone + Send + Sy ticker: String, platform_coin: Self::PlatformCoin, activation_request: &Self::ActivationRequest, + token_conf: Json, protocol_conf: Self::ProtocolInfo, task_handle: InitTokenTaskHandleShared, + is_custom: bool, ) -> Result>; /// Returns the result of the token activation. @@ -94,7 +99,8 @@ where return MmError::err(InitTokenError::TokenIsAlreadyActivated { ticker: request.ticker }); } - let (_, token_protocol): (_, Token::ProtocolInfo) = coin_conf_with_protocol(&ctx, &request.ticker)?; + let (token_conf, token_protocol): (_, Token::ProtocolInfo) = + coin_conf_with_protocol(&ctx, &request.ticker, request.protocol.clone())?; let platform_coin = lp_coinfind_or_err(&ctx, token_protocol.platform_coin_ticker()) .await @@ -111,6 +117,7 @@ where let task = InitTokenTask:: { ctx, request, + token_conf, token_protocol, platform_coin, }; @@ -174,6 +181,7 @@ pub async fn cancel_init_token( pub struct InitTokenTask { ctx: MmArc, request: InitTokenReq, + token_conf: Json, token_protocol: Token::ProtocolInfo, platform_coin: Token::PlatformCoin, } @@ -210,8 +218,10 @@ where ticker.clone(), self.platform_coin.clone(), &self.request.activation_params, + self.token_conf.clone(), self.token_protocol.clone(), task_handle.clone(), + self.request.protocol.is_some(), ) .await?; @@ -297,8 +307,8 @@ pub enum InitTokenError { TokenConfigIsNotFound(String), #[display(fmt = "Token {} protocol parsing failed: {}", ticker, error)] TokenProtocolParseError { ticker: String, error: String }, - #[display(fmt = "Unexpected platform protocol {:?} for {}", protocol, ticker)] - UnexpectedTokenProtocol { ticker: String, protocol: CoinProtocol }, + #[display(fmt = "Unexpected platform protocol {} for {}", protocol, ticker)] + UnexpectedTokenProtocol { ticker: String, protocol: Json }, #[display(fmt = "Error on platform coin {} creation: {}", ticker, error)] TokenCreationError { ticker: String, error: String }, #[display(fmt = "Could not fetch balance: {}", _0)] @@ -310,6 +320,8 @@ pub enum InitTokenError { platform_coin_ticker: String, token_ticker: String, }, + #[display(fmt = "Custom token error: {}", _0)] + CustomTokenError(CustomTokenError), #[display(fmt = "{}", _0)] HwError(HwRpcError), #[display(fmt = "Transport error: {}", _0)] @@ -331,6 +343,7 @@ impl From for InitTokenError { CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { InitTokenError::UnexpectedTokenProtocol { ticker, protocol } }, + CoinConfWithProtocolError::CustomTokenError(e) => InitTokenError::CustomTokenError(e), } } } @@ -354,7 +367,8 @@ impl HttpStatusCode for InitTokenError { | InitTokenError::TokenProtocolParseError { .. } | InitTokenError::UnexpectedTokenProtocol { .. } | InitTokenError::TokenCreationError { .. } - | InitTokenError::PlatformCoinIsNotActivated(_) => StatusCode::BAD_REQUEST, + | InitTokenError::PlatformCoinIsNotActivated(_) + | InitTokenError::CustomTokenError(_) => StatusCode::BAD_REQUEST, InitTokenError::TaskTimedOut { .. } => StatusCode::REQUEST_TIMEOUT, InitTokenError::HwError(_) => StatusCode::GONE, InitTokenError::CouldNotFetchBalance(_) diff --git a/mm2src/coins_activation/src/l2/init_l2.rs b/mm2src/coins_activation/src/l2/init_l2.rs index 20e66ebbde..e6b0888700 100644 --- a/mm2src/coins_activation/src/l2/init_l2.rs +++ b/mm2src/coins_activation/src/l2/init_l2.rs @@ -79,7 +79,7 @@ where return MmError::err(InitL2Error::L2IsAlreadyActivated(ticker)); } - let (coin_conf_json, protocol_conf): (Json, L2::ProtocolInfo) = coin_conf_with_protocol(&ctx, &ticker)?; + let (coin_conf_json, protocol_conf): (Json, L2::ProtocolInfo) = coin_conf_with_protocol(&ctx, &ticker, None)?; let coin_conf = L2::coin_conf_from_json(coin_conf_json)?; let platform_coin = lp_coinfind_or_err(&ctx, protocol_conf.platform_coin_ticker()) diff --git a/mm2src/coins_activation/src/l2/init_l2_error.rs b/mm2src/coins_activation/src/l2/init_l2_error.rs index d23fd73078..71eb57fd23 100644 --- a/mm2src/coins_activation/src/l2/init_l2_error.rs +++ b/mm2src/coins_activation/src/l2/init_l2_error.rs @@ -1,11 +1,11 @@ use crate::prelude::CoinConfWithProtocolError; -use coins::CoinProtocol; use common::{HttpStatusCode, StatusCode}; use derive_more::Display; use rpc_task::rpc_common::{CancelRpcTaskError, RpcTaskStatusError, RpcTaskUserActionError}; use rpc_task::RpcTaskError; use ser_error_derive::SerializeErrorType; use serde_derive::Serialize; +use serde_json::Value as Json; use std::time::Duration; pub type InitL2StatusError = RpcTaskStatusError; @@ -24,10 +24,10 @@ pub enum InitL2Error { ticker: String, error: String, }, - #[display(fmt = "Unexpected layer 2 protocol {:?} for {}", protocol, ticker)] + #[display(fmt = "Unexpected layer 2 protocol {} for {}", protocol, ticker)] UnexpectedL2Protocol { ticker: String, - protocol: CoinProtocol, + protocol: Json, }, #[display(fmt = "Platform coin {} is not activated", _0)] PlatformCoinIsNotActivated(String), @@ -62,6 +62,9 @@ impl From for InitL2Error { CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { InitL2Error::UnexpectedL2Protocol { ticker, protocol } }, + CoinConfWithProtocolError::CustomTokenError(e) => { + InitL2Error::Internal(format!("Custom tokens are not supported for L2: {}", e)) + }, } } } diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index 051dd22fc3..d5ee5cfbf0 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -5,8 +5,8 @@ use crate::prelude::*; use async_trait::async_trait; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tx_history_storage::{CreateTxHistoryStorageError, TxHistoryStorageBuilder}; -use coins::{lp_coinfind, lp_coinfind_any, CoinProtocol, CoinsContext, MmCoinEnum, PrivKeyPolicyNotAllowed, - UnexpectedDerivationMethod}; +use coins::{lp_coinfind, lp_coinfind_any, CoinProtocol, CoinsContext, CustomTokenError, MmCoinEnum, + PrivKeyPolicyNotAllowed, UnexpectedDerivationMethod}; use common::{log, HttpStatusCode, StatusCode, SuccessResponse}; use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use crypto::CryptoCtxError; @@ -38,6 +38,7 @@ pub type InitPlatformCoinWithTokensTaskManagerShared = #[derive(Clone, Debug, Deserialize)] pub struct TokenActivationRequest { ticker: String, + protocol: Option, #[serde(flatten)] request: Req, } @@ -51,8 +52,10 @@ pub trait TokenOf: Into { pub struct TokenActivationParams { pub(crate) ticker: String, + pub(crate) conf: Json, pub(crate) activation_request: Req, pub(crate) protocol: Protocol, + pub(crate) is_custom: bool, } #[async_trait] @@ -87,15 +90,15 @@ pub trait TokenAsMmCoinInitializer: Send + Sync { } pub enum InitTokensAsMmCoinsError { - TokenAlreadyActivated(String), TokenConfigIsNotFound(String), CouldNotFetchBalance(String), UnexpectedDerivationMethod(UnexpectedDerivationMethod), Internal(String), TokenProtocolParseError { ticker: String, error: String }, - UnexpectedTokenProtocol { ticker: String, protocol: CoinProtocol }, + UnexpectedTokenProtocol { ticker: String, protocol: Json }, Transport(String), InvalidPayload(String), + CustomTokenError(CustomTokenError), } impl From for InitTokensAsMmCoinsError { @@ -111,6 +114,7 @@ impl From for InitTokensAsMmCoinsError { CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { InitTokensAsMmCoinsError::UnexpectedTokenProtocol { ticker, protocol } }, + CoinConfWithProtocolError::CustomTokenError(e) => InitTokensAsMmCoinsError::CustomTokenError(e), } } } @@ -138,11 +142,14 @@ where let token_params = tokens_requests .into_iter() .map(|req| -> Result<_, MmError> { - let (_, protocol): (_, T::TokenProtocol) = coin_conf_with_protocol(&ctx, &req.ticker)?; + let (token_conf, protocol): (_, T::TokenProtocol) = + coin_conf_with_protocol(&ctx, &req.ticker, req.protocol.clone())?; Ok(TokenActivationParams { ticker: req.ticker, + conf: token_conf, activation_request: req.request, protocol, + is_custom: req.protocol.is_some(), }) }) .collect::, _>>()?; @@ -235,7 +242,6 @@ pub struct EnablePlatformCoinWithTokensReq { #[serde(tag = "error_type", content = "error_data")] pub enum EnablePlatformCoinWithTokensError { PlatformIsAlreadyActivated(String), - TokenIsAlreadyActivated(String), #[display(fmt = "Platform {} config is not found", _0)] PlatformConfigIsNotFound(String), #[display(fmt = "Platform coin {} protocol parsing failed: {}", ticker, error)] @@ -243,10 +249,10 @@ pub enum EnablePlatformCoinWithTokensError { ticker: String, error: String, }, - #[display(fmt = "Unexpected platform protocol {:?} for {}", protocol, ticker)] + #[display(fmt = "Unexpected platform protocol {} for {}", protocol, ticker)] UnexpectedPlatformProtocol { ticker: String, - protocol: CoinProtocol, + protocol: Json, }, #[display(fmt = "Token {} config is not found", _0)] TokenConfigIsNotFound(String), @@ -255,10 +261,10 @@ pub enum EnablePlatformCoinWithTokensError { ticker: String, error: String, }, - #[display(fmt = "Unexpected token protocol {:?} for {}", protocol, ticker)] + #[display(fmt = "Unexpected token protocol {} for {}", protocol, ticker)] UnexpectedTokenProtocol { ticker: String, - protocol: CoinProtocol, + protocol: Json, }, #[display(fmt = "Error on platform coin {} creation: {}", ticker, error)] PlatformCoinCreationError { @@ -283,6 +289,8 @@ pub enum EnablePlatformCoinWithTokensError { }, #[display(fmt = "Hardware policy must be activated within task manager")] UnexpectedDeviceActivationPolicy, + #[display(fmt = "Custom token error: {}", _0)] + CustomTokenError(CustomTokenError), } impl From for EnablePlatformCoinWithTokensError { @@ -300,6 +308,7 @@ impl From for EnablePlatformCoinWithTokensError { error: err.to_string(), } }, + CoinConfWithProtocolError::CustomTokenError(e) => EnablePlatformCoinWithTokensError::CustomTokenError(e), } } } @@ -307,9 +316,6 @@ impl From for EnablePlatformCoinWithTokensError { impl From for EnablePlatformCoinWithTokensError { fn from(err: InitTokensAsMmCoinsError) -> Self { match err { - InitTokensAsMmCoinsError::TokenAlreadyActivated(ticker) => { - EnablePlatformCoinWithTokensError::TokenIsAlreadyActivated(ticker) - }, InitTokensAsMmCoinsError::TokenConfigIsNotFound(ticker) => { EnablePlatformCoinWithTokensError::TokenConfigIsNotFound(ticker) }, @@ -327,6 +333,7 @@ impl From for EnablePlatformCoinWithTokensError { InitTokensAsMmCoinsError::UnexpectedDerivationMethod(e) => { EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(e.to_string()) }, + InitTokensAsMmCoinsError::CustomTokenError(e) => EnablePlatformCoinWithTokensError::CustomTokenError(e), } } } @@ -362,9 +369,9 @@ impl HttpStatusCode for EnablePlatformCoinWithTokensError { | EnablePlatformCoinWithTokensError::PrivKeyPolicyNotAllowed(_) | EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(_) | EnablePlatformCoinWithTokensError::Internal(_) - | EnablePlatformCoinWithTokensError::TaskTimedOut { .. } => StatusCode::INTERNAL_SERVER_ERROR, + | EnablePlatformCoinWithTokensError::TaskTimedOut { .. } + | EnablePlatformCoinWithTokensError::CustomTokenError(_) => StatusCode::INTERNAL_SERVER_ERROR, EnablePlatformCoinWithTokensError::PlatformIsAlreadyActivated(_) - | EnablePlatformCoinWithTokensError::TokenIsAlreadyActivated(_) | EnablePlatformCoinWithTokensError::PlatformConfigIsNotFound(_) | EnablePlatformCoinWithTokensError::TokenConfigIsNotFound(_) | EnablePlatformCoinWithTokensError::UnexpectedPlatformProtocol { .. } @@ -449,7 +456,7 @@ where )); } - let (platform_conf, platform_protocol) = coin_conf_with_protocol(&ctx, &req.ticker)?; + let (platform_conf, platform_protocol) = coin_conf_with_protocol(&ctx, &req.ticker, None)?; let platform_coin = Platform::enable_platform_coin( ctx.clone(), diff --git a/mm2src/coins_activation/src/prelude.rs b/mm2src/coins_activation/src/prelude.rs index d000170fa3..42c93c1377 100644 --- a/mm2src/coins_activation/src/prelude.rs +++ b/mm2src/coins_activation/src/prelude.rs @@ -2,12 +2,12 @@ use coins::siacoin::SiaCoinActivationParams; use coins::utxo::UtxoActivationParams; use coins::z_coin::ZcoinActivationParams; -use coins::{coin_conf, CoinBalance, CoinProtocol, DerivationMethodResponse, MmCoinEnum}; +use coins::{coin_conf, CoinBalance, CoinProtocol, CustomTokenError, DerivationMethodResponse, MmCoinEnum}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; use serde_derive::Serialize; -use serde_json::{self as json, Value as Json}; +use serde_json::{self as json, json, Value as Json}; use std::collections::{HashMap, HashSet}; pub trait CurrentBlock { @@ -64,31 +64,78 @@ pub trait TryFromCoinProtocol { pub enum CoinConfWithProtocolError { ConfigIsNotFound(String), CoinProtocolParseError { ticker: String, err: json::Error }, - UnexpectedProtocol { ticker: String, protocol: CoinProtocol }, + UnexpectedProtocol { ticker: String, protocol: Json }, + CustomTokenError(CustomTokenError), } /// Determines the coin configuration and protocol information for a given coin or NFT ticker. -/// In the case of NFT ticker, it's platform coin config will be returned. -#[allow(clippy::result_large_err)] pub fn coin_conf_with_protocol( ctx: &MmArc, coin: &str, + protocol_from_request: Option, ) -> Result<(Json, T), MmError> { let conf = coin_conf(ctx, coin); - if conf.is_null() { - return MmError::err(CoinConfWithProtocolError::ConfigIsNotFound(coin.into())); + let is_ticker_in_config = !conf.is_null(); + + // For `protocol_from_request`: None = config-based activation, Some = custom token activation + match (protocol_from_request, is_ticker_in_config) { + // Config-based activation requested with an existing configuration + // Proceed with parsing protocol info from config + (None, true) => parse_coin_protocol_from_config(conf, coin), + // Custom token activation requested and no matching ticker in config + // Proceed with custom token config creation from protocol info + (Some(protocol), false) => create_custom_token_config(ctx, coin, protocol), + // Custom token activation requested but a coin with the same ticker already exists in config + (Some(_), true) => Err(MmError::new(CoinConfWithProtocolError::CustomTokenError( + CustomTokenError::DuplicateTickerInConfig { + ticker_in_config: coin.to_string(), + }, + ))), + // Config-based activation requested but ticker not found in config + (None, false) => Err(MmError::new(CoinConfWithProtocolError::ConfigIsNotFound(coin.into()))), } - let coin_protocol: CoinProtocol = json::from_value(conf["protocol"].clone()).map_to_mm(|err| { +} + +fn parse_coin_protocol_from_config( + conf: Json, + coin: &str, +) -> Result<(Json, T), MmError> { + let protocol = json::from_value(conf["protocol"].clone()).map_to_mm(|err| { CoinConfWithProtocolError::CoinProtocolParseError { ticker: coin.into(), err, } })?; + let coin_protocol = - T::try_from_coin_protocol(coin_protocol).mm_err(|protocol| CoinConfWithProtocolError::UnexpectedProtocol { + T::try_from_coin_protocol(protocol).mm_err(|p| CoinConfWithProtocolError::UnexpectedProtocol { ticker: coin.into(), - protocol, + protocol: json!(p), })?; + + Ok((conf, coin_protocol)) +} + +fn create_custom_token_config( + ctx: &MmArc, + coin: &str, + protocol: CoinProtocol, +) -> Result<(Json, T), MmError> { + protocol + .custom_token_validations(ctx) + .mm_err(CoinConfWithProtocolError::CustomTokenError)?; + + let conf = json!({ + "protocol": protocol, + "wallet_only": true + }); + + let coin_protocol = + T::try_from_coin_protocol(protocol).mm_err(|p| CoinConfWithProtocolError::UnexpectedProtocol { + ticker: coin.into(), + protocol: json!(p), + })?; + Ok((conf, coin_protocol)) } diff --git a/mm2src/coins_activation/src/slp_token_activation.rs b/mm2src/coins_activation/src/slp_token_activation.rs index f9abc166f4..91dbf95ea7 100644 --- a/mm2src/coins_activation/src/slp_token_activation.rs +++ b/mm2src/coins_activation/src/slp_token_activation.rs @@ -7,6 +7,7 @@ use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum}; use mm2_err_handle::prelude::*; use rpc::v1::types::H256 as H256Json; use serde_derive::{Deserialize, Serialize}; +use serde_json::Value as Json; use std::collections::HashMap; impl TryPlatformCoinFromMmCoinEnum for BchCoin { @@ -82,7 +83,9 @@ impl TokenActivationOps for SlpToken { ticker: String, platform_coin: Self::PlatformCoin, activation_params: Self::ActivationParams, + _token_conf: Json, protocol_conf: Self::ProtocolInfo, + _is_custom: bool, ) -> Result<(Self, Self::ActivationResult), MmError> { // confirmation settings from activation params have the highest priority let required_confirmations = activation_params.required_confirmations.unwrap_or_else(|| { diff --git a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs index 314f3066b4..a90f53e968 100644 --- a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs +++ b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs @@ -90,7 +90,7 @@ where return MmError::err(InitStandaloneCoinError::CoinIsAlreadyActivated { ticker: request.ticker }); } - let (coin_conf, protocol_info) = coin_conf_with_protocol(&ctx, &request.ticker)?; + let (coin_conf, protocol_info) = coin_conf_with_protocol(&ctx, &request.ticker, None)?; let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(InitStandaloneCoinError::Internal)?; let spawner = ctx.spawner(); diff --git a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs index 1f0b5db764..21b1d696a9 100644 --- a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs +++ b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs @@ -1,5 +1,4 @@ use crate::prelude::CoinConfWithProtocolError; -use coins::CoinProtocol; use common::{HttpStatusCode, StatusCode}; use crypto::HwRpcError; use derive_more::Display; @@ -7,6 +6,7 @@ use rpc_task::rpc_common::{CancelRpcTaskError, RpcTaskStatusError, RpcTaskUserAc use rpc_task::{RpcTaskError, TaskId}; use ser_error_derive::SerializeErrorType; use serde_derive::Serialize; +use serde_json::Value as Json; use std::time::Duration; pub type InitStandaloneCoinStatusError = RpcTaskStatusError; @@ -26,8 +26,8 @@ pub enum InitStandaloneCoinError { CoinConfigIsNotFound(String), #[display(fmt = "Coin {} protocol parsing failed: {}", ticker, error)] CoinProtocolParseError { ticker: String, error: String }, - #[display(fmt = "Unexpected platform protocol {:?} for {}", protocol, ticker)] - UnexpectedCoinProtocol { ticker: String, protocol: CoinProtocol }, + #[display(fmt = "Unexpected platform protocol {} for {}", protocol, ticker)] + UnexpectedCoinProtocol { ticker: String, protocol: Json }, #[display(fmt = "Error on platform coin {} creation: {}", ticker, error)] CoinCreationError { ticker: String, error: String }, #[display(fmt = "{}", _0)] @@ -51,6 +51,10 @@ impl From for InitStandaloneCoinError { CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { InitStandaloneCoinError::UnexpectedCoinProtocol { ticker, protocol } }, + CoinConfWithProtocolError::CustomTokenError(e) => InitStandaloneCoinError::Internal(format!( + "Custom tokens are not supported for standalone coins: {}", + e + )), } } } diff --git a/mm2src/coins_activation/src/tendermint_token_activation.rs b/mm2src/coins_activation/src/tendermint_token_activation.rs index 29598969af..12808505a9 100644 --- a/mm2src/coins_activation/src/tendermint_token_activation.rs +++ b/mm2src/coins_activation/src/tendermint_token_activation.rs @@ -7,6 +7,7 @@ use coins::{tendermint::{TendermintCoin, TendermintToken, TendermintTokenActivat use common::Future01CompatExt; use mm2_err_handle::prelude::{MapMmError, MmError}; use serde::Serialize; +use serde_json::Value as Json; use std::collections::HashMap; impl From for EnableTokenError { @@ -54,7 +55,9 @@ impl TokenActivationOps for TendermintToken { ticker: String, platform_coin: Self::PlatformCoin, _activation_params: Self::ActivationParams, + _token_conf: Json, protocol_conf: Self::ProtocolInfo, + _is_custom: bool, ) -> Result<(Self, Self::ActivationResult), MmError> { let token = TendermintToken::new(ticker, platform_coin, protocol_conf.decimals, protocol_conf.denom)?; diff --git a/mm2src/coins_activation/src/token.rs b/mm2src/coins_activation/src/token.rs index 0493c68fdb..5001a0f3de 100644 --- a/mm2src/coins_activation/src/token.rs +++ b/mm2src/coins_activation/src/token.rs @@ -4,7 +4,7 @@ use crate::platform_coin_with_tokens::{self, RegisterTokenInfo}; use crate::prelude::*; use async_trait::async_trait; use coins::utxo::rpc_clients::UtxoRpcError; -use coins::{lp_coinfind, lp_coinfind_or_err, BalanceError, CoinProtocol, CoinsContext, MmCoinEnum, +use coins::{lp_coinfind, lp_coinfind_or_err, BalanceError, CoinProtocol, CoinsContext, CustomTokenError, MmCoinEnum, PrivKeyPolicyNotAllowed, RegisterCoinError, UnexpectedDerivationMethod}; use common::{HttpStatusCode, StatusCode}; use derive_more::Display; @@ -12,6 +12,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; +use serde_json::Value as Json; pub trait TokenProtocolParams { fn platform_coin_ticker(&self) -> &str; @@ -28,7 +29,9 @@ pub trait TokenActivationOps: Into + platform_coin_with_tokens::Toke ticker: String, platform_coin: Self::PlatformCoin, activation_params: Self::ActivationParams, + token_conf: Json, protocol_conf: Self::ProtocolInfo, + is_custom: bool, ) -> Result<(Self, Self::ActivationResult), MmError>; } @@ -44,10 +47,10 @@ pub enum EnableTokenError { ticker: String, error: String, }, - #[display(fmt = "Unexpected token protocol {:?} for {}", protocol, ticker)] + #[display(fmt = "Unexpected token protocol {} for {}", protocol, ticker)] UnexpectedTokenProtocol { ticker: String, - protocol: CoinProtocol, + protocol: Json, }, #[display(fmt = "Platform coin {} is not activated", _0)] PlatformCoinIsNotActivated(String), @@ -64,6 +67,8 @@ pub enum EnableTokenError { Internal(String), InvalidPayload(String), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + #[display(fmt = "Custom token error: {}", _0)] + CustomTokenError(CustomTokenError), } impl From for EnableTokenError { @@ -88,6 +93,7 @@ impl From for EnableTokenError { CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { EnableTokenError::UnexpectedTokenProtocol { ticker, protocol } }, + CoinConfWithProtocolError::CustomTokenError(e) => EnableTokenError::CustomTokenError(e), } } } @@ -105,6 +111,7 @@ impl From for EnableTokenError { #[derive(Debug, Deserialize)] pub struct EnableTokenRequest { ticker: String, + protocol: Option, activation_params: T, } @@ -121,7 +128,8 @@ where return MmError::err(EnableTokenError::TokenIsAlreadyActivated(req.ticker)); } - let (_, token_protocol): (_, Token::ProtocolInfo) = coin_conf_with_protocol(&ctx, &req.ticker)?; + let (token_conf, token_protocol): (_, Token::ProtocolInfo) = + coin_conf_with_protocol(&ctx, &req.ticker, req.protocol.clone())?; let platform_coin = lp_coinfind_or_err(&ctx, token_protocol.platform_coin_ticker()) .await @@ -134,8 +142,15 @@ where } })?; - let (token, activation_result) = - Token::enable_token(req.ticker, platform_coin.clone(), req.activation_params, token_protocol).await?; + let (token, activation_result) = Token::enable_token( + req.ticker, + platform_coin.clone(), + req.activation_params, + token_conf, + token_protocol, + req.protocol.is_some(), + ) + .await?; let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); coins_ctx.add_token(token.clone().into()).await?; @@ -164,7 +179,8 @@ impl HttpStatusCode for EnableTokenError { | EnableTokenError::PlatformCoinIsNotActivated(_) | EnableTokenError::TokenConfigIsNotFound { .. } | EnableTokenError::UnexpectedTokenProtocol { .. } - | EnableTokenError::InvalidPayload(_) => StatusCode::BAD_REQUEST, + | EnableTokenError::InvalidPayload(_) + | EnableTokenError::CustomTokenError(_) => StatusCode::BAD_REQUEST, EnableTokenError::TokenProtocolParseError { .. } | EnableTokenError::UnsupportedPlatformCoin { .. } | EnableTokenError::UnexpectedDerivationMethod(_) diff --git a/mm2src/mm2_bin_lib/src/mm2_native_lib.rs b/mm2src/mm2_bin_lib/src/mm2_native_lib.rs index 94ed6daf62..17d7a839bc 100644 --- a/mm2src/mm2_bin_lib/src/mm2_native_lib.rs +++ b/mm2src/mm2_bin_lib/src/mm2_native_lib.rs @@ -123,7 +123,7 @@ pub extern "C" fn mm2_test(torch: i32, log_cb: extern "C" fn(line: *const c_char }, }; let conf = json::to_string(&ctx.conf).unwrap(); - let hy_res = mm2_main::rpc::lp_commands_legacy::stop(ctx); + let hy_res = mm2_main::rpc::lp_commands::legacy::stop(ctx); let r = match block_on(hy_res) { Ok(r) => r, Err(err) => { diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index 85b61db612..1f0afd3234 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -44,9 +44,7 @@ cfg_native! { #[path = "rpc/dispatcher/dispatcher.rs"] mod dispatcher; #[path = "rpc/dispatcher/dispatcher_legacy.rs"] mod dispatcher_legacy; -#[path = "rpc/lp_commands/lp_commands.rs"] pub mod lp_commands; -#[path = "rpc/lp_commands/lp_commands_legacy.rs"] -pub mod lp_commands_legacy; +pub mod lp_commands; mod rate_limiter; /// Lists the RPC method not requiring the "userpass" authentication. diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index ba99379892..6b68666d6b 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -5,13 +5,16 @@ use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor use crate::lp_native_dex::init_metamask::{cancel_connect_metamask, connect_metamask, connect_metamask_status}; use crate::lp_ordermatch::{best_orders_rpc_v2, orderbook_rpc_v2, start_simple_market_maker_bot, stop_simple_market_maker_bot}; +use crate::lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, start_version_stat_collection, + stop_version_stat_collection, update_version_stat_collection}; use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swap_status_rpc}; +use crate::lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}; use crate::lp_wallet::{get_mnemonic_rpc, get_wallet_names_rpc}; +use crate::rpc::lp_commands::db_id::get_shared_db_id; +use crate::rpc::lp_commands::pubkey::*; +use crate::rpc::lp_commands::tokens::get_token_info; +use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; -use crate::{lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, start_version_stat_collection, - stop_version_stat_collection, update_version_stat_collection}, - lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}, - rpc::lp_commands::{get_public_key, get_public_key_hash, get_shared_db_id, trezor_connection_status}}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; @@ -184,6 +187,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, get_raw_transaction).await, "get_shared_db_id" => handle_mmrpc(ctx, request, get_shared_db_id).await, "get_staking_infos" => handle_mmrpc(ctx, request, get_staking_infos).await, + "get_token_info" => handle_mmrpc(ctx, request, get_token_info).await, "get_wallet_names" => handle_mmrpc(ctx, request, get_wallet_names_rpc).await, "max_maker_vol" => handle_mmrpc(ctx, request, max_maker_vol).await, "my_recent_swaps" => handle_mmrpc(ctx, request, my_recent_swaps_rpc).await, diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs index 03a1ee1a00..5f4b14f8b4 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs @@ -7,7 +7,7 @@ use mm2_core::mm_ctx::MmArc; use serde_json::{self as json, Value as Json}; use std::net::SocketAddr; -use super::lp_commands_legacy::*; +use super::lp_commands::legacy::*; use crate::lp_ordermatch::{best_orders_rpc, buy, cancel_all_orders_rpc, cancel_order_rpc, my_orders, order_status, orderbook_depth_rpc, orderbook_rpc, orders_history_by_filter, sell, set_price, update_maker_order_rpc}; diff --git a/mm2src/mm2_main/src/rpc/lp_commands/db_id.rs b/mm2src/mm2_main/src/rpc/lp_commands/db_id.rs new file mode 100644 index 0000000000..29fa399bd0 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/db_id.rs @@ -0,0 +1,18 @@ +use crate::rpc::lp_commands::pubkey::GetPublicKeyError; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::mm_error::MmError; +use rpc::v1::types::H160 as H160Json; +use serde_json::Value as Json; + +pub type GetSharedDbIdResult = Result>; +pub type GetSharedDbIdError = GetPublicKeyError; + +#[derive(Serialize)] +pub struct GetSharedDbIdResponse { + shared_db_id: H160Json, +} + +pub async fn get_shared_db_id(ctx: MmArc, _req: Json) -> GetSharedDbIdResult { + let shared_db_id = ctx.shared_db_id().to_owned().into(); + Ok(GetSharedDbIdResponse { shared_db_id }) +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs b/mm2src/mm2_main/src/rpc/lp_commands/legacy.rs similarity index 100% rename from mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs rename to mm2src/mm2_main/src/rpc/lp_commands/legacy.rs diff --git a/mm2src/mm2_main/src/rpc/lp_commands/mod.rs b/mm2src/mm2_main/src/rpc/lp_commands/mod.rs new file mode 100644 index 0000000000..002066c836 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod db_id; +pub mod legacy; +pub(crate) mod pubkey; +pub(crate) mod tokens; +pub(crate) mod trezor; diff --git a/mm2src/mm2_main/src/rpc/lp_commands/pubkey.rs b/mm2src/mm2_main/src/rpc/lp_commands/pubkey.rs new file mode 100644 index 0000000000..f5a5a95063 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/pubkey.rs @@ -0,0 +1,48 @@ +use common::HttpStatusCode; +use crypto::{CryptoCtx, CryptoCtxError}; +use derive_more::Display; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc::v1::types::H160 as H160Json; +use serde_json::Value as Json; + +pub type GetPublicKeyRpcResult = Result>; + +#[derive(Serialize, Display, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetPublicKeyError { + Internal(String), +} + +impl From for GetPublicKeyError { + fn from(_: CryptoCtxError) -> Self { GetPublicKeyError::Internal("public_key not available".to_string()) } +} + +#[derive(Serialize)] +pub struct GetPublicKeyResponse { + public_key: String, +} + +impl HttpStatusCode for GetPublicKeyError { + fn status_code(&self) -> StatusCode { + match self { + GetPublicKeyError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn get_public_key(ctx: MmArc, _req: Json) -> GetPublicKeyRpcResult { + let public_key = CryptoCtx::from_ctx(&ctx)?.mm2_internal_pubkey().to_string(); + Ok(GetPublicKeyResponse { public_key }) +} + +#[derive(Serialize)] +pub struct GetPublicKeyHashResponse { + public_key_hash: H160Json, +} + +pub async fn get_public_key_hash(ctx: MmArc, _req: Json) -> GetPublicKeyRpcResult { + let public_key_hash = ctx.rmd160().to_owned().into(); + Ok(GetPublicKeyHashResponse { public_key_hash }) +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs new file mode 100644 index 0000000000..c72e772a81 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs @@ -0,0 +1,95 @@ +use coins::eth::erc20::{get_erc20_ticker_by_contract_address, get_erc20_token_info, Erc20TokenInfo}; +use coins::eth::valid_addr_from_str; +use coins::{lp_coinfind_or_err, CoinFindError, CoinProtocol, MmCoinEnum}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; + +#[derive(Deserialize)] +pub struct TokenInfoRequest { + protocol: CoinProtocol, +} + +#[derive(Serialize)] +#[serde(tag = "type", content = "info")] +pub enum TokenInfo { + ERC20(Erc20TokenInfo), +} + +#[derive(Serialize)] +pub struct TokenInfoResponse { + #[serde(skip_serializing_if = "Option::is_none")] + config_ticker: Option, + #[serde(flatten)] + info: TokenInfo, +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum TokenInfoError { + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "Custom tokens are not supported for {} protocol yet!", protocol)] + UnsupportedTokenProtocol { protocol: String }, + #[display(fmt = "Invalid request {}", _0)] + InvalidRequest(String), + #[display(fmt = "Error retrieving token info {}", _0)] + RetrieveInfoError(String), +} + +impl HttpStatusCode for TokenInfoError { + fn status_code(&self) -> StatusCode { + match self { + TokenInfoError::NoSuchCoin { .. } => StatusCode::NOT_FOUND, + TokenInfoError::UnsupportedTokenProtocol { .. } | TokenInfoError::InvalidRequest(_) => { + StatusCode::BAD_REQUEST + }, + TokenInfoError::RetrieveInfoError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for TokenInfoError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => TokenInfoError::NoSuchCoin { coin }, + } + } +} + +pub async fn get_token_info(ctx: MmArc, req: TokenInfoRequest) -> MmResult { + // Check that the protocol is a token protocol + let platform = req.protocol.platform().ok_or(TokenInfoError::InvalidRequest(format!( + "Protocol '{:?}' is not a token protocol", + req.protocol + )))?; + // Platform coin should be activated + let platform_coin = lp_coinfind_or_err(&ctx, platform).await?; + match platform_coin { + MmCoinEnum::EthCoin(eth_coin) => { + let contract_address_str = + req.protocol + .contract_address() + .ok_or(TokenInfoError::UnsupportedTokenProtocol { + protocol: platform.to_string(), + })?; + let contract_address = valid_addr_from_str(contract_address_str).map_to_mm(|e| { + let error = format!("Invalid contract address: {}", e); + TokenInfoError::InvalidRequest(error) + })?; + + let config_ticker = get_erc20_ticker_by_contract_address(&ctx, platform, contract_address_str); + let token_info = get_erc20_token_info(ð_coin, contract_address) + .await + .map_to_mm(TokenInfoError::RetrieveInfoError)?; + Ok(TokenInfoResponse { + config_ticker, + info: TokenInfo::ERC20(token_info), + }) + }, + _ => MmError::err(TokenInfoError::UnsupportedTokenProtocol { + protocol: platform.to_string(), + }), + } +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs b/mm2src/mm2_main/src/rpc/lp_commands/trezor.rs similarity index 52% rename from mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs rename to mm2src/mm2_main/src/rpc/lp_commands/trezor.rs index ae992c6d3e..16698eb3cc 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/trezor.rs @@ -1,63 +1,9 @@ use common::HttpStatusCode; use crypto::{CryptoCtx, CryptoCtxError, HwConnectionStatus, HwPubkey}; -use derive_more::Display; use http::StatusCode; use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::*; -use rpc::v1::types::H160 as H160Json; -use serde_json::Value as Json; - -pub type GetPublicKeyRpcResult = Result>; -pub type GetSharedDbIdResult = Result>; -pub type GetSharedDbIdError = GetPublicKeyError; - -#[derive(Serialize, Display, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum GetPublicKeyError { - Internal(String), -} - -impl From for GetPublicKeyError { - fn from(_: CryptoCtxError) -> Self { GetPublicKeyError::Internal("public_key not available".to_string()) } -} - -#[derive(Serialize)] -pub struct GetPublicKeyResponse { - public_key: String, -} - -impl HttpStatusCode for GetPublicKeyError { - fn status_code(&self) -> StatusCode { - match self { - GetPublicKeyError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -pub async fn get_public_key(ctx: MmArc, _req: Json) -> GetPublicKeyRpcResult { - let public_key = CryptoCtx::from_ctx(&ctx)?.mm2_internal_pubkey().to_string(); - Ok(GetPublicKeyResponse { public_key }) -} - -#[derive(Serialize)] -pub struct GetPublicKeyHashResponse { - public_key_hash: H160Json, -} - -pub async fn get_public_key_hash(ctx: MmArc, _req: Json) -> GetPublicKeyRpcResult { - let public_key_hash = ctx.rmd160().to_owned().into(); - Ok(GetPublicKeyHashResponse { public_key_hash }) -} - -#[derive(Serialize)] -pub struct GetSharedDbIdResponse { - shared_db_id: H160Json, -} - -pub async fn get_shared_db_id(ctx: MmArc, _req: Json) -> GetSharedDbIdResult { - let shared_db_id = ctx.shared_db_id().to_owned().into(); - Ok(GetSharedDbIdResponse { shared_db_id }) -} +use mm2_err_handle::mm_error::{MmError, MmResult}; +use mm2_err_handle::or_mm_error::OrMmError; #[derive(Serialize, Display, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] 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 adc2da8574..1757f97d36 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -17,11 +17,11 @@ use crypto::privkey::key_pair_from_seed; use crypto::{CryptoCtx, DerivationPath, KeyPairPolicy}; use http::StatusCode; use mm2_number::{BigDecimal, BigRational, MmNumber}; -use mm2_test_helpers::for_tests::{account_balance, check_my_swap_status_amounts, disable_coin, disable_coin_err, - enable_eth_coin, enable_eth_with_tokens_v2, erc20_dev_conf, eth_dev_conf, - get_locked_amount, get_new_address, kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, - mycoin_conf, set_price, start_swaps, wait_for_swap_contract_negotiation, - wait_for_swap_negotiation_failure, MarketMakerIt, Mm2TestConf}; +use mm2_test_helpers::for_tests::{check_my_swap_status_amounts, disable_coin, disable_coin_err, enable_eth_coin, + enable_eth_with_tokens_v2, erc20_dev_conf, eth_dev_conf, get_locked_amount, + kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, mycoin_conf, set_price, start_swaps, + wait_for_swap_contract_negotiation, wait_for_swap_negotiation_failure, + MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::{get_passphrase, structs::*}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -5257,142 +5257,6 @@ fn test_sell_min_volume_dust() { assert!(!rc.0.is_success(), "!sell: {}", rc.1); } -#[test] -fn test_eth_erc20_hd() { - const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; - - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - - // Withdraw from HD account 0, change address 0, index 0 - let path_to_address = HDAccountAddressId::default(); - let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); - let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); - log!("Alice log path: {}", mm_hd.log_path.display()); - - let eth_enable = block_on(enable_eth_with_tokens_v2( - &mm_hd, - "ETH", - &["ERC20DEV"], - &swap_contract, - &[GETH_RPC_URL], - 60, - Some(path_to_address), - )); - let activation_result = match eth_enable { - EthWithTokensActivationResult::HD(hd) => hd, - _ => panic!("Expected EthWithTokensActivationResult::HD"), - }; - let balance = match activation_result.wallet_balance { - EnableCoinBalanceMap::HD(hd) => hd, - _ => panic!("Expected EnableCoinBalance::HD"), - }; - let account = balance.accounts.get(0).expect("Expected account at index 0"); - assert_eq!( - account.addresses[0].address, - "0x1737F1FaB40c6Fd3dc729B51C0F97DB3297CCA93" - ); - assert_eq!(account.addresses[0].balance.len(), 2); - assert!(account.addresses[0].balance.contains_key("ETH")); - assert!(account.addresses[0].balance.contains_key("ERC20DEV")); - - block_on(mm_hd.stop()).unwrap(); - - // Enable HD account 0, change address 0, index 1 - let path_to_address = HDAccountAddressId { - account_id: 0, - chain: Bip44Chain::External, - address_id: 1, - }; - let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); - let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); - log!("Alice log path: {}", mm_hd.log_path.display()); - - let eth_enable = block_on(enable_eth_with_tokens_v2( - &mm_hd, - "ETH", - &["ERC20DEV"], - &swap_contract, - &[GETH_RPC_URL], - 60, - Some(path_to_address), - )); - let activation_result = match eth_enable { - EthWithTokensActivationResult::HD(hd) => hd, - _ => panic!("Expected EthWithTokensActivationResult::HD"), - }; - let balance = match activation_result.wallet_balance { - EnableCoinBalanceMap::HD(hd) => hd, - _ => panic!("Expected EnableCoinBalance::HD"), - }; - let account = balance.accounts.get(0).expect("Expected account at index 0"); - assert_eq!( - account.addresses[1].address, - "0xDe841899aB4A22E23dB21634e54920aDec402397" - ); - assert_eq!(account.addresses[0].balance.len(), 2); - assert!(account.addresses[0].balance.contains_key("ETH")); - assert!(account.addresses[0].balance.contains_key("ERC20DEV")); - - let get_new_address = block_on(get_new_address(&mm_hd, "ETH", 0, Some(Bip44Chain::External))); - assert!(get_new_address.new_address.balance.contains_key("ETH")); - // Make sure balance is returned for any token enabled with ETH as platform coin - assert!(get_new_address.new_address.balance.contains_key("ERC20DEV")); - assert_eq!( - get_new_address.new_address.address, - "0x4249E165a68E4FF9C41B1C3C3b4245c30ecB43CC" - ); - // Make sure that the address is also added to tokens - let account_balance = block_on(account_balance(&mm_hd, "ERC20DEV", 0, Bip44Chain::External)); - assert_eq!( - account_balance.addresses[2].address, - "0x4249E165a68E4FF9C41B1C3C3b4245c30ecB43CC" - ); - - block_on(mm_hd.stop()).unwrap(); - - // Enable HD account 77, change address 0, index 7 - let path_to_address = HDAccountAddressId { - account_id: 77, - chain: Bip44Chain::External, - address_id: 7, - }; - let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); - let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); - log!("Alice log path: {}", mm_hd.log_path.display()); - - let eth_enable = block_on(enable_eth_with_tokens_v2( - &mm_hd, - "ETH", - &["ERC20DEV"], - &swap_contract, - &[GETH_RPC_URL], - 60, - Some(path_to_address), - )); - let activation_result = match eth_enable { - EthWithTokensActivationResult::HD(hd) => hd, - _ => panic!("Expected EthWithTokensActivationResult::HD"), - }; - let balance = match activation_result.wallet_balance { - EnableCoinBalanceMap::HD(hd) => hd, - _ => panic!("Expected EnableCoinBalance::HD"), - }; - let account = balance.accounts.get(0).expect("Expected account at index 0"); - assert_eq!( - account.addresses[7].address, - "0xa420a4DBd8C50e6240014Db4587d2ec8D0cE0e6B" - ); - assert_eq!(account.addresses[0].balance.len(), 2); - assert!(account.addresses[0].balance.contains_key("ETH")); - assert!(account.addresses[0].balance.contains_key("ERC20DEV")); - - block_on(mm_hd.stop()).unwrap(); -} - fn request_and_check_orderbook_depth(mm_alice: &MarketMakerIt) { let rc = block_on(mm_alice.rpc(&json! ({ "userpass": mm_alice.userpass, diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index 22675745fc..7ea038a8f7 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -31,9 +31,13 @@ use ethereum_types::U256; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use mm2_core::mm_ctx::MmArc; use mm2_number::{BigDecimal, BigUint}; -use mm2_test_helpers::for_tests::{erc20_dev_conf, eth_dev_conf, nft_dev_conf}; +use mm2_test_helpers::for_tests::{account_balance, disable_coin, enable_erc20_token_v2, enable_eth_with_tokens_v2, + erc20_dev_conf, eth_dev_conf, get_new_address, get_token_info, nft_dev_conf, + MarketMakerIt, Mm2TestConf}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use mm2_test_helpers::for_tests::{eth_sepolia_conf, sepolia_erc20_dev_conf}; +use mm2_test_helpers::structs::{Bip44Chain, EnableCoinBalanceMap, EthWithTokensActivationResult, HDAccountAddressId, + TokenInfo}; use serde_json::Value as Json; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use std::str::FromStr; @@ -2452,3 +2456,285 @@ fn send_and_spend_maker_payment_erc20() { log!("Taker spent maker ERC20 payment, tx hash: {:02x}", spend_tx.tx_hash()); wait_for_confirmations(&taker_coin, &spend_tx, 100); } + +#[test] +fn test_eth_erc20_hd() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + let swap_contract = format!("0x{}", hex::encode(swap_contract())); + + // Withdraw from HD account 0, change address 0, index 0 + let path_to_address = HDAccountAddressId::default(); + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); + log!("Alice log path: {}", mm_hd.log_path.display()); + + let eth_enable = block_on(enable_eth_with_tokens_v2( + &mm_hd, + "ETH", + &["ERC20DEV"], + &swap_contract, + &[GETH_RPC_URL], + 60, + Some(path_to_address), + )); + let activation_result = match eth_enable { + EthWithTokensActivationResult::HD(hd) => hd, + _ => panic!("Expected EthWithTokensActivationResult::HD"), + }; + let balance = match activation_result.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); + assert_eq!( + account.addresses[0].address, + "0x1737F1FaB40c6Fd3dc729B51C0F97DB3297CCA93" + ); + assert_eq!(account.addresses[0].balance.len(), 2); + assert!(account.addresses[0].balance.contains_key("ETH")); + assert!(account.addresses[0].balance.contains_key("ERC20DEV")); + + block_on(mm_hd.stop()).unwrap(); + + // Enable HD account 0, change address 0, index 1 + let path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, + }; + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); + log!("Alice log path: {}", mm_hd.log_path.display()); + + let eth_enable = block_on(enable_eth_with_tokens_v2( + &mm_hd, + "ETH", + &["ERC20DEV"], + &swap_contract, + &[GETH_RPC_URL], + 60, + Some(path_to_address), + )); + let activation_result = match eth_enable { + EthWithTokensActivationResult::HD(hd) => hd, + _ => panic!("Expected EthWithTokensActivationResult::HD"), + }; + let balance = match activation_result.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); + assert_eq!( + account.addresses[1].address, + "0xDe841899aB4A22E23dB21634e54920aDec402397" + ); + assert_eq!(account.addresses[0].balance.len(), 2); + assert!(account.addresses[0].balance.contains_key("ETH")); + assert!(account.addresses[0].balance.contains_key("ERC20DEV")); + + let get_new_address = block_on(get_new_address(&mm_hd, "ETH", 0, Some(Bip44Chain::External))); + assert!(get_new_address.new_address.balance.contains_key("ETH")); + // Make sure balance is returned for any token enabled with ETH as platform coin + assert!(get_new_address.new_address.balance.contains_key("ERC20DEV")); + assert_eq!( + get_new_address.new_address.address, + "0x4249E165a68E4FF9C41B1C3C3b4245c30ecB43CC" + ); + // Make sure that the address is also added to tokens + let account_balance = block_on(account_balance(&mm_hd, "ERC20DEV", 0, Bip44Chain::External)); + assert_eq!( + account_balance.addresses[2].address, + "0x4249E165a68E4FF9C41B1C3C3b4245c30ecB43CC" + ); + + block_on(mm_hd.stop()).unwrap(); + + // Enable HD account 77, change address 0, index 7 + let path_to_address = HDAccountAddressId { + account_id: 77, + chain: Bip44Chain::External, + address_id: 7, + }; + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); + log!("Alice log path: {}", mm_hd.log_path.display()); + + let eth_enable = block_on(enable_eth_with_tokens_v2( + &mm_hd, + "ETH", + &["ERC20DEV"], + &swap_contract, + &[GETH_RPC_URL], + 60, + Some(path_to_address), + )); + let activation_result = match eth_enable { + EthWithTokensActivationResult::HD(hd) => hd, + _ => panic!("Expected EthWithTokensActivationResult::HD"), + }; + let balance = match activation_result.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); + assert_eq!( + account.addresses[7].address, + "0xa420a4DBd8C50e6240014Db4587d2ec8D0cE0e6B" + ); + assert_eq!(account.addresses[0].balance.len(), 2); + assert!(account.addresses[0].balance.contains_key("ETH")); + assert!(account.addresses[0].balance.contains_key("ERC20DEV")); + + block_on(mm_hd.stop()).unwrap(); +} + +#[test] +fn test_enable_custom_erc20() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let coins = json!([eth_dev_conf()]); + let swap_contract = format!("0x{}", hex::encode(swap_contract())); + + let path_to_address = HDAccountAddressId::default(); + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); + log!("Alice log path: {}", mm_hd.log_path.display()); + + // Enable platform coin in HD mode + block_on(enable_eth_with_tokens_v2( + &mm_hd, + "ETH", + &[], + &swap_contract, + &[GETH_RPC_URL], + 60, + Some(path_to_address.clone()), + )); + + // Test `get_token_info` rpc, we also use it to get the token symbol to use it as the ticker + let protocol = erc20_dev_conf(&erc20_contract_checksum())["protocol"].clone(); + let TokenInfo::ERC20(custom_token_info) = block_on(get_token_info(&mm_hd, protocol.clone())).info; + let ticker = custom_token_info.symbol; + assert_eq!(ticker, "QTC"); + assert_eq!(custom_token_info.decimals, 8); + + // Enable the custom token in HD mode + block_on(enable_erc20_token_v2( + &mm_hd, + &ticker, + Some(protocol.clone()), + 60, + Some(path_to_address.clone()), + )) + .unwrap(); + + // Test that the custom token is wallet only by using it in a swap + let buy = block_on(mm_hd.rpc(&json!({ + "userpass": mm_hd.userpass, + "method": "buy", + "base": "ETH", + "rel": ticker, + "price": "1", + "volume": "1", + }))) + .unwrap(); + assert!(!buy.0.is_success(), "buy success, but should fail: {}", buy.1); + assert!( + buy.1.contains(&format!("Rel coin {} is wallet only", ticker)), + "Expected error message indicating that the token is wallet only, but got: {}", + buy.1 + ); + + // Enabling the same custom token using a different ticker should fail + let err = block_on(enable_erc20_token_v2( + &mm_hd, + "ERC20DEV", + Some(protocol.clone()), + 60, + Some(path_to_address), + )) + .unwrap_err(); + let expected_error_type = "CustomTokenError"; + assert_eq!(err["error_type"], expected_error_type); + let expected_error_data = json!({ + "TokenWithSameContractAlreadyActivated": { + "ticker": ticker, + "contract_address": protocol["protocol_data"]["contract_address"] + } + }); + assert_eq!(err["error_data"], expected_error_data); + + // Disable the custom token + block_on(disable_coin(&mm_hd, &ticker, true)); +} + +#[test] +fn test_enable_custom_erc20_with_duplicate_contract_in_config() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let erc20_dev_conf = erc20_dev_conf(&erc20_contract_checksum()); + let coins = json!([eth_dev_conf(), erc20_dev_conf]); + let swap_contract = format!("0x{}", hex::encode(swap_contract())); + + let path_to_address = HDAccountAddressId::default(); + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); + log!("Alice log path: {}", mm_hd.log_path.display()); + + // Enable platform coin in HD mode + block_on(enable_eth_with_tokens_v2( + &mm_hd, + "ETH", + &[], + &swap_contract, + &[GETH_RPC_URL], + 60, + Some(path_to_address.clone()), + )); + + let protocol = erc20_dev_conf["protocol"].clone(); + // Enable the custom token in HD mode. + // Since the contract is already in the coins config, this should fail with an error + // that specifies the ticker in config so that the user can enable the right coin. + let err = block_on(enable_erc20_token_v2( + &mm_hd, + "QTC", + Some(protocol.clone()), + 60, + Some(path_to_address.clone()), + )) + .unwrap_err(); + let expected_error_type = "CustomTokenError"; + assert_eq!(err["error_type"], expected_error_type); + let expected_error_data = json!({ + "DuplicateContractInConfig": { + "ticker_in_config": "ERC20DEV" + } + }); + assert_eq!(err["error_data"], expected_error_data); + + // Another way is to use the `get_token_info` RPC and use the config ticker to enable the token. + let custom_token_info = block_on(get_token_info(&mm_hd, protocol)); + assert!(custom_token_info.config_ticker.is_some()); + let config_ticker = custom_token_info.config_ticker.unwrap(); + assert_eq!(config_ticker, "ERC20DEV"); + // Parameters passed here are for normal enabling of a coin in config and not for a custom token + block_on(enable_erc20_token_v2( + &mm_hd, + &config_ticker, + None, + 60, + Some(path_to_address), + )) + .unwrap(); + + // Disable the custom token, this to check that it was enabled correctly + block_on(disable_coin(&mm_hd, &config_ticker, true)); +} diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 880edc777a..115f120574 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -3253,6 +3253,103 @@ pub async fn enable_eth_with_tokens_v2( } } +async fn init_erc20_token( + mm: &MarketMakerIt, + ticker: &str, + protocol: Option, + path_to_address: Option, +) -> Result<(StatusCode, Json), Json> { + let (status, response, _) = mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "task::enable_erc20::init", + "mmrpc": "2.0", + "params": { + "ticker": ticker, + "protocol": protocol, + "activation_params": { + "path_to_address": path_to_address.unwrap_or_default(), + } + } + })) + .await + .unwrap(); + + if status.is_success() { + Ok((status, json::from_str(&response).unwrap())) + } else { + Err(json::from_str(&response).unwrap()) + } +} + +async fn init_erc20_token_status(mm: &MarketMakerIt, task_id: u64) -> Json { + let request = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "task::enable_erc20::status", + "mmrpc": "2.0", + "params": { + "task_id": task_id, + } + })) + .await + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'task::enable_erc20::status' failed: {}", + request.1 + ); + json::from_str(&request.1).unwrap() +} + +pub async fn enable_erc20_token_v2( + mm: &MarketMakerIt, + ticker: &str, + protocol: Option, + timeout: u64, + path_to_address: Option, +) -> Result { + let init = init_erc20_token(mm, ticker, protocol, path_to_address).await?.1; + let init: RpcV2Response = json::from_value(init).unwrap(); + let timeout = wait_until_ms(timeout * 1000); + + loop { + if now_ms() > timeout { + panic!("{} initialization timed out", ticker); + } + + let status = init_erc20_token_status(mm, init.result.task_id).await; + let status: RpcV2Response = json::from_value(status).unwrap(); + match status.result { + InitErc20TokenStatus::Ok(result) => break Ok(result), + InitErc20TokenStatus::Error(e) => break Err(e), + _ => Timer::sleep(1.).await, + } + } +} + +pub async fn get_token_info(mm: &MarketMakerIt, protocol: Json) -> TokenInfoResponse { + let response = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "get_token_info", + "mmrpc": "2.0", + "params": { + "protocol": protocol, + } + })) + .await + .unwrap(); + assert_eq!( + response.0, + StatusCode::OK, + "'get_token_info' failed: {}", + response.1 + ); + let response_json: Json = json::from_str(&response.1).unwrap(); + json::from_value(response_json["result"].clone()).unwrap() +} + /// Note that mm2 ignores `volume` if `max` is true. pub async fn set_price( mm: &MarketMakerIt, diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index 96cad93739..baba173461 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -714,6 +714,15 @@ pub enum InitEthWithTokensStatus { UserActionRequired(Json), } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields, tag = "status", content = "details")] +pub enum InitErc20TokenStatus { + Ok(InitTokenActivationResult), + Error(Json), + InProgress(Json), + UserActionRequired(Json), +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, tag = "status", content = "details")] pub enum InitLightningStatus { @@ -911,6 +920,17 @@ pub enum EthWithTokensActivationResult { HD(HDEthWithTokensActivationResult), } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct InitTokenActivationResult { + pub ticker: String, + pub platform_coin: String, + pub token_contract_address: String, + pub current_block: u64, + pub required_confirmations: u64, + pub wallet_balance: EnableCoinBalanceMap, +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct EnableBchWithTokensResponse { @@ -1186,3 +1206,25 @@ pub struct ActiveSwapsResponse { pub uuids: Vec, pub statuses: Option>, } + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Erc20TokenInfo { + pub symbol: String, + pub decimals: u8, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(tag = "type", content = "info")] +pub enum TokenInfo { + ERC20(Erc20TokenInfo), +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TokenInfoResponse { + pub config_ticker: Option, + #[serde(flatten)] + pub info: TokenInfo, +} From 0a9410258c7bf58e78d26b33b7a0ffb749c7ba16 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Tue, 19 Nov 2024 19:16:47 +0200 Subject: [PATCH 4/6] fix(watchers): align taker fee validation retries with makers (#2263) This also fixes propagation of health check messages. --- mm2src/coins/utxo/utxo_common.rs | 2 +- mm2src/mm2_main/src/lp_healthcheck.rs | 27 ++++++------- mm2src/mm2_main/src/lp_network.rs | 20 +++++++--- mm2src/mm2_main/src/lp_swap.rs | 5 ++- mm2src/mm2_main/src/lp_swap/maker_swap.rs | 7 ++-- mm2src/mm2_main/src/lp_swap/swap_watcher.rs | 44 ++++++++++++--------- 6 files changed, 63 insertions(+), 42 deletions(-) diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 37955e9030..70c8522b58 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2085,7 +2085,7 @@ pub fn watcher_validate_taker_fee( if tx_confirmed_before_block { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Fee tx {:?} confirmed before min_block {}", - EARLY_CONFIRMATION_ERR_LOG, taker_fee_tx, min_block_number + EARLY_CONFIRMATION_ERR_LOG, tx_from_rpc, min_block_number ))); } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 820a5ad619..20a6004c95 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -9,6 +9,7 @@ use instant::{Duration, Instant}; use lazy_static::lazy_static; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; +use mm2_err_handle::prelude::*; use mm2_libp2p::p2p_ctx::P2PContext; use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, PeerAddress, TopicPrefix}; use ser_error_derive::SerializeErrorType; @@ -16,7 +17,7 @@ use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::sync::Mutex; -use crate::lp_network::broadcast_p2p_msg; +use crate::lp_network::{broadcast_p2p_msg, P2PRequestError, P2PRequestResult}; pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; @@ -279,7 +280,10 @@ pub async fn peer_connection_healthcheck_rpc( Ok(rx.timeout(timeout_duration).await == Ok(Ok(()))) } -pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_libp2p::GossipsubMessage) { +pub(crate) async fn process_p2p_healthcheck_message( + ctx: &MmArc, + message: mm2_libp2p::GossipsubMessage, +) -> P2PRequestResult<()> { macro_rules! try_or_return { ($exp:expr, $msg: expr) => { match $exp { @@ -292,24 +296,17 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li }; } - let data = try_or_return!( - HealthcheckMessage::decode(&message.data), - "Couldn't decode healthcheck message" - ); + let data = HealthcheckMessage::decode(&message.data) + .map_to_mm(|e| P2PRequestError::DecodeError(format!("Couldn't decode healthcheck message: {}", e)))?; + let sender_peer = data.is_received_message_valid().map_to_mm(|e| { + P2PRequestError::ValidationFailed(format!("Received an invalid healthcheck message. Error: {}", e)) + })?; let ctx = ctx.clone(); // Pass the remaining work to another thread to free up this one as soon as possible, // so KDF can handle a high amount of healthcheck messages more efficiently. ctx.spawner().spawn(async move { - let sender_peer = match data.is_received_message_valid() { - Ok(t) => t, - Err(e) => { - log::error!("Received an invalid healthcheck message. Error: {e}"); - return; - }, - }; - if data.should_reply() { // Reply the message so they know we are healthy. @@ -337,6 +334,8 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li }; } }); + + Ok(()) } #[cfg(any(test, target_arch = "wasm32"))] diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index 08ae5f8b3e..b2ef53f3fb 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -62,6 +62,7 @@ pub enum P2PRequestError { ResponseError(String), #[display(fmt = "Expected 1 response, found {}", _0)] ExpectedSingleResponseError(usize), + ValidationFailed(String), } /// Enum covering error cases that can happen during P2P message processing. @@ -190,15 +191,16 @@ async fn process_p2p_message( to_propagate = true; }, Some(lp_swap::TX_HELPER_PREFIX) => { - if let Some(pair) = split.next() { - if let Ok(Some(coin)) = lp_coinfind(&ctx, pair).await { + if let Some(ticker) = split.next() { + if let Ok(Some(coin)) = lp_coinfind(&ctx, ticker).await { if let Err(e) = coin.tx_enum_from_bytes(&message.data) { log::error!("Message cannot continue the process due to: {:?}", e); return; }; - let fut = coin.send_raw_tx_bytes(&message.data); - ctx.spawner().spawn(async { + if coin.is_utxo_in_native_mode() { + let fut = coin.send_raw_tx_bytes(&message.data); + ctx.spawner().spawn(async { match fut.compat().await { Ok(id) => log::debug!("Transaction broadcasted successfully: {:?} ", id), // TODO (After https://github.com/KomodoPlatform/atomicDEX-API/pull/1433) @@ -207,11 +209,19 @@ async fn process_p2p_message( Err(e) => log::error!("Broadcast transaction failed (ignore this error if the transaction already sent by another seednode). {}", e), }; }) + } } + + to_propagate = true; } }, Some(lp_healthcheck::PEER_HEALTHCHECK_PREFIX) => { - lp_healthcheck::process_p2p_healthcheck_message(&ctx, message).await + if let Err(e) = lp_healthcheck::process_p2p_healthcheck_message(&ctx, message).await { + log::error!("{}", e); + return; + } + + to_propagate = true; }, None | Some(_) => (), } diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 7692503c18..0acb7fc443 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -151,8 +151,11 @@ pub const TX_HELPER_PREFIX: TopicPrefix = "txhlp"; pub(crate) const LEGACY_SWAP_TYPE: u8 = 0; pub(crate) const MAKER_SWAP_V2_TYPE: u8 = 1; pub(crate) const TAKER_SWAP_V2_TYPE: u8 = 2; -const MAX_STARTED_AT_DIFF: u64 = 60; +pub(crate) const TAKER_FEE_VALIDATION_ATTEMPTS: usize = 6; +pub(crate) const TAKER_FEE_VALIDATION_RETRY_DELAY_SECS: f64 = 10.; + +const MAX_STARTED_AT_DIFF: u64 = 60; const NEGOTIATE_SEND_INTERVAL: f64 = 30.; /// If a certain P2P message is not received, swap will be aborted after this time expires. diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 84c1bbc6aa..0eb72b8a71 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -9,7 +9,8 @@ use super::{broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_msg_e wait_for_maker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, SavedTradeFee, SecretHashAlgo, SwapConfirmationsSettings, SwapError, SwapMsg, SwapPubkeys, SwapTxDataMsg, - SwapsContext, TransactionIdentifier, INCLUDE_REFUND_FEE, NO_REFUND_FEE, WAIT_CONFIRM_INTERVAL_SEC}; + SwapsContext, TransactionIdentifier, INCLUDE_REFUND_FEE, NO_REFUND_FEE, TAKER_FEE_VALIDATION_ATTEMPTS, + TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::lp_network::subscribe_to_topic; use crate::lp_ordermatch::MakerOrderBuilder; @@ -771,13 +772,13 @@ impl MakerSwap { { Ok(_) => break, Err(err) => { - if attempts >= 6 { + if attempts >= TAKER_FEE_VALIDATION_ATTEMPTS { return Ok((Some(MakerSwapCommand::Finish), vec![ MakerSwapEvent::TakerFeeValidateFailed(ERRL!("{}", err).into()), ])); } else { attempts += 1; - Timer::sleep(10.).await; + Timer::sleep(TAKER_FEE_VALIDATION_RETRY_DELAY_SECS).await; } }, }; diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index 95a1d1e88a..312f62e5c5 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -1,5 +1,6 @@ use super::{broadcast_p2p_tx_msg, get_payment_locktime, lp_coinfind, taker_payment_spend_deadline, tx_helper_topic, - H256Json, SwapsContext, WAIT_CONFIRM_INTERVAL_SEC}; + H256Json, SwapsContext, TAKER_FEE_VALIDATION_ATTEMPTS, TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, + WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_network::{P2PRequestError, P2PRequestResult}; use crate::MmError; @@ -181,24 +182,31 @@ impl State for ValidateTakerFee { async fn on_changed(self: Box, watcher_ctx: &mut WatcherStateMachine) -> StateResult { debug!("Watcher validate taker fee"); - let validated_f = watcher_ctx - .taker_coin - .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: watcher_ctx.data.taker_fee_hash.clone(), - sender_pubkey: watcher_ctx.verified_pub.clone(), - min_block_number: watcher_ctx.data.taker_coin_start_block, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.clone(), - lock_duration: watcher_ctx.data.lock_duration, - }) - .compat(); - - if let Err(err) = validated_f.await { - return Self::change_state(Stopped::from_reason(StopReason::Error( - WatcherError::InvalidTakerFee(format!("{:?}", err)).into(), - ))); - }; - Self::change_state(ValidateTakerPayment {}) + let validation_result = retry_on_err!(async { + watcher_ctx + .taker_coin + .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: watcher_ctx.data.taker_fee_hash.clone(), + sender_pubkey: watcher_ctx.verified_pub.clone(), + min_block_number: watcher_ctx.data.taker_coin_start_block, + fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.clone(), + lock_duration: watcher_ctx.data.lock_duration, + }) + .compat() + .await + }) + .repeat_every_secs(TAKER_FEE_VALIDATION_RETRY_DELAY_SECS) + .attempts(TAKER_FEE_VALIDATION_ATTEMPTS) + .inspect_err(|e| error!("Error validating taker fee: {}", e)) + .await; + + match validation_result { + Ok(_) => Self::change_state(ValidateTakerPayment {}), + Err(repeat_err) => Self::change_state(Stopped::from_reason(StopReason::Error( + WatcherError::InvalidTakerFee(repeat_err.to_string()).into(), + ))), + } } } From 4e0b5a9d3e0a0dcf635c8938a5e632185cae8949 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Tue, 19 Nov 2024 19:54:50 +0200 Subject: [PATCH 5/6] chore(release): add changelog entries for v2.2.0-beta (#2240) --- CHANGELOG.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a2424397..65d3926bc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,92 @@ +## v2.2.0-beta - 2024-11-01 + +**Features:** +- Connection Healthcheck + - Connection healthcheck implementation for peers was introduced. [#2194](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2194) +- Custom Tokens Activation + - Support for enabling custom EVM (ERC20, PLG20, etc..) tokens without requiring them to be in the coins config was added. [#2141](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2141) + - This allows users to interact with any ERC20 token by providing the contract address. + +**Enhancements/Fixes:** +- Trading Protocol Upgrade [#1895](https://github.com/KomodoPlatform/atomicDEX-API/issues/1895) + - EVM TPU taker methods were implemented and enhancements were made to ETH docker tests. [#2169](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2169) + - EVM TPU maker methods were implemented. [#2211](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2211) +- NFT integration [#900](https://github.com/KomodoPlatform/atomicDEX-API/issues/900) + - Refund methods for NFT swaps were completed. [#2129](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2129) + - `token_id` field was added to the tx history primary key. [#2209](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2209) +- Graceful Shutdown + - CTRL-C signal handling with graceful shutdown was implemented. [#2213](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2213) +- Seed Management [#1939](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1939) + - A new `get_wallet_names` RPC was added to retrieve information about all wallet names and the currently active one. [#2202](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2202) +- Cosmos Integration [#1432](https://github.com/KomodoPlatform/atomicDEX-API/issues/1432) + - Cosmos tx broadcasting error was fixed by upgrading cosmrs to version 15. [#2238](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2238) + - Cosmos transaction history implementation was incorrectly parsing addresses (using the relayer address instead of the cross-chain address) from IBC transactions. The address parsing logic was fixed in [#2245](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2245) +- Order Management + - Cancel order race condition was addressed using time-based cache. [#2232](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2232) +- Swap Improvements + - A legacy swap issue was resolved where taker spent maker payment transactions were sometimes incorrectly marked as successful when they were actually reverted or not confirmed, particularly in EVM-based swaps. [#2199](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2199) + - Two new events were added: "MakerPaymentSpendConfirmed" and "MakerPaymentSpendConfirmFailed" + - A fix was introduced where Takers don't need to confirm their own payment as they can wait for the spending of it straight away. [#2249](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2249) + - This invalidates this fix [#1442](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1442), a better solution will be introduced where taker rebroadcasts their transaction if it's not on the chain. + - A fix was introduced for recover funds for takers when the swap was marked as unsuccessful due to the maker payment spend transaction not being confirmed. [#2242](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2242) + - The required confirmations from coin config for taker/maker payment spend are now used instead of using 1 confirmation max. This is because some chains require more than 1 confirmation for finality, e.g. Polygon. +- Swap watchers [#1431](https://github.com/KomodoPlatform/atomicDEX-API/issues/1431) + - Taker fee validation retries now work the same way as for makers. [#2263](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2263) +- Electrum Client + - Electrum client was refactored to add min/max connection controls, with server priority based on list order. [#1966](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1966) + - Electrum client can now operate in single-server mode (1,1) to reduce resource usage (especially beneficial for mobile) or multi-server (legacy) mode for reliability. + - Higher priority servers automatically replace lower priority ones when reconnecting during periodic retries or when connection count drops below minimum. +- Coins Activation + - EVM addresses are now displayed in full in iguana v2 activation response. [#2254](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2254) +- HD Wallet [#1838](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1838) + - Balance is now returned as `CoinBalanceMap` for both UTXOs and QTUM. [#2259](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2259) + - This is to return the same type/json across all coins for GUIs since EVM uses `CoinBalanceMap`. + - EVM addresses are displayed in full in `get_new_address` response after [#2264](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2264) +- RPC Service + - A fix was introduced to run rpc request futures till completion in [#1966](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1966) + - This ensures RPC request futures complete fully even if clients disconnect, preventing partial state updates and maintaining data consistency. +- Security Enhancements + - Message lifetime overflows were added to prevent creating messages for proxy with too long lifetimes. [#2233](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2233) + - Remote files are now handled in a safer way in CI. [#2217](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2217) +- Build Process + - `wasm-opt` overriding was removed. [#2200](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2200) +- Escaped response body in native RPC was removed. [#2219](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2219) +- Creation of the all-zeroes dir on KDF start was stopped. [#2218](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2218) +- OPTIONS requests to KDF server were added. [#2191](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2191) + +**Removals:** +- Solana Support [#1085](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1085) + - Solana implementation was removed until it can be redone using the latest Solana SDK. [#2239](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2239) +- Adex-CLI [#1682](https://github.com/KomodoPlatform/atomicDEX-API/issues/1682) + - adex-cli was deprecated pending work on a simpler, more maintainable implementation. [#2234](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2234) + +**Other Changes:** +- Documentation + - Issue link in README was updated. [#2227](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2227) + - Commit badges were updated to use dev branch in README. [#2193](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2193) + - Leftover subcommands were removed from help message. [#2235](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2235) [#2270](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2270) +- Code Structure + - lib.rs was replaced by mm2.rs as the root lib for mm2_main. [#2178](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2178) +- Code Improvements + - P2P feature was added to mm2_net dependency to allow the coins crate to be compiled and tested independently. [#2210](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2210) + - Coins mod clippy warnings in WASM were fixed. [#2224](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2224) + - Nonsense CLI arguments were removed. [#2216](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2216) +- Tests + - Tendermint IBC tests were fixed by preparing IBC channels inside the container. [#2246](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2246) + - `.wait()` usage was replaced with `block_on` in tests to ensure consistent runtime usage, fixing issues with tokio TCP streams in non-tokio runtimes. [#2220](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2220) + - Debug assertions for tests were enabled. [#2204](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2204) + - More Sepolia test endpoints were added in [#2262](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2262) + +**NB - Backwards compatibility breaking changes:** +- RPC Renaming + - `get_peers_info` RPC was renamed to `get_directly_connected_peers`. [#2195](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2195) +- Cosmos Integration [#1432](https://github.com/KomodoPlatform/atomicDEX-API/issues/1432) + - Updates to Tendermint activation payloads: + - 'rpc_urls' field (previously a list of plain string values) is replaced with 'nodes' (a list of JSON objects). [#2173](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2173) +- Komodo DeFi Proxy + - All RPC methods fields controlling komodo-defi-proxy are renamed to 'komodo_proxy', affecting various activations, including ETH/EVM. [#2173](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2173) + + ## v2.1.0-beta - 2024-07-31 **Features:** From af8901171fdbbba7a80ba29f68dfb9b35cb6941b Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Tue, 19 Nov 2024 19:57:11 +0200 Subject: [PATCH 6/6] chore(release): update v2.2.0-beta date (#2277) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d3926bc3..f3d8816316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## v2.2.0-beta - 2024-11-01 +## v2.2.0-beta - 2024-11-22 **Features:** - Connection Healthcheck