diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a2424397..f3d8816316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,92 @@ +## v2.2.0-beta - 2024-11-22 + +**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:** diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 76ca3787da..dee2f28340 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -161,6 +161,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}; @@ -890,7 +893,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. @@ -914,7 +917,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. @@ -1075,14 +1078,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() } @@ -6390,32 +6393,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 6a97cac477..63ef649d2d 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}; @@ -69,6 +70,8 @@ pub enum EthActivationV2Error { InvalidHardwareWalletCall, #[from_stringify("WalletConnectError")] WalletConnectError(String), + #[display(fmt = "Custom token error: {}", _0)] + CustomTokenError(CustomTokenError), } impl From for EthActivationV2Error { @@ -100,6 +103,7 @@ impl From for EthActivationV2Error { EthActivationV2Error::UnexpectedDerivationMethod(err) }, EthTokenActivationError::PrivKeyPolicyNotAllowed(e) => EthActivationV2Error::PrivKeyPolicyNotAllowed(e), + EthTokenActivationError::CustomTokenError(e) => EthActivationV2Error::CustomTokenError(e), } } } @@ -219,6 +223,7 @@ pub enum EthTokenActivationError { Transport(String), UnexpectedDerivationMethod(UnexpectedDerivationMethod), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + CustomTokenError(CustomTokenError), } impl From for EthTokenActivationError { @@ -384,9 +389,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. @@ -395,9 +402,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() @@ -412,7 +434,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`, @@ -423,11 +449,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 9c9d631fa3..6297f6f4c8 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -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, @@ -3275,6 +3275,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) } @@ -4255,6 +4259,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. @@ -4427,10 +4522,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/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/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 99db217441..6c62196638 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -2300,6 +2300,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_ledger_connection() diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 446d838172..143ff23e9e 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_ledger_connection() diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index a0c9662e86..dff5288971 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2088,7 +2088,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/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 3523e273a9..aef5ff3d20 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -89,6 +89,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), } } } @@ -122,6 +123,7 @@ impl From for InitTokensAsMmCoinsError { InitTokensAsMmCoinsError::UnexpectedDerivationMethod(e) }, EthTokenActivationError::PrivKeyPolicyNotAllowed(e) => InitTokensAsMmCoinsError::Internal(e.to_string()), + EthTokenActivationError::CustomTokenError(e) => InitTokensAsMmCoinsError::CustomTokenError(e), } } } @@ -147,7 +149,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); } @@ -187,7 +195,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 037997ec99..1a60cd73bf 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), } } } @@ -139,10 +143,14 @@ where .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 +243,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 +250,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 +262,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 +290,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 +309,7 @@ impl From for EnablePlatformCoinWithTokensError { error: err.to_string(), } }, + CoinConfWithProtocolError::CustomTokenError(e) => EnablePlatformCoinWithTokensError::CustomTokenError(e), } } } @@ -307,9 +317,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 +334,7 @@ impl From for EnablePlatformCoinWithTokensError { InitTokensAsMmCoinsError::UnexpectedDerivationMethod(e) => { EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(e.to_string()) }, + InitTokensAsMmCoinsError::CustomTokenError(e) => EnablePlatformCoinWithTokensError::CustomTokenError(e), } } } @@ -362,9 +370,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 +457,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/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 5e9db51111..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"; @@ -114,7 +115,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 { @@ -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(), + ))), + } } } diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index 6d9e5fcf56..b8d228fe3f 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; pub mod wc_commands; diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 0632af3adc..17f34cf3ed 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -6,8 +6,15 @@ 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::rpc::wc_commands::{new_connection, ping_session}; use crate::{lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, start_version_stat_collection, @@ -186,6 +193,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 d89b456874..1757f97d36 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -5257,127 +5257,6 @@ fn test_sell_min_volume_dust() { assert!(!rc.0.is_success(), "!sell: {}", rc.1); } -#[test] -fn test_enable_eth_erc20_coins_with_enable_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")); - - 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 00dd58658a..6401b16c43 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -3256,6 +3256,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, +}