From 0a9410258c7bf58e78d26b33b7a0ffb749c7ba16 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Tue, 19 Nov 2024 19:16:47 +0200 Subject: [PATCH 01/27] fix(watchers): align taker fee validation retries with makers (#2263) This also fixes propagation of health check messages. --- mm2src/coins/utxo/utxo_common.rs | 2 +- mm2src/mm2_main/src/lp_healthcheck.rs | 27 ++++++------- mm2src/mm2_main/src/lp_network.rs | 20 +++++++--- mm2src/mm2_main/src/lp_swap.rs | 5 ++- mm2src/mm2_main/src/lp_swap/maker_swap.rs | 7 ++-- mm2src/mm2_main/src/lp_swap/swap_watcher.rs | 44 ++++++++++++--------- 6 files changed, 63 insertions(+), 42 deletions(-) diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 37955e9030..70c8522b58 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2085,7 +2085,7 @@ pub fn watcher_validate_taker_fee( if tx_confirmed_before_block { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Fee tx {:?} confirmed before min_block {}", - EARLY_CONFIRMATION_ERR_LOG, taker_fee_tx, min_block_number + EARLY_CONFIRMATION_ERR_LOG, tx_from_rpc, min_block_number ))); } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 820a5ad619..20a6004c95 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -9,6 +9,7 @@ use instant::{Duration, Instant}; use lazy_static::lazy_static; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; +use mm2_err_handle::prelude::*; use mm2_libp2p::p2p_ctx::P2PContext; use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, PeerAddress, TopicPrefix}; use ser_error_derive::SerializeErrorType; @@ -16,7 +17,7 @@ use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::sync::Mutex; -use crate::lp_network::broadcast_p2p_msg; +use crate::lp_network::{broadcast_p2p_msg, P2PRequestError, P2PRequestResult}; pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; @@ -279,7 +280,10 @@ pub async fn peer_connection_healthcheck_rpc( Ok(rx.timeout(timeout_duration).await == Ok(Ok(()))) } -pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_libp2p::GossipsubMessage) { +pub(crate) async fn process_p2p_healthcheck_message( + ctx: &MmArc, + message: mm2_libp2p::GossipsubMessage, +) -> P2PRequestResult<()> { macro_rules! try_or_return { ($exp:expr, $msg: expr) => { match $exp { @@ -292,24 +296,17 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li }; } - let data = try_or_return!( - HealthcheckMessage::decode(&message.data), - "Couldn't decode healthcheck message" - ); + let data = HealthcheckMessage::decode(&message.data) + .map_to_mm(|e| P2PRequestError::DecodeError(format!("Couldn't decode healthcheck message: {}", e)))?; + let sender_peer = data.is_received_message_valid().map_to_mm(|e| { + P2PRequestError::ValidationFailed(format!("Received an invalid healthcheck message. Error: {}", e)) + })?; let ctx = ctx.clone(); // Pass the remaining work to another thread to free up this one as soon as possible, // so KDF can handle a high amount of healthcheck messages more efficiently. ctx.spawner().spawn(async move { - let sender_peer = match data.is_received_message_valid() { - Ok(t) => t, - Err(e) => { - log::error!("Received an invalid healthcheck message. Error: {e}"); - return; - }, - }; - if data.should_reply() { // Reply the message so they know we are healthy. @@ -337,6 +334,8 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li }; } }); + + Ok(()) } #[cfg(any(test, target_arch = "wasm32"))] diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index 08ae5f8b3e..b2ef53f3fb 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -62,6 +62,7 @@ pub enum P2PRequestError { ResponseError(String), #[display(fmt = "Expected 1 response, found {}", _0)] ExpectedSingleResponseError(usize), + ValidationFailed(String), } /// Enum covering error cases that can happen during P2P message processing. @@ -190,15 +191,16 @@ async fn process_p2p_message( to_propagate = true; }, Some(lp_swap::TX_HELPER_PREFIX) => { - if let Some(pair) = split.next() { - if let Ok(Some(coin)) = lp_coinfind(&ctx, pair).await { + if let Some(ticker) = split.next() { + if let Ok(Some(coin)) = lp_coinfind(&ctx, ticker).await { if let Err(e) = coin.tx_enum_from_bytes(&message.data) { log::error!("Message cannot continue the process due to: {:?}", e); return; }; - let fut = coin.send_raw_tx_bytes(&message.data); - ctx.spawner().spawn(async { + if coin.is_utxo_in_native_mode() { + let fut = coin.send_raw_tx_bytes(&message.data); + ctx.spawner().spawn(async { match fut.compat().await { Ok(id) => log::debug!("Transaction broadcasted successfully: {:?} ", id), // TODO (After https://github.com/KomodoPlatform/atomicDEX-API/pull/1433) @@ -207,11 +209,19 @@ async fn process_p2p_message( Err(e) => log::error!("Broadcast transaction failed (ignore this error if the transaction already sent by another seednode). {}", e), }; }) + } } + + to_propagate = true; } }, Some(lp_healthcheck::PEER_HEALTHCHECK_PREFIX) => { - lp_healthcheck::process_p2p_healthcheck_message(&ctx, message).await + if let Err(e) = lp_healthcheck::process_p2p_healthcheck_message(&ctx, message).await { + log::error!("{}", e); + return; + } + + to_propagate = true; }, None | Some(_) => (), } diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 7692503c18..0acb7fc443 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -151,8 +151,11 @@ pub const TX_HELPER_PREFIX: TopicPrefix = "txhlp"; pub(crate) const LEGACY_SWAP_TYPE: u8 = 0; pub(crate) const MAKER_SWAP_V2_TYPE: u8 = 1; pub(crate) const TAKER_SWAP_V2_TYPE: u8 = 2; -const MAX_STARTED_AT_DIFF: u64 = 60; +pub(crate) const TAKER_FEE_VALIDATION_ATTEMPTS: usize = 6; +pub(crate) const TAKER_FEE_VALIDATION_RETRY_DELAY_SECS: f64 = 10.; + +const MAX_STARTED_AT_DIFF: u64 = 60; const NEGOTIATE_SEND_INTERVAL: f64 = 30.; /// If a certain P2P message is not received, swap will be aborted after this time expires. diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 84c1bbc6aa..0eb72b8a71 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -9,7 +9,8 @@ use super::{broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_msg_e wait_for_maker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, SavedTradeFee, SecretHashAlgo, SwapConfirmationsSettings, SwapError, SwapMsg, SwapPubkeys, SwapTxDataMsg, - SwapsContext, TransactionIdentifier, INCLUDE_REFUND_FEE, NO_REFUND_FEE, WAIT_CONFIRM_INTERVAL_SEC}; + SwapsContext, TransactionIdentifier, INCLUDE_REFUND_FEE, NO_REFUND_FEE, TAKER_FEE_VALIDATION_ATTEMPTS, + TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::lp_network::subscribe_to_topic; use crate::lp_ordermatch::MakerOrderBuilder; @@ -771,13 +772,13 @@ impl MakerSwap { { Ok(_) => break, Err(err) => { - if attempts >= 6 { + if attempts >= TAKER_FEE_VALIDATION_ATTEMPTS { return Ok((Some(MakerSwapCommand::Finish), vec![ MakerSwapEvent::TakerFeeValidateFailed(ERRL!("{}", err).into()), ])); } else { attempts += 1; - Timer::sleep(10.).await; + Timer::sleep(TAKER_FEE_VALIDATION_RETRY_DELAY_SECS).await; } }, }; diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index 95a1d1e88a..312f62e5c5 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -1,5 +1,6 @@ use super::{broadcast_p2p_tx_msg, get_payment_locktime, lp_coinfind, taker_payment_spend_deadline, tx_helper_topic, - H256Json, SwapsContext, WAIT_CONFIRM_INTERVAL_SEC}; + H256Json, SwapsContext, TAKER_FEE_VALIDATION_ATTEMPTS, TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, + WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_network::{P2PRequestError, P2PRequestResult}; use crate::MmError; @@ -181,24 +182,31 @@ impl State for ValidateTakerFee { async fn on_changed(self: Box, watcher_ctx: &mut WatcherStateMachine) -> StateResult { debug!("Watcher validate taker fee"); - let validated_f = watcher_ctx - .taker_coin - .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: watcher_ctx.data.taker_fee_hash.clone(), - sender_pubkey: watcher_ctx.verified_pub.clone(), - min_block_number: watcher_ctx.data.taker_coin_start_block, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.clone(), - lock_duration: watcher_ctx.data.lock_duration, - }) - .compat(); - - if let Err(err) = validated_f.await { - return Self::change_state(Stopped::from_reason(StopReason::Error( - WatcherError::InvalidTakerFee(format!("{:?}", err)).into(), - ))); - }; - Self::change_state(ValidateTakerPayment {}) + let validation_result = retry_on_err!(async { + watcher_ctx + .taker_coin + .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: watcher_ctx.data.taker_fee_hash.clone(), + sender_pubkey: watcher_ctx.verified_pub.clone(), + min_block_number: watcher_ctx.data.taker_coin_start_block, + fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.clone(), + lock_duration: watcher_ctx.data.lock_duration, + }) + .compat() + .await + }) + .repeat_every_secs(TAKER_FEE_VALIDATION_RETRY_DELAY_SECS) + .attempts(TAKER_FEE_VALIDATION_ATTEMPTS) + .inspect_err(|e| error!("Error validating taker fee: {}", e)) + .await; + + match validation_result { + Ok(_) => Self::change_state(ValidateTakerPayment {}), + Err(repeat_err) => Self::change_state(Stopped::from_reason(StopReason::Error( + WatcherError::InvalidTakerFee(repeat_err.to_string()).into(), + ))), + } } } From 4e0b5a9d3e0a0dcf635c8938a5e632185cae8949 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Tue, 19 Nov 2024 19:54:50 +0200 Subject: [PATCH 02/27] chore(release): add changelog entries for v2.2.0-beta (#2240) --- CHANGELOG.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a2424397..65d3926bc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,92 @@ +## v2.2.0-beta - 2024-11-01 + +**Features:** +- Connection Healthcheck + - Connection healthcheck implementation for peers was introduced. [#2194](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2194) +- Custom Tokens Activation + - Support for enabling custom EVM (ERC20, PLG20, etc..) tokens without requiring them to be in the coins config was added. [#2141](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2141) + - This allows users to interact with any ERC20 token by providing the contract address. + +**Enhancements/Fixes:** +- Trading Protocol Upgrade [#1895](https://github.com/KomodoPlatform/atomicDEX-API/issues/1895) + - EVM TPU taker methods were implemented and enhancements were made to ETH docker tests. [#2169](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2169) + - EVM TPU maker methods were implemented. [#2211](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2211) +- NFT integration [#900](https://github.com/KomodoPlatform/atomicDEX-API/issues/900) + - Refund methods for NFT swaps were completed. [#2129](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2129) + - `token_id` field was added to the tx history primary key. [#2209](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2209) +- Graceful Shutdown + - CTRL-C signal handling with graceful shutdown was implemented. [#2213](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2213) +- Seed Management [#1939](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1939) + - A new `get_wallet_names` RPC was added to retrieve information about all wallet names and the currently active one. [#2202](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2202) +- Cosmos Integration [#1432](https://github.com/KomodoPlatform/atomicDEX-API/issues/1432) + - Cosmos tx broadcasting error was fixed by upgrading cosmrs to version 15. [#2238](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2238) + - Cosmos transaction history implementation was incorrectly parsing addresses (using the relayer address instead of the cross-chain address) from IBC transactions. The address parsing logic was fixed in [#2245](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2245) +- Order Management + - Cancel order race condition was addressed using time-based cache. [#2232](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2232) +- Swap Improvements + - A legacy swap issue was resolved where taker spent maker payment transactions were sometimes incorrectly marked as successful when they were actually reverted or not confirmed, particularly in EVM-based swaps. [#2199](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2199) + - Two new events were added: "MakerPaymentSpendConfirmed" and "MakerPaymentSpendConfirmFailed" + - A fix was introduced where Takers don't need to confirm their own payment as they can wait for the spending of it straight away. [#2249](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2249) + - This invalidates this fix [#1442](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1442), a better solution will be introduced where taker rebroadcasts their transaction if it's not on the chain. + - A fix was introduced for recover funds for takers when the swap was marked as unsuccessful due to the maker payment spend transaction not being confirmed. [#2242](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2242) + - The required confirmations from coin config for taker/maker payment spend are now used instead of using 1 confirmation max. This is because some chains require more than 1 confirmation for finality, e.g. Polygon. +- Swap watchers [#1431](https://github.com/KomodoPlatform/atomicDEX-API/issues/1431) + - Taker fee validation retries now work the same way as for makers. [#2263](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2263) +- Electrum Client + - Electrum client was refactored to add min/max connection controls, with server priority based on list order. [#1966](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1966) + - Electrum client can now operate in single-server mode (1,1) to reduce resource usage (especially beneficial for mobile) or multi-server (legacy) mode for reliability. + - Higher priority servers automatically replace lower priority ones when reconnecting during periodic retries or when connection count drops below minimum. +- Coins Activation + - EVM addresses are now displayed in full in iguana v2 activation response. [#2254](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2254) +- HD Wallet [#1838](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1838) + - Balance is now returned as `CoinBalanceMap` for both UTXOs and QTUM. [#2259](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2259) + - This is to return the same type/json across all coins for GUIs since EVM uses `CoinBalanceMap`. + - EVM addresses are displayed in full in `get_new_address` response after [#2264](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2264) +- RPC Service + - A fix was introduced to run rpc request futures till completion in [#1966](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1966) + - This ensures RPC request futures complete fully even if clients disconnect, preventing partial state updates and maintaining data consistency. +- Security Enhancements + - Message lifetime overflows were added to prevent creating messages for proxy with too long lifetimes. [#2233](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2233) + - Remote files are now handled in a safer way in CI. [#2217](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2217) +- Build Process + - `wasm-opt` overriding was removed. [#2200](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2200) +- Escaped response body in native RPC was removed. [#2219](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2219) +- Creation of the all-zeroes dir on KDF start was stopped. [#2218](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2218) +- OPTIONS requests to KDF server were added. [#2191](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2191) + +**Removals:** +- Solana Support [#1085](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1085) + - Solana implementation was removed until it can be redone using the latest Solana SDK. [#2239](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2239) +- Adex-CLI [#1682](https://github.com/KomodoPlatform/atomicDEX-API/issues/1682) + - adex-cli was deprecated pending work on a simpler, more maintainable implementation. [#2234](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2234) + +**Other Changes:** +- Documentation + - Issue link in README was updated. [#2227](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2227) + - Commit badges were updated to use dev branch in README. [#2193](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2193) + - Leftover subcommands were removed from help message. [#2235](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2235) [#2270](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2270) +- Code Structure + - lib.rs was replaced by mm2.rs as the root lib for mm2_main. [#2178](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2178) +- Code Improvements + - P2P feature was added to mm2_net dependency to allow the coins crate to be compiled and tested independently. [#2210](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2210) + - Coins mod clippy warnings in WASM were fixed. [#2224](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2224) + - Nonsense CLI arguments were removed. [#2216](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2216) +- Tests + - Tendermint IBC tests were fixed by preparing IBC channels inside the container. [#2246](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2246) + - `.wait()` usage was replaced with `block_on` in tests to ensure consistent runtime usage, fixing issues with tokio TCP streams in non-tokio runtimes. [#2220](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2220) + - Debug assertions for tests were enabled. [#2204](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2204) + - More Sepolia test endpoints were added in [#2262](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2262) + +**NB - Backwards compatibility breaking changes:** +- RPC Renaming + - `get_peers_info` RPC was renamed to `get_directly_connected_peers`. [#2195](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2195) +- Cosmos Integration [#1432](https://github.com/KomodoPlatform/atomicDEX-API/issues/1432) + - Updates to Tendermint activation payloads: + - 'rpc_urls' field (previously a list of plain string values) is replaced with 'nodes' (a list of JSON objects). [#2173](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2173) +- Komodo DeFi Proxy + - All RPC methods fields controlling komodo-defi-proxy are renamed to 'komodo_proxy', affecting various activations, including ETH/EVM. [#2173](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2173) + + ## v2.1.0-beta - 2024-07-31 **Features:** From af8901171fdbbba7a80ba29f68dfb9b35cb6941b Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Tue, 19 Nov 2024 19:57:11 +0200 Subject: [PATCH 03/27] chore(release): update v2.2.0-beta date (#2277) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d3926bc3..f3d8816316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## v2.2.0-beta - 2024-11-01 +## v2.2.0-beta - 2024-11-22 **Features:** - Connection Healthcheck From 8f83308a8333d2ca9a8a117e2fdcc964970a96ee Mon Sep 17 00:00:00 2001 From: dimxy Date: Tue, 3 Dec 2024 20:06:27 +0500 Subject: [PATCH 04/27] feat(LRAPI): add 1inch classic swap rpc (#2222) This commit adds initial code to connect to 1inch Liquidity Routing API (LRAPI) provider and two new RPCs to use 1inch classic swap API. It also adds 'approve' and 'allowance' RPCs (for ERC20 tokens). --- Cargo.lock | 23 + Cargo.toml | 1 + mm2src/coins/eth.rs | 41 +- mm2src/coins/eth/eip1559_gas_fee.rs | 52 +-- mm2src/coins/eth/eth_rpc.rs | 4 +- mm2src/coins/eth/eth_tests.rs | 7 + mm2src/coins/lp_coins.rs | 7 +- .../coins/rpc_command/get_estimated_fees.rs | 18 +- mm2src/common/Cargo.toml | 1 + mm2src/common/common.rs | 32 ++ mm2src/mm2_main/Cargo.toml | 3 + mm2src/mm2_main/src/ext_api.rs | 3 + .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 15 + mm2src/mm2_main/src/rpc/lp_commands/mod.rs | 1 + .../mm2_main/src/rpc/lp_commands/one_inch.rs | 5 + .../src/rpc/lp_commands/one_inch/errors.rs | 99 ++++ .../src/rpc/lp_commands/one_inch/rpcs.rs | 439 ++++++++++++++++++ .../src/rpc/lp_commands/one_inch/types.rs | 213 +++++++++ mm2src/mm2_main/src/rpc/lp_commands/tokens.rs | 82 +++- .../tests/docker_tests/docker_tests_inner.rs | 77 ++- .../tests/docker_tests/eth_docker_tests.rs | 16 +- mm2src/trading_api/Cargo.toml | 28 ++ mm2src/trading_api/src/lib.rs | 3 + mm2src/trading_api/src/one_inch_api.rs | 5 + mm2src/trading_api/src/one_inch_api/client.rs | 176 +++++++ mm2src/trading_api/src/one_inch_api/errors.rs | 130 ++++++ mm2src/trading_api/src/one_inch_api/types.rs | 411 ++++++++++++++++ 27 files changed, 1820 insertions(+), 72 deletions(-) create mode 100644 mm2src/mm2_main/src/ext_api.rs create mode 100644 mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs create mode 100644 mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs create mode 100644 mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs create mode 100644 mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs create mode 100644 mm2src/trading_api/Cargo.toml create mode 100644 mm2src/trading_api/src/lib.rs create mode 100644 mm2src/trading_api/src/one_inch_api.rs create mode 100644 mm2src/trading_api/src/one_inch_api/client.rs create mode 100644 mm2src/trading_api/src/one_inch_api/errors.rs create mode 100644 mm2src/trading_api/src/one_inch_api/types.rs diff --git a/Cargo.lock b/Cargo.lock index 2bc1c3d543..7fce54111d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1013,6 +1013,7 @@ dependencies = [ "log", "parking_lot", "parking_lot_core 0.6.2", + "paste", "primitive-types", "rand 0.7.3", "regex", @@ -4076,6 +4077,7 @@ dependencies = [ "num-traits", "parity-util-mem", "parking_lot", + "primitive-types", "primitives", "prost", "prost-build", @@ -4105,6 +4107,7 @@ dependencies = [ "spv_validation", "testcontainers", "tokio", + "trading_api", "trie-db", "trie-root 0.16.0", "url", @@ -7265,6 +7268,26 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trading_api" +version = "0.1.0" +dependencies = [ + "common", + "derive_more", + "enum_derives", + "ethereum-types", + "lazy_static", + "mm2_core", + "mm2_err_handle", + "mm2_net", + "mm2_number", + "mocktopus", + "serde", + "serde_derive", + "serde_json", + "url", +] + [[package]] name = "trezor" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index ab18c83da1..507c2e5c31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ members = [ "mm2src/proxy_signature", "mm2src/rpc_task", "mm2src/trezor", + "mm2src/trading_api", ] exclude = [ diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 7934bdf485..b8db8c7c12 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -524,20 +524,6 @@ pub type Web3RpcFut = Box> pub type Web3RpcResult = Result>; type EthPrivKeyPolicy = PrivKeyPolicy; -#[macro_export] -macro_rules! wei_from_gwei_decimal { - ($big_decimal: expr) => { - $crate::eth::wei_from_big_decimal($big_decimal, $crate::eth::ETH_GWEI_DECIMALS) - }; -} - -#[macro_export] -macro_rules! wei_to_gwei_decimal { - ($gwei: expr) => { - $crate::eth::u256_to_big_decimal($gwei, $crate::eth::ETH_GWEI_DECIMALS) - }; -} - #[derive(Clone, Debug)] pub(crate) struct LegacyGasPrice { pub(crate) gas_price: U256, @@ -582,11 +568,11 @@ impl TryFrom for PayForGasOption { fn try_from(param: PayForGasParams) -> Result { match param { PayForGasParams::Legacy(legacy) => Ok(Self::Legacy(LegacyGasPrice { - gas_price: wei_from_gwei_decimal!(&legacy.gas_price)?, + gas_price: wei_from_gwei_decimal(&legacy.gas_price)?, })), PayForGasParams::Eip1559(eip1559) => Ok(Self::Eip1559(Eip1559FeePerGas { - max_fee_per_gas: wei_from_gwei_decimal!(&eip1559.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal!(&eip1559.max_priority_fee_per_gas)?, + max_fee_per_gas: wei_from_gwei_decimal(&eip1559.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal(&eip1559.max_priority_fee_per_gas)?, })), } } @@ -1082,6 +1068,9 @@ impl EthCoinImpl { let guard = self.erc20_tokens_infos.lock().unwrap(); (*guard).clone() } + + #[inline(always)] + pub fn chain_id(&self) -> u64 { self.chain_id } } async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> RawTransactionResult { @@ -1204,8 +1193,8 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; Ok(TransactionNftDetails { - tx_hex: BytesJson::from(signed_bytes.to_vec()), - tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), + tx_hex: BytesJson::from(signed_bytes.to_vec()), // TODO: should we return tx_hex 0x-prefixed (everywhere)? + tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), // TODO: add 0x hash (use unified hash format for eth wherever it is returned) from: vec![eth_coin.my_address()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc1155, @@ -1296,7 +1285,7 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd Ok(TransactionNftDetails { tx_hex: BytesJson::from(signed_bytes.to_vec()), - tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), + tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), // TODO: add 0x hash (use unified hash format for eth wherever it is returned) from: vec![eth_coin.my_address()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc721, @@ -2456,7 +2445,7 @@ impl MarketCoinOps for EthCoin { let fut = async move { coin.send_raw_transaction(bytes.into()) .await - .map(|res| format!("{:02x}", res)) + .map(|res| format!("{:02x}", res)) // TODO: add 0x hash (use unified hash format for eth wherever it is returned) .map_err(|e| ERRL!("{}", e)) }; @@ -4762,7 +4751,7 @@ impl EthCoin { self.call(request, Some(BlockId::Number(BlockNumber::Latest))).await } - fn allowance(&self, spender: Address) -> Web3RpcFut { + pub fn allowance(&self, spender: Address) -> Web3RpcFut { let coin = self.clone(); let fut = async move { match coin.coin_type { @@ -4827,7 +4816,7 @@ impl EthCoin { Box::new(fut.boxed().compat()) } - fn approve(&self, spender: Address, amount: U256) -> EthTxFut { + pub fn approve(&self, spender: Address, amount: U256) -> EthTxFut { let coin = self.clone(); let fut = async move { let token_addr = match coin.coin_type { @@ -6194,6 +6183,12 @@ pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResu U256::from_dec_str(&amount).map_to_mm(|e| NumConversError::new(format!("{:?}", e))) } +pub fn wei_from_gwei_decimal(bigdec: &BigDecimal) -> NumConversResult { + wei_from_big_decimal(bigdec, ETH_GWEI_DECIMALS) +} + +pub fn wei_to_gwei_decimal(wei: U256) -> NumConversResult { u256_to_big_decimal(wei, ETH_GWEI_DECIMALS) } + impl Transaction for SignedEthTx { fn tx_hex(&self) -> Vec { rlp::encode(self).to_vec() } diff --git a/mm2src/coins/eth/eip1559_gas_fee.rs b/mm2src/coins/eth/eip1559_gas_fee.rs index 4d33781f39..32ac186169 100644 --- a/mm2src/coins/eth/eip1559_gas_fee.rs +++ b/mm2src/coins/eth/eip1559_gas_fee.rs @@ -1,8 +1,8 @@ //! Provides estimations of base and priority fee per gas or fetch estimations from a gas api provider use super::web3_transport::FeeHistoryResult; -use super::{Web3RpcError, Web3RpcResult}; -use crate::{wei_from_gwei_decimal, wei_to_gwei_decimal, EthCoin, NumConversError}; +use super::{wei_from_gwei_decimal, wei_to_gwei_decimal, Web3RpcError, Web3RpcResult}; +use crate::{EthCoin, NumConversError}; use ethereum_types::U256; use mm2_err_handle::mm_error::MmError; use mm2_err_handle::or_mm_error::OrMmError; @@ -104,24 +104,24 @@ impl TryFrom for FeePerGasEstimated { fn try_from(infura_fees: InfuraFeePerGas) -> Result { Ok(Self { - base_fee: wei_from_gwei_decimal!(&infura_fees.estimated_base_fee)?, + base_fee: wei_from_gwei_decimal(&infura_fees.estimated_base_fee)?, low: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.low.suggested_max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.low.suggested_max_priority_fee_per_gas)?, + max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.low.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal(&infura_fees.low.suggested_max_priority_fee_per_gas)?, min_wait_time: Some(infura_fees.low.min_wait_time_estimate), max_wait_time: Some(infura_fees.low.max_wait_time_estimate), }, medium: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.medium.suggested_max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal!( - &infura_fees.medium.suggested_max_priority_fee_per_gas + max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.medium.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal( + &infura_fees.medium.suggested_max_priority_fee_per_gas, )?, min_wait_time: Some(infura_fees.medium.min_wait_time_estimate), max_wait_time: Some(infura_fees.medium.max_wait_time_estimate), }, high: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.high.suggested_max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.high.suggested_max_priority_fee_per_gas)?, + max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.high.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal(&infura_fees.high.suggested_max_priority_fee_per_gas)?, min_wait_time: Some(infura_fees.high.min_wait_time_estimate), max_wait_time: Some(infura_fees.high.max_wait_time_estimate), }, @@ -143,33 +143,33 @@ impl TryFrom for FeePerGasEstimated { return Ok(FeePerGasEstimated::default()); } Ok(Self { - base_fee: wei_from_gwei_decimal!(&block_prices.block_prices[0].base_fee_per_gas)?, + base_fee: wei_from_gwei_decimal(&block_prices.block_prices[0].base_fee_per_gas)?, low: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[2].max_fee_per_gas + max_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[2].max_fee_per_gas, )?, - max_priority_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[2].max_priority_fee_per_gas + max_priority_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[2].max_priority_fee_per_gas, )?, min_wait_time: None, max_wait_time: None, }, medium: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[1].max_fee_per_gas + max_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[1].max_fee_per_gas, )?, - max_priority_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[1].max_priority_fee_per_gas + max_priority_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[1].max_priority_fee_per_gas, )?, min_wait_time: None, max_wait_time: None, }, high: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[0].max_fee_per_gas + max_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[0].max_fee_per_gas, )?, - max_priority_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[0].max_priority_fee_per_gas + max_priority_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[0].max_priority_fee_per_gas, )?, min_wait_time: None, max_wait_time: None, @@ -260,7 +260,7 @@ impl FeePerGasSimpleEstimator { let max_priority_fee_per_gas = Self::percentile_of(&level_rewards, Self::PRIORITY_FEE_PERCENTILES[level_index]); // Convert the priority fee to BigDecimal gwei, falling back to 0 on error. let max_priority_fee_per_gas_gwei = - wei_to_gwei_decimal!(max_priority_fee_per_gas).unwrap_or_else(|_| BigDecimal::from(0)); + wei_to_gwei_decimal(max_priority_fee_per_gas).unwrap_or_else(|_| BigDecimal::from(0)); // Calculate the max fee per gas by adjusting the base fee and adding the priority fee. let adjust_max_fee = @@ -273,7 +273,7 @@ impl FeePerGasSimpleEstimator { Ok(FeePerGasLevel { max_priority_fee_per_gas, - max_fee_per_gas: wei_from_gwei_decimal!(&max_fee_per_gas_dec)?, + max_fee_per_gas: wei_from_gwei_decimal(&max_fee_per_gas_dec)?, // TODO: Consider adding default wait times if applicable (and mark them as uncertain). min_wait_time: None, max_wait_time: None, @@ -290,7 +290,7 @@ impl FeePerGasSimpleEstimator { .first() .cloned() .unwrap_or_else(|| U256::from(0)); - let latest_base_fee_dec = wei_to_gwei_decimal!(latest_base_fee).unwrap_or_else(|_| BigDecimal::from(0)); + let latest_base_fee_dec = wei_to_gwei_decimal(latest_base_fee).unwrap_or_else(|_| BigDecimal::from(0)); // The predicted base fee is not used for calculating eip1559 values here and is provided for other purposes // (f.e if the caller would like to do own estimates of max fee and max priority fee) diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs index 922e219fbd..3dc6711126 100644 --- a/mm2src/coins/eth/eth_rpc.rs +++ b/mm2src/coins/eth/eth_rpc.rs @@ -241,8 +241,8 @@ impl EthCoin { .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } - /// Get chain id - pub(crate) async fn chain_id(&self) -> Result { + /// Get chain id from network + pub(crate) async fn network_chain_id(&self) -> Result { self.try_rpc_send("eth_chainId", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 19f45d10ae..799480a924 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1040,3 +1040,10 @@ fn test_gas_limit_conf() { && eth_coin.gas_limit.eth_max_trade_gas == 150_000 ); } + +#[test] +fn test_h256_to_str() { + let h = H256::from_str("5136701f11060010841c9708c3eb26f6606a070b8ae43f4b98b6d7b10a545258").unwrap(); + let b: BytesJson = h.0.to_vec().into(); + println!("H256={}", format!("0x{:02x}", b)); +} diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 5e40eb9221..16ade17a6b 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -220,9 +220,8 @@ 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}; + GetEthAddressError, GetValidEthWithdrawAddError, SignedEthTx}; pub mod hd_wallet; use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, @@ -661,6 +660,10 @@ impl TransactionErr { } } +impl std::fmt::Display for TransactionErr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.get_plain_text_format()) } +} + #[derive(Debug, PartialEq)] pub enum FoundSwapTxSpend { Spent(TransactionEnum), diff --git a/mm2src/coins/rpc_command/get_estimated_fees.rs b/mm2src/coins/rpc_command/get_estimated_fees.rs index b62e572756..811ecb448e 100644 --- a/mm2src/coins/rpc_command/get_estimated_fees.rs +++ b/mm2src/coins/rpc_command/get_estimated_fees.rs @@ -1,7 +1,7 @@ //! RPCs to start/stop gas fee estimator and get estimated base and priority fee per gas -use crate::eth::{EthCoin, EthCoinType, FeeEstimatorContext, FeeEstimatorState, FeePerGasEstimated}; -use crate::{lp_coinfind_or_err, wei_to_gwei_decimal, AsyncMutex, CoinFindError, MmCoinEnum, NumConversError}; +use crate::eth::{wei_to_gwei_decimal, EthCoin, EthCoinType, FeeEstimatorContext, FeeEstimatorState, FeePerGasEstimated}; +use crate::{lp_coinfind_or_err, AsyncMutex, CoinFindError, MmCoinEnum, NumConversError}; use common::executor::{spawn_abortable, Timer}; use common::log::debug; use common::{HttpStatusCode, StatusCode}; @@ -66,22 +66,22 @@ impl TryFrom for FeePerGasEstimatedExt { fn try_from(fees: FeePerGasEstimated) -> Result { Ok(Self { - base_fee: wei_to_gwei_decimal!(fees.base_fee)?, + base_fee: wei_to_gwei_decimal(fees.base_fee)?, low: FeePerGasLevel { - max_fee_per_gas: wei_to_gwei_decimal!(fees.low.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.low.max_priority_fee_per_gas)?, + max_fee_per_gas: wei_to_gwei_decimal(fees.low.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal(fees.low.max_priority_fee_per_gas)?, min_wait_time: fees.low.min_wait_time, max_wait_time: fees.low.max_wait_time, }, medium: FeePerGasLevel { - max_fee_per_gas: wei_to_gwei_decimal!(fees.medium.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.medium.max_priority_fee_per_gas)?, + max_fee_per_gas: wei_to_gwei_decimal(fees.medium.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal(fees.medium.max_priority_fee_per_gas)?, min_wait_time: fees.medium.min_wait_time, max_wait_time: fees.medium.max_wait_time, }, high: FeePerGasLevel { - max_fee_per_gas: wei_to_gwei_decimal!(fees.high.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.high.max_priority_fee_per_gas)?, + max_fee_per_gas: wei_to_gwei_decimal(fees.high.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal(fees.high.max_priority_fee_per_gas)?, min_wait_time: fees.high.min_wait_time, max_wait_time: fees.high.max_wait_time, }, diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index 6a5395b360..b775741cf8 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -34,6 +34,7 @@ lazy_static = "1.4" log = "0.4.17" parking_lot = { version = "0.12.0", features = ["nightly"] } parking_lot_core = { version = "0.6", features = ["nightly"] } +paste = "1.0" primitive-types = "0.11.1" rand = { version = "0.7", features = ["std", "small_rng"] } rustc-hash = "1.1.0" diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index de201856d8..e0b6b4d233 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -152,6 +152,7 @@ use futures01::{future, Future}; use http::header::CONTENT_TYPE; use http::Response; use parking_lot::{Mutex as PaMutex, MutexGuard as PaMutexGuard}; +pub use paste::paste; use rand::RngCore; use rand::{rngs::SmallRng, SeedableRng}; use serde::{de, ser}; @@ -1129,6 +1130,37 @@ pub fn http_uri_to_ws_address(uri: http::Uri) -> String { format!("{}{}{}{}", address_prefix, host_address, port, path) } +/// If 0x prefix exists in an str strip it or return the str as-is +#[macro_export] +macro_rules! str_strip_0x { + ($s: expr) => { + $s.strip_prefix("0x").unwrap_or($s) + }; +} + +/// If value is 'some' push key and value (as string) into an array containing (key, value) elements +#[macro_export] +macro_rules! push_if_some { + ($arr: expr, $k: expr, $v: expr) => { + if let Some(v) = $v { + $arr.push(($k, v.to_string())) + } + }; +} + +/// Define 'with_...' method to set a parameter with an optional value in a builder +#[macro_export] +macro_rules! def_with_opt_param { + ($var: ident, $var_type: ty) => { + $crate::paste! { + pub fn [](&mut self, $var: Option<$var_type>) -> &mut Self { + self.$var = $var; + self + } + } + }; +} + #[test] fn test_http_uri_to_ws_address() { let uri = "https://cosmos-rpc.polkachu.com".parse::().unwrap(); diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 6900009b2f..13d53b8eea 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -71,10 +71,12 @@ mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"]} mm2_state_machine = { path = "../mm2_state_machine" } +trading_api = { path = "../trading_api" } num-traits = "0.2" parity-util-mem = "0.11" parking_lot = { version = "0.12.0", features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } +primitive-types = "0.11.1" prost = "0.12" rand = { version = "0.7", features = ["std", "small_rng"] } rand6 = { version = "0.6", package = "rand" } @@ -123,6 +125,7 @@ winapi = "0.3" coins = { path = "../coins", features = ["for-tests"] } coins_activation = { path = "../coins_activation", features = ["for-tests"] } mm2_test_helpers = { path = "../mm2_test_helpers" } +trading_api = { path = "../trading_api", features = ["mocktopus"] } mocktopus = "0.8.0" testcontainers = "0.15.0" web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false, features = ["http-rustls-tls"] } diff --git a/mm2src/mm2_main/src/ext_api.rs b/mm2src/mm2_main/src/ext_api.rs new file mode 100644 index 0000000000..f1b92c145f --- /dev/null +++ b/mm2src/mm2_main/src/ext_api.rs @@ -0,0 +1,3 @@ +//! RPCs for integration with external third party trading APIs. + +pub mod one_inch; diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 6b68666d6b..937db9631b 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -11,8 +11,14 @@ use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swa 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::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc, + one_inch_v6_0_classic_swap_create_rpc, + one_inch_v6_0_classic_swap_liquidity_sources_rpc, + one_inch_v6_0_classic_swap_quote_rpc, + one_inch_v6_0_classic_swap_tokens_rpc}; use crate::rpc::lp_commands::pubkey::*; use crate::rpc::lp_commands::tokens::get_token_info; +use crate::rpc::lp_commands::tokens::{approve_token_rpc, get_token_allowance_rpc}; use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; use coins::eth::EthCoin; @@ -162,6 +168,8 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, active_swaps_rpc).await, "add_delegation" => handle_mmrpc(ctx, request, add_delegation).await, "add_node_to_version_stat" => handle_mmrpc(ctx, request, add_node_to_version_stat).await, + "approve_token" => handle_mmrpc(ctx, request, approve_token_rpc).await, + "get_token_allowance" => handle_mmrpc(ctx, request, get_token_allowance_rpc).await, "best_orders" => handle_mmrpc(ctx, request, best_orders_rpc_v2).await, "clear_nft_db" => handle_mmrpc(ctx, request, clear_nft_db).await, "enable_bch_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, @@ -221,6 +229,13 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, set_swap_transaction_fee_policy).await, "send_asked_data" => handle_mmrpc(ctx, request, send_asked_data_rpc).await, "z_coin_tx_history" => handle_mmrpc(ctx, request, coins::my_tx_history_v2::z_coin_tx_history_rpc).await, + "1inch_v6_0_classic_swap_contract" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_contract_rpc).await, + "1inch_v6_0_classic_swap_quote" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_quote_rpc).await, + "1inch_v6_0_classic_swap_create" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_create_rpc).await, + "1inch_v6_0_classic_swap_liquidity_sources" => { + handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_liquidity_sources_rpc).await + }, + "1inch_v6_0_classic_swap_tokens" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_tokens_rpc).await, _ => MmError::err(DispatcherError::NoSuchMethod), } } diff --git a/mm2src/mm2_main/src/rpc/lp_commands/mod.rs b/mm2src/mm2_main/src/rpc/lp_commands/mod.rs index 002066c836..e61d5aead8 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/mod.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod db_id; pub mod legacy; +pub(crate) mod one_inch; pub(crate) mod pubkey; pub(crate) mod tokens; pub(crate) mod trezor; diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs new file mode 100644 index 0000000000..3d47853294 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs @@ -0,0 +1,5 @@ +//! RPC implementation for integration with 1inch swap API provider. + +pub mod errors; +pub mod rpcs; +pub mod types; diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs new file mode 100644 index 0000000000..8ee65af984 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs @@ -0,0 +1,99 @@ +use coins::{eth::u256_to_big_decimal, NumConversError}; +use common::{HttpStatusCode, StatusCode}; +use enum_derives::EnumFromStringify; +use mm2_number::BigDecimal; +use ser_error_derive::SerializeErrorType; +use serde::Serialize; +use trading_api::one_inch_api::errors::ApiClientError; + +#[derive(Debug, Display, Serialize, SerializeErrorType, EnumFromStringify)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ApiIntegrationRpcError { + #[from_stringify("coins::CoinFindError")] + NoSuchCoin(String), + #[display(fmt = "EVM token needed")] + CoinTypeError, + #[display(fmt = "NFT not supported")] + NftNotSupported, + #[display(fmt = "Chain not supported")] + ChainNotSupported, + #[display(fmt = "Must be same chain")] + DifferentChains, + #[from_stringify("coins::UnexpectedDerivationMethod")] + MyAddressError(String), + InvalidParam(String), + #[display(fmt = "Parameter {param} out of bounds, value: {value}, min: {min} max: {max}")] + OutOfBounds { + param: String, + value: String, + min: String, + max: String, + }, + #[display(fmt = "allowance not enough for 1inch contract, available: {allowance}, needed: {amount}")] + OneInchAllowanceNotEnough { + allowance: BigDecimal, + amount: BigDecimal, + }, + #[display(fmt = "1inch API error: {}", _0)] + OneInchError(ApiClientError), + ApiDataError(String), +} + +impl HttpStatusCode for ApiIntegrationRpcError { + fn status_code(&self) -> StatusCode { + match self { + ApiIntegrationRpcError::NoSuchCoin { .. } => StatusCode::NOT_FOUND, + ApiIntegrationRpcError::CoinTypeError + | ApiIntegrationRpcError::NftNotSupported + | ApiIntegrationRpcError::ChainNotSupported + | ApiIntegrationRpcError::DifferentChains + | ApiIntegrationRpcError::MyAddressError(_) + | ApiIntegrationRpcError::InvalidParam(_) + | ApiIntegrationRpcError::OutOfBounds { .. } + | ApiIntegrationRpcError::OneInchAllowanceNotEnough { .. } => StatusCode::BAD_REQUEST, + ApiIntegrationRpcError::OneInchError(_) | ApiIntegrationRpcError::ApiDataError(_) => { + StatusCode::BAD_GATEWAY + }, + } + } +} + +impl ApiIntegrationRpcError { + pub(crate) fn from_api_error(error: ApiClientError, decimals: Option) -> Self { + match error { + ApiClientError::InvalidParam(error) => ApiIntegrationRpcError::InvalidParam(error), + ApiClientError::OutOfBounds { param, value, min, max } => { + ApiIntegrationRpcError::OutOfBounds { param, value, min, max } + }, + ApiClientError::TransportError(_) + | ApiClientError::ParseBodyError { .. } + | ApiClientError::GeneralApiError { .. } => ApiIntegrationRpcError::OneInchError(error), + ApiClientError::AllowanceNotEnough { allowance, amount, .. } => { + ApiIntegrationRpcError::OneInchAllowanceNotEnough { + allowance: u256_to_big_decimal(allowance, decimals.unwrap_or_default()).unwrap_or_default(), + amount: u256_to_big_decimal(amount, decimals.unwrap_or_default()).unwrap_or_default(), + } + }, + } + } +} + +/// Error aggregator for errors of conversion of api returned values +#[derive(Debug, Display, Serialize)] +pub(crate) struct FromApiValueError(String); + +impl From for FromApiValueError { + fn from(err: NumConversError) -> Self { Self(err.to_string()) } +} + +impl From for FromApiValueError { + fn from(err: primitive_types::Error) -> Self { Self(format!("{:?}", err)) } +} + +impl From for FromApiValueError { + fn from(err: hex::FromHexError) -> Self { Self(err.to_string()) } +} + +impl From for FromApiValueError { + fn from(err: ethereum_types::FromDecStrErr) -> Self { Self(err.to_string()) } +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs new file mode 100644 index 0000000000..a0c384463d --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs @@ -0,0 +1,439 @@ +use super::errors::ApiIntegrationRpcError; +use super::types::{AggregationContractRequest, ClassicSwapCreateRequest, ClassicSwapLiquiditySourcesRequest, + ClassicSwapLiquiditySourcesResponse, ClassicSwapQuoteRequest, ClassicSwapResponse, + ClassicSwapTokensRequest, ClassicSwapTokensResponse}; +use coins::eth::{display_eth_address, wei_from_big_decimal, EthCoin, EthCoinType}; +use coins::{lp_coinfind_or_err, CoinWithDerivationMethod, MmCoin, MmCoinEnum}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use trading_api::one_inch_api::client::ApiClient; +use trading_api::one_inch_api::types::{ClassicSwapCreateParams, ClassicSwapQuoteParams, ProtocolsResponse, + TokensResponse}; + +/// "1inch_v6_0_classic_swap_contract" rpc impl +/// used to get contract address (for e.g. to approve funds) +pub async fn one_inch_v6_0_classic_swap_contract_rpc( + _ctx: MmArc, + _req: AggregationContractRequest, +) -> MmResult { + Ok(ApiClient::classic_swap_contract().to_owned()) +} + +/// "1inch_classic_swap_quote" rpc impl +pub async fn one_inch_v6_0_classic_swap_quote_rpc( + ctx: MmArc, + req: ClassicSwapQuoteRequest, +) -> MmResult { + let (base, base_contract) = get_coin_for_one_inch(&ctx, &req.base).await?; + let (rel, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; + api_supports_pair(&base, &rel)?; + let sell_amount = wei_from_big_decimal(&req.amount.to_decimal(), base.decimals()) + .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; + let query_params = ClassicSwapQuoteParams::new(base_contract, rel_contract, sell_amount.to_string()) + .with_fee(req.fee) + .with_protocols(req.protocols) + .with_gas_price(req.gas_price) + .with_complexity_level(req.complexity_level) + .with_parts(req.parts) + .with_main_route_parts(req.main_route_parts) + .with_gas_limit(req.gas_limit) + .with_include_tokens_info(Some(req.include_tokens_info)) + .with_include_protocols(Some(req.include_protocols)) + .with_include_gas(Some(req.include_gas)) + .with_connector_tokens(req.connector_tokens) + .build_query_params() + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; + let quote = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? + .call_swap_api( + base.chain_id(), + ApiClient::get_quote_method().to_owned(), + Some(query_params), + ) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; // use 'base' as amount in errors is in the src coin + ClassicSwapResponse::from_api_classic_swap_data(quote, rel.decimals()) // use 'rel' as quote value is in the dst coin + .mm_err(|err| ApiIntegrationRpcError::ApiDataError(err.to_string())) +} + +/// "1inch_classic_swap_create" rpc implementation +/// This rpc actually returns a transaction to call the 1inch swap aggregation contract. GUI should sign it and send to the chain. +/// We don't verify the transaction in any way and trust the 1inch api. +pub async fn one_inch_v6_0_classic_swap_create_rpc( + ctx: MmArc, + req: ClassicSwapCreateRequest, +) -> MmResult { + let (base, base_contract) = get_coin_for_one_inch(&ctx, &req.base).await?; + let (rel, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; + api_supports_pair(&base, &rel)?; + let sell_amount = wei_from_big_decimal(&req.amount.to_decimal(), base.decimals()) + .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; + let single_address = base.derivation_method().single_addr_or_err().await?; + + let query_params = ClassicSwapCreateParams::new( + base_contract, + rel_contract, + sell_amount.to_string(), + display_eth_address(&single_address), + req.slippage, + ) + .with_fee(req.fee) + .with_protocols(req.protocols) + .with_gas_price(req.gas_price) + .with_complexity_level(req.complexity_level) + .with_parts(req.parts) + .with_main_route_parts(req.main_route_parts) + .with_gas_limit(req.gas_limit) + .with_include_tokens_info(Some(req.include_tokens_info)) + .with_include_protocols(Some(req.include_protocols)) + .with_include_gas(Some(req.include_gas)) + .with_connector_tokens(req.connector_tokens) + .with_excluded_protocols(req.excluded_protocols) + .with_permit(req.permit) + .with_compatibility(req.compatibility) + .with_receiver(req.receiver) + .with_referrer(req.referrer) + .with_disable_estimate(req.disable_estimate) + .with_allow_partial_fill(req.allow_partial_fill) + .with_use_permit2(req.use_permit2) + .build_query_params() + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; + let swap_with_tx = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? + .call_swap_api( + base.chain_id(), + ApiClient::get_swap_method().to_owned(), + Some(query_params), + ) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; // use 'base' as amount in errors is in the src coin + ClassicSwapResponse::from_api_classic_swap_data(swap_with_tx, base.decimals()) // use 'base' as we spend in the src coin + .mm_err(|err| ApiIntegrationRpcError::ApiDataError(err.to_string())) +} + +/// "1inch_v6_0_classic_swap_liquidity_sources" rpc implementation. +/// Returns list of DEX available for routing with the 1inch Aggregation contract +pub async fn one_inch_v6_0_classic_swap_liquidity_sources_rpc( + ctx: MmArc, + req: ClassicSwapLiquiditySourcesRequest, +) -> MmResult { + let response: ProtocolsResponse = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))? + .call_swap_api(req.chain_id, ApiClient::get_liquidity_sources_method().to_owned(), None) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))?; + Ok(ClassicSwapLiquiditySourcesResponse { + protocols: response.protocols, + }) +} + +/// "1inch_classic_swap_tokens" rpc implementation. +/// Returns list of tokens available for 1inch classic swaps +pub async fn one_inch_v6_0_classic_swap_tokens_rpc( + ctx: MmArc, + req: ClassicSwapTokensRequest, +) -> MmResult { + let response: TokensResponse = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))? + .call_swap_api(req.chain_id, ApiClient::get_tokens_method().to_owned(), None) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))?; + Ok(ClassicSwapTokensResponse { + tokens: response.tokens, + }) +} + +async fn get_coin_for_one_inch(ctx: &MmArc, ticker: &str) -> MmResult<(EthCoin, String), ApiIntegrationRpcError> { + let coin = match lp_coinfind_or_err(ctx, ticker).await? { + MmCoinEnum::EthCoin(coin) => coin, + _ => return Err(MmError::new(ApiIntegrationRpcError::CoinTypeError)), + }; + let contract = match coin.coin_type { + EthCoinType::Eth => ApiClient::eth_special_contract().to_owned(), + EthCoinType::Erc20 { token_addr, .. } => display_eth_address(&token_addr), + EthCoinType::Nft { .. } => return Err(MmError::new(ApiIntegrationRpcError::NftNotSupported)), + }; + Ok((coin, contract)) +} + +#[allow(clippy::result_large_err)] +fn api_supports_pair(base: &EthCoin, rel: &EthCoin) -> MmResult<(), ApiIntegrationRpcError> { + if !ApiClient::is_chain_supported(base.chain_id()) { + return MmError::err(ApiIntegrationRpcError::ChainNotSupported); + } + if base.chain_id() != rel.chain_id() { + return MmError::err(ApiIntegrationRpcError::DifferentChains); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::rpc::lp_commands::one_inch::{rpcs::{one_inch_v6_0_classic_swap_create_rpc, + one_inch_v6_0_classic_swap_quote_rpc}, + types::{ClassicSwapCreateRequest, ClassicSwapQuoteRequest}}; + use coins::eth::EthCoin; + use coins_activation::platform_for_tests::init_platform_coin_with_tokens_loop; + use common::block_on; + use crypto::CryptoCtx; + use mm2_core::mm_ctx::MmCtxBuilder; + use mm2_number::{BigDecimal, MmNumber}; + use mocktopus::mocking::{MockResult, Mockable}; + use std::str::FromStr; + use trading_api::one_inch_api::{client::ApiClient, types::ClassicSwapData}; + + #[test] + fn test_classic_swap_response_conversion() { + let ticker_coin = "ETH".to_owned(); + let ticker_token = "JST".to_owned(); + let eth_conf = json!({ + "coin": ticker_coin, + "name": "ethereum", + "derivation_path": "m/44'/1'", + "chain_id": 1, + "protocol": { + "type": "ETH" + }, + "trezor_coin": "Ethereum" + }); + let jst_conf = json!({ + "coin": ticker_token, + "name": "jst", + "chain_id": 1, + "protocol": { + "type": "ERC20", + "protocol_data": { + "platform": "ETH", + "contract_address": "0x09d0d71FBC00D7CCF9CFf132f5E6825C88293F19" + } + }, + }); + + let conf = json!({ + "coins": [eth_conf, jst_conf], + "1inch_api": "https://api.1inch.dev" + }); + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); + CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "123").unwrap(); + + block_on(init_platform_coin_with_tokens_loop::( + ctx.clone(), + serde_json::from_value(json!({ + "ticker": ticker_coin, + "rpc_mode": "Default", + "nodes": [ + {"url": "https://rpc2.sepolia.org"}, + {"url": "https://rpc.sepolia.org/"} + ], + "swap_contract_address": "0xeA6D65434A15377081495a9E7C5893543E7c32cB", + "erc20_tokens_requests": [{"ticker": ticker_token}], + "priv_key_policy": "ContextPrivKey" + })) + .unwrap(), + )) + .unwrap(); + + let response_quote_raw = json!({ + "dstAmount": "13", + "srcToken": { + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "symbol": ticker_coin, + "name": "Ether", + "decimals": 18, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.png", + "tags": [ + "crosschain", + "GROUP:ETH", + "native", + "PEG:ETH" + ] + }, + "dstToken": { + "address": "0x1234567890123456789012345678901234567890", + "symbol": ticker_token, + "name": "Test just token", + "decimals": 6, + "eip2612": false, + "isFoT": false, + "logoURI": "https://example.org/0x1234567890123456789012345678901234567890.png", + "tags": [ + "crosschain", + "GROUP:JSTT", + "PEG:JST", + "tokens" + ] + }, + "protocols": [ + [ + [ + { + "name": "SUSHI", + "part": 100, + "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "toTokenAddress": "0xf16e81dce15b08f326220742020379b855b87df9" + } + ], + [ + { + "name": "ONE_INCH_LIMIT_ORDER_V3", + "part": 100, + "fromTokenAddress": "0xf16e81dce15b08f326220742020379b855b87df9", + "toTokenAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7" + } + ] + ] + ], + "gas": 452704 + }); + + let response_create_raw = json!({ + "dstAmount": "13", + "tx": { + "from": "0x590559f6fb7720f24ff3e2fccf6015b466e9c92c", + "to": "0x111111125421ca6dc452d289314280a0f8842a65", + "data": "0x07ed23790000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000590559f6fb7720f24ff3e2fccf6015b466e9c92c0000000000000000000000000000000000000000000000000000000000989680000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000648e8755f7ac30b5e4fa3f9c00e2cb6667501797b8bc01a7a367a4b2889ca6a05d9c31a31a781c12a4c3bdfc2ef1e02942e388b6565989ebe860bd67925bda74fbe0000000000000000000000000000000000000000000000000005ea0005bc00a007e5c0d200000000000000000000000000000000059800057e00018500009500001a4041c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2d0e30db00c20c02aaa39b223fe8d0a0e5c4f27ead9083c756cc27b73644935b8e68019ac6356c40661e1bc3158606ae4071118002dc6c07b73644935b8e68019ac6356c40661e1bc3158600000000000000000000000000000000000000000000000000294932ccadc9c58c02aaa39b223fe8d0a0e5c4f27ead9083c756cc251204dff5675ecff96b565ba3804dd4a63799ccba406761d38e5ddf6ccf6cf7c55759d5210750b5d60f30044e331d039000000000000000000000000761d38e5ddf6ccf6cf7c55759d5210750b5d60f3000000000000000000000000111111111117dc0aa78b770fa6a738034120c302000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002f8a744a79be00000000000000000000000042f527f50f16a103b6ccab48bccca214500c10210000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec00a0860a32ec00000000000000000000000000000000000000000000000000003005635d54300003d05120ead050515e10fdb3540ccd6f8236c46790508a76111111111117dc0aa78b770fa6a738034120c30200c4e525b10b000000000000000000000000000000000000000000000000000000000000002000000000000000000000000022b1a53ac4be63cdc1f47c99572290eff1edd8020000000000000000000000006a32cc044dd6359c27bb66e7b02dce6dd0fda2470000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000111111111117dc0aa78b770fa6a738034120c302000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003005635d5430000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000067138e8c00000000000000000000000000000000000000000000000000030fb9b1525d8185f8d63fbcbe42e5999263c349cb5d81000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000026000000000000000000000000067297ee4eb097e072b4ab6f1620268061ae8046400000000000000000000000060cba82ddbf4b5ddcd4398cdd05354c6a790c309000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041d26038ef66344af785ff342b86db3da06c4cc6a62f0ca80ffd78affc0a95ccad44e814acebb1deda729bbfe3050bec14a47af487cc1cadc75f43db2d073016c31c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041a66cd52a747c5f60b9db637ffe30d0e413ec87858101832b4c5c1ae154bf247f3717c8ed4133e276ddf68d43a827f280863c91d6c42bc6ad1ec7083b2315b6fd1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020d6bdbf78dac17f958d2ee523a2206206994597c13d831ec780a06c4eca27dac17f958d2ee523a2206206994597c13d831ec7111111125421ca6dc452d289314280a0f8842a65000000000000000000000000000000000000000000000000c095c0a2", + "value": "10000000", + "gas": 721429, + "gasPrice": "9525172167" + }, + "srcToken": { + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "symbol": ticker_coin, + "name": "Ether", + "decimals": 18, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.png", + "tags": [ + "crosschain", + "GROUP:ETH", + "native", + "PEG:ETH" + ] + }, + "dstToken": { + "address": "0x1234567890123456789012345678901234567890", + "symbol": ticker_token, + "name": "Just Token", + "decimals": 6, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0x1234567890123456789012345678901234567890.png", + "tags": [ + "crosschain", + "GROUP:USDT", + "PEG:USD", + "tokens" + ] + }, + "protocols": [ + [ + [ + { + "name": "UNISWAP_V2", + "part": 100, + "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "toTokenAddress": "0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3" + } + ], + [ + { + "name": "ONE_INCH_LP_1_1", + "part": 100, + "fromTokenAddress": "0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3", + "toTokenAddress": "0x111111111117dc0aa78b770fa6a738034120c302" + } + ], + [ + { + "name": "PMM11", + "part": 100, + "fromTokenAddress": "0x111111111117dc0aa78b770fa6a738034120c302", + "toTokenAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7" + } + ] + ] + ] + }); + + let quote_req = ClassicSwapQuoteRequest { + base: ticker_coin.clone(), + rel: ticker_token.clone(), + amount: MmNumber::from("1.0"), + fee: None, + protocols: None, + gas_price: None, + complexity_level: None, + parts: None, + main_route_parts: None, + gas_limit: None, + include_tokens_info: true, + include_protocols: true, + include_gas: true, + connector_tokens: None, + }; + + let create_req = ClassicSwapCreateRequest { + base: ticker_coin.clone(), + rel: ticker_token.clone(), + amount: MmNumber::from("1.0"), + fee: None, + protocols: None, + gas_price: None, + complexity_level: None, + parts: None, + main_route_parts: None, + gas_limit: None, + include_tokens_info: true, + include_protocols: true, + include_gas: true, + connector_tokens: None, + slippage: 0.0, + excluded_protocols: None, + permit: None, + compatibility: None, + receiver: None, + referrer: None, + disable_estimate: None, + allow_partial_fill: None, + use_permit2: None, + }; + + ApiClient::call_swap_api::.mock_safe(move |_, _, _, _| { + let response_quote_raw = response_quote_raw.clone(); + MockResult::Return(Box::pin(async move { + Ok(serde_json::from_value::(response_quote_raw).unwrap()) + })) + }); + + let quote_response = block_on(one_inch_v6_0_classic_swap_quote_rpc(ctx.clone(), quote_req)).unwrap(); + assert_eq!( + quote_response.dst_amount.amount, + BigDecimal::from_str("0.000000000000000013").unwrap() + ); + assert_eq!(quote_response.src_token.as_ref().unwrap().symbol, ticker_coin); + assert_eq!(quote_response.src_token.as_ref().unwrap().decimals, 18); + assert_eq!(quote_response.dst_token.as_ref().unwrap().symbol, ticker_token); + assert_eq!(quote_response.dst_token.as_ref().unwrap().decimals, 6); + assert_eq!(quote_response.gas.unwrap(), 452704_u128); + + ApiClient::call_swap_api::.mock_safe(move |_, _, _, _| { + let response_create_raw = response_create_raw.clone(); + MockResult::Return(Box::pin(async move { + Ok(serde_json::from_value::(response_create_raw).unwrap()) + })) + }); + let create_response = block_on(one_inch_v6_0_classic_swap_create_rpc(ctx, create_req)).unwrap(); + assert_eq!( + create_response.dst_amount.amount, + BigDecimal::from_str("0.000000000000000013").unwrap() + ); + assert_eq!(create_response.src_token.as_ref().unwrap().symbol, ticker_coin); + assert_eq!(create_response.src_token.as_ref().unwrap().decimals, 18); + assert_eq!(create_response.dst_token.as_ref().unwrap().symbol, ticker_token); + assert_eq!(create_response.dst_token.as_ref().unwrap().decimals, 6); + assert_eq!(create_response.tx.as_ref().unwrap().data.len(), 1960); + } +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs new file mode 100644 index 0000000000..202eb0dcf2 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs @@ -0,0 +1,213 @@ +use crate::rpc::lp_commands::one_inch::errors::FromApiValueError; +use coins::eth::{u256_to_big_decimal, wei_to_gwei_decimal}; +use common::true_f; +use ethereum_types::{Address, U256}; +use mm2_err_handle::prelude::*; +use mm2_number::{construct_detailed, BigDecimal, MmNumber}; +use rpc::v1::types::Bytes as BytesJson; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use trading_api::one_inch_api::{self, + types::{ProtocolImage, ProtocolInfo, TokenInfo}}; + +construct_detailed!(DetailedAmount, amount); + +#[derive(Clone, Debug, Deserialize)] +pub struct AggregationContractRequest {} + +/// Request to get quote for 1inch classic swap. +/// See 1inch docs for more details: https://portal.1inch.dev/documentation/apis/swap/classic-swap/Parameter%20Descriptions/quote_params +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ClassicSwapQuoteRequest { + /// Base coin ticker + pub base: String, + /// Rel coin ticker + pub rel: String, + /// Swap amount in coins (with fraction) + pub amount: MmNumber, + /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. + /// Should be the same for quote and swap rpc. Default is 0 + pub fee: Option, + /// Specify liquidity sources + /// e.g.: &protocols=WETH,CURVE,BALANCER,...,ZRX + /// (by default - all used) + pub protocols: Option, + /// Network price per gas, in Gwei for this rpc. + /// 1inch takes in account gas expenses to determine exchange route. Should be the same for a quote and swap. + /// If not set the 'fast' network gas price will be used + pub gas_price: Option, + /// Maximum number of token-connectors to be used in a transaction, min: 0; max: 3; default: 2 + pub complexity_level: Option, + /// Limit maximum number of parts each main route parts can be split into. + /// Should be the same for a quote and swap. Default: 20; max: 100 + pub parts: Option, + /// Limit maximum number of main route parts. Should be the same for a quote and swap. Default: 20; max: 50; + pub main_route_parts: Option, + /// Maximum amount of gas for a swap. + /// Should be the same for a quote and swap. Default: 11500000; max: 11500000 + pub gas_limit: Option, + /// Return fromToken and toToken info in response (default is true) + #[serde(default = "true_f")] + pub include_tokens_info: bool, + /// Return used swap protocols in response (default is true) + #[serde(default = "true_f")] + pub include_protocols: bool, + /// Include estimated gas in return value (default is true) + #[serde(default = "true_f")] + pub include_gas: bool, + /// Token-connectors can be specified via this parameter. If not set, default token-connectors will be used + pub connector_tokens: Option, +} + +/// Request to create transaction for 1inch classic swap. +/// See 1inch docs for more details: https://portal.1inch.dev/documentation/apis/swap/classic-swap/Parameter%20Descriptions/swap_params +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ClassicSwapCreateRequest { + /// Base coin ticker + pub base: String, + /// Rel coin ticker + pub rel: String, + /// Swap amount in coins (with fraction) + pub amount: MmNumber, + /// Allowed slippage, min: 0; max: 50 + pub slippage: f32, + /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. + /// Should be the same for quote and swap rpc. Default is 0 + pub fee: Option, + /// Specify liquidity sources + /// e.g.: &protocols=WETH,CURVE,BALANCER,...,ZRX + /// (by default - all used) + pub protocols: Option, + /// Network price per gas, in Gwei for this rpc. + /// 1inch takes in account gas expenses to determine exchange route. Should be the same for a quote and swap. + /// If not set the 'fast' network gas price will be used + pub gas_price: Option, + /// Maximum number of token-connectors to be used in a transaction, min: 0; max: 3; default: 2 + pub complexity_level: Option, + /// Limit maximum number of parts each main route parts can be split into. + /// Should be the same for a quote and swap. Default: 20; max: 100 + pub parts: Option, + /// Limit maximum number of main route parts. Should be the same for a quote and swap. Default: 20; max: 50; + pub main_route_parts: Option, + /// Maximum amount of gas for a swap. + /// Should be the same for a quote and swap. Default: 11500000; max: 11500000 + pub gas_limit: Option, + /// Return fromToken and toToken info in response (default is true) + #[serde(default = "true_f")] + pub include_tokens_info: bool, + /// Return used swap protocols in response (default is true) + #[serde(default = "true_f")] + pub include_protocols: bool, + /// Include estimated gas in response (default is true) + #[serde(default = "true_f")] + pub include_gas: bool, + /// Token-connectors can be specified via this parameter. If not set, default token-connectors will be used + pub connector_tokens: Option, + /// Excluded supported liquidity sources. Should be the same for a quote and swap, max: 5 + pub excluded_protocols: Option, + /// Used according https://eips.ethereum.org/EIPS/eip-2612 + pub permit: Option, + /// Exclude the Unoswap method + pub compatibility: Option, + /// This address will receive funds after the swap. By default same address as 'my address' + pub receiver: Option, + /// Address to receive the partner fee. Must be set explicitly if fee is also set + pub referrer: Option, + /// if true, disable most of the checks, default: false + pub disable_estimate: Option, + /// if true, the algorithm can cancel part of the route, if the rate has become less attractive. + /// Unswapped tokens will return to 'my address'. Default: true + pub allow_partial_fill: Option, + /// Enable this flag for auto approval by Permit2 contract if you did an approval to Uniswap Permit2 smart contract for this token. + /// Default is false + pub use_permit2: Option, +} + +/// Response for both classic swap quote or create swap calls +#[derive(Serialize, Debug)] +pub struct ClassicSwapResponse { + /// Destination token amount, in coins (with fraction) + pub dst_amount: DetailedAmount, + /// Source (base) token info + #[serde(skip_serializing_if = "Option::is_none")] + pub src_token: Option, + /// Destination (rel) token info + #[serde(skip_serializing_if = "Option::is_none")] + pub dst_token: Option, + /// Used liquidity sources + #[serde(skip_serializing_if = "Option::is_none")] + pub protocols: Option>>>, + /// Swap tx fields (returned only for create swap rpc) + #[serde(skip_serializing_if = "Option::is_none")] + pub tx: Option, + /// Estimated (returned only for quote rpc) + pub gas: Option, +} + +impl ClassicSwapResponse { + pub(crate) fn from_api_classic_swap_data( + data: one_inch_api::types::ClassicSwapData, + decimals: u8, + ) -> MmResult { + Ok(Self { + dst_amount: MmNumber::from(u256_to_big_decimal(U256::from_dec_str(&data.dst_amount)?, decimals)?).into(), + src_token: data.src_token, + dst_token: data.dst_token, + protocols: data.protocols, + tx: data + .tx + .map(|tx| TxFields::from_api_tx_fields(tx, decimals)) + .transpose()?, + gas: data.gas, + }) + } +} + +#[derive(Serialize, Debug)] +pub struct TxFields { + pub from: Address, + pub to: Address, + pub data: BytesJson, + pub value: BigDecimal, + /// Estimated gas price in gwei + pub gas_price: BigDecimal, + pub gas: u128, // TODO: in eth EthTxFeeDetails rpc we use u64. Better have identical u128 everywhere +} + +impl TxFields { + pub(crate) fn from_api_tx_fields( + tx_fields: one_inch_api::types::TxFields, + decimals: u8, + ) -> MmResult { + Ok(Self { + from: tx_fields.from, + to: tx_fields.to, + data: BytesJson::from(hex::decode(str_strip_0x!(tx_fields.data.as_str()))?), + value: u256_to_big_decimal(U256::from_dec_str(&tx_fields.value)?, decimals)?, + gas_price: wei_to_gwei_decimal(U256::from_dec_str(&tx_fields.gas_price)?)?, + gas: tx_fields.gas, + }) + } +} + +#[derive(Deserialize)] +pub struct ClassicSwapLiquiditySourcesRequest { + pub chain_id: u64, +} + +#[derive(Serialize)] +pub struct ClassicSwapLiquiditySourcesResponse { + pub protocols: Vec, +} + +#[derive(Deserialize)] +pub struct ClassicSwapTokensRequest { + pub chain_id: u64, +} + +#[derive(Serialize)] +pub struct ClassicSwapTokensResponse { + pub tokens: HashMap, +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs index c72e772a81..78697530c1 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs @@ -1,10 +1,18 @@ +//! This source file is for RPCs specific for EVM platform 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 coins::eth::{u256_to_big_decimal, wei_from_big_decimal, EthCoin, Web3RpcError}; +use coins::{lp_coinfind_or_err, CoinFindError, CoinProtocol, MmCoin, MmCoinEnum, NumConversError, Transaction, + TransactionErr}; use common::HttpStatusCode; +use derive_more::Display; +use enum_derives::EnumFromStringify; +use ethereum_types::Address as EthAddress; +use futures::compat::Future01CompatExt; use http::StatusCode; use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::*; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmError, prelude::MmResult}; +use mm2_number::BigDecimal; #[derive(Deserialize)] pub struct TokenInfoRequest { @@ -93,3 +101,73 @@ pub async fn get_token_info(ctx: MmArc, req: TokenInfoRequest) -> MmResult StatusCode { + match self { + Erc20CallError::NoSuchCoin { .. } + | Erc20CallError::CoinNotSupported { .. } + | Erc20CallError::InvalidParam(_) => StatusCode::BAD_REQUEST, + Erc20CallError::TransactionError(_) | Erc20CallError::Web3RpcError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct Erc20AllowanceRequest { + coin: String, + spender: EthAddress, +} + +/// Call allowance method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#approve). +/// Returns BigDecimal allowance value. +pub async fn get_token_allowance_rpc(ctx: MmArc, req: Erc20AllowanceRequest) -> MmResult { + let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; + let wei = eth_coin.allowance(req.spender).compat().await?; + let amount = u256_to_big_decimal(wei, eth_coin.decimals())?; + Ok(amount) +} + +#[derive(Debug, Deserialize)] +pub struct Erc20ApproveRequest { + coin: String, + spender: EthAddress, + amount: BigDecimal, +} + +/// Call approve method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#allowance). +/// Returns approval transaction hash. +pub async fn approve_token_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> MmResult { + let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; + let amount = wei_from_big_decimal(&req.amount, eth_coin.decimals())?; + let tx = eth_coin.approve(req.spender, amount).compat().await?; + Ok(format!("0x{:02x}", tx.tx_hash_as_bytes())) +} + +async fn find_erc20_eth_coin(ctx: &MmArc, coin: &str) -> Result> { + match lp_coinfind_or_err(ctx, coin).await { + Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin), + Ok(_) => Err(MmError::new(Erc20CallError::CoinNotSupported { + coin: coin.to_string(), + })), + Err(_) => Err(MmError::new(Erc20CallError::NoSuchCoin { coin: coin.to_string() })), + } +} 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 1757f97d36..2a253760bc 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -21,7 +21,7 @@ use mm2_test_helpers::for_tests::{check_my_swap_status_amounts, disable_coin, di enable_eth_with_tokens_v2, erc20_dev_conf, eth_dev_conf, get_locked_amount, kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, mycoin_conf, set_price, start_swaps, wait_for_swap_contract_negotiation, wait_for_swap_negotiation_failure, - MarketMakerIt, Mm2TestConf}; + MarketMakerIt, Mm2TestConf, DEFAULT_RPC_PASSWORD}; use mm2_test_helpers::{get_passphrase, structs::*}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -5396,3 +5396,78 @@ fn test_orderbook_depth() { block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); } + +#[test] +fn test_approve_erc20() { + let privkey = random_secp256k1_secret(); + fill_eth_erc20_with_private_key(privkey); + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + let mm = MarketMakerIt::start( + Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins).conf, + DEFAULT_RPC_PASSWORD.to_string(), + None, + ) + .unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("Node log path: {}", mm.log_path.display()); + + let swap_contract = format!("0x{}", hex::encode(swap_contract())); + let _eth_enable = block_on(enable_eth_coin( + &mm, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + let _erc20_enable = block_on(enable_eth_coin( + &mm, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method":"approve_token", + "mmrpc":"2.0", + "id": 0, + "params":{ + "coin": "ERC20DEV", + "spender": swap_contract, + "amount": BigDecimal::from_str("11.0").unwrap(), + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "approve_token error: {}", rc.1); + let res = serde_json::from_str::(&rc.1).unwrap(); + assert!( + hex::decode(str_strip_0x!(res["result"].as_str().unwrap())).is_ok(), + "approve_token result incorrect" + ); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method":"get_token_allowance", + "mmrpc":"2.0", + "id": 0, + "params":{ + "coin": "ERC20DEV", + "spender": swap_contract, + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "get_token_allowance error: {}", rc.1); + let res = serde_json::from_str::(&rc.1).unwrap(); + assert_eq!( + BigDecimal::from_str(res["result"].as_str().unwrap()).unwrap(), + BigDecimal::from_str("11.0").unwrap(), + "get_token_allowance result incorrect" + ); + + block_on(mm.stop()).unwrap(); +} diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index 7ea038a8f7..abcdc2ce9a 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -13,18 +13,18 @@ use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, EthActivation use coins::eth::{checksum_address, eth_addr_to_hex, eth_coin_from_conf_and_request, EthCoin, EthCoinType, EthPrivKeyBuildPolicy, SignedEthTx, SwapV2Contracts, ERC20_ABI}; use coins::nft::nft_structs::{Chain, ContractType, NftInfo}; -use coins::{lp_coinfind, CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaymentInput, - DerivationMethod, Eip1559Ops, FoundSwapTxSpend, MakerNftSwapOpsV2, MarketCoinOps, NftSwapInfo, - ParseCoinAssocTypes, ParseNftAssocTypes, PrivKeyBuildPolicy, RefundNftMakerPaymentArgs, RefundPaymentArgs, - SearchForSwapTxSpendInput, SendNftMakerPaymentArgs, SendPaymentArgs, SpendNftMakerPaymentArgs, - SpendPaymentArgs, SwapOps, SwapTxFeePolicy, SwapTxTypeWithSecretHash, ToBytes, Transaction, - ValidateNftMakerPaymentArgs}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -use coins::{CoinsContext, DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, +use coins::{lp_coinfind, CoinsContext, DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoinEnum, MmCoinStruct, RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, RefundTakerPaymentArgs, SendMakerPaymentArgs, SendTakerFundingArgs, SpendMakerPaymentArgs, TakerCoinSwapOpsV2, TxPreimageWithSig, ValidateMakerPaymentArgs, ValidateTakerFundingArgs}; +use coins::{CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaymentInput, DerivationMethod, + Eip1559Ops, FoundSwapTxSpend, MakerNftSwapOpsV2, MarketCoinOps, NftSwapInfo, ParseCoinAssocTypes, + ParseNftAssocTypes, PrivKeyBuildPolicy, RefundNftMakerPaymentArgs, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendNftMakerPaymentArgs, SendPaymentArgs, SpendNftMakerPaymentArgs, + SpendPaymentArgs, SwapOps, SwapTxFeePolicy, SwapTxTypeWithSecretHash, ToBytes, Transaction, + ValidateNftMakerPaymentArgs}; use common::{block_on, block_on_f01, now_sec}; use crypto::Secp256k1Secret; use ethereum_types::U256; @@ -55,6 +55,8 @@ const SEPOLIA_MAKER_PRIV: &str = "6e2f3a6223b928a05a3a3622b0c3f3573d03663b704a61 const SEPOLIA_TAKER_PRIV: &str = "e0be82dca60ff7e4c6d6db339ac9e1ae249af081dba2110bddd281e711608f16"; const NFT_ETH: &str = "NFT_ETH"; const ETH: &str = "ETH"; + +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] const ERC20: &str = "ERC20DEV"; /// # Safety diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml new file mode 100644 index 0000000000..4fd9514fb9 --- /dev/null +++ b/mm2src/trading_api/Cargo.toml @@ -0,0 +1,28 @@ +[package] +# integration with external trading api +name = "trading_api" +version = "0.1.0" +edition = "2018" + +[dependencies] +common = { path = "../common" } +enum_derives = { path = "../derives/enum_derives" } +mm2_core = { path = "../mm2_core" } +mm2_err_handle = { path = "../mm2_err_handle" } +mm2_net = { path = "../mm2_net" } +mm2_number = { path = "../mm2_number" } +mocktopus = { version = "0.8.0", optional = true } + +derive_more = "0.99" +ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +lazy_static = "1.4" +serde = "1.0" +serde_derive = "1.0" +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +url = { version = "2.2.2", features = ["serde"] } + +[features] +test-ext-api = [] # use test config to connect to an external api + +[dev-dependencies] +mocktopus = { version = "0.8.0" } \ No newline at end of file diff --git a/mm2src/trading_api/src/lib.rs b/mm2src/trading_api/src/lib.rs new file mode 100644 index 0000000000..183e6d9bcd --- /dev/null +++ b/mm2src/trading_api/src/lib.rs @@ -0,0 +1,3 @@ +//! This module is for indirect connection to third-party trading APIs, processing their results and errors + +pub mod one_inch_api; diff --git a/mm2src/trading_api/src/one_inch_api.rs b/mm2src/trading_api/src/one_inch_api.rs new file mode 100644 index 0000000000..9b0af1625e --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api.rs @@ -0,0 +1,5 @@ +//! Wrapper for 1inch API. + +pub mod client; +pub mod errors; +pub mod types; diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs new file mode 100644 index 0000000000..9c7136148a --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -0,0 +1,176 @@ +use super::errors::ApiClientError; +use crate::one_inch_api::errors::NativeError; +use common::StatusCode; +#[cfg(feature = "test-ext-api")] use lazy_static::lazy_static; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_mm_error::MapMmError, + map_to_mm::MapToMmResult, + mm_error::{MmError, MmResult}}; +use mm2_net::transport::slurp_url_with_headers; +use serde::de::DeserializeOwned; +use url::Url; + +#[cfg(any(test, feature = "mocktopus"))] +use mocktopus::macros::*; + +const ONE_INCH_API_ENDPOINT_V6_0: &str = "swap/v6.0/"; +const SWAP_METHOD: &str = "swap"; +const QUOTE_METHOD: &str = "quote"; +const LIQUIDITY_SOURCES_METHOD: &str = "liquidity-sources"; +const TOKENS_METHOD: &str = "tokens"; + +const ONE_INCH_AGGREGATION_ROUTER_CONTRACT_V6_0: &str = "0x111111125421ca6dc452d289314280a0f8842a65"; +const ONE_INCH_ETH_SPECIAL_CONTRACT: &str = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; + +#[cfg(test)] +const ONE_INCH_API_TEST_URL: &str = "https://api.1inch.dev"; + +#[cfg(feature = "test-ext-api")] +lazy_static! { + /// API key for testing + static ref ONE_INCH_API_TEST_AUTH: String = std::env::var("ONE_INCH_API_TEST_AUTH").unwrap_or_default(); +} + +pub(crate) type QueryParams<'life> = Vec<(&'life str, String)>; + +/// 1inch v6.0 supported eth-based chains +const ONE_INCH_V6_0_SUPPORTED_CHAINS: &[(&str, u64)] = &[ + ("Ethereum", 1), + ("Optimism", 10), + ("BSC", 56), + ("Gnosis", 100), + ("Polygon", 137), + ("Fantom", 250), + ("ZkSync", 324), + ("Klaytn", 8217), + ("Base", 8453), + ("Arbitrum", 42161), + ("Avalanche", 43114), + ("Aurora", 1313161554), +]; + +pub(crate) struct UrlBuilder<'a> { + base_url: Url, + endpoint: &'a str, + chain_id: u64, + method_name: String, + query_params: QueryParams<'a>, +} + +impl<'a> UrlBuilder<'a> { + pub(crate) fn new(api_client: &ApiClient, chain_id: u64, method_name: String) -> Self { + Self { + base_url: api_client.base_url.clone(), + endpoint: ApiClient::get_swap_endpoint(), + chain_id, + method_name, + query_params: vec![], + } + } + + pub(crate) fn with_query_params(&mut self, mut more_params: QueryParams<'a>) -> &mut Self { + self.query_params.append(&mut more_params); + self + } + + #[allow(clippy::result_large_err)] + pub(crate) fn build(&self) -> MmResult { + let url = self + .base_url + .join(self.endpoint)? + .join(&format!("{}/", self.chain_id.to_string()))? + .join(self.method_name.as_str())?; + Ok(Url::parse_with_params( + url.as_str(), + self.query_params + .iter() + .map(|v| (v.0, v.1.as_str())) + .collect::>(), + )?) + } +} + +/// 1-inch API caller +pub struct ApiClient { + base_url: Url, +} + +#[allow(clippy::swap_ptr_to_ref)] // need for moctopus +#[cfg_attr(any(test, feature = "mocktopus"), mockable)] +impl ApiClient { + #[allow(unused_variables)] + #[allow(clippy::result_large_err)] + pub fn new(ctx: MmArc) -> MmResult { + #[cfg(not(test))] + let url_cfg = ctx.conf["1inch_api"] + .as_str() + .ok_or(ApiClientError::InvalidParam("No API config param".to_owned()))?; + + #[cfg(test)] + let url_cfg = ONE_INCH_API_TEST_URL; + + Ok(Self { + base_url: Url::parse(url_cfg)?, + }) + } + + pub const fn eth_special_contract() -> &'static str { ONE_INCH_ETH_SPECIAL_CONTRACT } + + pub const fn classic_swap_contract() -> &'static str { ONE_INCH_AGGREGATION_ROUTER_CONTRACT_V6_0 } + + pub fn is_chain_supported(chain_id: u64) -> bool { + ONE_INCH_V6_0_SUPPORTED_CHAINS.iter().any(|(_name, id)| *id == chain_id) + } + + fn get_headers() -> Vec<(&'static str, &'static str)> { + vec![ + #[cfg(feature = "test-ext-api")] + ("Authorization", ONE_INCH_API_TEST_AUTH.as_str()), + ("accept", "application/json"), + ] + } + + fn get_swap_endpoint() -> &'static str { ONE_INCH_API_ENDPOINT_V6_0 } + + pub const fn get_swap_method() -> &'static str { SWAP_METHOD } + + pub const fn get_quote_method() -> &'static str { QUOTE_METHOD } + + pub const fn get_liquidity_sources_method() -> &'static str { LIQUIDITY_SOURCES_METHOD } + + pub const fn get_tokens_method() -> &'static str { TOKENS_METHOD } + + pub(crate) async fn call_api(api_url: &Url) -> MmResult { + let (status_code, _, body) = slurp_url_with_headers(api_url.as_str(), ApiClient::get_headers()) + .await + .mm_err(ApiClientError::TransportError)?; + let body = serde_json::from_slice(&body).map_to_mm(|err| ApiClientError::ParseBodyError { + error_msg: err.to_string(), + })?; + if status_code != StatusCode::OK { + let error = NativeError::new(status_code, body); + return Err(MmError::new(ApiClientError::from_native_error(error))); + } + serde_json::from_value(body).map_err(|err| { + ApiClientError::ParseBodyError { + error_msg: err.to_string(), + } + .into() + }) + } + + pub async fn call_swap_api<'l, T: DeserializeOwned>( + &self, + chain_id: u64, + method: String, + params: Option>, + ) -> MmResult { + let mut builder = UrlBuilder::new(self, chain_id, method); + if let Some(params) = params { + builder.with_query_params(params); + } + let api_url = builder.build()?; + + ApiClient::call_api(&api_url).await + } +} diff --git a/mm2src/trading_api/src/one_inch_api/errors.rs b/mm2src/trading_api/src/one_inch_api/errors.rs new file mode 100644 index 0000000000..d92f8e144b --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/errors.rs @@ -0,0 +1,130 @@ +use common::StatusCode; +use derive_more::Display; +use enum_derives::EnumFromStringify; +use ethereum_types::U256; +use mm2_net::transport::SlurpError; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Display, Serialize, EnumFromStringify)] +pub enum ApiClientError { + #[from_stringify("url::ParseError")] + InvalidParam(String), + #[display(fmt = "Parameter {param} out of bounds, value: {value}, min: {min} max: {max}")] + OutOfBounds { + param: String, + value: String, + min: String, + max: String, + }, + TransportError(SlurpError), + ParseBodyError { + error_msg: String, + }, + #[display(fmt = "General API error: {error_msg} description: {description}")] + GeneralApiError { + error_msg: String, + description: String, + status_code: u16, + }, + #[display(fmt = "Allowance not enough, needed: {amount} allowance: {allowance}")] + AllowanceNotEnough { + error_msg: String, + description: String, + status_code: u16, + /// Amount to approve for the API contract + amount: U256, + /// Existing allowance for the API contract + allowance: U256, + }, +} + +// API error meta 'type' field known values +const META_TYPE_ALLOWANCE: &str = "allowance"; +const META_TYPE_AMOUNT: &str = "amount"; + +#[derive(Debug, Deserialize)] +pub(crate) struct Error400 { + pub error: String, + pub description: Option, + #[serde(rename = "statusCode")] + pub status_code: u16, + pub meta: Option>, + #[allow(dead_code)] + #[serde(rename = "requestId")] + pub request_id: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct Meta { + #[serde(rename = "type")] + pub meta_type: String, + #[serde(rename = "value")] + pub meta_value: String, +} + +#[derive(Debug)] +pub(crate) enum NativeError { + HttpError { error_msg: String, status_code: u16 }, + HttpError400(Error400), + ParseError { error_msg: String }, +} + +impl NativeError { + pub(crate) fn new(status_code: StatusCode, body: Value) -> Self { + if status_code == StatusCode::BAD_REQUEST { + match serde_json::from_value(body) { + Ok(err) => Self::HttpError400(err), + Err(err) => Self::ParseError { + error_msg: format!("could not parse error response: {}", err.to_string()), + }, + } + } else { + Self::HttpError { + error_msg: body["error"].as_str().unwrap_or_default().to_owned(), + status_code: status_code.into(), + } + } + } +} + +impl ApiClientError { + /// Convert from native API errors to lib errors + /// Look for known API errors. If none found return as general API error + pub(crate) fn from_native_error(api_error: NativeError) -> ApiClientError { + match api_error { + NativeError::HttpError400(error_400) => { + if let Some(meta) = error_400.meta { + // Try if it's "Not enough allowance" error 'meta' data: + if let Some(meta_allowance) = meta.iter().find(|m| m.meta_type == META_TYPE_ALLOWANCE) { + // try find 'amount' value + let amount = if let Some(meta_amount) = meta.iter().find(|m| m.meta_type == META_TYPE_AMOUNT) { + U256::from_dec_str(&meta_amount.meta_value).unwrap_or_default() + } else { + Default::default() + }; + let allowance = U256::from_dec_str(&meta_allowance.meta_value).unwrap_or_default(); + return ApiClientError::AllowanceNotEnough { + error_msg: error_400.error, + status_code: error_400.status_code, + description: error_400.description.unwrap_or_default(), + amount, + allowance, + }; + } + } + ApiClientError::GeneralApiError { + error_msg: error_400.error, + status_code: error_400.status_code, + description: error_400.description.unwrap_or_default(), + } + }, + NativeError::HttpError { error_msg, status_code } => ApiClientError::GeneralApiError { + error_msg, + status_code, + description: Default::default(), + }, + NativeError::ParseError { error_msg } => ApiClientError::ParseBodyError { error_msg }, + } + } +} diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs new file mode 100644 index 0000000000..f13e943768 --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -0,0 +1,411 @@ +#![allow(clippy::result_large_err)] + +use super::client::QueryParams; +use super::errors::ApiClientError; +use common::{def_with_opt_param, push_if_some}; +use ethereum_types::Address; +use mm2_err_handle::mm_error::{MmError, MmResult}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use url::Url; + +const ONE_INCH_MAX_SLIPPAGE: f32 = 50.0; +const ONE_INCH_MAX_FEE_SHARE: f32 = 3.0; +const ONE_INCH_MAX_GAS: u128 = 11500000; +const ONE_INCH_MAX_PARTS: u32 = 100; +const ONE_INCH_MAX_MAIN_ROUTE_PARTS: u32 = 50; +const ONE_INCH_MAX_COMPLEXITY_LEVEL: u32 = 3; + +const BAD_URL_IN_RESPONSE_ERROR: &str = "unsupported url in response"; +const ONE_INCH_DOMAIN: &str = "1inch.io"; + +/// API params builder for swap quote +#[derive(Default)] +pub struct ClassicSwapQuoteParams { + /// Source token address + src: String, + /// Destination token address + dst: String, + amount: String, + // Optional fields + fee: Option, + protocols: Option, + gas_price: Option, + complexity_level: Option, + parts: Option, + main_route_parts: Option, + gas_limit: Option, + include_tokens_info: Option, + include_protocols: Option, + include_gas: Option, + connector_tokens: Option, +} + +impl ClassicSwapQuoteParams { + pub fn new(src: String, dst: String, amount: String) -> Self { + Self { + src, + dst, + amount, + ..Default::default() + } + } + + def_with_opt_param!(fee, f32); + def_with_opt_param!(protocols, String); + def_with_opt_param!(gas_price, String); + def_with_opt_param!(complexity_level, u32); + def_with_opt_param!(parts, u32); + def_with_opt_param!(main_route_parts, u32); + def_with_opt_param!(gas_limit, u128); + def_with_opt_param!(include_tokens_info, bool); + def_with_opt_param!(include_protocols, bool); + def_with_opt_param!(include_gas, bool); + def_with_opt_param!(connector_tokens, String); + + pub fn build_query_params(&self) -> MmResult, ApiClientError> { + self.validate_params()?; + + let mut params = vec![ + ("src", self.src.clone()), + ("dst", self.dst.clone()), + ("amount", self.amount.clone()), + ]; + + push_if_some!(params, "fee", self.fee); + push_if_some!(params, "protocols", &self.protocols); + push_if_some!(params, "gasPrice", &self.gas_price); + push_if_some!(params, "complexityLevel", self.complexity_level); + push_if_some!(params, "parts", self.parts); + push_if_some!(params, "mainRouteParts", self.main_route_parts); + push_if_some!(params, "gasLimit", self.gas_limit); + push_if_some!(params, "includeTokensInfo", self.include_tokens_info); + push_if_some!(params, "includeProtocols", self.include_protocols); + push_if_some!(params, "includeGas", self.include_gas); + push_if_some!(params, "connectorTokens", &self.connector_tokens); + Ok(params) + } + + /// Validate params by 1inch rules (to avoid extra requests) + fn validate_params(&self) -> MmResult<(), ApiClientError> { + validate_fee(&self.fee)?; + validate_complexity_level(&self.complexity_level)?; + validate_gas_limit(&self.gas_limit)?; + validate_parts(&self.parts)?; + validate_main_route_parts(&self.main_route_parts)?; + Ok(()) + } +} + +/// API params builder to create a tx for swap +#[derive(Default)] +pub struct ClassicSwapCreateParams { + src: String, + dst: String, + amount: String, + from: String, + slippage: f32, + // Optional fields + fee: Option, + protocols: Option, + gas_price: Option, + complexity_level: Option, + parts: Option, + main_route_parts: Option, + gas_limit: Option, + include_tokens_info: Option, + include_protocols: Option, + include_gas: Option, + connector_tokens: Option, + excluded_protocols: Option, + permit: Option, + compatibility: Option, + receiver: Option, + referrer: Option, + disable_estimate: Option, + allow_partial_fill: Option, + use_permit2: Option, +} + +impl ClassicSwapCreateParams { + pub fn new(src: String, dst: String, amount: String, from: String, slippage: f32) -> Self { + Self { + src, + dst, + amount, + from, + slippage, + ..Default::default() + } + } + + def_with_opt_param!(fee, f32); + def_with_opt_param!(protocols, String); + def_with_opt_param!(gas_price, String); + def_with_opt_param!(complexity_level, u32); + def_with_opt_param!(parts, u32); + def_with_opt_param!(main_route_parts, u32); + def_with_opt_param!(gas_limit, u128); + def_with_opt_param!(include_tokens_info, bool); + def_with_opt_param!(include_protocols, bool); + def_with_opt_param!(include_gas, bool); + def_with_opt_param!(connector_tokens, String); + def_with_opt_param!(excluded_protocols, String); + def_with_opt_param!(permit, String); + def_with_opt_param!(compatibility, bool); + def_with_opt_param!(receiver, String); + def_with_opt_param!(referrer, String); + def_with_opt_param!(disable_estimate, bool); + def_with_opt_param!(allow_partial_fill, bool); + def_with_opt_param!(use_permit2, bool); + + pub fn build_query_params(&self) -> MmResult, ApiClientError> { + self.validate_params()?; + + let mut params = vec![ + ("src", self.src.clone()), + ("dst", self.dst.clone()), + ("amount", self.amount.clone()), + ("from", self.from.clone()), + ("slippage", self.slippage.to_string()), + ]; + + push_if_some!(params, "fee", self.fee); + push_if_some!(params, "protocols", &self.protocols); + push_if_some!(params, "gasPrice", &self.gas_price); + push_if_some!(params, "complexityLevel", self.complexity_level); + push_if_some!(params, "parts", self.parts); + push_if_some!(params, "mainRouteParts", self.main_route_parts); + push_if_some!(params, "gasLimit", self.gas_limit); + push_if_some!(params, "includeTokensInfo", self.include_tokens_info); + push_if_some!(params, "includeProtocols", self.include_protocols); + push_if_some!(params, "includeGas", self.include_gas); + push_if_some!(params, "connectorTokens", &self.connector_tokens); + push_if_some!(params, "excludedProtocols", &self.excluded_protocols); + push_if_some!(params, "permit", &self.permit); + push_if_some!(params, "compatibility", &self.compatibility); + push_if_some!(params, "receiver", &self.receiver); + push_if_some!(params, "referrer", &self.referrer); + push_if_some!(params, "disableEstimate", self.disable_estimate); + push_if_some!(params, "allowPartialFill", self.allow_partial_fill); + push_if_some!(params, "usePermit2", self.use_permit2); + + Ok(params) + } + + /// Validate params by 1inch rules (to avoid extra requests) + fn validate_params(&self) -> MmResult<(), ApiClientError> { + validate_slippage(self.slippage)?; + validate_fee(&self.fee)?; + validate_complexity_level(&self.complexity_level)?; + validate_gas_limit(&self.gas_limit)?; + validate_parts(&self.parts)?; + validate_main_route_parts(&self.main_route_parts)?; + Ok(()) + } +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct TokenInfo { + pub address: Address, + pub symbol: String, + pub name: String, + pub decimals: u32, + pub eip2612: bool, + #[serde(rename = "isFoT", default)] + pub is_fot: bool, + #[serde(rename = "logoURI", with = "serde_one_inch_link")] + pub logo_uri: String, + pub tags: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ProtocolInfo { + pub name: String, + pub part: f64, + #[serde(rename = "fromTokenAddress")] + pub from_token_address: Address, + #[serde(rename = "toTokenAddress")] + pub to_token_address: Address, +} + +#[derive(Deserialize, Debug)] +pub struct ClassicSwapData { + /// dst token amount to receive, in api is a decimal number as string + #[serde(rename = "dstAmount")] + pub dst_amount: String, + #[serde(rename = "srcToken")] + pub src_token: Option, + #[serde(rename = "dstToken")] + pub dst_token: Option, + pub protocols: Option>>>, + pub tx: Option, + pub gas: Option, +} + +#[derive(Deserialize, Debug)] +pub struct TxFields { + pub from: Address, + pub to: Address, + pub data: String, + /// tx value, in api is a decimal number as string + pub value: String, + /// gas price, in api is a decimal number as string + #[serde(rename = "gasPrice")] + pub gas_price: String, + /// gas limit, in api is a decimal number + pub gas: u128, +} + +#[derive(Deserialize, Serialize)] +pub struct ProtocolImage { + pub id: String, + pub title: String, + #[serde(with = "serde_one_inch_link")] + pub img: String, + #[serde(with = "serde_one_inch_link")] + pub img_color: String, +} + +#[derive(Deserialize)] +pub struct ProtocolsResponse { + pub protocols: Vec, +} + +#[derive(Deserialize)] +pub struct TokensResponse { + pub tokens: HashMap, +} + +mod serde_one_inch_link { + use super::validate_one_inch_link; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + /// Just forward to the normal serializer + pub(super) fn serialize(s: &String, serializer: S) -> Result + where + S: Serializer, + { + s.serialize(serializer) + } + + /// Deserialise String with checking links + pub(super) fn deserialize<'a, D>(deserializer: D) -> Result + where + D: Deserializer<'a>, + { + ::deserialize(deserializer) + .map(|value| validate_one_inch_link(&value).unwrap_or_default()) + } +} + +fn validate_slippage(slippage: f32) -> MmResult<(), ApiClientError> { + if !(0.0..=ONE_INCH_MAX_SLIPPAGE).contains(&slippage) { + return Err(ApiClientError::OutOfBounds { + param: "slippage".to_owned(), + value: slippage.to_string(), + min: 0.0.to_string(), + max: ONE_INCH_MAX_SLIPPAGE.to_string(), + } + .into()); + } + Ok(()) +} + +fn validate_fee(fee: &Option) -> MmResult<(), ApiClientError> { + if let Some(fee) = fee { + if !(0.0..=ONE_INCH_MAX_FEE_SHARE).contains(fee) { + return Err(ApiClientError::OutOfBounds { + param: "fee".to_owned(), + value: fee.to_string(), + min: 0.0.to_string(), + max: ONE_INCH_MAX_FEE_SHARE.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_gas_limit(gas_limit: &Option) -> MmResult<(), ApiClientError> { + if let Some(gas_limit) = gas_limit { + if gas_limit > &ONE_INCH_MAX_GAS { + return Err(ApiClientError::OutOfBounds { + param: "gas_limit".to_owned(), + value: gas_limit.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_GAS.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_parts(parts: &Option) -> MmResult<(), ApiClientError> { + if let Some(parts) = parts { + if parts > &ONE_INCH_MAX_PARTS { + return Err(ApiClientError::OutOfBounds { + param: "parts".to_owned(), + value: parts.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_PARTS.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_main_route_parts(main_route_parts: &Option) -> MmResult<(), ApiClientError> { + if let Some(main_route_parts) = main_route_parts { + if main_route_parts > &ONE_INCH_MAX_MAIN_ROUTE_PARTS { + return Err(ApiClientError::OutOfBounds { + param: "main route parts".to_owned(), + value: main_route_parts.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_MAIN_ROUTE_PARTS.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_complexity_level(complexity_level: &Option) -> MmResult<(), ApiClientError> { + if let Some(complexity_level) = complexity_level { + if complexity_level > &ONE_INCH_MAX_COMPLEXITY_LEVEL { + return Err(ApiClientError::OutOfBounds { + param: "complexity level".to_owned(), + value: complexity_level.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_COMPLEXITY_LEVEL.to_string(), + } + .into()); + } + } + Ok(()) +} + +/// Check if url is valid and is a subdomain of 1inch domain (simple anti-phishing check) +fn validate_one_inch_link(s: &str) -> MmResult { + let url = Url::parse(s).map_err(|_err| ApiClientError::ParseBodyError { + error_msg: BAD_URL_IN_RESPONSE_ERROR.to_owned(), + })?; + if let Some(host) = url.host() { + if host.to_string().ends_with(ONE_INCH_DOMAIN) { + return Ok(s.to_owned()); + } + } + MmError::err(ApiClientError::ParseBodyError { + error_msg: BAD_URL_IN_RESPONSE_ERROR.to_owned(), + }) +} + +#[test] +fn test_validate_one_inch_link() { + assert!(validate_one_inch_link("https://cdn.1inch.io/liquidity-sources-logo/wmatic_color.png").is_ok()); + assert!(validate_one_inch_link("https://example.org/somepath/somefile.png").is_err()); + assert!(validate_one_inch_link("https://inch.io/somepath/somefile.png").is_err()); + assert!(validate_one_inch_link("127.0.0.1").is_err()); +} From c406aa292cf8dd555725e8dc2448787ec1351dbe Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:20:33 +0200 Subject: [PATCH 05/27] chore(release): bump mm2 version to 2.3.0-beta (#2285) --- Cargo.lock | 2 +- mm2src/mm2_bin_lib/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7fce54111d..7332358679 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3841,7 +3841,7 @@ dependencies = [ [[package]] name = "mm2_bin_lib" -version = "2.2.0-beta" +version = "2.3.0-beta" dependencies = [ "chrono", "common", diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index 7415f21b4f..bf95f316e4 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "mm2_bin_lib" -version = "2.2.0-beta" +version = "2.3.0-beta" authors = ["James Lee", "Artem Pikulin", "Artem Grinblat", "Omar S.", "Onur Ozkan", "Alina Sharon", "Caglar Kaya", "Cipi", "Sergey Boiko", "Samuel Onoja", "Roman Sztergbaum", "Kadan Stadelmann ", "Dimxy", "Omer Yacine"] edition = "2018" default-run = "kdf" From 704731e23b9d3ff5632733a74a6b81f78fe36937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 9 Dec 2024 13:40:04 +0300 Subject: [PATCH 06/27] improvement(error-handling): main files (#2288) Makes KDF to check main files (config/coins/etc..) before reading them to prevent potential panics. --- .github/workflows/pr-lint.yml | 5 +++-- mm2src/common/common.rs | 39 ++++++++++++++++++++++++++++------- mm2src/mm2_main/src/mm2.rs | 13 ++++++------ 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 59e176a5e0..704c614549 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -22,6 +22,7 @@ jobs: types: | feat fix + improvement chore docs deps @@ -38,9 +39,9 @@ jobs: TITLE: ${{ github.event.pull_request.title }} run: | title_length=${#TITLE} - if [ $title_length -gt 72 ] + if [ $title_length -gt 85 ] then - echo "PR title is too long (greater than 72 characters)" + echo "PR title is too long (greater than 85 characters)" exit 1 fi diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index e0b6b4d233..288882d0ae 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -162,14 +162,14 @@ use std::convert::TryInto; use std::fmt::Write as FmtWrite; use std::fs::File; use std::future::Future as Future03; -use std::io::{BufReader, Read, Write}; +use std::io::{self, BufReader, Read, Write}; use std::iter::Peekable; use std::mem::{forget, zeroed}; use std::num::{NonZeroUsize, TryFromIntError}; use std::ops::{Add, Deref, Div, RangeInclusive}; use std::os::raw::c_void; use std::panic::{set_hook, PanicInfo}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::ptr::read_volatile; use std::sync::atomic::Ordering; use std::time::{Duration, SystemTime, SystemTimeError}; @@ -792,7 +792,7 @@ pub fn kdf_app_dir() -> Option { } /// Returns path of the coins file. -pub fn kdf_coins_file() -> PathBuf { +pub fn kdf_coins_file() -> Result { #[cfg(not(target_arch = "wasm32"))] let value_from_env = env::var("MM_COINS_PATH").ok(); @@ -803,7 +803,7 @@ pub fn kdf_coins_file() -> PathBuf { } /// Returns path of the config file. -pub fn kdf_config_file() -> PathBuf { +pub fn kdf_config_file() -> Result { #[cfg(not(target_arch = "wasm32"))] let value_from_env = env::var("MM_CONF_PATH").ok(); @@ -819,16 +819,41 @@ pub fn kdf_config_file() -> PathBuf { /// 1- From the environment variable. /// 2- From the current directory where app is called. /// 3- From the root application directory. -pub fn find_kdf_dependency_file(value_from_env: Option, path_leaf: &str) -> PathBuf { +fn find_kdf_dependency_file(value_from_env: Option, path_leaf: &str) -> Result { if let Some(path) = value_from_env { - return PathBuf::from(path); + let path = PathBuf::from(path); + require_file(&path)?; + return Ok(path); } let from_current_dir = PathBuf::from(path_leaf); - if from_current_dir.exists() { + + let path = if from_current_dir.exists() { from_current_dir } else { kdf_app_dir().unwrap_or_default().join(path_leaf) + }; + + require_file(&path)?; + return Ok(path); + + fn require_file(path: &Path) -> Result<(), io::Error> { + if path.is_dir() { + // TODO: use `IsADirectory` variant which is stabilized with 1.83 + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Expected file but '{}' is a directory.", path.display()), + )); + } + + if !path.exists() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("File '{}' is not present.", path.display()), + )); + } + + Ok(()) } } diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index dd7c7bed27..600702b921 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -303,22 +303,23 @@ pub fn mm2_main(version: String, datetime: String) { /// Parses and returns the `first_arg` as JSON. /// Attempts to load the config from `MM2.json` file if `first_arg` is None pub fn get_mm2config(first_arg: Option<&str>) -> Result { - let conf_path = common::kdf_config_file(); - let conf_from_file = slurp(&conf_path); let conf = match first_arg { - Some(s) => s, + Some(s) => s.to_owned(), None => { + let conf_path = common::kdf_config_file().map_err(|e| e.to_string())?; + let conf_from_file = slurp(&conf_path); + if conf_from_file.is_empty() { return ERR!( "Config is not set from command line arg and {} file doesn't exist.", conf_path.display() ); } - try_s!(std::str::from_utf8(&conf_from_file)) + try_s!(String::from_utf8(conf_from_file)) }, }; - let mut conf: Json = match json::from_str(conf) { + let mut conf: Json = match json::from_str(&conf) { Ok(json) => json, // Syntax or io errors may include the conf string in the error message so we don't want to take risks and show these errors internals in the log. // If new variants are added to the Error enum, there can be a risk of exposing the conf string in the error message when updating serde_json so @@ -327,7 +328,7 @@ pub fn get_mm2config(first_arg: Option<&str>) -> Result { }; if conf["coins"].is_null() { - let coins_path = common::kdf_coins_file(); + let coins_path = common::kdf_coins_file().map_err(|e| e.to_string())?; let coins_from_file = slurp(&coins_path); if coins_from_file.is_empty() { From df1077bbf83cc1d76af4925d39572c5ec90971a2 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:51:05 +0200 Subject: [PATCH 07/27] fix(rpc): remove character check blocking password input (#2287) This commit removes check for <, >, & characters in RPC request bodies that was incorrectly blocking valid password characters in get_mnemonic RPC call. These special characters should be allowed in passwords. This aligns native behavior with WASM implementation. --- mm2src/mm2_main/src/rpc.rs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index 1f0afd3234..94f8d9fd0d 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -22,10 +22,10 @@ use crate::rpc::rate_limiter::RateLimitError; use common::log::{error, info}; -use common::{err_to_rpc_json_string, err_tp_rpc_json, HttpStatusCode, APPLICATION_JSON}; +use common::{err_to_rpc_json_string, err_tp_rpc_json, HttpStatusCode}; use derive_more::Display; use futures::future::{join_all, FutureExt}; -use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE}; +use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN}; use http::request::Parts; use http::{Method, Request, Response, StatusCode}; use mm2_core::mm_ctx::MmArc; @@ -203,8 +203,6 @@ async fn process_single_request(ctx: MmArc, req: Json, client: SocketAddr) -> Re #[cfg(not(target_arch = "wasm32"))] async fn rpc_service(req: Request, ctx_h: u32, client: SocketAddr) -> Response { - const NON_ALLOWED_CHARS: &[char] = &['<', '>', '&']; - /// Unwraps a result or propagates its error 500 response with the specified headers (if they are present). macro_rules! try_sf { ($value: expr $(, $header_key:expr => $header_val:expr)*) => { @@ -263,19 +261,6 @@ async fn rpc_service(req: Request, ctx_h: u32, client: SocketAddr) -> Resp let req_json = { let req_bytes = try_sf!(hyper::body::to_bytes(req_body).await, ACCESS_CONTROL_ALLOW_ORIGIN => rpc_cors); - let req_str = String::from_utf8_lossy(req_bytes.as_ref()); - let is_invalid_input = req_str.chars().any(|c| NON_ALLOWED_CHARS.contains(&c)); - if is_invalid_input { - return Response::builder() - .status(500) - .header(ACCESS_CONTROL_ALLOW_ORIGIN, rpc_cors) - .header(CONTENT_TYPE, APPLICATION_JSON) - .body(Body::from(err_to_rpc_json_string(&format!( - "Invalid input: contains one or more of the following non-allowed characters: {:?}", - NON_ALLOWED_CHARS - )))) - .unwrap(); - } try_sf!(json::from_slice(&req_bytes), ACCESS_CONTROL_ALLOW_ORIGIN => rpc_cors) }; From cb37bd4a807e5bef72279375b4bf6dc4662c8141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Tue, 10 Dec 2024 15:26:10 +0300 Subject: [PATCH 08/27] don't rely on core (#2289) Signed-off-by: onur-ozkan --- mm2src/mm2_bitcoin/rpc/src/lib.rs | 1 - mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs | 4 ++-- mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mm2src/mm2_bitcoin/rpc/src/lib.rs b/mm2src/mm2_bitcoin/rpc/src/lib.rs index d901dbdb22..70efe15dc7 100644 --- a/mm2src/mm2_bitcoin/rpc/src/lib.rs +++ b/mm2src/mm2_bitcoin/rpc/src/lib.rs @@ -1,4 +1,3 @@ -extern crate core; #[cfg(test)] extern crate lazy_static; extern crate log; extern crate rustc_hex as hex; diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs index 9ff3d3e36e..5368985a08 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs @@ -85,8 +85,8 @@ impl ops::Deref for Bytes { fn deref(&self) -> &Self::Target { &self.0 } } -impl ::core::fmt::LowerHex for Bytes { - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { +impl ::std::fmt::LowerHex for Bytes { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { for i in &self.0[..] { write!(f, "{:02x}", i)?; } diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs index 1911d11781..00ecf4f52f 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs @@ -147,8 +147,8 @@ macro_rules! impl_hash { } } - impl ::core::fmt::LowerHex for $name { - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + impl ::std::fmt::LowerHex for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { for i in &self.0[..] { write!(f, "{:02x}", i)?; } From ac926dda0a89b239f753eae833ced97a3b703544 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Tue, 10 Dec 2024 18:59:00 +0100 Subject: [PATCH 09/27] chore(ctx): replace gstuff constructible with oncelock (#2267) --- mm2src/coins/eth/eth_tests.rs | 4 +- .../coins/hd_wallet/storage/sqlite_storage.rs | 2 +- mm2src/coins/lightning/ln_utils.rs | 1 + mm2src/coins/nft/nft_structs.rs | 5 +- .../sql_tx_history_storage_v2.rs | 11 +- .../utxo/utxo_block_header_storage/mod.rs | 9 +- .../storage/blockdb/blockdb_sql_storage.rs | 20 ++-- mm2src/crypto/src/crypto_ctx.rs | 8 +- mm2src/mm2_bin_lib/src/lib.rs | 2 +- mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs | 2 +- mm2src/mm2_core/Cargo.toml | 3 +- mm2src/mm2_core/src/mm_ctx.rs | 111 +++++++++--------- .../src/account/storage/sqlite_storage.rs | 2 +- mm2src/mm2_main/src/lp_native_dex.rs | 4 +- mm2src/mm2_main/src/lp_wallet.rs | 6 +- .../src/lp_wallet/mnemonics_storage.rs | 1 + .../src/lp_wallet/mnemonics_wasm_db.rs | 1 + mm2src/mm2_main/src/mm2.rs | 4 +- mm2src/mm2_main/src/ordermatch_tests.rs | 15 ++- mm2src/mm2_main/src/rpc.rs | 12 +- .../tests/docker_tests/swap_watcher_tests.rs | 2 +- mm2src/mm2_test_helpers/src/for_tests.rs | 26 ++-- 22 files changed, 145 insertions(+), 106 deletions(-) diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 799480a924..e799ba43d3 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1,13 +1,13 @@ use super::*; use crate::IguanaPrivKey; -use common::{block_on, block_on_f01}; +use common::block_on; use mm2_core::mm_ctx::MmCtxBuilder; cfg_native!( use crate::eth::for_tests::{eth_coin_for_test, eth_coin_from_keypair}; use crate::DexFee; - use common::now_sec; + use common::{now_sec, block_on_f01}; use ethkey::{Generator, Random}; use mm2_test_helpers::for_tests::{ETH_MAINNET_CHAIN_ID, ETH_MAINNET_NODE, ETH_SEPOLIA_CHAIN_ID, ETH_SEPOLIA_NODES, ETH_SEPOLIA_TOKEN_CONTRACT}; diff --git a/mm2src/coins/hd_wallet/storage/sqlite_storage.rs b/mm2src/coins/hd_wallet/storage/sqlite_storage.rs index 898f4c8823..f430eac042 100644 --- a/mm2src/coins/hd_wallet/storage/sqlite_storage.rs +++ b/mm2src/coins/hd_wallet/storage/sqlite_storage.rs @@ -101,7 +101,7 @@ impl HDWalletStorageInternalOps for HDWalletSqliteStorage { where Self: Sized, { - let shared = ctx.shared_sqlite_conn.as_option().or_mm_err(|| { + let shared = ctx.shared_sqlite_conn.get().or_mm_err(|| { HDWalletStorageError::Internal("'MmCtx::shared_sqlite_conn' is not initialized".to_owned()) })?; let storage = HDWalletSqliteStorage { diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index 5b4ac5698d..79868908fa 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -87,6 +87,7 @@ pub async fn init_db(ctx: &MmArc, ticker: String) -> EnableLightningResult Result, String> { - Ok(try_s!(from_ctx(&ctx.nft_ctx, move || { + from_ctx(&ctx.nft_ctx, move || { let async_sqlite_connection = ctx .async_sqlite_connection + .get() .ok_or("async_sqlite_connection is not initialized".to_owned())?; Ok(NftCtx { nft_cache_db: async_sqlite_connection.clone(), }) - }))) + }) } #[cfg(target_arch = "wasm32")] diff --git a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs index cf0575f973..f5b1312a65 100644 --- a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs @@ -376,11 +376,12 @@ pub struct SqliteTxHistoryStorage(Arc>); impl SqliteTxHistoryStorage { pub fn new(ctx: &MmArc) -> Result> { - let sqlite_connection = ctx - .sqlite_connection - .ok_or(MmError::new(CreateTxHistoryStorageError::Internal( - "sqlite_connection is not initialized".to_owned(), - )))?; + let sqlite_connection = + ctx.sqlite_connection + .get() + .ok_or(MmError::new(CreateTxHistoryStorageError::Internal( + "sqlite_connection is not initialized".to_owned(), + )))?; Ok(SqliteTxHistoryStorage(sqlite_connection.clone())) } } diff --git a/mm2src/coins/utxo/utxo_block_header_storage/mod.rs b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs index 89266af2f6..c065016176 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/mod.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs @@ -27,7 +27,7 @@ impl Debug for BlockHeaderStorage { impl BlockHeaderStorage { #[cfg(all(not(test), not(target_arch = "wasm32")))] pub(crate) fn new_from_ctx(ctx: MmArc, ticker: String) -> Result { - let sqlite_connection = ctx.sqlite_connection.ok_or(BlockHeaderStorageError::Internal( + let sqlite_connection = ctx.sqlite_connection.get().ok_or(BlockHeaderStorageError::Internal( "sqlite_connection is not initialized".to_owned(), ))?; Ok(BlockHeaderStorage { @@ -50,8 +50,11 @@ impl BlockHeaderStorage { use db_common::sqlite::rusqlite::Connection; use std::sync::{Arc, Mutex}; - let conn = Arc::new(Mutex::new(Connection::open_in_memory().unwrap())); - let conn = ctx.sqlite_connection.clone_or(conn); + let conn = ctx + .sqlite_connection + .get() + .cloned() + .unwrap_or_else(|| Arc::new(Mutex::new(Connection::open_in_memory().unwrap()))); Ok(BlockHeaderStorage { inner: Box::new(SqliteBlockHeadersStorage { ticker, conn }), diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs index 7523360807..fea9a93277 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs @@ -49,10 +49,9 @@ impl BlockDbImpl { async_blocking(move || { let conn = Connection::open(path).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; let conn = Arc::new(Mutex::new(conn)); - let conn_clone = conn.clone(); - let conn_clone = conn_clone.lock().unwrap(); - run_optimization_pragmas(&conn_clone).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; - conn_clone + let conn_lock = conn.lock().unwrap(); + run_optimization_pragmas(&conn_lock).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + conn_lock .execute( "CREATE TABLE IF NOT EXISTS compactblocks ( height INTEGER PRIMARY KEY, @@ -61,6 +60,7 @@ impl BlockDbImpl { [], ) .map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + drop(conn_lock); Ok(Self { db: conn, ticker }) }) @@ -73,11 +73,12 @@ impl BlockDbImpl { async_blocking(move || { let conn = ctx .sqlite_connection - .clone_or(Arc::new(Mutex::new(Connection::open_in_memory().unwrap()))); - let conn_clone = conn.clone(); - let conn_clone = conn_clone.lock().unwrap(); - run_optimization_pragmas(&conn_clone).map_err(|err| ZcoinStorageError::DbError(err.to_string()))?; - conn_clone + .get() + .cloned() + .unwrap_or_else(|| Arc::new(Mutex::new(Connection::open_in_memory().unwrap()))); + let conn_lock = conn.lock().unwrap(); + run_optimization_pragmas(&conn_lock).map_err(|err| ZcoinStorageError::DbError(err.to_string()))?; + conn_lock .execute( "CREATE TABLE IF NOT EXISTS compactblocks ( height INTEGER PRIMARY KEY, @@ -86,6 +87,7 @@ impl BlockDbImpl { [], ) .map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + drop(conn_lock); Ok(BlockDbImpl { db: conn, ticker }) }) diff --git a/mm2src/crypto/src/crypto_ctx.rs b/mm2src/crypto/src/crypto_ctx.rs index 92ac1f2196..ffc83603a6 100644 --- a/mm2src/crypto/src/crypto_ctx.rs +++ b/mm2src/crypto/src/crypto_ctx.rs @@ -316,10 +316,12 @@ impl CryptoCtx { *ctx_field = Some(result.clone()); drop(ctx_field); - ctx.rmd160.pin(rmd160).map_to_mm(CryptoInitError::Internal)?; + ctx.rmd160 + .set(rmd160) + .map_to_mm(|_| CryptoInitError::Internal("Already Initialized".to_string()))?; ctx.shared_db_id - .pin(shared_db_id) - .map_to_mm(CryptoInitError::Internal)?; + .set(shared_db_id) + .map_to_mm(|_| CryptoInitError::Internal("Already Initialized".to_string()))?; info!("Public key hash: {rmd160}"); info!("Shared Database ID: {shared_db_id}"); diff --git a/mm2src/mm2_bin_lib/src/lib.rs b/mm2src/mm2_bin_lib/src/lib.rs index c78233e64a..7ac292aa63 100644 --- a/mm2src/mm2_bin_lib/src/lib.rs +++ b/mm2src/mm2_bin_lib/src/lib.rs @@ -41,7 +41,7 @@ fn mm2_status() -> MainStatus { Err(_) => return MainStatus::NoRpc, }; - if ctx.rpc_started.copy_or(false) { + if *ctx.rpc_started.get().unwrap_or(&false) { MainStatus::RpcIsUp } else { MainStatus::NoRpc diff --git a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs index ee56bd4045..f878e1b914 100644 --- a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs +++ b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs @@ -216,7 +216,7 @@ pub async fn mm2_rpc(payload: JsValue) -> Result { Err(_) => return Err(Mm2RpcErr::NotRunning.into()), }; - let wasm_rpc = ctx.wasm_rpc.ok_or(JsValue::from(Mm2RpcErr::NotRunning))?; + let wasm_rpc = ctx.wasm_rpc.get().ok_or(JsValue::from(Mm2RpcErr::NotRunning))?; let response: Mm2RpcResponse = wasm_rpc.request(request_json).await.into(); serialize_to_js(&response).map_err(|e| { diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index d0df9dbe7c..78fffea53b 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -15,6 +15,7 @@ common = { path = "../common" } db_common = { path = "../db_common" } derive_more = "0.99" futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } +gstuff = { version = "0.7", features = ["nightly"] } hex = "0.4.2" lazy_static = "1.4" libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.4", default-features = false, features = ["identify"] } @@ -31,13 +32,11 @@ shared_ref_counter = { path = "../common/shared_ref_counter" } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -gstuff = { version = "0.7", features = ["nightly"] } instant = { version = "0.1.12", features = ["wasm-bindgen"] } mm2_rpc = { path = "../mm2_rpc", features = [ "rpc_facilities" ] } wasm-bindgen-test = { version = "0.3.2" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rustls = { version = "0.21", default-features = false } -gstuff = { version = "0.7", features = ["nightly"] } instant = "0.1.12" tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 8c417f2ce1..0a1afb2eea 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -7,7 +7,7 @@ use common::{executor::{abortable_queue::{AbortableQueue, WeakSpawner}, expirable_map::ExpirableMap}; use futures::channel::oneshot; use futures::lock::Mutex as AsyncMutex; -use gstuff::{try_s, Constructible, ERR, ERRL}; +use gstuff::{try_s, ERR, ERRL}; use lazy_static::lazy_static; use libp2p::PeerId; use mm2_event_stream::{controller::Controller, Event, EventStreamConfiguration}; @@ -22,7 +22,7 @@ use std::collections::HashSet; use std::fmt; use std::future::Future; use std::ops::Deref; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use crate::data_asker::DataAsker; @@ -76,9 +76,9 @@ pub struct MmCtx { /// Should be refactored away in the future. State should always be valid. /// If there are things that are loaded in background then they should be separately optional, /// without invalidating the entire state. - pub initialized: Constructible, + pub initialized: OnceLock, /// True if the RPC HTTP server was started. - pub rpc_started: Constructible, + pub rpc_started: OnceLock, /// Controller for continuously streaming data using streaming channels of `mm2_event_stream`. pub stream_channel_controller: Controller, /// Data transfer bridge between server and client where server (which is the mm2 runtime) initiates the request. @@ -86,10 +86,10 @@ pub struct MmCtx { /// Configuration of event streaming used for SSE. pub event_stream_configuration: Option, /// True if the MarketMaker instance needs to stop. - pub stop: Constructible, + pub stop: OnceLock, /// Unique context identifier, allowing us to more easily pass the context through the FFI boundaries. /// 0 if the handler ID is allocated yet. - pub ffi_handle: Constructible, + pub ffi_handle: OnceLock, /// The context belonging to the `ordermatch` mod: `OrdermatchContext`. pub ordermatch_ctx: Mutex>>, pub rate_limit_ctx: Mutex>>, @@ -104,10 +104,10 @@ pub struct MmCtx { pub crypto_ctx: Mutex>>, /// RIPEMD160(SHA256(x)) where x is secp256k1 pubkey derived from passphrase. /// This hash is **unique** among Iguana and each HD accounts derived from the same passphrase. - pub rmd160: Constructible, + pub rmd160: OnceLock, /// A shared DB identifier - RIPEMD160(SHA256(x)) where x is secp256k1 pubkey derived from (passphrase + magic salt). /// This hash is **the same** for Iguana and all HD accounts derived from the same passphrase. - pub shared_db_id: Constructible, + pub shared_db_id: OnceLock, /// Coins that should be enabled to kick start the interrupted swaps and orders. pub coins_needed_for_kick_start: Mutex>, /// The context belonging to the `lp_swap` mod: `SwapsContext`. @@ -115,19 +115,19 @@ pub struct MmCtx { /// The context belonging to the `lp_stats` mod: `StatsContext` pub stats_ctx: Mutex>>, /// Wallet name for this mm2 instance. Optional for backwards compatibility. - pub wallet_name: Constructible>, + pub wallet_name: OnceLock>, /// The context belonging to the `lp_wallet` mod: `WalletsContext`. #[cfg(target_arch = "wasm32")] pub wallets_ctx: Mutex>>, /// The RPC sender forwarding requests to writing part of underlying stream. #[cfg(target_arch = "wasm32")] - pub wasm_rpc: Constructible, + pub wasm_rpc: OnceLock, /// Deprecated, please use `async_sqlite_connection` for new implementations. #[cfg(not(target_arch = "wasm32"))] - pub sqlite_connection: Constructible>>, + pub sqlite_connection: OnceLock>>, /// Deprecated, please create `shared_async_sqlite_conn` for new implementations and call db `KOMODEFI-shared.db`. #[cfg(not(target_arch = "wasm32"))] - pub shared_sqlite_conn: Constructible>>, + pub shared_sqlite_conn: OnceLock>>, pub mm_version: String, pub datetime: String, pub mm_init_ctx: Mutex>>, @@ -144,7 +144,7 @@ pub struct MmCtx { pub nft_ctx: Mutex>>, /// asynchronous handle for rusqlite connection. #[cfg(not(target_arch = "wasm32"))] - pub async_sqlite_connection: Constructible>>, + pub async_sqlite_connection: OnceLock>>, /// Links the RPC context to the P2P context to handle health check responses. pub healthcheck_response_handler: AsyncMutex>>, } @@ -155,13 +155,13 @@ impl MmCtx { conf: Json::Object(json::Map::new()), log: log::LogArc::new(log), metrics: MetricsArc::new(), - initialized: Constructible::default(), - rpc_started: Constructible::default(), + initialized: OnceLock::default(), + rpc_started: OnceLock::default(), stream_channel_controller: Controller::new(), data_asker: DataAsker::default(), event_stream_configuration: None, - stop: Constructible::default(), - ffi_handle: Constructible::default(), + stop: OnceLock::default(), + ffi_handle: OnceLock::default(), ordermatch_ctx: Mutex::new(None), rate_limit_ctx: Mutex::new(None), simple_market_maker_bot_ctx: Mutex::new(None), @@ -172,20 +172,20 @@ impl MmCtx { coins_ctx: Mutex::new(None), coins_activation_ctx: Mutex::new(None), crypto_ctx: Mutex::new(None), - rmd160: Constructible::default(), - shared_db_id: Constructible::default(), + rmd160: OnceLock::default(), + shared_db_id: OnceLock::default(), coins_needed_for_kick_start: Mutex::new(HashSet::new()), swaps_ctx: Mutex::new(None), stats_ctx: Mutex::new(None), - wallet_name: Constructible::default(), + wallet_name: OnceLock::default(), #[cfg(target_arch = "wasm32")] wallets_ctx: Mutex::new(None), #[cfg(target_arch = "wasm32")] - wasm_rpc: Constructible::default(), + wasm_rpc: OnceLock::default(), #[cfg(not(target_arch = "wasm32"))] - sqlite_connection: Constructible::default(), + sqlite_connection: OnceLock::default(), #[cfg(not(target_arch = "wasm32"))] - shared_sqlite_conn: Constructible::default(), + shared_sqlite_conn: OnceLock::default(), mm_version: "".into(), datetime: "".into(), mm_init_ctx: Mutex::new(None), @@ -195,7 +195,7 @@ impl MmCtx { db_namespace: DbNamespaceId::Main, nft_ctx: Mutex::new(None), #[cfg(not(target_arch = "wasm32"))] - async_sqlite_connection: Constructible::default(), + async_sqlite_connection: OnceLock::default(), healthcheck_response_handler: AsyncMutex::new(ExpirableMap::default()), } } @@ -204,14 +204,14 @@ impl MmCtx { lazy_static! { static ref DEFAULT: H160 = [0; 20].into(); } - self.rmd160.or(&|| &*DEFAULT) + self.rmd160.get().unwrap_or(&*DEFAULT) } pub fn shared_db_id(&self) -> &H160 { lazy_static! { static ref DEFAULT: H160 = [0; 20].into(); } - self.shared_db_id.or(&|| &*DEFAULT) + self.shared_db_id.get().unwrap_or(&*DEFAULT) } #[cfg(not(target_arch = "wasm32"))] @@ -346,7 +346,7 @@ impl MmCtx { pub fn spawner(&self) -> MmFutSpawner { MmFutSpawner::new(&self.abortable_system) } /// True if the MarketMaker instance needs to stop. - pub fn is_stopping(&self) -> bool { self.stop.copy_or(false) } + pub fn is_stopping(&self) -> bool { *self.stop.get().unwrap_or(&false) } pub fn gui(&self) -> Option<&str> { self.conf["gui"].as_str() } @@ -357,7 +357,10 @@ impl MmCtx { let sqlite_file_path = self.dbdir().join("MM2.db"); log_sqlite_file_open_attempt(&sqlite_file_path); let connection = try_s!(Connection::open(sqlite_file_path)); - try_s!(self.sqlite_connection.pin(Arc::new(Mutex::new(connection)))); + try_s!(self + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already initialized".to_string())); Ok(()) } @@ -366,7 +369,10 @@ impl MmCtx { let sqlite_file_path = self.shared_dbdir().join("MM2-shared.db"); log_sqlite_file_open_attempt(&sqlite_file_path); let connection = try_s!(Connection::open(sqlite_file_path)); - try_s!(self.shared_sqlite_conn.pin(Arc::new(Mutex::new(connection)))); + try_s!(self + .shared_sqlite_conn + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already initialized".to_string())); Ok(()) } @@ -375,19 +381,23 @@ impl MmCtx { let sqlite_file_path = self.dbdir().join("KOMODEFI.db"); log_sqlite_file_open_attempt(&sqlite_file_path); let async_conn = try_s!(AsyncConnection::open(sqlite_file_path).await); - try_s!(self.async_sqlite_connection.pin(Arc::new(AsyncMutex::new(async_conn)))); + try_s!(self + .async_sqlite_connection + .set(Arc::new(AsyncMutex::new(async_conn))) + .map_err(|_| "Already initialized".to_string())); Ok(()) } #[cfg(not(target_arch = "wasm32"))] pub fn sqlite_conn_opt(&self) -> Option> { - self.sqlite_connection.as_option().map(|conn| conn.lock().unwrap()) + self.sqlite_connection.get().map(|conn| conn.lock().unwrap()) } #[cfg(not(target_arch = "wasm32"))] pub fn sqlite_connection(&self) -> MutexGuard { self.sqlite_connection - .or(&|| panic!("sqlite_connection is not initialized")) + .get() + .expect("sqlite_connection is not initialized") .lock() .unwrap() } @@ -395,7 +405,8 @@ impl MmCtx { #[cfg(not(target_arch = "wasm32"))] pub fn shared_sqlite_conn(&self) -> MutexGuard { self.shared_sqlite_conn - .or(&|| panic!("shared_sqlite_conn is not initialized")) + .get() + .expect("shared_sqlite_conn is not initialized") .lock() .unwrap() } @@ -409,7 +420,7 @@ impl Drop for MmCtx { fn drop(&mut self) { let ffi_handle = self .ffi_handle - .as_option() + .get() .map(|handle| handle.to_string()) .unwrap_or_else(|| "UNKNOWN".to_owned()); log::info!("MmCtx ({}) has been dropped", ffi_handle) @@ -512,7 +523,7 @@ impl MmArc { #[cfg(not(target_arch = "wasm32"))] try_s!(self.close_async_connection().await); - try_s!(self.stop.pin(true)); + try_s!(self.stop.set(true)); // Notify shutdown listeners. self.graceful_shutdown_registry.abort_all().warn_log(); @@ -527,7 +538,7 @@ impl MmArc { #[cfg(not(target_arch = "wasm32"))] async fn close_async_connection(&self) -> Result<(), db_common::async_sql_conn::AsyncConnError> { - if let Some(async_conn) = self.async_sqlite_connection.as_option() { + if let Some(async_conn) = self.async_sqlite_connection.get() { let mut conn = async_conn.lock().await; conn.close().await?; } @@ -560,7 +571,7 @@ impl MmArc { /// Unique context identifier, allowing us to more easily pass the context through the FFI boundaries. pub fn ffi_handle(&self) -> Result { let mut mm_ctx_ffi = try_s!(MM_CTX_FFI.lock()); - if let Some(have) = self.ffi_handle.as_option() { + if let Some(have) = self.ffi_handle.get() { return Ok(*have); } let mut tries = 0; @@ -579,7 +590,7 @@ impl MmArc { Entry::Occupied(_) => continue, // Try another ID. Entry::Vacant(ve) => { ve.insert(self.weak()); - try_s!(self.ffi_handle.pin(rid)); + try_s!(self.ffi_handle.set(rid)); return Ok(rid); }, } @@ -698,25 +709,19 @@ impl SpawnAbortable for MmFutSpawner { /// /// * `ctx_field` - A dedicated crate context field in `MmCtx`, such as the `MmCtx::portfolio_ctx`. /// * `constructor` - Generates the initial crate context. -pub fn from_ctx( - ctx_field: &Mutex>>, - constructor: C, -) -> Result, String> +pub fn from_ctx(ctx: &Mutex>>, init: F) -> Result, String> where - C: FnOnce() -> Result, T: 'static + Send + Sync, + F: FnOnce() -> Result, { - let mut ctx_field = try_s!(ctx_field.lock()); - if let Some(ref ctx) = *ctx_field { - let ctx: Arc = match ctx.clone().downcast() { - Ok(p) => p, - Err(_) => return ERR!("Error casting the context field"), - }; - return Ok(ctx); + let mut guard = try_s!(ctx.lock()); + if let Some(ctx) = guard.as_ref() { + return ctx.clone().downcast().map_err(|_| "Context type mismatch".to_string()); } - let arc = Arc::new(try_s!(constructor())); - *ctx_field = Some(arc.clone()); - Ok(arc) + + let new_ctx = Arc::new(init()?); + *guard = Some(new_ctx.clone()); + Ok(new_ctx) } #[derive(Default)] diff --git a/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs b/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs index 916854de63..4e2be2acac 100644 --- a/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs +++ b/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs @@ -118,7 +118,7 @@ impl SqliteAccountStorage { pub(crate) fn new(ctx: &MmArc) -> AccountStorageResult { let shared = ctx .sqlite_connection - .as_option() + .get() .or_mm_err(|| AccountStorageError::Internal("'MmCtx::sqlite_connection' is not initialized".to_owned()))?; Ok(SqliteAccountStorage { conn: Arc::clone(shared), diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index 8e1e91ec13..1e5f7feff0 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -476,7 +476,9 @@ pub async fn lp_init_continue(ctx: MmArc) -> MmInitResult<()> { let balance_update_ordermatch_handler = BalanceUpdateOrdermatchHandler::new(ctx.clone()); register_balance_update_handler(ctx.clone(), Box::new(balance_update_ordermatch_handler)).await; - ctx.initialized.pin(true).map_to_mm(MmInitError::Internal)?; + ctx.initialized + .set(true) + .map_to_mm(|_| MmInitError::Internal("Already Initialized".to_string()))?; // launch kickstart threads before RPC is available, this will prevent the API user to place // an order and start new swap that might get started 2 times because of kick-start diff --git a/mm2src/mm2_main/src/lp_wallet.rs b/mm2src/mm2_main/src/lp_wallet.rs index 559821a26a..20a154b28c 100644 --- a/mm2src/mm2_main/src/lp_wallet.rs +++ b/mm2src/mm2_main/src/lp_wallet.rs @@ -305,8 +305,8 @@ fn initialize_crypto_context(ctx: &MmArc, passphrase: &str) -> WalletInitResult< pub(crate) async fn initialize_wallet_passphrase(ctx: &MmArc) -> WalletInitResult<()> { let (wallet_name, passphrase) = deserialize_wallet_config(ctx)?; ctx.wallet_name - .pin(wallet_name.clone()) - .map_to_mm(WalletInitError::InternalError)?; + .set(wallet_name.clone()) + .map_to_mm(|_| WalletInitError::InternalError("Already Initialized".to_string()))?; let passphrase = process_passphrase_logic(ctx, wallet_name, passphrase).await?; if let Some(passphrase) = passphrase { @@ -541,7 +541,7 @@ pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult>` to handle the case where the wallet name is not set. // `wallet_name` can be `None` in the case of no-login mode. - let activated_wallet = ctx.wallet_name.ok_or(GetWalletsError::Internal( + let activated_wallet = ctx.wallet_name.get().ok_or(GetWalletsError::Internal( "`wallet_name` not initialized yet!".to_string(), ))?; diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs index c873b2d5ff..e779f7b86a 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs @@ -47,6 +47,7 @@ pub(super) async fn save_encrypted_passphrase( pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> WalletsStorageResult> { let wallet_name = ctx .wallet_name + .get() .ok_or(WalletsStorageError::Internal( "`wallet_name` not initialized yet!".to_string(), ))? diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs index a815bfcca1..fa66cada1c 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs @@ -126,6 +126,7 @@ pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> Walle let wallet_name = ctx .wallet_name + .get() .ok_or(WalletsDBError::Internal( "`wallet_name` not initialized yet!".to_string(), ))? diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index 600702b921..4876069e6d 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -277,7 +277,7 @@ pub fn mm2_main(version: String, datetime: String) { } if first_arg == Some("--version") || first_arg == Some("-v") || first_arg == Some("version") { - println!("AtomicDEX API: {version}"); + println!("Komodo DeFi Framework: {version}"); return; } @@ -291,7 +291,7 @@ pub fn mm2_main(version: String, datetime: String) { return; } - log!("AtomicDEX API {} DT {}", version, datetime); + log!("Komodo DeFi Framework {} DT {}", version, datetime); if let Err(err) = run_lp_main(first_arg, &|_| (), version, datetime) { log!("{}", err); diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 1ac83697af..3bf81d6370 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -1055,7 +1055,10 @@ fn test_cancel_by_single_coin() { let rx = prepare_for_cancel_by(&ctx); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); delete_my_maker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); delete_my_taker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); @@ -1074,7 +1077,10 @@ fn test_cancel_by_pair() { let rx = prepare_for_cancel_by(&ctx); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); delete_my_maker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); delete_my_taker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); @@ -1097,7 +1103,10 @@ fn test_cancel_by_all() { let rx = prepare_for_cancel_by(&ctx); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); delete_my_maker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); delete_my_taker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index 94f8d9fd0d..4e4947e151 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -410,7 +410,7 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { $port, now_sec() ); - let _ = $ctx.rpc_started.pin(true); + let _ = $ctx.rpc_started.set(true); server }); } @@ -479,7 +479,7 @@ pub fn spawn_rpc(ctx_h: u32) { use std::sync::Mutex; let ctx = MmArc::from_ffi_handle(ctx_h).expect("No context"); - if ctx.wasm_rpc.is_some() { + if ctx.wasm_rpc.get().is_some() { error!("RPC is initialized already"); return; } @@ -512,12 +512,12 @@ pub fn spawn_rpc(ctx_h: u32) { ctx.spawner().spawn(fut); // even if the [`MmCtx::wasm_rpc`] is initialized already, the spawned future above will be shutdown - if let Err(e) = ctx.wasm_rpc.pin(request_tx) { - error!("'MmCtx::wasm_rpc' is initialized already: {}", e); + if ctx.wasm_rpc.set(request_tx).is_err() { + error!("'MmCtx::wasm_rpc' is initialized already"); return; }; - if let Err(e) = ctx.rpc_started.pin(true) { - error!("'MmCtx::rpc_started' is set already: {}", e); + if ctx.rpc_started.set(true).is_err() { + error!("'MmCtx::rpc_started' is set already"); return; } diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs index 49abc5c77f..43eb715324 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -3300,5 +3300,5 @@ fn test_watcher_reward() { let watcher_reward = block_on(utxo_coin.get_maker_watcher_reward(&MmCoinEnum::UtxoCoin(utxo_coin.clone()), None, timeout)).unwrap(); - assert!(matches!(watcher_reward, None)); + assert!(watcher_reward.is_none()); } diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 115f120574..7592384696 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -241,7 +241,11 @@ pub const ETH_MAINNET_NODE: &str = "https://mainnet.infura.io/v3/c01c1b4cf666425 pub const ETH_MAINNET_CHAIN_ID: u64 = 1; pub const ETH_MAINNET_SWAP_CONTRACT: &str = "0x24abe4c71fc658c91313b6552cd40cd808b3ea80"; -pub const ETH_SEPOLIA_NODES: &[&str] = &["https://ethereum-sepolia-rpc.publicnode.com","https://rpc2.sepolia.org","https://1rpc.io/sepolia"]; +pub const ETH_SEPOLIA_NODES: &[&str] = &[ + "https://ethereum-sepolia-rpc.publicnode.com", + "https://rpc2.sepolia.org", + "https://1rpc.io/sepolia", +]; pub const ETH_SEPOLIA_CHAIN_ID: u64 = 11155111; pub const ETH_SEPOLIA_SWAP_CONTRACT: &str = "0xeA6D65434A15377081495a9E7C5893543E7c32cB"; pub const ETH_SEPOLIA_TOKEN_CONTRACT: &str = "0x09d0d71FBC00D7CCF9CFf132f5E6825C88293F19"; @@ -1129,10 +1133,16 @@ pub fn mm_ctx_with_custom_db_with_conf(conf: Option) -> MmArc { let ctx = ctx_builder.into_mm_arc(); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.shared_sqlite_conn.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .shared_sqlite_conn + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); ctx } @@ -1146,7 +1156,10 @@ pub async fn mm_ctx_with_custom_async_db() -> MmArc { let ctx = MmCtxBuilder::new().into_mm_arc(); let connection = AsyncConnection::open_in_memory().await.unwrap(); - let _ = ctx.async_sqlite_connection.pin(Arc::new(AsyncMutex::new(connection))); + let _ = ctx + .async_sqlite_connection + .set(Arc::new(AsyncMutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); ctx } @@ -1428,8 +1441,7 @@ impl MarketMakerIt { } let ctx = { - let builder = MmCtxBuilder::new() - .with_conf(conf.clone()); + let builder = MmCtxBuilder::new().with_conf(conf.clone()); let builder = if let Some(ns) = db_namespace_id { builder.with_test_db_namespace_with_id(ns) @@ -1522,7 +1534,7 @@ impl MarketMakerIt { let wasm_rpc = self .ctx .wasm_rpc - .as_option() + .get() .expect("'MmCtx::rpc' must be initialized already"); match wasm_rpc.request(payload.clone()).await { // Please note a new type of error will be introduced soon. From 0b090de5a4f7331d1dcdd254f29eec430cc2ace4 Mon Sep 17 00:00:00 2001 From: DeckerSU Date: Tue, 10 Dec 2024 21:52:00 +0100 Subject: [PATCH 10/27] chore(adex-cli): use "Komodo DeFi Framework" name in adex_cli (#2290) --- mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs b/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs index d58b035f73..1fef03bf30 100644 --- a/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs +++ b/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs @@ -107,7 +107,7 @@ impl Mm2Cfg { self.dbdir = CustomType::>::new("What is dbdir") .with_placeholder(DEFAULT_OPTION_PLACEHOLDER) - .with_help_message("AtomicDEX API database path. Optional, defaults to a subfolder named DB in the path of your mm2 binary") + .with_help_message("Komodo DeFi Framework database path. Optional, defaults to a subfolder named DB in the path of your mm2 binary") .with_validator(is_reachable_dir) .prompt() .map_err(|error| @@ -128,7 +128,7 @@ impl Mm2Cfg { fn inquire_net_id(&mut self) -> Result<()> { self.netid = CustomType::::new("What is the network `mm2` is going to be a part, netid:") .with_default(DEFAULT_NET_ID) - .with_help_message(r#"Network ID number, telling the AtomicDEX API which network to join. 8762 is the current main network, though alternative netids can be used for testing or "private" trades"#) + .with_help_message(r#"Network ID number, telling the Komodo DeFi Framework which network to join. 8762 is the current main network, though alternative netids can be used for testing or "private" trades"#) .with_placeholder(format!("{DEFAULT_NET_ID}").as_str()) .prompt() .map_err(|error| @@ -268,7 +268,7 @@ impl Mm2Cfg { .with_formatter(DEFAULT_OPTION_BOOL_FORMATTER) .with_default_value_formatter(DEFAULT_DEFAULT_OPTION_BOOL_FORMATTER) .with_default(InquireOption::None) - .with_help_message("If false the AtomicDEX API will allow rpc methods sent from external IP addresses. Optional, defaults to true. Warning: Only use this if you know what you are doing, and have put the appropriate security measures in place.") + .with_help_message("If false the Komodo DeFi Framework will allow rpc methods sent from external IP addresses. Optional, defaults to true. Warning: Only use this if you know what you are doing, and have put the appropriate security measures in place.") .prompt() .map_err(|error| error_anyhow!("Failed to get rpc_local_only: {error}") @@ -283,7 +283,7 @@ impl Mm2Cfg { .with_formatter(DEFAULT_OPTION_BOOL_FORMATTER) .with_default_value_formatter(DEFAULT_DEFAULT_OPTION_BOOL_FORMATTER) .with_default(InquireOption::None) - .with_help_message("Runs AtomicDEX API as a seed node mode (acting as a relay for AtomicDEX API clients). Optional, defaults to false. Use of this mode is not reccomended on the main network (8762) as it could result in a pubkey ban if non-compliant. on alternative testing or private networks, at least one seed node is required to relay information to other AtomicDEX API clients using the same netID.") + .with_help_message("Runs Komodo DeFi Framework as a seed node mode (acting as a relay for Komodo DeFi Framework clients). Optional, defaults to false. Use of this mode is not reccomended on the main network (8762) as it could result in a pubkey ban if non-compliant. on alternative testing or private networks, at least one seed node is required to relay information to other Komodo DeFi Framework clients using the same netID.") .prompt() .map_err(|error| error_anyhow!("Failed to get i_am_a_seed: {error}") From fe5a274f9e7581644c0d9e2e3112ad64ebc6c056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Wed, 18 Dec 2024 09:32:58 +0300 Subject: [PATCH 11/27] bump libp2p (#2296) Signed-off-by: onur-ozkan --- Cargo.lock | 44 +++++++++++++++---------------- mm2src/mm2_core/Cargo.toml | 2 +- mm2src/mm2_p2p/Cargo.toml | 4 +-- mm2src/proxy_signature/Cargo.toml | 2 +- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7332358679..8e69b25e20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3100,7 +3100,7 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libp2p" version = "0.52.1" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "bytes 1.4.0", "futures 0.3.28", @@ -3132,7 +3132,7 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3143,7 +3143,7 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3154,7 +3154,7 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "either", "fnv", @@ -3181,7 +3181,7 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "futures 0.3.28", "libp2p-core", @@ -3195,7 +3195,7 @@ dependencies = [ [[package]] name = "libp2p-floodsub" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "asynchronous-codec", "cuckoofilter", @@ -3215,7 +3215,7 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" version = "0.45.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "asynchronous-codec", "base64 0.21.7", @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "libp2p-identify" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "asynchronous-codec", "either", @@ -3286,7 +3286,7 @@ dependencies = [ [[package]] name = "libp2p-mdns" version = "0.44.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "data-encoding", "futures 0.3.28", @@ -3306,7 +3306,7 @@ dependencies = [ [[package]] name = "libp2p-metrics" version = "0.13.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "instant", "libp2p-core", @@ -3322,7 +3322,7 @@ dependencies = [ [[package]] name = "libp2p-noise" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "bytes 1.4.0", "curve25519-dalek 3.2.0", @@ -3346,7 +3346,7 @@ dependencies = [ [[package]] name = "libp2p-ping" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "either", "futures 0.3.28", @@ -3363,7 +3363,7 @@ dependencies = [ [[package]] name = "libp2p-request-response" version = "0.25.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "async-trait", "futures 0.3.28", @@ -3380,7 +3380,7 @@ dependencies = [ [[package]] name = "libp2p-swarm" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "either", "fnv", @@ -3402,7 +3402,7 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" version = "0.33.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "heck", "proc-macro-warning", @@ -3414,7 +3414,7 @@ dependencies = [ [[package]] name = "libp2p-tcp" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "futures 0.3.28", "futures-timer", @@ -3430,7 +3430,7 @@ dependencies = [ [[package]] name = "libp2p-wasm-ext" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "futures 0.3.28", "js-sys", @@ -3443,7 +3443,7 @@ dependencies = [ [[package]] name = "libp2p-websocket" version = "0.42.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "either", "futures 0.3.28", @@ -3462,7 +3462,7 @@ dependencies = [ [[package]] name = "libp2p-yamux" version = "0.44.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "either", "futures 0.3.28", @@ -4380,7 +4380,7 @@ checksum = "d8883adfde9756c1d30b0f519c9b8c502a94b41ac62f696453c37c7fc0a958ce" [[package]] name = "multistream-select" version = "0.13.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "bytes 1.4.0", "futures 0.3.28", @@ -5112,7 +5112,7 @@ dependencies = [ [[package]] name = "quick-protobuf-codec" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "asynchronous-codec", "bytes 1.4.0", @@ -5852,7 +5852,7 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rw-stream-sink" version = "0.4.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "futures 0.3.28", "pin-project", diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index 78fffea53b..365592bffe 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -18,7 +18,7 @@ futures = { version = "0.3", package = "futures", features = ["compat", "async-a gstuff = { version = "0.7", features = ["nightly"] } hex = "0.4.2" lazy_static = "1.4" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.4", default-features = false, features = ["identify"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["identify"] } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_metrics = { path = "../mm2_metrics" } diff --git a/mm2src/mm2_p2p/Cargo.toml b/mm2src/mm2_p2p/Cargo.toml index 6b7f43e7f4..80465a254d 100644 --- a/mm2src/mm2_p2p/Cargo.toml +++ b/mm2src/mm2_p2p/Cargo.toml @@ -38,13 +38,13 @@ void = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] futures-rustls = "0.24" instant = "0.1.12" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.4", default-features = false, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } tokio = { version = "1.20", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] futures-rustls = "0.22" instant = { version = "0.1.12", features = ["wasm-bindgen"] } -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.4", default-features = false, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } [dev-dependencies] async-std = "1.6.2" diff --git a/mm2src/proxy_signature/Cargo.toml b/mm2src/proxy_signature/Cargo.toml index 5392b9862a..bbad60cbb8 100644 --- a/mm2src/proxy_signature/Cargo.toml +++ b/mm2src/proxy_signature/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [dependencies] chrono = "0.4" http = "0.2" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.4", default-features = false, features = ["identify"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["identify"] } serde = "1" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } From 87be260f29a63155ea9ad0d2b9d08bdd70b019f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 23 Dec 2024 15:06:02 +0300 Subject: [PATCH 12/27] feat(p2p): ensure time synchronization in the network (#2255) * add time validation core logic Signed-off-by: onur-ozkan * nit fixes Signed-off-by: onur-ozkan * handle time gap Signed-off-by: onur-ozkan * improve logging Signed-off-by: onur-ozkan * add more trackable processing logs Signed-off-by: onur-ozkan * improve info log and remove debugging leftover Signed-off-by: onur-ozkan * rename `NetworkInfoRequest` to `PeerInfoRequest` Signed-off-by: onur-ozkan * handle recently dialed peers Signed-off-by: onur-ozkan * add useful logs Signed-off-by: onur-ozkan * create function for pre-dial check Signed-off-by: onur-ozkan * set max cap for timestamp channel Signed-off-by: onur-ozkan * remove leftover Signed-off-by: onur-ozkan * use `Multiaddr` as key Signed-off-by: onur-ozkan * fix p2p tests Signed-off-by: onur-ozkan * update logs Signed-off-by: onur-ozkan * rename leftovers Signed-off-by: onur-ozkan * update timing values Signed-off-by: onur-ozkan * minor fixes Signed-off-by: onur-ozkan * update pre dial check calls Signed-off-by: onur-ozkan * apply nit fixes Signed-off-by: onur-ozkan * don't update existing expiries Signed-off-by: onur-ozkan * revert breakage Signed-off-by: onur-ozkan --------- Signed-off-by: onur-ozkan --- Cargo.lock | 14 +- mm2src/common/Cargo.toml | 2 +- mm2src/mm2_main/src/lp_network.rs | 4 +- mm2src/mm2_main/src/lp_ordermatch.rs | 1 - mm2src/mm2_main/src/lp_stats.rs | 19 ++- mm2src/mm2_main/src/lp_swap.rs | 4 +- mm2src/mm2_p2p/Cargo.toml | 2 + .../request_response/network_info.rs | 2 + mm2src/mm2_p2p/src/behaviours/atomicdex.rs | 127 +++++++++++++++++- mm2src/mm2_p2p/src/behaviours/mod.rs | 34 ++--- .../mm2_p2p/src/behaviours/peers_exchange.rs | 11 +- 11 files changed, 178 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e69b25e20..60016ffc57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4250,6 +4250,7 @@ dependencies = [ "sha2 0.10.7", "smallvec 1.6.1", "syn 2.0.38", + "timed-map", "tokio", "void", ] @@ -5713,9 +5714,9 @@ checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustc-hex" @@ -6955,6 +6956,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "timed-map" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b102d4d896895d697f1dff4141dff28307532dac57a376b2b5665a55b280dc6" +dependencies = [ + "rustc-hash", +] + [[package]] name = "tiny-keccak" version = "1.4.4" diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index b775741cf8..e9752134db 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -37,7 +37,7 @@ parking_lot_core = { version = "0.6", features = ["nightly"] } paste = "1.0" primitive-types = "0.11.1" rand = { version = "0.7", features = ["std", "small_rng"] } -rustc-hash = "1.1.0" +rustc-hash = "2.0" regex = "1" serde = "1" serde_derive = "1" diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index b2ef53f3fb..6ed8719fa5 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -238,9 +238,11 @@ fn process_p2p_request( response_channel: mm2_libp2p::AdexResponseChannel, ) -> P2PRequestResult<()> { let request = decode_message::(&request)?; + log::debug!("Got P2PRequest {:?}", request); + let result = match request { P2PRequest::Ordermatch(req) => lp_ordermatch::process_peer_request(ctx.clone(), req), - P2PRequest::NetworkInfo(req) => lp_stats::process_info_request(ctx.clone(), req), + P2PRequest::NetworkInfo(req) => lp_stats::process_info_request(ctx.clone(), req).map(Some), }; let res = match result { diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 620cb79bfb..dba2139998 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -638,7 +638,6 @@ impl TryFromBytes for Uuid { } pub fn process_peer_request(ctx: MmArc, request: OrdermatchRequest) -> Result>, String> { - log::debug!("Got ordermatch request {:?}", request); match request { OrdermatchRequest::GetOrderbook { base, rel } => process_get_orderbook_request(ctx, base, rel), OrdermatchRequest::SyncPubkeyOrderbookState { pubkey, trie_roots } => { diff --git a/mm2src/mm2_main/src/lp_stats.rs b/mm2src/mm2_main/src/lp_stats.rs index 185996ecd1..3aedc1cb5c 100644 --- a/mm2src/mm2_main/src/lp_stats.rs +++ b/mm2src/mm2_main/src/lp_stats.rs @@ -11,6 +11,7 @@ use mm2_libp2p::application::request_response::network_info::NetworkInfoRequest; use mm2_libp2p::{encode_message, NetworkInfo, PeerId, RelayAddress, RelayAddressError}; use serde_json::{self as json, Value as Json}; use std::collections::{HashMap, HashSet}; +use std::convert::TryInto; use std::sync::Arc; use crate::lp_network::{add_reserved_peer_addresses, lp_network_ports, request_peers, NetIdError, ParseAddressError, @@ -170,16 +171,24 @@ struct Mm2VersionRes { nodes: HashMap, } -fn process_get_version_request(ctx: MmArc) -> Result>, String> { +fn process_get_version_request(ctx: MmArc) -> Result, String> { let response = ctx.mm_version().to_string(); - let encoded = try_s!(encode_message(&response)); - Ok(Some(encoded)) + encode_message(&response).map_err(|e| e.to_string()) } -pub fn process_info_request(ctx: MmArc, request: NetworkInfoRequest) -> Result>, String> { - log::debug!("Got stats request {:?}", request); +fn process_get_peer_utc_timestamp_request() -> Result, String> { + let timestamp = common::get_utc_timestamp(); + let timestamp: u64 = timestamp + .try_into() + .unwrap_or_else(|_| panic!("`common::get_utc_timestamp` returned invalid data: {}", timestamp)); + + encode_message(×tamp).map_err(|e| e.to_string()) +} + +pub fn process_info_request(ctx: MmArc, request: NetworkInfoRequest) -> Result, String> { match request { NetworkInfoRequest::GetMm2Version => process_get_version_request(ctx), + NetworkInfoRequest::GetPeerUtcTimestamp => process_get_peer_utc_timestamp_request(), } } diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 0acb7fc443..507b9a6f51 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -74,6 +74,7 @@ use derive_more::Display; use http::Response; use mm2_core::mm_ctx::{from_ctx, MmArc}; use mm2_err_handle::prelude::*; +use mm2_libp2p::behaviours::atomicdex::MAX_TIME_GAP_FOR_CONNECTED_PEER; use mm2_libp2p::{decode_signed, encode_and_sign, pub_sub_topic, PeerId, TopicPrefix}; use mm2_number::{BigDecimal, BigRational, MmNumber, MmNumberMultiRepr}; use mm2_state_machine::storable_state_machine::StateMachineStorage; @@ -155,12 +156,13 @@ pub(crate) const TAKER_SWAP_V2_TYPE: u8 = 2; 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. const NEGOTIATION_TIMEOUT_SEC: u64 = 90; +const MAX_STARTED_AT_DIFF: u64 = MAX_TIME_GAP_FOR_CONNECTED_PEER * 3; + cfg_wasm32! { use mm2_db::indexed_db::{ConstructibleDb, DbLocked}; use saved_swap::migrate_swaps_data; diff --git a/mm2src/mm2_p2p/Cargo.toml b/mm2src/mm2_p2p/Cargo.toml index 80465a254d..85efa47879 100644 --- a/mm2src/mm2_p2p/Cargo.toml +++ b/mm2src/mm2_p2p/Cargo.toml @@ -39,12 +39,14 @@ void = "1.0" futures-rustls = "0.24" instant = "0.1.12" libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } +timed-map = { version = "1.1.1", features = ["rustc-hash"] } tokio = { version = "1.20", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] futures-rustls = "0.22" instant = { version = "0.1.12", features = ["wasm-bindgen"] } libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } +timed-map = { version = "1.1.1", features = ["rustc-hash"] } [dev-dependencies] async-std = "1.6.2" diff --git a/mm2src/mm2_p2p/src/application/request_response/network_info.rs b/mm2src/mm2_p2p/src/application/request_response/network_info.rs index c8dece2ef5..4d610d932c 100644 --- a/mm2src/mm2_p2p/src/application/request_response/network_info.rs +++ b/mm2src/mm2_p2p/src/application/request_response/network_info.rs @@ -6,4 +6,6 @@ use serde::{Deserialize, Serialize}; pub enum NetworkInfoRequest { /// Get MM2 version of nodes added to stats collection GetMm2Version, + /// Get UTC timestamp in seconds from the target peer + GetPeerUtcTimestamp, } diff --git a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs index 9d58da4e1e..943ad08126 100644 --- a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs +++ b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs @@ -7,6 +7,7 @@ use futures::{channel::oneshot, use futures_rustls::rustls; use futures_ticker::Ticker; use instant::Duration; +use lazy_static::lazy_static; use libp2p::core::transport::Boxed as BoxedTransport; use libp2p::core::{ConnectedPoint, Endpoint}; use libp2p::floodsub::{Floodsub, FloodsubEvent, Topic as FloodsubTopic}; @@ -23,16 +24,20 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::iter; use std::net::IpAddr; +use std::sync::{Mutex, MutexGuard}; use std::task::{Context, Poll}; +use timed_map::{MapKind, StdClock, TimedMap}; use super::peers_exchange::{PeerAddresses, PeersExchange, PeersExchangeRequest, PeersExchangeResponse}; use super::ping::AdexPing; use super::request_response::{build_request_response_behaviour, PeerRequest, PeerResponse, RequestResponseBehaviour, RequestResponseSender}; +use crate::application::request_response::network_info::NetworkInfoRequest; +use crate::application::request_response::P2PRequest; use crate::network::{get_all_network_seednodes, DEFAULT_NETID}; use crate::relay_address::{RelayAddress, RelayAddressError}; use crate::swarm_runtime::SwarmRuntime; -use crate::{NetworkInfo, NetworkPorts, RequestResponseBehaviourEvent}; +use crate::{decode_message, encode_message, NetworkInfo, NetworkPorts, RequestResponseBehaviourEvent}; pub use libp2p::gossipsub::{Behaviour as Gossipsub, IdentTopic, MessageAuthenticity, MessageId, Topic, TopicHash}; pub use libp2p::gossipsub::{ConfigBuilder as GossipsubConfigBuilder, Event as GossipsubEvent, @@ -50,6 +55,21 @@ const ANNOUNCE_INTERVAL: Duration = Duration::from_secs(600); const ANNOUNCE_INITIAL_DELAY: Duration = Duration::from_secs(60); const CHANNEL_BUF_SIZE: usize = 1024 * 8; +/// Used in time validation logic for each peer which runs immediately after the +/// `ConnectionEstablished` event. +/// +/// Be careful when updating this value, we have some defaults (like for swaps) +/// depending on this. +pub const MAX_TIME_GAP_FOR_CONNECTED_PEER: u64 = 20; + +/// Used for storing peers in [`RECENTLY_DIALED_PEERS`]. +const DIAL_RETRY_DELAY: Duration = Duration::from_secs(60 * 5); + +lazy_static! { + /// Tracks recently dialed peers to avoid repeated connection attempts. + static ref RECENTLY_DIALED_PEERS: Mutex> = Mutex::new(TimedMap::new_with_map_kind(MapKind::FxHashMap)); +} + pub const DEPRECATED_NETID_LIST: &[u16] = &[ 7777, // TODO: keep it inaccessible until Q2 of 2024. ]; @@ -162,6 +182,24 @@ pub enum AdexBehaviourCmd { }, } +/// Determines if a dial attempt to the remote should be made. +/// +/// Returns `false` if a dial attempt to the given address has already been made, +/// in which case the caller must skip the dial attempt. +fn check_and_mark_dialed( + recently_dialed_peers: &mut MutexGuard>, + addr: &Multiaddr, +) -> bool { + if recently_dialed_peers.get(addr).is_some() { + info!("Connection attempt was already made recently to '{addr}'."); + return false; + } + + recently_dialed_peers.insert_expirable_unchecked(addr.clone(), (), DIAL_RETRY_DELAY); + + true +} + /// Returns info about directly connected peers. pub async fn get_directly_connected_peers(mut cmd_tx: AdexCmdTx) -> HashMap> { let (result_tx, rx) = oneshot::channel(); @@ -199,6 +237,46 @@ pub async fn get_relay_mesh(mut cmd_tx: AdexCmdTx) -> Vec { rx.await.expect("Tx should be present") } +async fn validate_peer_time(peer: PeerId, mut response_tx: Sender>, rp_sender: RequestResponseSender) { + let request = P2PRequest::NetworkInfo(NetworkInfoRequest::GetPeerUtcTimestamp); + let encoded_request = encode_message(&request) + .expect("Static type `PeerInfoRequest::GetPeerUtcTimestamp` should never fail in serialization."); + + match request_one_peer(peer, encoded_request, rp_sender).await { + PeerResponse::Ok { res } => { + if let Ok(timestamp) = decode_message::(&res) { + let now = common::get_utc_timestamp(); + let now: u64 = now + .try_into() + .unwrap_or_else(|_| panic!("`common::get_utc_timestamp` returned invalid data: {}", now)); + + let diff = now.abs_diff(timestamp); + + // If time diff is in the acceptable gap, end the validation here. + if diff <= MAX_TIME_GAP_FOR_CONNECTED_PEER { + debug!( + "Peer '{peer}' is within the acceptable time gap ({MAX_TIME_GAP_FOR_CONNECTED_PEER} seconds); time difference is {diff} seconds." + ); + response_tx.send(None).await.unwrap(); + return; + } + }; + }, + other => { + error!("Unexpected response `{other:?}` from peer `{peer}`"); + // TODO: Ideally, we should send `Some(peer)` to end the connection, + // but we don't want to cause a breaking change yet. + response_tx.send(None).await.unwrap(); + return; + }, + } + + // If the function reaches this point, this means validation has failed. + // Send the peer ID to disconnect from it. + error!("Failed to validate the time for peer `{peer}`; disconnecting."); + response_tx.send(Some(peer)).await.unwrap(); +} + async fn request_one_peer(peer: PeerId, req: Vec, mut request_response_tx: RequestResponseSender) -> PeerResponse { // Use the internal receiver to receive a response to this request. let (internal_response_tx, internal_response_rx) = oneshot::channel(); @@ -711,12 +789,18 @@ fn start_gossipsub( _ => (), } + let mut recently_dialed_peers = RECENTLY_DIALED_PEERS.lock().unwrap(); for relay in bootstrap.choose_multiple(&mut rng, mesh_n) { + if !check_and_mark_dialed(&mut recently_dialed_peers, relay) { + continue; + } + match libp2p::Swarm::dial(&mut swarm, relay.clone()) { Ok(_) => info!("Dialed {}", relay), Err(e) => error!("Dial {:?} failed: {:?}", relay, e), } } + drop(recently_dialed_peers); let mut check_connected_relays_interval = Ticker::new_with_next(CONNECTED_RELAYS_CHECK_INTERVAL, CONNECTED_RELAYS_CHECK_INTERVAL); @@ -724,6 +808,7 @@ fn start_gossipsub( let mut announce_interval = Ticker::new_with_next(ANNOUNCE_INTERVAL, ANNOUNCE_INITIAL_DELAY); let mut listening = false; + let (timestamp_tx, mut timestamp_rx) = futures::channel::mpsc::channel(mesh_n_high); let polling_fut = poll_fn(move |cx: &mut Context| { loop { match swarm.behaviour_mut().cmd_rx.poll_next_unpin(cx) { @@ -733,11 +818,27 @@ fn start_gossipsub( } } + while let Poll::Ready(Some(Some(peer_id))) = timestamp_rx.poll_next_unpin(cx) { + if swarm.disconnect_peer_id(peer_id).is_err() { + error!("Disconnection from `{peer_id}` failed unexpectedly, which should never happen."); + } + } + loop { match swarm.poll_next_unpin(cx) { Poll::Ready(Some(event)) => { debug!("Swarm event {:?}", event); + if let SwarmEvent::ConnectionEstablished { peer_id, .. } = &event { + info!("Validating time data for peer `{peer_id}`."); + let future = validate_peer_time( + *peer_id, + timestamp_tx.clone(), + swarm.behaviour().core.request_response.sender(), + ); + swarm.behaviour().spawn(future); + } + if let SwarmEvent::Behaviour(event) = event { if swarm.behaviour_mut().netid != DEFAULT_NETID { if let AdexBehaviourEvent::Floodsub(FloodsubEvent::Message(message)) = &event { @@ -798,19 +899,29 @@ fn maintain_connection_to_relays(swarm: &mut AtomicDexSwarm, bootstrap_addresses let mut rng = rand::thread_rng(); if connected_relays.len() < mesh_n_low { + let mut recently_dialed_peers = RECENTLY_DIALED_PEERS.lock().unwrap(); let to_connect_num = mesh_n - connected_relays.len(); - let to_connect = swarm - .behaviour_mut() - .core - .peers_exchange - .get_random_peers(to_connect_num, |peer| !connected_relays.contains(peer)); + let to_connect = + swarm + .behaviour_mut() + .core + .peers_exchange + .get_random_peers(to_connect_num, |peer, addresses| { + !connected_relays.contains(peer) + && addresses + .iter() + .any(|addr| check_and_mark_dialed(&mut recently_dialed_peers, addr)) + }); // choose some random bootstrap addresses to connect if peers exchange returned not enough peers if to_connect.len() < to_connect_num { let connect_bootstrap_num = to_connect_num - to_connect.len(); for addr in bootstrap_addresses .iter() - .filter(|addr| !swarm.behaviour().core.gossipsub.is_connected_to_addr(addr)) + .filter(|addr| { + !swarm.behaviour().core.gossipsub.is_connected_to_addr(addr) + && check_and_mark_dialed(&mut recently_dialed_peers, addr) + }) .collect::>() .choose_multiple(&mut rng, connect_bootstrap_num) { @@ -824,11 +935,13 @@ fn maintain_connection_to_relays(swarm: &mut AtomicDexSwarm, bootstrap_addresses if swarm.behaviour().core.gossipsub.is_connected_to_addr(&addr) { continue; } + if let Err(e) = libp2p::Swarm::dial(swarm, addr.clone()) { error!("Peer {} address {} dial error {}", peer, addr, e); } } } + drop(recently_dialed_peers); } if connected_relays.len() > max_n { diff --git a/mm2src/mm2_p2p/src/behaviours/mod.rs b/mm2src/mm2_p2p/src/behaviours/mod.rs index cdfda38c8d..36436efe4e 100644 --- a/mm2src/mm2_p2p/src/behaviours/mod.rs +++ b/mm2src/mm2_p2p/src/behaviours/mod.rs @@ -106,17 +106,16 @@ mod tests { let node1_port = next_port(); let node1 = Node::spawn(node1_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; request_received_cpy.store(true, Ordering::Relaxed); - assert_eq!(request, b"test request"); let res = AdexResponse::Ok { response: b"test response".to_vec(), @@ -157,19 +156,17 @@ mod tests { impl RequestHandler { fn handle(&mut self, mut cmd_tx: mpsc::Sender, event: AdexBehaviourEvent) { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; self.requests += 1; - assert_eq!(request, b"test request"); - // the first time we should respond the none if self.requests == 1 { let res = AdexResponse::None; @@ -249,17 +246,16 @@ mod tests { let node1_port = next_port(); let _node1 = Node::spawn(node1_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; request_received_cpy.store(true, Ordering::Relaxed); - assert_eq!(request, b"test request"); let res = AdexResponse::None; cmd_tx @@ -293,17 +289,15 @@ mod tests { let receiver1_port = next_port(); let receiver1 = Node::spawn(receiver1_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; - assert_eq!(request, b"test request"); - let res = AdexResponse::None; cmd_tx .try_send(AdexBehaviourCmd::SendResponse { res, response_channel }) @@ -313,17 +307,15 @@ mod tests { let receiver2_port = next_port(); let receiver2 = Node::spawn(receiver2_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; - assert_eq!(request, b"test request"); - let res = AdexResponse::Err { error: "test error".into(), }; @@ -335,17 +327,15 @@ mod tests { let receiver3_port = next_port(); let receiver3 = Node::spawn(receiver3_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; - assert_eq!(request, b"test request"); - let res = AdexResponse::Ok { response: b"test response".to_vec(), }; diff --git a/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs b/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs index 412fa16355..1bede91995 100644 --- a/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs +++ b/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs @@ -330,11 +330,18 @@ impl PeersExchange { pub fn get_random_peers( &mut self, num: usize, - mut filter: impl FnMut(&PeerId) -> bool, + mut filter: impl FnMut(&PeerId, HashSet) -> bool, ) -> HashMap { let mut result = HashMap::with_capacity(num); let mut rng = rand::thread_rng(); - let peer_ids = self.known_peers.iter().filter(|peer| filter(peer)).collect::>(); + let peer_ids = self + .known_peers + .iter() + .filter(|peer| { + let addresses = self.request_response.addresses_of_peer(peer).into_iter().collect(); + filter(peer, addresses) + }) + .collect::>(); for peer_id in peer_ids.choose_multiple(&mut rng, num) { let addresses = self.request_response.addresses_of_peer(peer_id).into_iter().collect(); From 72bc73ec955de74e0687af44d0ca080e358c2821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Tue, 24 Dec 2024 10:24:42 +0300 Subject: [PATCH 13/27] chore(p2p): follow-up nits (#2302) * handle p2p features properly Signed-off-by: onur-ozkan * use `PeerId` type for timestamp channel straight away Signed-off-by: onur-ozkan --------- Signed-off-by: onur-ozkan --- mm2src/coins/Cargo.toml | 2 +- mm2src/mm2_main/Cargo.toml | 2 +- mm2src/mm2_p2p/Cargo.toml | 2 +- mm2src/mm2_p2p/src/behaviours/atomicdex.rs | 10 ++++------ 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 290a0dd7f5..49616d1fa4 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -69,7 +69,7 @@ mm2_io = { path = "../mm2_io" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number"} -mm2_p2p = { path = "../mm2_p2p" } +mm2_p2p = { path = "../mm2_p2p", default-features = false } mm2_rpc = { path = "../mm2_rpc" } mm2_state_machine = { path = "../mm2_state_machine" } mocktopus = "0.8.0" diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 13d53b8eea..22bb3e705b 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -65,7 +65,7 @@ mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_gui_storage = { path = "../mm2_gui_storage" } mm2_io = { path = "../mm2_io" } -mm2_libp2p = { path = "../mm2_p2p", package = "mm2_p2p", features = ["application"] } +mm2_libp2p = { path = "../mm2_p2p", package = "mm2_p2p" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } diff --git a/mm2src/mm2_p2p/Cargo.toml b/mm2src/mm2_p2p/Cargo.toml index 85efa47879..41a82c3c16 100644 --- a/mm2src/mm2_p2p/Cargo.toml +++ b/mm2src/mm2_p2p/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [features] -default = [] +default = ["application"] application = ["dep:mm2_number"] [lib] diff --git a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs index 943ad08126..6d3ccb9d69 100644 --- a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs +++ b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs @@ -237,7 +237,7 @@ pub async fn get_relay_mesh(mut cmd_tx: AdexCmdTx) -> Vec { rx.await.expect("Tx should be present") } -async fn validate_peer_time(peer: PeerId, mut response_tx: Sender>, rp_sender: RequestResponseSender) { +async fn validate_peer_time(peer: PeerId, mut response_tx: Sender, rp_sender: RequestResponseSender) { let request = P2PRequest::NetworkInfo(NetworkInfoRequest::GetPeerUtcTimestamp); let encoded_request = encode_message(&request) .expect("Static type `PeerInfoRequest::GetPeerUtcTimestamp` should never fail in serialization."); @@ -257,16 +257,14 @@ async fn validate_peer_time(peer: PeerId, mut response_tx: Sender debug!( "Peer '{peer}' is within the acceptable time gap ({MAX_TIME_GAP_FOR_CONNECTED_PEER} seconds); time difference is {diff} seconds." ); - response_tx.send(None).await.unwrap(); return; } }; }, other => { error!("Unexpected response `{other:?}` from peer `{peer}`"); - // TODO: Ideally, we should send `Some(peer)` to end the connection, + // TODO: Ideally, we should send `peer` to end the connection, // but we don't want to cause a breaking change yet. - response_tx.send(None).await.unwrap(); return; }, } @@ -274,7 +272,7 @@ async fn validate_peer_time(peer: PeerId, mut response_tx: Sender // If the function reaches this point, this means validation has failed. // Send the peer ID to disconnect from it. error!("Failed to validate the time for peer `{peer}`; disconnecting."); - response_tx.send(Some(peer)).await.unwrap(); + response_tx.send(peer).await.unwrap(); } async fn request_one_peer(peer: PeerId, req: Vec, mut request_response_tx: RequestResponseSender) -> PeerResponse { @@ -818,7 +816,7 @@ fn start_gossipsub( } } - while let Poll::Ready(Some(Some(peer_id))) = timestamp_rx.poll_next_unpin(cx) { + while let Poll::Ready(Some(peer_id)) = timestamp_rx.poll_next_unpin(cx) { if swarm.disconnect_peer_id(peer_id).is_err() { error!("Disconnection from `{peer_id}` failed unexpectedly, which should never happen."); } From ea5f3076084e568f528e6927f5aa9f6fb0b03435 Mon Sep 17 00:00:00 2001 From: smk762 <35845239+smk762@users.noreply.github.com> Date: Tue, 24 Dec 2024 15:25:52 +0800 Subject: [PATCH 14/27] chore(ci): Update docker build for wasm (#2294) * update wasm build for docker * lock wasm-pack version on build runners Signed-off-by: onur-ozkan --------- Signed-off-by: onur-ozkan Co-authored-by: onur-ozkan --- .docker/Dockerfile | 11 ++++++----- .github/workflows/dev-build.yml | 2 +- .github/workflows/release-build.yml | 2 +- .github/workflows/test.yml | 3 +++ docs/WASM_BUILD.md | 19 ++++++++++++++++++- 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 8156a6934b..677d5f8152 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -1,6 +1,6 @@ FROM docker.io/debian:10 -MAINTAINER Onur Özkan +LABEL authors="Onur Özkan " RUN apt-get update -y @@ -11,6 +11,7 @@ RUN apt-get install -y \ curl \ wget \ unzip \ + libudev-dev \ gnupg RUN ln -s /usr/bin/python3 /bin/python @@ -49,8 +50,8 @@ RUN apt-get install -y \ docker-buildx-plugin RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --profile minimal --default-toolchain nightly-2023-06-01 -y - -RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.1/protoc-3.20.1-linux-x86_64.zip -RUN unzip protoc-3.20.1-linux-x86_64.zip && mv ./include/google /usr/include/google - ENV PATH="/root/.cargo/bin:$PATH" +RUN cargo install wasm-pack --version 0.10.3 +RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v25.3/protoc-25.3-linux-x86_64.zip +RUN unzip protoc-25.3-linux-x86_64.zip && mv ./include/google /usr/include/google + diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index a4fce83cbe..534b29b068 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -368,7 +368,7 @@ jobs: rustup target add wasm32-unknown-unknown - name: Install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | bash -s -- -y + run: CARGO_HOME=/root/.cargo cargo install wasm-pack --version 0.10.3 - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index a74a589d10..d78f04c593 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -334,7 +334,7 @@ jobs: rustup target add wasm32-unknown-unknown - name: Install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | bash -s -- -y + run: CARGO_HOME=/root/.cargo cargo install wasm-pack --version 0.10.3 - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12a60bbc3c..5c9aa8d96e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -245,6 +245,9 @@ jobs: deps: ('protoc') - name: Install wasm-pack + # Use the latest wasm-pack for up-to-date compatibility coverage on KDF. + # As we don't share any build artifacts from this pipeline, we don't need + # to lock the version here. run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Download geckodriver diff --git a/docs/WASM_BUILD.md b/docs/WASM_BUILD.md index eb62fa7731..718b8d5bd1 100644 --- a/docs/WASM_BUILD.md +++ b/docs/WASM_BUILD.md @@ -1,5 +1,20 @@ # Building WASM binary +## From Container: + +If you want to build from source without installing prerequisites to your host system, you can do so by binding the source code inside a container and compiling it there. + +Build the image: + +```sh +docker build -t kdf-build-container -f .docker/Dockerfile . +``` + +Bind source code into container and compile it: +```sh +docker run -v "$(pwd)":/app -w /app kdf-build-container wasm-pack build mm2src/mm2_bin_lib --target web --out-dir wasm_build/deps/pkg/ +``` + ## Setting up the environment To build WASM binary from source, the following prerequisites are required: @@ -39,4 +54,6 @@ If you want to disable optimizations to reduce the compilation time, run `wasm-p wasm-pack build mm2src/mm2_bin_lib --target web --out-dir wasm_build/deps/pkg/ --dev ``` -Please don't forget to specify `CC` and `AR` if you run the command on OSX. \ No newline at end of file +Please don't forget to specify `CC` and `AR` if you run the command on OSX. + + From c5f544ce7b9cf7eeb09a5d8e7b45a65cae4e3fc7 Mon Sep 17 00:00:00 2001 From: Alina Sharon <52405288+laruh@users.noreply.github.com> Date: Wed, 25 Dec 2024 15:09:30 +0700 Subject: [PATCH 15/27] fix(wasm): add test-ext-api feature to mm2_main and mm2_bin_lib tomls (#2295) * add feature flag to mm2_main and mm2_bin_lib tomls * filter 'status:' pr labels * remove old labels * dont allow to pass blocked and invalid statuses * pass in progress and pending review statuses * update statuses list --- .github/workflows/pr-lint.yml | 6 ++++-- mm2src/mm2_bin_lib/Cargo.toml | 1 + mm2src/mm2_main/Cargo.toml | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 704c614549..6ce7dedd0a 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -48,7 +48,9 @@ jobs: - name: Check PR labels env: LABEL_NAMES: ${{ toJson(github.event.pull_request.labels.*.name) }} - if: contains(env.LABEL_NAMES, 'under review') == contains(env.LABEL_NAMES, 'in progress') + if: "!((contains(env.LABEL_NAMES, 'pending review') && !contains(env.LABEL_NAMES, 'in progress') && !contains(env.LABEL_NAMES, 'blocked')) + || (!contains(env.LABEL_NAMES, 'pending review') && contains(env.LABEL_NAMES, 'in progress') && !contains(env.LABEL_NAMES, 'blocked')) + || (!contains(env.LABEL_NAMES, 'pending review') && !contains(env.LABEL_NAMES, 'in progress') && contains(env.LABEL_NAMES, 'blocked')))" run: | - echo "PR must have "exactly one" of these labels: ['under review', 'in progress']." + echo "PR must have "exactly one" of these labels: ['status: pending review', 'status: in progress', 'status: blocked']." exit 1 diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index bf95f316e4..3019fb6add 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -15,6 +15,7 @@ custom-swap-locktime = ["mm2_main/custom-swap-locktime"] # only for testing purp native = ["mm2_main/native"] # Deprecated track-ctx-pointer = ["mm2_main/track-ctx-pointer"] zhtlc-native-tests = ["mm2_main/zhtlc-native-tests"] +test-ext-api = ["mm2_main/test-ext-api"] [[bin]] name = "mm2" diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 22bb3e705b..b80377a822 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -24,6 +24,7 @@ run-device-tests = [] enable-sia = ["coins/enable-sia", "coins_activation/enable-sia"] sepolia-maker-swap-v2-tests = [] sepolia-taker-swap-v2-tests = [] +test-ext-api = ["trading_api/test-ext-api"] [dependencies] async-std = { version = "1.5", features = ["unstable"] } From 7c4bc27d678587a9d3d635ede6b43b408ff32941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 26 Dec 2024 09:58:27 +0300 Subject: [PATCH 16/27] improvement(CI): switch to proper rust caching (#2303) * switch to proper rust caching Signed-off-by: onur-ozkan * fix linting Signed-off-by: onur-ozkan * rename cargo-cache to build-cache Signed-off-by: onur-ozkan * try caching Signed-off-by: onur-ozkan * switch to rust-cache Signed-off-by: onur-ozkan --------- Signed-off-by: onur-ozkan --- .github/actions/build-cache/action.yml | 10 ++++++ .github/actions/cargo-cache/action.yml | 20 ----------- .github/workflows/dev-build.yml | 36 +++++++++---------- .github/workflows/fmt-and-lint.yml | 8 ++--- .github/workflows/release-build.yml | 36 +++++++++---------- .github/workflows/test.yml | 32 ++++++++--------- mm2src/coins/eth/eth_tests.rs | 2 +- mm2src/trading_api/src/one_inch_api/client.rs | 2 +- mm2src/trading_api/src/one_inch_api/errors.rs | 2 +- 9 files changed, 69 insertions(+), 79 deletions(-) create mode 100644 .github/actions/build-cache/action.yml delete mode 100644 .github/actions/cargo-cache/action.yml diff --git a/.github/actions/build-cache/action.yml b/.github/actions/build-cache/action.yml new file mode 100644 index 0000000000..c5af862e1b --- /dev/null +++ b/.github/actions/build-cache/action.yml @@ -0,0 +1,10 @@ +name: 'Set up build cache' +description: 'Sets up caching for KDF builds' +runs: + using: 'composite' + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up rust-cache + uses: Swatinem/rust-cache@v2 diff --git a/.github/actions/cargo-cache/action.yml b/.github/actions/cargo-cache/action.yml deleted file mode 100644 index 89069e7797..0000000000 --- a/.github/actions/cargo-cache/action.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: 'Set up cargo cache' -description: 'Sets up the cargo cache for the workflow' -runs: - using: 'composite' - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up cargo cache - uses: actions/cache@v3 - continue-on-error: false - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 534b29b068..ccc959509d 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -46,8 +46,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -119,8 +119,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -180,8 +180,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -240,8 +240,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -303,8 +303,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -378,8 +378,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -430,8 +430,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -505,8 +505,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -582,8 +582,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | diff --git a/.github/workflows/fmt-and-lint.yml b/.github/workflows/fmt-and-lint.yml index f5ea217eee..b5212320dd 100644 --- a/.github/workflows/fmt-and-lint.yml +++ b/.github/workflows/fmt-and-lint.yml @@ -26,8 +26,8 @@ jobs: with: deps: ('protoc' 'libudev-dev') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: fmt check # Format checks aren't OS dependant. @@ -54,8 +54,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: clippy lint run: cargo clippy --target wasm32-unknown-unknown -- --D warnings diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index d78f04c593..9cf66098bb 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -46,8 +46,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -110,8 +110,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -165,8 +165,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -219,8 +219,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -275,8 +275,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -344,8 +344,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -393,8 +393,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -462,8 +462,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -533,8 +533,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c9aa8d96e..53debcb4b2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,8 +33,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -61,8 +61,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -89,8 +89,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -117,8 +117,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -149,8 +149,8 @@ jobs: - name: Set loopback address run: ./scripts/ci/lo0_config.sh - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -178,8 +178,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Download wget64 uses: ./.github/actions/download-and-verify @@ -214,8 +214,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -264,8 +264,8 @@ jobs: sudo tar -xzvf geckodriver-v0.32.2-linux64.tar.gz -C /bin sudo chmod +x /bin/geckodriver - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: WASM_BINDGEN_TEST_TIMEOUT=480 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index e799ba43d3..a02aa9eaf8 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1045,5 +1045,5 @@ fn test_gas_limit_conf() { fn test_h256_to_str() { let h = H256::from_str("5136701f11060010841c9708c3eb26f6606a070b8ae43f4b98b6d7b10a545258").unwrap(); let b: BytesJson = h.0.to_vec().into(); - println!("H256={}", format!("0x{:02x}", b)); + println!("H256=0x{:02x}", b); } diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs index 9c7136148a..ef3c61ef6b 100644 --- a/mm2src/trading_api/src/one_inch_api/client.rs +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -78,7 +78,7 @@ impl<'a> UrlBuilder<'a> { let url = self .base_url .join(self.endpoint)? - .join(&format!("{}/", self.chain_id.to_string()))? + .join(&format!("{}/", self.chain_id))? .join(self.method_name.as_str())?; Ok(Url::parse_with_params( url.as_str(), diff --git a/mm2src/trading_api/src/one_inch_api/errors.rs b/mm2src/trading_api/src/one_inch_api/errors.rs index d92f8e144b..70264a9b89 100644 --- a/mm2src/trading_api/src/one_inch_api/errors.rs +++ b/mm2src/trading_api/src/one_inch_api/errors.rs @@ -76,7 +76,7 @@ impl NativeError { match serde_json::from_value(body) { Ok(err) => Self::HttpError400(err), Err(err) => Self::ParseError { - error_msg: format!("could not parse error response: {}", err.to_string()), + error_msg: format!("could not parse error response: {}", err), }, } } else { From ae5c6c1ea62b74bfde4abdc6683b1aab854ea3f2 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Thu, 26 Dec 2024 12:29:54 +0200 Subject: [PATCH 17/27] chore(eth-websocket): remove some unnecessary wrappers (#2291) * remove unnecessary Arc< The inners are already `Arc<`ed * eth websocket: avoid locking control message sender and clone it instead * review(sami): move tx declaration close to where it's being used * review(sami): free rx end out of the Arc and arc the entire controller channel instead --- .../eth/web3_transport/websocket_transport.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 6d19573781..36b13bdb78 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -52,8 +52,8 @@ pub struct WebsocketTransport { #[derive(Debug)] struct ControllerChannel { - tx: Arc>>, - rx: Arc>>, + tx: UnboundedSender, + rx: AsyncMutex>, } enum ControllerMessage { @@ -86,11 +86,10 @@ impl WebsocketTransport { node, event_handlers, request_id: Arc::new(AtomicUsize::new(1)), - controller_channel: ControllerChannel { - tx: Arc::new(AsyncMutex::new(req_tx)), - rx: Arc::new(AsyncMutex::new(req_rx)), - } - .into(), + controller_channel: Arc::new(ControllerChannel { + tx: req_tx, + rx: AsyncMutex::new(req_rx), + }), connection_guard: Arc::new(AsyncMutex::new(())), proxy_sign_keypair: None, last_request_failed: Arc::new(AtomicBool::new(false)), @@ -298,7 +297,7 @@ impl WebsocketTransport { } pub(crate) async fn stop_connection_loop(&self) { - let mut tx = self.controller_channel.tx.lock().await; + let mut tx = self.controller_channel.tx.clone(); tx.send(ControllerMessage::Close) .await .expect("receiver channel must be alive"); @@ -357,12 +356,11 @@ async fn send_request( serialized_request = serde_json::to_string(&wrapper)?; } - let mut tx = transport.controller_channel.tx.lock().await; - let (notification_sender, notification_receiver) = oneshot::channel::>(); event_handlers.on_outgoing_request(&request_bytes); + let mut tx = transport.controller_channel.tx.clone(); tx.send(ControllerMessage::Request(WsRequest { request_id, serialized_request, From 10711a2062efa3513afbfab76dacee233e105e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 26 Dec 2024 14:14:55 +0300 Subject: [PATCH 18/27] replace broken rpc link (#2305) Signed-off-by: onur-ozkan --- mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs index 9224ea997f..4a5d13c05f 100644 --- a/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs +++ b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs @@ -138,7 +138,7 @@ mod tests { #[wasm_bindgen_test] async fn test_get_abci_info() { - let client = HttpClient::new("https://rpc.sentry-02.theta-testnet.polypore.xyz", None).unwrap(); + let client = HttpClient::new("http://34.80.202.172:26657", None).unwrap(); client.abci_info().await.unwrap(); } } From d90170b1b71fb8a727e8d5c3e67d837ef2470b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 26 Dec 2024 14:22:30 +0300 Subject: [PATCH 19/27] add `wasm` feature on WASM for timed-map (#2306) Signed-off-by: onur-ozkan --- mm2src/mm2_p2p/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/mm2_p2p/Cargo.toml b/mm2src/mm2_p2p/Cargo.toml index 41a82c3c16..3fc156b6ca 100644 --- a/mm2src/mm2_p2p/Cargo.toml +++ b/mm2src/mm2_p2p/Cargo.toml @@ -46,7 +46,7 @@ tokio = { version = "1.20", default-features = false } futures-rustls = "0.22" instant = { version = "0.1.12", features = ["wasm-bindgen"] } libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } -timed-map = { version = "1.1.1", features = ["rustc-hash"] } +timed-map = { version = "1.1.1", features = ["rustc-hash", "wasm"] } [dev-dependencies] async-std = "1.6.2" From 7464be19ce70abf5c45a0477907ab9bb75e401a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 26 Dec 2024 19:48:42 +0300 Subject: [PATCH 20/27] chore(CI): unlock wasm-pack version (#2307) * unlock wasm-pack version Signed-off-by: onur-ozkan * force yes on docker build Signed-off-by: onur-ozkan --------- Signed-off-by: onur-ozkan --- .docker/Dockerfile | 3 ++- .github/workflows/dev-build.yml | 3 ++- .github/workflows/release-build.yml | 3 ++- docs/WASM_BUILD.md | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 677d5f8152..467dc4ebc4 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -51,7 +51,8 @@ RUN apt-get install -y \ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --profile minimal --default-toolchain nightly-2023-06-01 -y ENV PATH="/root/.cargo/bin:$PATH" -RUN cargo install wasm-pack --version 0.10.3 +# TODO: Lock wasm-pack version +RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | bash -s -- -y RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v25.3/protoc-25.3-linux-x86_64.zip RUN unzip protoc-25.3-linux-x86_64.zip && mv ./include/google /usr/include/google diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index ccc959509d..8877bb518f 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -368,7 +368,8 @@ jobs: rustup target add wasm32-unknown-unknown - name: Install wasm-pack - run: CARGO_HOME=/root/.cargo cargo install wasm-pack --version 0.10.3 + # TODO: Lock wasm-pack version + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 9cf66098bb..cae080b3d9 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -334,7 +334,8 @@ jobs: rustup target add wasm32-unknown-unknown - name: Install wasm-pack - run: CARGO_HOME=/root/.cargo cargo install wasm-pack --version 0.10.3 + # TODO: Lock wasm-pack version + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' diff --git a/docs/WASM_BUILD.md b/docs/WASM_BUILD.md index 718b8d5bd1..28355bbf80 100644 --- a/docs/WASM_BUILD.md +++ b/docs/WASM_BUILD.md @@ -21,7 +21,7 @@ To build WASM binary from source, the following prerequisites are required: 1. Install `wasm-pack` ``` - cargo install wasm-pack + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh ``` 2. OSX specific: install `llvm` ``` From 075c55b61cbd606cdd6b160110eeda8c0fa67e58 Mon Sep 17 00:00:00 2001 From: Alina Sharon <52405288+laruh@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:05:25 +0700 Subject: [PATCH 21/27] update Cargo.lock (#2308) --- Cargo.lock | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 60016ffc57..1e0790f40f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6963,6 +6963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b102d4d896895d697f1dff4141dff28307532dac57a376b2b5665a55b280dc6" dependencies = [ "rustc-hash", + "web-time", ] [[package]] @@ -7784,6 +7785,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web3" version = "0.19.0" From c191e8dd34cacc4c032d9e3443d92c74f5576ade Mon Sep 17 00:00:00 2001 From: dimxy Date: Mon, 6 Jan 2025 11:05:39 +0500 Subject: [PATCH 22/27] fix mm2_p2p dev build (#2311) --- mm2src/coins/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 49616d1fa4..a706d9d22c 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -160,6 +160,7 @@ winapi = "0.3" [dev-dependencies] mm2_test_helpers = { path = "../mm2_test_helpers" } +mm2_p2p = { path = "../mm2_p2p", features = ["application"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wagyu-zcash-parameters = { version = "0.2" } From 1eb4bf7a4f6c15b36710a8fa9d5a5f190c0aee99 Mon Sep 17 00:00:00 2001 From: dimxy Date: Tue, 7 Jan 2025 13:07:21 +0500 Subject: [PATCH 23/27] test(P2P): add test for peer time sync validation (#2304) * fix peer response error log (only if error) * add test for peer time difference validation * fix linux clippy * Revert "fix linux clippy" This reverts commit 2bfe34cade88e19cf8576b9feb3118620384c664. * Revert "add test for peer time difference validation" This reverts commit c8d1165eb0347133cf41603e91adf9d3876741b6. * Revert "fix peer response error log (only if error)" This reverts commit b6d6fbc2d1ecb7fe3fca6ba8f7a29196726586d7. * add docker test for peer time sync validation * remove unneeded test println * use Mm2TestConf::seednode * review(borngraced): refactor get_utc_timestamp with test code * use const in peer time sync test --- mm2src/common/Cargo.toml | 1 + mm2src/common/common.rs | 12 ++- mm2src/mm2_main/Cargo.toml | 1 + .../tests/docker_tests/docker_tests_inner.rs | 81 +++++++++++++++++++ mm2src/mm2_p2p/Cargo.toml | 1 + 5 files changed, 95 insertions(+), 1 deletion(-) diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index e9752134db..7f5b4803ed 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -10,6 +10,7 @@ path = "common.rs" doctest = false [features] +for-tests = [] track-ctx-pointer = ["shared_ref_counter/enable", "shared_ref_counter/log"] [dependencies] diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 288882d0ae..117a84bff9 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -1080,7 +1080,17 @@ impl Default for PagingOptionsEnum { } #[inline(always)] -pub fn get_utc_timestamp() -> i64 { Utc::now().timestamp() } +pub fn get_utc_timestamp() -> i64 { + // get_utc_timestamp for tests allowing to add some bias to 'now' + #[cfg(feature = "for-tests")] + return Utc::now().timestamp() + + std::env::var("TEST_TIMESTAMP_OFFSET") + .map(|s| s.as_str().parse::().unwrap_or_default()) + .unwrap_or_default(); + + #[cfg(not(feature = "for-tests"))] + return Utc::now().timestamp(); +} #[inline(always)] pub fn get_utc_timestamp_nanos() -> i64 { Utc::now().timestamp_nanos() } diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index b80377a822..27f937cf6f 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -125,6 +125,7 @@ winapi = "0.3" [dev-dependencies] coins = { path = "../coins", features = ["for-tests"] } coins_activation = { path = "../coins_activation", features = ["for-tests"] } +common = { path = "../common", features = ["for-tests"] } mm2_test_helpers = { path = "../mm2_test_helpers" } trading_api = { path = "../trading_api", features = ["mocktopus"] } mocktopus = "0.8.0" 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 2a253760bc..b4f074857f 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -16,6 +16,7 @@ use common::{block_on, block_on_f01, executor::Timer, get_utc_timestamp, now_sec use crypto::privkey::key_pair_from_seed; use crypto::{CryptoCtx, DerivationPath, KeyPairPolicy}; use http::StatusCode; +use mm2_libp2p::behaviours::atomicdex::MAX_TIME_GAP_FOR_CONNECTED_PEER; use mm2_number::{BigDecimal, BigRational, MmNumber}; use mm2_test_helpers::for_tests::{check_my_swap_status_amounts, disable_coin, disable_coin_err, enable_eth_coin, enable_eth_with_tokens_v2, erc20_dev_conf, eth_dev_conf, get_locked_amount, @@ -25,6 +26,7 @@ use mm2_test_helpers::for_tests::{check_my_swap_status_amounts, disable_coin, di use mm2_test_helpers::{get_passphrase, structs::*}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; +use std::convert::TryInto; use std::env; use std::iter::FromIterator; use std::str::FromStr; @@ -5471,3 +5473,82 @@ fn test_approve_erc20() { block_on(mm.stop()).unwrap(); } + +#[test] +fn test_peer_time_sync_validation() { + let timeoffset_tolerable = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() - 1; + let timeoffset_too_big = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() + 1; + + let start_peers_with_time_offset = |offset: i64| -> (Json, Json) { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 10.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 10.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let bob_conf = Mm2TestConf::seednode(&hex::encode(bob_priv_key), &coins); + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( + bob_conf.conf, + bob_conf.rpc_password, + None, + &[], + )) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + let alice_conf = + Mm2TestConf::light_node(&hex::encode(alice_priv_key), &coins, &[mm_bob.ip.to_string().as_str()]); + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + alice_conf.conf, + alice_conf.rpc_password, + None, + &[("TEST_TIMESTAMP_OFFSET", offset.to_string().as_str())], + )) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + + let res_bob = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "get_directly_connected_peers", + }))) + .unwrap(); + assert!(res_bob.0.is_success(), "!get_directly_connected_peers: {}", res_bob.1); + let bob_peers = serde_json::from_str::(&res_bob.1).unwrap(); + + let res_alice = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "get_directly_connected_peers", + }))) + .unwrap(); + assert!( + res_alice.0.is_success(), + "!get_directly_connected_peers: {}", + res_alice.1 + ); + let alice_peers = serde_json::from_str::(&res_alice.1).unwrap(); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); + (bob_peers, alice_peers) + }; + + // check with small time offset: + let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_tolerable); + assert!( + bob_peers["result"].as_object().unwrap().len() == 1, + "bob must have one peer" + ); + assert!( + alice_peers["result"].as_object().unwrap().len() == 1, + "alice must have one peer" + ); + + // check with too big time offset: + let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_too_big); + assert!( + bob_peers["result"].as_object().unwrap().is_empty(), + "bob must have no peers" + ); + assert!( + alice_peers["result"].as_object().unwrap().is_empty(), + "alice must have no peers" + ); +} diff --git a/mm2src/mm2_p2p/Cargo.toml b/mm2src/mm2_p2p/Cargo.toml index 3fc156b6ca..cbbe711783 100644 --- a/mm2src/mm2_p2p/Cargo.toml +++ b/mm2src/mm2_p2p/Cargo.toml @@ -51,3 +51,4 @@ timed-map = { version = "1.1.1", features = ["rustc-hash", "wasm"] } [dev-dependencies] async-std = "1.6.2" env_logger = "0.9.3" +common = { path = "../common", features = ["for-tests"] } From a9402c6a6c3cba6269a01beb0027d7ef510e438c Mon Sep 17 00:00:00 2001 From: Alina Sharon <52405288+laruh@users.noreply.github.com> Date: Tue, 7 Jan 2025 18:52:07 +0700 Subject: [PATCH 24/27] chore(CI): validate Cargo lock file (#2309) * Validate Cargo.lock * Revert "Validate Cargo.lock" and add validate-cargo-lock.yml * review: run only `cargo update` and simplify yml --- .github/workflows/validate-cargo-lock.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/validate-cargo-lock.yml diff --git a/.github/workflows/validate-cargo-lock.yml b/.github/workflows/validate-cargo-lock.yml new file mode 100644 index 0000000000..eb59173d28 --- /dev/null +++ b/.github/workflows/validate-cargo-lock.yml @@ -0,0 +1,16 @@ +name: Validate Cargo.lock +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + validate-cargo-lock: + name: Checking Cargo.lock file + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Validate Cargo.lock + run: cargo update -w --locked From 1908a2e8355bda8bb8717ccece94b8c59affd90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Wed, 8 Jan 2025 11:12:15 +0300 Subject: [PATCH 25/27] feat(tendermint): validators RPC (#2310) * save dev state Signed-off-by: onur-ozkan * save dev state Signed-off-by: onur-ozkan * make proto types serializable for RPC endpoint Signed-off-by: onur-ozkan * remove dummy test Signed-off-by: onur-ozkan * add RPC error type Signed-off-by: onur-ozkan * add TODO Signed-off-by: onur-ozkan * fix status filtering Signed-off-by: onur-ozkan * fix clippy warn Signed-off-by: onur-ozkan * resolve `todo!()`s Signed-off-by: onur-ozkan * remove inline attribute Signed-off-by: onur-ozkan * improve `validators_rpc` Signed-off-by: onur-ozkan * add coverage for tendermint_validators RPC Signed-off-by: onur-ozkan * apply nit changes Signed-off-by: onur-ozkan * document `ValidatorStatus` Signed-off-by: onur-ozkan * use proper error variant on coin filtering Signed-off-by: onur-ozkan * apply nits Signed-off-by: onur-ozkan --------- Signed-off-by: onur-ozkan --- mm2src/coins/rpc_command/tendermint/mod.rs | 1 + .../coins/rpc_command/tendermint/staking.rs | 150 ++++++++++++++++++ mm2src/coins/tendermint/tendermint_coin.rs | 54 ++++++- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 2 + .../tests/docker_tests/tendermint_tests.rs | 28 +++- mm2src/mm2_test_helpers/src/for_tests.rs | 57 +++++-- 6 files changed, 269 insertions(+), 23 deletions(-) create mode 100644 mm2src/coins/rpc_command/tendermint/staking.rs diff --git a/mm2src/coins/rpc_command/tendermint/mod.rs b/mm2src/coins/rpc_command/tendermint/mod.rs index 3e2b664aec..9a3d714bd3 100644 --- a/mm2src/coins/rpc_command/tendermint/mod.rs +++ b/mm2src/coins/rpc_command/tendermint/mod.rs @@ -1,5 +1,6 @@ mod ibc_chains; mod ibc_transfer_channels; +pub mod staking; pub use ibc_chains::*; pub use ibc_transfer_channels::*; diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs new file mode 100644 index 0000000000..c6ac3dca4e --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -0,0 +1,150 @@ +use common::{HttpStatusCode, PagingOptions, StatusCode}; +use cosmrs::staking::{Commission, Description, Validator}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; + +use crate::{lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum}; + +/// Represents current status of the validator. +#[derive(Default, Deserialize)] +pub(crate) enum ValidatorStatus { + All, + /// Validator is in the active set and participates in consensus. + #[default] + Bonded, + /// Validator is not in the active set and does not participate in consensus. + /// Accordingly, they do not receive rewards and cannot be slashed. + /// It is still possible to delegate tokens to a validator in this state. + Unbonded, +} + +impl ToString for ValidatorStatus { + fn to_string(&self) -> String { + match self { + // An empty string doesn't filter any validators and we get an unfiltered result. + ValidatorStatus::All => String::default(), + ValidatorStatus::Bonded => "BOND_STATUS_BONDED".into(), + ValidatorStatus::Unbonded => "BOND_STATUS_UNBONDED".into(), + } + } +} + +#[derive(Deserialize)] +pub struct ValidatorsRPC { + #[serde(rename = "ticker")] + coin: String, + #[serde(flatten)] + paging: PagingOptions, + #[serde(default)] + filter_by_status: ValidatorStatus, +} + +#[derive(Clone, Serialize)] +pub struct ValidatorsRPCResponse { + validators: Vec, +} + +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType, PartialEq)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ValidatorsRPCError { + #[display(fmt = "Coin '{ticker}' could not be found in coins configuration.")] + CoinNotFound { ticker: String }, + #[display(fmt = "'{ticker}' is not a Cosmos coin.")] + UnexpectedCoinType { ticker: String }, + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for ValidatorsRPCError { + fn status_code(&self) -> common::StatusCode { + match self { + ValidatorsRPCError::Transport(_) => StatusCode::SERVICE_UNAVAILABLE, + ValidatorsRPCError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + ValidatorsRPCError::CoinNotFound { .. } => StatusCode::NOT_FOUND, + ValidatorsRPCError::UnexpectedCoinType { .. } => StatusCode::BAD_REQUEST, + } + } +} + +impl From for ValidatorsRPCError { + fn from(e: TendermintCoinRpcError) -> Self { + match e { + TendermintCoinRpcError::InvalidResponse(e) + | TendermintCoinRpcError::PerformError(e) + | TendermintCoinRpcError::RpcClientError(e) => ValidatorsRPCError::Transport(e), + TendermintCoinRpcError::Prost(e) | TendermintCoinRpcError::InternalError(e) => ValidatorsRPCError::InternalError(e), + TendermintCoinRpcError::UnexpectedAccountType { .. } => ValidatorsRPCError::InternalError( + "RPC client got an unexpected error 'TendermintCoinRpcError::UnexpectedAccountType', this isn't normal." + .into(), + ), + } + } +} + +pub async fn validators_rpc( + ctx: MmArc, + req: ValidatorsRPC, +) -> Result> { + fn maybe_jsonize_description(description: Option) -> Option { + description.map(|d| { + json!({ + "moniker": d.moniker, + "identity": d.identity, + "website": d.website, + "security_contact": d.security_contact, + "details": d.details, + }) + }) + } + + fn maybe_jsonize_commission(commission: Option) -> Option { + commission.map(|c| { + let rates = c.commission_rates.map(|cr| { + json!({ + "rate": cr.rate, + "max_rate": cr.max_rate, + "max_change_rate": cr.max_change_rate + }) + }); + + json!({ + "commission_rates": rates, + "update_time": c.update_time + }) + }) + } + + fn jsonize_validator(v: Validator) -> serde_json::Value { + json!({ + "operator_address": v.operator_address, + "consensus_pubkey": v.consensus_pubkey, + "jailed": v.jailed, + "status": v.status, + "tokens": v.tokens, + "delegator_shares": v.delegator_shares, + "description": maybe_jsonize_description(v.description), + "unbonding_height": v.unbonding_height, + "unbonding_time": v.unbonding_time, + "commission": maybe_jsonize_commission(v.commission), + "min_self_delegation": v.min_self_delegation, + }) + } + + let validators = match lp_coinfind_or_err(&ctx, &req.coin).await { + Ok(MmCoinEnum::Tendermint(coin)) => coin.validators_list(req.filter_by_status, req.paging).await?, + Ok(MmCoinEnum::TendermintToken(token)) => { + token + .platform_coin + .validators_list(req.filter_by_status, req.paging) + .await? + }, + Ok(_) => return MmError::err(ValidatorsRPCError::UnexpectedCoinType { ticker: req.coin }), + Err(_) => return MmError::err(ValidatorsRPCError::CoinNotFound { ticker: req.coin }), + }; + + Ok(ValidatorsRPCResponse { + validators: validators.into_iter().map(jsonize_validator).collect(), + }) +} diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 323637599d..9573e6de4b 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -6,6 +6,7 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; +use crate::rpc_command::tendermint::staking::ValidatorStatus; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -35,17 +36,21 @@ use bitcrypto::{dhash160, sha256}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; use common::executor::{AbortedError, Timer}; use common::log::{debug, warn}; -use common::{get_utc_timestamp, now_sec, Future01CompatExt, DEX_FEE_ADDR_PUBKEY}; +use common::{get_utc_timestamp, now_sec, Future01CompatExt, PagingOptions, DEX_FEE_ADDR_PUBKEY}; use cosmrs::bank::MsgSend; use cosmrs::crypto::secp256k1::SigningKey; use cosmrs::proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest, QueryAccountResponse}; use cosmrs::proto::cosmos::bank::v1beta1::{MsgSend as MsgSendProto, QueryBalanceRequest, QueryBalanceResponse}; +use cosmrs::proto::cosmos::base::query::v1beta1::PageRequest; use cosmrs::proto::cosmos::base::tendermint::v1beta1::{GetBlockByHeightRequest, GetBlockByHeightResponse, GetLatestBlockRequest, GetLatestBlockResponse}; use cosmrs::proto::cosmos::base::v1beta1::Coin as CoinProto; +use cosmrs::proto::cosmos::staking::v1beta1::{QueryValidatorsRequest, + QueryValidatorsResponse as QueryValidatorsResponseProto}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; use cosmrs::proto::prost::{DecodeError, Message}; +use cosmrs::staking::{QueryValidatorsResponse, Validator}; use cosmrs::tendermint::block::Height; use cosmrs::tendermint::chain::Id as ChainId; use cosmrs::tendermint::PublicKey; @@ -89,6 +94,7 @@ const ABCI_QUERY_ACCOUNT_PATH: &str = "/cosmos.auth.v1beta1.Query/Account"; const ABCI_QUERY_BALANCE_PATH: &str = "/cosmos.bank.v1beta1.Query/Balance"; const ABCI_GET_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTx"; const ABCI_GET_TXS_EVENT_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTxsEvent"; +const ABCI_VALIDATORS_PATH: &str = "/cosmos.staking.v1beta1.Query/Validators"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -423,6 +429,8 @@ pub enum TendermintInitErrorKind { CantUseWatchersWithPubkeyPolicy, } +/// TODO: Rename this into `ClientRpcError` because this is very +/// confusing atm. #[derive(Display, Debug, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum TendermintCoinRpcError { @@ -454,8 +462,9 @@ impl From for BalanceError { match err { TendermintCoinRpcError::InvalidResponse(e) => BalanceError::InvalidResponse(e), TendermintCoinRpcError::Prost(e) => BalanceError::InvalidResponse(e), - TendermintCoinRpcError::PerformError(e) => BalanceError::Transport(e), - TendermintCoinRpcError::RpcClientError(e) => BalanceError::Transport(e), + TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { + BalanceError::Transport(e) + }, TendermintCoinRpcError::InternalError(e) => BalanceError::Internal(e), TendermintCoinRpcError::UnexpectedAccountType { prefix } => { BalanceError::Internal(format!("Account type '{prefix}' is not supported for HTLCs")) @@ -469,8 +478,9 @@ impl From for ValidatePaymentError { match err { TendermintCoinRpcError::InvalidResponse(e) => ValidatePaymentError::InvalidRpcResponse(e), TendermintCoinRpcError::Prost(e) => ValidatePaymentError::InvalidRpcResponse(e), - TendermintCoinRpcError::PerformError(e) => ValidatePaymentError::Transport(e), - TendermintCoinRpcError::RpcClientError(e) => ValidatePaymentError::Transport(e), + TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { + ValidatePaymentError::Transport(e) + }, TendermintCoinRpcError::InternalError(e) => ValidatePaymentError::InternalError(e), TendermintCoinRpcError::UnexpectedAccountType { prefix } => { ValidatePaymentError::InvalidParameter(format!("Account type '{prefix}' is not supported for HTLCs")) @@ -2080,6 +2090,40 @@ impl TendermintCoin { None } + + pub(crate) async fn validators_list( + &self, + filter_status: ValidatorStatus, + paging: PagingOptions, + ) -> MmResult, TendermintCoinRpcError> { + let request = QueryValidatorsRequest { + status: filter_status.to_string(), + pagination: Some(PageRequest { + key: vec![], + offset: ((paging.page_number.get() - 1usize) * paging.limit) as u64, + limit: paging.limit as u64, + count_total: false, + reverse: false, + }), + }; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_VALIDATORS_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + let decoded_proto = QueryValidatorsResponseProto::decode(raw_response.value.as_slice())?; + let typed_response = QueryValidatorsResponse::try_from(decoded_proto) + .map_err(|e| TendermintCoinRpcError::InternalError(e.to_string()))?; + + Ok(typed_response.validators) + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 937db9631b..fd9babd2c5 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -23,6 +23,7 @@ use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; +use coins::rpc_command::tendermint::staking::validators_rpc; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, @@ -212,6 +213,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, start_version_stat_collection).await, "stop_simple_market_maker_bot" => handle_mmrpc(ctx, request, stop_simple_market_maker_bot).await, "stop_version_stat_collection" => handle_mmrpc(ctx, request, stop_version_stat_collection).await, + "tendermint_validators" => handle_mmrpc(ctx, request, validators_rpc).await, "trade_preimage" => handle_mmrpc(ctx, request, trade_preimage_rpc).await, "trezor_connection_status" => handle_mmrpc(ctx, request, trezor_connection_status).await, "update_nft" => handle_mmrpc(ctx, request, update_nft).await, diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index 9fe3858736..c602c93662 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -5,7 +5,7 @@ use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_ enable_tendermint_token, enable_tendermint_without_balance, get_tendermint_my_tx_history, ibc_withdraw, iris_ibc_nucleus_testnet_conf, my_balance, nucleus_testnet_conf, orderbook, orderbook_v2, send_raw_transaction, - set_price, withdraw_v1, MarketMakerIt, Mm2TestConf}; + set_price, tendermint_validators, withdraw_v1, MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, OrderbookAddress, OrderbookV2Response, RpcV2Response, TendermintActivationResult, TransactionDetails}; use serde_json::json; @@ -651,6 +651,32 @@ fn test_passive_coin_and_force_disable() { block_on(disable_coin_err(&mm, token, false)); } +#[test] +fn test_tendermint_validators_rpc() { + let coins = json!([nucleus_testnet_conf()]); + let platform_coin = coins[0]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode(TENDERMINT_TEST_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_tendermint( + &mm, + platform_coin, + &[], + NUCLEUS_TESTNET_RPC_URLS, + false, + )); + assert!(&activation_res.get("result").unwrap().get("address").is_some()); + + let validators_raw_response = block_on(tendermint_validators(&mm, platform_coin, "All", 10, 1)); + + assert_eq!( + validators_raw_response["result"]["validators"][0]["operator_address"], + "nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu" + ); + assert_eq!(validators_raw_response["result"]["validators"][0]["jailed"], false); +} + mod swap { use super::*; diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 7592384696..b367c4653c 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -3091,6 +3091,33 @@ pub async fn enable_tendermint_token(mm: &MarketMakerIt, coin: &str) -> Json { json::from_str(&request.1).unwrap() } +pub async fn tendermint_validators( + mm: &MarketMakerIt, + coin: &str, + filter_by_status: &str, + limit: usize, + page_number: usize, +) -> Json { + let rpc_endpoint = "tendermint_validators"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "ticker": coin, + "filter_by_status": filter_by_status, + "limit": limit, + "page_number": page_number + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let response = mm.rpc(&request).await.unwrap(); + assert_eq!(response.0, StatusCode::OK, "{rpc_endpoint} failed: {}", response.1); + log!("{rpc_endpoint} response {}", response.1); + json::from_str(&response.1).unwrap() +} + pub async fn init_utxo_electrum( mm: &MarketMakerIt, coin: &str, @@ -3271,18 +3298,19 @@ async fn init_erc20_token( 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(), + 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(); @@ -3352,12 +3380,7 @@ pub async fn get_token_info(mm: &MarketMakerIt, protocol: Json) -> TokenInfoResp })) .await .unwrap(); - assert_eq!( - response.0, - StatusCode::OK, - "'get_token_info' failed: {}", - response.1 - ); + 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() } From 07416d1b951a53da4805587b04c84598655fc307 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:41:00 +0200 Subject: [PATCH 26/27] fix(legacy_swap): check for existing maker/taker payment before timeout (#2283) This commit moves payment existence check in maker_payment/send_taker_payment before timeout validation and skips timeout if payment is already sent, as the taker swap should proceed to waiting for maker to spend the taker payment. --- mm2src/mm2_main/src/lp_swap.rs | 10 +- mm2src/mm2_main/src/lp_swap/maker_swap.rs | 161 +++++---- mm2src/mm2_main/src/lp_swap/swap_watcher.rs | 31 +- mm2src/mm2_main/src/lp_swap/taker_restart.rs | 11 +- mm2src/mm2_main/src/lp_swap/taker_swap.rs | 322 ++++++++++-------- mm2src/mm2_main/src/mm2.rs | 13 +- .../tests/docker_tests/swap_watcher_tests.rs | 150 ++++---- 7 files changed, 382 insertions(+), 316 deletions(-) diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 507b9a6f51..fc8fcfc277 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -92,7 +92,7 @@ use std::sync::{Arc, Mutex, Weak}; use std::time::Duration; use uuid::Uuid; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] use std::sync::atomic::{AtomicU64, Ordering}; mod check_balance; @@ -422,13 +422,13 @@ async fn recv_swap_msg( /// in order to give different and/or heavy communication channels a chance. const BASIC_COMM_TIMEOUT: u64 = 90; -#[cfg(not(feature = "custom-swap-locktime"))] +#[cfg(not(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests")))] /// Default atomic swap payment locktime, in seconds. /// Maker sends payment with LOCKTIME * 2 /// Taker sends payment with LOCKTIME const PAYMENT_LOCKTIME: u64 = 3600 * 2 + 300 * 2; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] /// Default atomic swap payment locktime, in seconds. /// Maker sends payment with LOCKTIME * 2 /// Taker sends payment with LOCKTIME @@ -437,9 +437,9 @@ pub(crate) static PAYMENT_LOCKTIME: AtomicU64 = AtomicU64::new(super::CUSTOM_PAY #[inline] /// Returns `PAYMENT_LOCKTIME` pub fn get_payment_locktime() -> u64 { - #[cfg(not(feature = "custom-swap-locktime"))] + #[cfg(not(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests")))] return PAYMENT_LOCKTIME; - #[cfg(feature = "custom-swap-locktime")] + #[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] PAYMENT_LOCKTIME.load(Ordering::Relaxed) } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 0eb72b8a71..e5e8c23c7a 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -20,7 +20,7 @@ use coins::lp_price::fetch_swap_coins_price; use coins::{CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapTxTypeWithSecretHash, TradeFee, - TradePreimageValue, TransactionEnum, ValidateFeeArgs, ValidatePaymentInput}; + TradePreimageValue, TransactionEnum, ValidateFeeArgs, ValidatePaymentInput, WatcherReward}; use common::log::{debug, error, info, warn}; use common::{bits256, executor::Timer, now_ms, DEX_FEE_ADDR_RAW_PUBKEY}; use common::{now_sec, wait_until_sec}; @@ -793,83 +793,54 @@ impl MakerSwap { Ok((Some(MakerSwapCommand::SendPayment), swap_events)) } - async fn maker_payment(&self) -> Result<(Option, Vec), String> { - let lock_duration = self.r().data.lock_duration; - let timeout = self.r().data.started_at + lock_duration / 3; - let now = now_sec(); - if now > timeout { - return Ok((Some(MakerSwapCommand::Finish), vec![ - MakerSwapEvent::MakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), - ])); + /// Sets up the watcher reward for the maker's payment in the swap. + /// + /// The reward mainly serves as compensation to watchers for the mining fees + /// paid to execute the transactions. + /// + /// The reward configuration depends on the specific requirements of the coins + /// involved in the swap. + /// Some coins may not support watcher rewards at all. + async fn setup_watcher_reward(&self, wait_maker_payment_until: u64) -> Result, String> { + if !self.r().watcher_reward { + return Ok(None); } + self.maker_coin + .get_maker_watcher_reward(&self.taker_coin, self.watcher_reward_amount(), wait_maker_payment_until) + .await + .map_err(|err| err.into_inner().to_string()) + } + + async fn maker_payment(&self) -> Result<(Option, Vec), String> { + // Extract values from lock before async operations + let lock_duration = self.r().data.lock_duration; let maker_payment_lock = self.r().data.maker_payment_lock; let other_maker_coin_htlc_pub = self.r().other_maker_coin_htlc_pub; let secret_hash = self.secret_hash(); let maker_coin_swap_contract_address = self.r().data.maker_coin_swap_contract_address.clone(); let unique_data = self.unique_swap_data(); let payment_instructions = self.r().payment_instructions.clone(); - let transaction_f = self.maker_coin.check_if_my_payment_sent(CheckIfMyPaymentSentArgs { - time_lock: maker_payment_lock, - other_pub: &*other_maker_coin_htlc_pub, - secret_hash: secret_hash.as_slice(), - search_from_block: self.r().data.maker_coin_start_block, - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - amount: &self.maker_amount, - payment_instructions: &payment_instructions, - }); - + let maker_coin_start_block = self.r().data.maker_coin_start_block; let wait_maker_payment_until = wait_for_maker_payment_conf_until(self.r().data.started_at, lock_duration); - let watcher_reward = if self.r().watcher_reward { - match self - .maker_coin - .get_maker_watcher_reward(&self.taker_coin, self.watcher_reward_amount(), wait_maker_payment_until) - .await - { - Ok(reward) => reward, - Err(err) => { - return Ok((Some(MakerSwapCommand::Finish), vec![ - MakerSwapEvent::MakerPaymentTransactionFailed(err.into_inner().to_string().into()), - ])) - }, - } - } else { - None - }; - let transaction = match transaction_f.await { - Ok(res) => match res { - Some(tx) => tx, - None => { - let payment = self - .maker_coin - .send_maker_payment(SendPaymentArgs { - time_lock_duration: lock_duration, - time_lock: maker_payment_lock, - other_pubkey: &*other_maker_coin_htlc_pub, - secret_hash: secret_hash.as_slice(), - amount: self.maker_amount.clone(), - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - payment_instructions: &payment_instructions, - watcher_reward, - wait_for_confirmation_until: wait_maker_payment_until, - }) - .await; - - match payment { - Ok(t) => t, - Err(err) => { - return Ok((Some(MakerSwapCommand::Finish), vec![ - MakerSwapEvent::MakerPaymentTransactionFailed( - ERRL!("{}", err.get_plain_text_format()).into(), - ), - ])); - }, - } - }, - }, + // Look for previously sent maker payment in case of restart + let maybe_existing_payment = match self + .maker_coin + .check_if_my_payment_sent(CheckIfMyPaymentSentArgs { + time_lock: maker_payment_lock, + other_pub: &*other_maker_coin_htlc_pub, + secret_hash: secret_hash.as_slice(), + search_from_block: maker_coin_start_block, + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + amount: &self.maker_amount, + payment_instructions: &payment_instructions, + }) + .await + { + Ok(Some(tx)) => Some(tx), + Ok(None) => None, Err(e) => { return Ok((Some(MakerSwapCommand::Finish), vec![ MakerSwapEvent::MakerPaymentTransactionFailed(ERRL!("{}", e).into()), @@ -877,6 +848,60 @@ impl MakerSwap { }, }; + // If the payment is not yet sent, make sure we didn't miss the deadline for sending it. + if maybe_existing_payment.is_none() { + let timeout = self.r().data.started_at + lock_duration / 3; + let now = now_sec(); + if now > timeout { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), + ])); + } + } + + // Set up watcher reward if enabled + let watcher_reward = match self.setup_watcher_reward(wait_maker_payment_until).await { + Ok(reward) => reward, + Err(err) => { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentTransactionFailed(err.into()), + ])) + }, + }; + + // Use existing payment or create new one + let transaction = match maybe_existing_payment { + Some(tx) => tx, + None => { + match self + .maker_coin + .send_maker_payment(SendPaymentArgs { + time_lock_duration: lock_duration, + time_lock: maker_payment_lock, + other_pubkey: &*other_maker_coin_htlc_pub, + secret_hash: secret_hash.as_slice(), + amount: self.maker_amount.clone(), + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + payment_instructions: &payment_instructions, + watcher_reward, + wait_for_confirmation_until: wait_maker_payment_until, + }) + .await + { + Ok(t) => t, + Err(err) => { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentTransactionFailed( + ERRL!("{}", err.get_plain_text_format()).into(), + ), + ])); + }, + } + }, + }; + + // Build transaction identifier and prepare events let tx_hash = transaction.tx_hash_as_bytes(); info!("{}: Maker payment tx {:02x}", MAKER_PAYMENT_SENT_LOG, tx_hash); diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index 312f62e5c5..bba8479b7c 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -258,10 +258,7 @@ impl State for ValidateTakerPayment { let validate_input = WatcherValidatePaymentInput { payment_tx: taker_payment_hex.clone(), taker_payment_refund_preimage: watcher_ctx.data.taker_payment_refund_preimage.clone(), - time_lock: match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => watcher_ctx.data.swap_started_at, - Err(_) => watcher_ctx.taker_locktime(), - }, + time_lock: watcher_ctx.taker_locktime(), taker_pub: watcher_ctx.verified_pub.clone(), maker_pub: watcher_ctx.data.maker_pub.clone(), secret_hash: watcher_ctx.data.secret_hash.clone(), @@ -451,20 +448,18 @@ impl State for RefundTakerPayment { async fn on_changed(self: Box, watcher_ctx: &mut WatcherStateMachine) -> StateResult { debug!("Watcher refund taker payment"); - if std::env::var("USE_TEST_LOCKTIME").is_err() { - loop { - match watcher_ctx - .taker_coin - .can_refund_htlc(watcher_ctx.taker_locktime()) - .await - { - Ok(CanRefundHtlc::CanRefundNow) => break, - Ok(CanRefundHtlc::HaveToWait(to_sleep)) => Timer::sleep(to_sleep as f64).await, - Err(e) => { - error!("Error {} on can_refund_htlc, retrying in 30 seconds", e); - Timer::sleep(30.).await; - }, - } + loop { + match watcher_ctx + .taker_coin + .can_refund_htlc(watcher_ctx.taker_locktime()) + .await + { + Ok(CanRefundHtlc::CanRefundNow) => break, + Ok(CanRefundHtlc::HaveToWait(to_sleep)) => Timer::sleep(to_sleep as f64).await, + Err(e) => { + error!("Error {} on can_refund_htlc, retrying in 30 seconds", e); + Timer::sleep(30.).await; + }, } } diff --git a/mm2src/mm2_main/src/lp_swap/taker_restart.rs b/mm2src/mm2_main/src/lp_swap/taker_restart.rs index d934b6b11e..014f671bd1 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_restart.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_restart.rs @@ -154,11 +154,7 @@ pub async fn check_taker_payment_spend(swap: &TakerSwap) -> Result swap.r().data.started_at, - Err(_) => swap.r().data.taker_payment_lock, - }; + let taker_payment_lock = swap.r().data.taker_payment_lock; let secret_hash = swap.r().secret_hash.0.clone(); let unique_data = swap.unique_swap_data(); let watcher_reward = swap.r().watcher_reward; @@ -223,10 +219,7 @@ pub async fn add_taker_payment_refunded_by_watcher_event( ) -> Result { let other_maker_coin_htlc_pub = swap.r().other_maker_coin_htlc_pub; let taker_coin_swap_contract_address = swap.r().data.taker_coin_swap_contract_address.clone(); - let taker_payment_lock = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => swap.r().data.started_at, - Err(_) => swap.r().data.taker_payment_lock, - }; + let taker_payment_lock = swap.r().data.taker_payment_lock; let secret_hash = swap.r().secret_hash.0.clone(); let validate_input = ValidateWatcherSpendInput { diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index c7b1cf59a9..90c2f3655d 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -20,7 +20,7 @@ use coins::lp_price::fetch_swap_coins_price; use coins::{lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapTxTypeWithSecretHash, - TradeFee, TradePreimageValue, ValidatePaymentInput, WaitForHTLCTxSpendArgs}; + TradeFee, TradePreimageValue, TransactionEnum, ValidatePaymentInput, WaitForHTLCTxSpendArgs, WatcherReward}; use common::executor::Timer; use common::log::{debug, error, info, warn}; use common::{bits256, now_ms, now_sec, wait_until_sec, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -1506,6 +1506,104 @@ impl TakerSwap { } } + /// Sets up the watcher reward for the taker's payment in the swap. + /// + /// The reward mainly serves as compensation to watchers for the mining fees + /// paid to execute the transactions. + /// + /// The reward configuration depends on the specific requirements of the coins + /// involved in the swap. + /// Some coins may not support watcher rewards at all. + async fn setup_watcher_reward(&self, taker_payment_lock: u64) -> Result, String> { + if !self.r().watcher_reward { + return Ok(None); + } + + let reward_amount = self.r().reward_amount.clone(); + self.taker_coin + .get_taker_watcher_reward( + &self.maker_coin, + Some(self.taker_amount.clone().into()), + Some(self.maker_amount.clone().into()), + reward_amount, + taker_payment_lock, + ) + .await + .map(Some) + .map_err(|err| ERRL!("Watcher reward error: {}", err.to_string())) + } + + /// Processes watcher-related logic for the swap by preparing and broadcasting necessary data. + /// + /// This function creates spend/refund preimages and broadcasts them to watchers if both coins + /// support watcher functionality and watchers are enabled. + /// + /// The preimages allow watchers to either complete the swap by spending the maker payment + /// or refund the taker payment if needed. + async fn process_watcher_logic(&self, transaction: &TransactionEnum) -> Option { + let watchers_enabled_and_supported = self.ctx.use_watchers() + && self.taker_coin.is_supported_by_watchers() + && self.maker_coin.is_supported_by_watchers(); + + if !watchers_enabled_and_supported { + return None; + } + + let maker_payment_spend_preimage_fut = self.maker_coin.create_maker_payment_spend_preimage( + &self.r().maker_payment.as_ref().unwrap().tx_hex, + self.maker_payment_lock.load(Ordering::Relaxed), + self.r().other_maker_coin_htlc_pub.as_slice(), + &self.r().secret_hash.0, + &self.unique_swap_data()[..], + ); + + let taker_payment_refund_preimage_fut = self.taker_coin.create_taker_payment_refund_preimage( + &transaction.tx_hex(), + self.r().data.taker_payment_lock, + &*self.r().other_taker_coin_htlc_pub, + &self.r().secret_hash.0, + &self.r().data.taker_coin_swap_contract_address, + &self.unique_swap_data(), + ); + + match try_join( + maker_payment_spend_preimage_fut.compat(), + taker_payment_refund_preimage_fut.compat(), + ) + .await + { + Ok((maker_payment_spend, taker_payment_refund)) => { + let watcher_data = self.create_watcher_data( + transaction.tx_hash_as_bytes().into_vec(), + maker_payment_spend.tx_hex(), + taker_payment_refund.tx_hex(), + ); + let swpmsg_watcher = SwapWatcherMsg::TakerSwapWatcherMsg(watcher_data); + + let htlc_keypair = self.taker_coin.derive_htlc_key_pair(&self.unique_swap_data()); + broadcast_swap_message( + &self.ctx, + watcher_topic(&self.r().data.taker_coin), + swpmsg_watcher, + &Some(htlc_keypair), + ); + + info!("{}", WATCHER_MESSAGE_SENT_LOG); + Some(TakerSwapEvent::WatcherMessageSent( + Some(maker_payment_spend.tx_hex()), + Some(taker_payment_refund.tx_hex()), + )) + }, + Err(e) => { + error!( + "The watcher message could not be sent, error creating at least one of the preimages: {}", + e.get_plain_text_format() + ); + None + }, + } + } + async fn send_taker_payment(&self) -> Result<(Option, Vec), String> { #[cfg(test)] if self.fail_at == Some(FailAt::TakerPayment) { @@ -1514,96 +1612,33 @@ impl TakerSwap { ])); } - let timeout = self.r().data.maker_payment_wait; - let now = now_sec(); - if now > timeout { - return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), - ])); - } - + // Extract values from the lock before async operations let taker_payment_lock = self.r().data.taker_payment_lock; let other_taker_coin_htlc_pub = self.r().other_taker_coin_htlc_pub; let secret_hash = self.r().secret_hash.clone(); + let taker_coin_start_block = self.r().data.taker_coin_start_block; let taker_coin_swap_contract_address = self.r().data.taker_coin_swap_contract_address.clone(); let unique_data = self.unique_swap_data(); let taker_amount_decimal = self.taker_amount.to_decimal(); let payment_instructions = self.r().payment_instructions.clone(); - let f = self.taker_coin.check_if_my_payment_sent(CheckIfMyPaymentSentArgs { - time_lock: taker_payment_lock, - other_pub: other_taker_coin_htlc_pub.as_slice(), - secret_hash: &secret_hash.0, - search_from_block: self.r().data.taker_coin_start_block, - swap_contract_address: &taker_coin_swap_contract_address, - swap_unique_data: &unique_data, - amount: &taker_amount_decimal, - payment_instructions: &payment_instructions, - }); - - let reward_amount = self.r().reward_amount.clone(); - let wait_until = taker_payment_lock; - let watcher_reward = if self.r().watcher_reward { - match self - .taker_coin - .get_taker_watcher_reward( - &self.maker_coin, - Some(self.taker_amount.clone().into()), - Some(self.maker_amount.clone().into()), - reward_amount, - wait_until, - ) - .await - { - Ok(reward) => Some(reward), - Err(err) => { - return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerPaymentTransactionFailed( - ERRL!("Watcher reward error: {}", err.to_string()).into(), - ), - ])) - }, - } - } else { - None - }; - let transaction = match f.await { - Ok(res) => match res { - Some(tx) => tx, - None => { - let time_lock = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => self.r().data.started_at, - Err(_) => taker_payment_lock, - }; - let lock_duration = self.r().data.lock_duration; - let payment = self - .taker_coin - .send_taker_payment(SendPaymentArgs { - time_lock_duration: lock_duration, - time_lock, - other_pubkey: &*other_taker_coin_htlc_pub, - secret_hash: &secret_hash.0, - amount: taker_amount_decimal, - swap_contract_address: &taker_coin_swap_contract_address, - swap_unique_data: &unique_data, - payment_instructions: &payment_instructions, - watcher_reward, - wait_for_confirmation_until: taker_payment_lock, - }) - .await; - - match payment { - Ok(t) => t, - Err(err) => { - return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerPaymentTransactionFailed( - ERRL!("{}", err.get_plain_text_format()).into(), - ), - ])); - }, - } - }, - }, + // Look for previously sent taker payment in case of restart + let maybe_existing_payment = match self + .taker_coin + .check_if_my_payment_sent(CheckIfMyPaymentSentArgs { + time_lock: taker_payment_lock, + other_pub: other_taker_coin_htlc_pub.as_slice(), + secret_hash: &secret_hash.0, + search_from_block: taker_coin_start_block, + swap_contract_address: &taker_coin_swap_contract_address, + swap_unique_data: &unique_data, + amount: &taker_amount_decimal, + payment_instructions: &payment_instructions, + }) + .await + { + Ok(Some(tx)) => Some(tx), + Ok(None) => None, Err(e) => { return Ok((Some(TakerSwapCommand::Finish), vec![ TakerSwapEvent::TakerPaymentTransactionFailed(ERRL!("{}", e).into()), @@ -1611,6 +1646,61 @@ impl TakerSwap { }, }; + // If the payment is not yet sent, make sure we didn't miss the deadline for sending it. + if maybe_existing_payment.is_none() { + let timeout = self.r().data.maker_payment_wait; + let now = now_sec(); + if now > timeout { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), + ])); + } + } + + // Set up watcher reward if enable + let watcher_reward = match self.setup_watcher_reward(taker_payment_lock).await { + Ok(reward) => reward, + Err(err) => { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentTransactionFailed(err.into()), + ])); + }, + }; + + // Use existing payment or create new one + let transaction = match maybe_existing_payment { + Some(tx) => tx, + None => { + let lock_duration = self.r().data.lock_duration; + match self + .taker_coin + .send_taker_payment(SendPaymentArgs { + time_lock_duration: lock_duration, + time_lock: taker_payment_lock, + other_pubkey: &*other_taker_coin_htlc_pub, + secret_hash: &secret_hash.0, + amount: taker_amount_decimal, + swap_contract_address: &taker_coin_swap_contract_address, + swap_unique_data: &unique_data, + payment_instructions: &payment_instructions, + watcher_reward, + wait_for_confirmation_until: taker_payment_lock, + }) + .await + { + Ok(t) => t, + Err(err) => { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentTransactionFailed( + ERRL!("{}", err.get_plain_text_format()).into(), + ), + ])) + }, + } + }, + }; + + // Create transaction identifier and prepare `TakerPaymentSent` success event let tx_hash = transaction.tx_hash_as_bytes(); let tx_hex = BytesJson::from(transaction.tx_hex()); info!("Taker payment tx hash {:02x}", tx_hash); @@ -1618,65 +1708,11 @@ impl TakerSwap { tx_hex: tx_hex.clone(), tx_hash, }; - let mut swap_events = vec![TakerSwapEvent::TakerPaymentSent(tx_ident)]; - if self.ctx.use_watchers() - && self.taker_coin.is_supported_by_watchers() - && self.maker_coin.is_supported_by_watchers() - { - let maker_payment_spend_preimage_fut = self.maker_coin.create_maker_payment_spend_preimage( - &self.r().maker_payment.as_ref().unwrap().tx_hex, - self.maker_payment_lock.load(Ordering::Relaxed), - self.r().other_maker_coin_htlc_pub.as_slice(), - &self.r().secret_hash.0, - &self.unique_swap_data()[..], - ); - let time_lock = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => self.r().data.started_at, - Err(_) => self.r().data.taker_payment_lock, - }; - let taker_payment_refund_preimage_fut = self.taker_coin.create_taker_payment_refund_preimage( - &transaction.tx_hex(), - time_lock, - &*self.r().other_taker_coin_htlc_pub, - &self.r().secret_hash.0, - &self.r().data.taker_coin_swap_contract_address, - &self.unique_swap_data(), - ); - let payment_fut_pair = try_join( - maker_payment_spend_preimage_fut.compat(), - taker_payment_refund_preimage_fut.compat(), - ); - - match payment_fut_pair.await { - Ok((maker_payment_spend, taker_payment_refund)) => { - let watcher_data = self.create_watcher_data( - transaction.tx_hash_as_bytes().into_vec(), - maker_payment_spend.tx_hex(), - taker_payment_refund.tx_hex(), - ); - let swpmsg_watcher = SwapWatcherMsg::TakerSwapWatcherMsg(watcher_data); - - let htlc_keypair = self.taker_coin.derive_htlc_key_pair(&self.unique_swap_data()); - broadcast_swap_message( - &self.ctx, - watcher_topic(&self.r().data.taker_coin), - swpmsg_watcher, - &Some(htlc_keypair), - ); - - swap_events.push(TakerSwapEvent::WatcherMessageSent( - Some(maker_payment_spend.tx_hex()), - Some(taker_payment_refund.tx_hex()), - )); - info!("{}", WATCHER_MESSAGE_SENT_LOG); - }, - Err(e) => error!( - "The watcher message could not be sent, error creating at least one of the preimages: {}", - e.get_plain_text_format() - ), - } + // Process watcher logic if enabled and supported by both coins + if let Some(watcher_event) = self.process_watcher_logic(&transaction).await { + swap_events.push(watcher_event); } Ok((Some(TakerSwapCommand::WaitForTakerPaymentSpend), swap_events)) @@ -1733,11 +1769,7 @@ impl TakerSwap { info!("Waiting for maker to spend taker payment!"); - let wait_until = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => self.r().data.started_at, - Err(_) => self.r().data.taker_payment_lock, - }; - + let wait_until = self.r().data.taker_payment_lock; let secret_hash = self.r().secret_hash.clone(); let taker_coin_start_block = self.r().data.taker_coin_start_block; let taker_coin_swap_contract_address = self.r().data.taker_coin_swap_contract_address.clone(); diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index 4876069e6d..151b6de1eb 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -47,10 +47,11 @@ use common::log::LogLevel; use common::password_policy::password_policy; use mm2_core::mm_ctx::MmCtxBuilder; -#[cfg(feature = "custom-swap-locktime")] use common::log::warn; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] +use common::log::warn; +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] use lp_swap::PAYMENT_LOCKTIME; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] use std::sync::atomic::Ordering; use gstuff::slurp; @@ -85,7 +86,7 @@ pub mod rpc; pub const PASSWORD_MAXIMUM_CONSECUTIVE_CHARACTERS: usize = 3; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] const CUSTOM_PAYMENT_LOCKTIME_DEFAULT: u64 = 900; pub struct LpMainParams { @@ -102,7 +103,7 @@ impl LpMainParams { } } -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] /// Reads `payment_locktime` from conf arg and assigns it into `PAYMENT_LOCKTIME` in lp_swap. /// Assigns 900 if `payment_locktime` is invalid or not provided. fn initialize_payment_locktime(conf: &Json) { @@ -150,7 +151,7 @@ pub async fn lp_main( } } - #[cfg(feature = "custom-swap-locktime")] + #[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] initialize_payment_locktime(&conf); let ctx = MmCtxBuilder::new() diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs index 43eb715324..32efc4288e 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -92,6 +92,7 @@ fn start_swaps_and_get_balances( alice_privkey: &str, bob_privkey: &str, watcher_privkey: &str, + custom_locktime: Option, ) -> BalanceResult { let coins = json!([ eth_dev_conf(), @@ -100,7 +101,10 @@ fn start_swaps_and_get_balances( mycoin1_conf(1000) ]); - let alice_conf = Mm2TestConf::seednode(&format!("0x{}", alice_privkey), &coins); + let mut alice_conf = Mm2TestConf::seednode(&format!("0x{}", alice_privkey), &coins); + if let Some(locktime) = custom_locktime { + alice_conf.conf["payment_locktime"] = locktime.into(); + } let mut mm_alice = block_on(MarketMakerIt::start_with_envs( alice_conf.conf.clone(), alice_conf.rpc_password.clone(), @@ -111,7 +115,10 @@ fn start_swaps_and_get_balances( let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - let bob_conf = Mm2TestConf::light_node(&format!("0x{}", bob_privkey), &coins, &[&mm_alice.ip.to_string()]); + let mut bob_conf = Mm2TestConf::light_node(&format!("0x{}", bob_privkey), &coins, &[&mm_alice.ip.to_string()]); + if let Some(locktime) = custom_locktime { + bob_conf.conf["payment_locktime"] = locktime.into(); + } let mut mm_bob = block_on(MarketMakerIt::start_with_envs( bob_conf.conf.clone(), bob_conf.rpc_password, @@ -157,13 +164,16 @@ fn start_swaps_and_get_balances( ), }; - let watcher_conf = Mm2TestConf::watcher_light_node( + let mut watcher_conf = Mm2TestConf::watcher_light_node( &format!("0x{}", watcher_privkey), &coins, &[&mm_alice.ip.to_string()], watcher_conf, ) .conf; + if let Some(locktime) = custom_locktime { + watcher_conf["payment_locktime"] = locktime.into(); + } let mut mm_watcher = block_on(MarketMakerIt::start_with_envs( watcher_conf, @@ -270,9 +280,17 @@ fn check_actual_events(mm_alice: &MarketMakerIt, uuid: &str, expected_events: &[ status_response } -fn run_taker_node(coins: &Value, envs: &[(&str, &str)], seednodes: &[&str]) -> (MarketMakerIt, Mm2TestConf) { +fn run_taker_node( + coins: &Value, + envs: &[(&str, &str)], + seednodes: &[&str], + custom_locktime: Option, +) -> (MarketMakerIt, Mm2TestConf) { let privkey = hex::encode(random_secp256k1_secret()); - let conf = Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes); + let mut conf = Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes); + if let Some(locktime) = custom_locktime { + conf.conf["payment_locktime"] = locktime.into(); + } let mm = block_on(MarketMakerIt::start_with_envs( conf.conf.clone(), conf.rpc_password.clone(), @@ -309,13 +327,21 @@ fn restart_taker_and_wait_until(conf: &Mm2TestConf, envs: &[(&str, &str)], wait_ mm_alice } -fn run_maker_node(coins: &Value, envs: &[(&str, &str)], seednodes: &[&str]) -> MarketMakerIt { +fn run_maker_node( + coins: &Value, + envs: &[(&str, &str)], + seednodes: &[&str], + custom_locktime: Option, +) -> MarketMakerIt { let privkey = hex::encode(random_secp256k1_secret()); - let conf = if seednodes.is_empty() { + let mut conf = if seednodes.is_empty() { Mm2TestConf::seednode(&format!("0x{}", privkey), coins) } else { Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes) }; + if let Some(locktime) = custom_locktime { + conf.conf["payment_locktime"] = locktime.into(); + } let mm = block_on(MarketMakerIt::start_with_envs( conf.conf.clone(), conf.rpc_password, @@ -339,9 +365,13 @@ fn run_watcher_node( envs: &[(&str, &str)], seednodes: &[&str], watcher_conf: WatcherConf, + custom_locktime: Option, ) -> MarketMakerIt { let privkey = hex::encode(random_secp256k1_secret()); - let conf = Mm2TestConf::watcher_light_node(&format!("0x{}", privkey), coins, seednodes, watcher_conf).conf; + let mut conf = Mm2TestConf::watcher_light_node(&format!("0x{}", privkey), coins, seednodes, watcher_conf).conf; + if let Some(locktime) = custom_locktime { + conf["payment_locktime"] = locktime.into(); + } let mm = block_on(MarketMakerIt::start_with_envs( conf, DEFAULT_RPC_PASSWORD.to_string(), @@ -363,11 +393,13 @@ fn run_watcher_node( #[test] fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = - run_taker_node(&coins, &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], &[ - &mm_bob.ip.to_string(), - ]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], + &[&mm_bob.ip.to_string()], + None, + ); let watcher_conf = WatcherConf { wait_taker_payment: 0., @@ -375,7 +407,7 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker refund_start_factor: 1.5, search_interval: 1.0, }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf, None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -422,11 +454,13 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker #[test] fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = - run_taker_node(&coins, &[("TAKER_FAIL_AT", "maker_payment_spend_panic")], &[&mm_bob - .ip - .to_string()]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[("TAKER_FAIL_AT", "maker_payment_spend_panic")], + &[&mm_bob.ip.to_string()], + None, + ); let watcher_conf = WatcherConf { wait_taker_payment: 0., @@ -434,7 +468,7 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_ refund_start_factor: 1.5, search_interval: 1.0, }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf, None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -477,15 +511,13 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_ #[test] fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_wait_for_taker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_seednode = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[]); - let mut mm_bob = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[&mm_seednode.ip.to_string()]); + let mm_seednode = run_maker_node(&coins, &[], &[], Some(60)); + let mut mm_bob = run_maker_node(&coins, &[], &[&mm_seednode.ip.to_string()], Some(60)); let (mut mm_alice, mut alice_conf) = run_taker_node( &coins, - &[ - ("USE_TEST_LOCKTIME", ""), - ("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic"), - ], + &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], &[&mm_seednode.ip.to_string()], + Some(60), ); let watcher_conf = WatcherConf { @@ -494,12 +526,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa refund_start_factor: 0., search_interval: 1., }; - let mut mm_watcher = run_watcher_node( - &coins, - &[("USE_TEST_LOCKTIME", "")], - &[&mm_seednode.ip.to_string()], - watcher_conf, - ); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_seednode.ip.to_string()], watcher_conf, Some(60)); let uuids = block_on(start_swaps( &mut mm_bob, @@ -521,11 +548,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa block_on(mm_alice.stop()).unwrap(); - let mm_alice = restart_taker_and_wait_until( - &alice_conf, - &[("USE_TEST_LOCKTIME", "")], - &format!("[swap uuid={}] Finished", &uuids[0]), - ); + let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); let expected_events = [ "Started", @@ -550,15 +573,13 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa #[test] fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_taker_payment_refund() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_seednode = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[]); - let mut mm_bob = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[&mm_seednode.ip.to_string()]); + let mm_seednode = run_maker_node(&coins, &[], &[], Some(60)); + let mut mm_bob = run_maker_node(&coins, &[], &[&mm_seednode.ip.to_string()], Some(60)); let (mut mm_alice, mut alice_conf) = run_taker_node( &coins, - &[ - ("USE_TEST_LOCKTIME", ""), - ("TAKER_FAIL_AT", "taker_payment_refund_panic"), - ], + &[("TAKER_FAIL_AT", "taker_payment_refund_panic")], &[&mm_seednode.ip.to_string()], + Some(60), ); let watcher_conf = WatcherConf { @@ -567,12 +588,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa refund_start_factor: 0., search_interval: 1., }; - let mut mm_watcher = run_watcher_node( - &coins, - &[("USE_TEST_LOCKTIME", "")], - &[&mm_seednode.ip.to_string()], - watcher_conf, - ); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_seednode.ip.to_string()], watcher_conf, Some(60)); let uuids = block_on(start_swaps( &mut mm_bob, @@ -594,11 +610,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa block_on(mm_alice.stop()).unwrap(); - let mm_alice = restart_taker_and_wait_until( - &alice_conf, - &[("USE_TEST_LOCKTIME", "")], - &format!("[swap uuid={}] Finished", &uuids[0]), - ); + let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); let expected_events = [ "Started", @@ -626,8 +638,8 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa #[test] fn test_taker_completes_swap_after_restart() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()], None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -671,8 +683,8 @@ fn test_taker_completes_swap_after_restart() { #[test] fn test_taker_completes_swap_after_taker_payment_spent_while_offline() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()], None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -736,6 +748,7 @@ fn test_watcher_spends_maker_payment_utxo_utxo() { &alice_privkey, &bob_privkey, &watcher_privkey, + None, ); let acoin_volume = BigDecimal::from_str("50").unwrap(); @@ -776,6 +789,7 @@ fn test_watcher_spends_maker_payment_utxo_eth() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); @@ -805,6 +819,7 @@ fn test_watcher_spends_maker_payment_eth_utxo() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let eth_volume = BigDecimal::from_str("0.01").unwrap(); @@ -847,6 +862,7 @@ fn test_watcher_spends_maker_payment_eth_erc20() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let eth_volume = BigDecimal::from_str("0.01").unwrap(); @@ -880,6 +896,7 @@ fn test_watcher_spends_maker_payment_erc20_eth() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let jst_volume = BigDecimal::from_str("1").unwrap(); @@ -910,6 +927,7 @@ fn test_watcher_spends_maker_payment_utxo_erc20() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); @@ -943,6 +961,7 @@ fn test_watcher_spends_maker_payment_erc20_utxo() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); @@ -991,11 +1010,12 @@ fn test_watcher_refunds_taker_payment_utxo() { 25., 25., 2., - &[("USE_TEST_LOCKTIME", "")], + &[], SwapFlow::WatcherRefundsTakerPayment, alice_privkey, bob_privkey, watcher_privkey, + Some(60), ); assert_eq!( @@ -1017,11 +1037,12 @@ fn test_watcher_refunds_taker_payment_eth() { 0.01, 0.01, 1., - &[("USE_TEST_LOCKTIME", ""), ("USE_WATCHER_REWARD", "")], + &[("USE_WATCHER_REWARD", "")], SwapFlow::WatcherRefundsTakerPayment, &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + Some(60), ); assert_eq!(balances.alice_bcoin_balance_after, balances.alice_bcoin_balance_before); @@ -1040,15 +1061,12 @@ fn test_watcher_refunds_taker_payment_erc20() { 100., 100., 0.01, - &[ - ("USE_TEST_LOCKTIME", ""), - ("TEST_COIN_PRICE", "0.01"), - ("USE_WATCHER_REWARD", ""), - ], + &[("TEST_COIN_PRICE", "0.01"), ("USE_WATCHER_REWARD", "")], SwapFlow::WatcherRefundsTakerPayment, &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + Some(60), ); let erc20_volume = BigDecimal::from_str("1").unwrap(); @@ -1080,6 +1098,7 @@ fn test_watcher_waits_for_taker_utxo() { alice_privkey, bob_privkey, watcher_privkey, + None, ); } @@ -1100,6 +1119,7 @@ fn test_watcher_waits_for_taker_eth() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); } From 405bcb7daf316477497eb6925cba0ca9640ab383 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Thu, 9 Jan 2025 16:17:57 +0100 Subject: [PATCH 27/27] fix(crypto): allow non bip39 mnemonics storage (#2312) Fixes an issue where passphrases were incorrectly validated as BIP39 mnemonics during storage decryption. Now, passphrases no longer require bip39 validation. --- mm2src/crypto/src/mnemonic.rs | 32 +++++++++++++++++++++++--------- mm2src/mm2_main/src/lp_wallet.rs | 4 ++-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/mm2src/crypto/src/mnemonic.rs b/mm2src/crypto/src/mnemonic.rs index c92e23c05b..922b292da0 100644 --- a/mm2src/crypto/src/mnemonic.rs +++ b/mm2src/crypto/src/mnemonic.rs @@ -104,7 +104,7 @@ pub fn encrypt_mnemonic(mnemonic: &str, password: &str) -> MmResult MmResult { +pub fn decrypt_mnemonic(encrypted_data: &EncryptedData, password: &str) -> MmResult { // Re-create the salts from Base64-encoded strings let (salt_aes, salt_hmac) = match &encrypted_data.key_derivation_details { KeyDerivationDetails::Argon2 { @@ -126,8 +126,7 @@ pub fn decrypt_mnemonic(encrypted_data: &EncryptedData, password: &str) -> MmRes // Convert decrypted data back to a string let mnemonic_str = String::from_utf8(decrypted_data).map_to_mm(|e| MnemonicError::DecodeError(e.to_string()))?; - let mnemonic = Mnemonic::parse_normalized(&mnemonic_str)?; - Ok(mnemonic) + Ok(mnemonic_str) } #[cfg(any(test, target_arch = "wasm32"))] @@ -144,10 +143,23 @@ mod tests { let mnemonic = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; let password = "password"; - // Verify that the mnemonic is valid - let parsed_mnemonic = Mnemonic::parse_normalized(mnemonic); - assert!(parsed_mnemonic.is_ok()); - let parsed_mnemonic = parsed_mnemonic.unwrap(); + // Encrypt the mnemonic + let encrypted_data = encrypt_mnemonic(mnemonic, password); + assert!(encrypted_data.is_ok()); + let encrypted_data = encrypted_data.unwrap(); + + // Decrypt the mnemonic + let decrypted_mnemonic = decrypt_mnemonic(&encrypted_data, password); + assert!(decrypted_mnemonic.is_ok()); + let decrypted_mnemonic = decrypted_mnemonic.unwrap(); + + // Verify if decrypted mnemonic matches the original + assert_eq!(decrypted_mnemonic, mnemonic); + }); + + cross_test!(test_encrypt_decrypt_non_bip39_mnemonic, { + let mnemonic = "Helloworld"; + let password = "Helloworld"; // Encrypt the mnemonic let encrypted_data = encrypt_mnemonic(mnemonic, password); @@ -160,7 +172,7 @@ mod tests { let decrypted_mnemonic = decrypted_mnemonic.unwrap(); // Verify if decrypted mnemonic matches the original - assert_eq!(decrypted_mnemonic, parsed_mnemonic); + assert_eq!(decrypted_mnemonic, mnemonic); }); cross_test!(test_mnemonic_with_last_byte_zero, { @@ -173,7 +185,9 @@ mod tests { let encrypted_data = encrypted_data.unwrap(); // Decrypt the mnemonic - let decrypted_mnemonic = decrypt_mnemonic(&encrypted_data, password); + let decrypted_mnemonic_str = decrypt_mnemonic(&encrypted_data, password); + assert!(decrypted_mnemonic_str.is_ok()); + let decrypted_mnemonic = Mnemonic::parse_normalized(&decrypted_mnemonic_str.unwrap()); assert!(decrypted_mnemonic.is_err()); // Verify that the error is due to parsing and not padding diff --git a/mm2src/mm2_main/src/lp_wallet.rs b/mm2src/mm2_main/src/lp_wallet.rs index 20a154b28c..abe5663024 100644 --- a/mm2src/mm2_main/src/lp_wallet.rs +++ b/mm2src/mm2_main/src/lp_wallet.rs @@ -139,7 +139,7 @@ async fn read_and_decrypt_passphrase_if_available( Some(encrypted_passphrase) => { let mnemonic = decrypt_mnemonic(&encrypted_passphrase, wallet_password) .mm_err(|e| ReadPassphraseError::DecryptionError(e.to_string()))?; - Ok(Some(mnemonic.to_string())) + Ok(Some(mnemonic)) }, None => Ok(None), } @@ -214,7 +214,7 @@ async fn decrypt_validate_or_save_passphrase( wallet_password: &str, ) -> WalletInitResult> { // Decrypt the provided encrypted passphrase - let decrypted_passphrase = decrypt_mnemonic(&encrypted_passphrase_data, wallet_password)?.to_string(); + let decrypted_passphrase = decrypt_mnemonic(&encrypted_passphrase_data, wallet_password)?; match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? { Some(passphrase_from_file) if decrypted_passphrase == passphrase_from_file => {