diff --git a/.gitignore b/.gitignore index 42f63e5115..186bf7ec6b 100755 --- a/.gitignore +++ b/.gitignore @@ -74,9 +74,11 @@ MM2.json # mergetool *.orig +# Dumpster (files not inteded for tracking) +hidden # Ignore containers runtime directories for dockerized tests # This directory contains temporary data used by Docker containers during tests execution. # It is recreated from container-state data each time test containers are started, # and should not be tracked in version control. -.docker/container-runtime/ \ No newline at end of file +.docker/container-runtime/ diff --git a/Cargo.lock b/Cargo.lock index f0cef862ae..5b50c19c51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3963,6 +3963,7 @@ dependencies = [ "futures 0.3.28", "parking_lot", "serde", + "serde_json", "tokio", "wasm-bindgen-test", ] @@ -5639,10 +5640,12 @@ dependencies = [ "derive_more", "futures 0.3.28", "mm2_err_handle", + "mm2_event_stream", "ser_error", "ser_error_derive", "serde", "serde_derive", + "serde_json", ] [[package]] diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 068c750d37..aa02924ab9 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -54,8 +54,8 @@ use async_trait::async_trait; use bitcrypto::{dhash160, keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry, RetryOnError}; use common::custom_futures::timeout::FutureTimerExt; -use common::executor::{abortable_queue::AbortableQueue, AbortOnDropHandle, AbortSettings, AbortableSystem, - AbortedError, SpawnAbortable, Timer}; +use common::executor::{abortable_queue::AbortableQueue, AbortSettings, AbortableSystem, AbortedError, SpawnAbortable, + Timer}; use common::log::{debug, error, info, warn}; use common::number_type_casting::SafeTypeCastingNumbers; use common::{now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -63,6 +63,7 @@ use crypto::privkey::key_pair_from_secret; use crypto::{Bip44Chain, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; use derive_more::Display; use enum_derives::EnumFromStringify; + use ethabi::{Contract, Function, Token}; use ethcore_transaction::tx_builders::TxBuilderError; use ethcore_transaction::{Action, TransactionWrapper, TransactionWrapperBuilder as UnSignedEthTxBuilder, @@ -77,7 +78,6 @@ use futures01::Future; use http::Uri; use instant::Instant; use mm2_core::mm_ctx::{MmArc, MmWeak}; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; use mm2_number::bigdecimal_custom::CheckedDivision; use mm2_number::{BigDecimal, BigUint, MmNumber}; #[cfg(test)] use mocktopus::macros::*; @@ -109,30 +109,30 @@ cfg_wasm32! { } use super::{coin_conf, lp_coinfind_or_err, AsyncMutex, BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, - CoinBalance, CoinFutSpawner, CoinProtocol, CoinTransportMetrics, CoinsContext, ConfirmPaymentInput, - EthValidateFeeArgs, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, - MarketCoinOps, MmCoin, MmCoinEnum, MyAddressError, MyWalletAddress, NegotiateSwapContractAddrErr, - NumConversError, NumConversResult, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, - RawTransactionRequest, RawTransactionRes, RawTransactionResult, RefundError, RefundPaymentArgs, - RefundResult, RewardTarget, RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignEthTransactionParams, - SignRawTransactionEnum, SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, - SwapOps, SwapTxFeePolicy, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, - TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails, TransactionEnum, TransactionErr, - TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, - WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, - WithdrawRequest, WithdrawResult, EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, + CoinBalance, CoinProtocol, CoinTransportMetrics, CoinsContext, ConfirmPaymentInput, EthValidateFeeArgs, + FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, MarketCoinOps, + MmCoin, MmCoinEnum, MyAddressError, MyWalletAddress, NegotiateSwapContractAddrErr, NumConversError, + NumConversResult, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, + RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, RewardTarget, RpcClientType, + RpcTransportEventHandler, RpcTransportEventHandlerShared, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignEthTransactionParams, SignRawTransactionEnum, + SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, SwapTxFeePolicy, + TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, + TradePreimageValue, Transaction, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, + TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, + WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, + WithdrawResult, EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_PAYMENT_STATE_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG}; pub use rlp; cfg_native! { use std::path::PathBuf; } -mod eth_balance_events; +pub mod eth_balance_events; mod eth_rpc; #[cfg(test)] mod eth_tests; #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; @@ -153,10 +153,9 @@ use eth_withdraw::{EthWithdraw, InitEthWithdraw, StandardEthWithdraw}; mod nonce; use nonce::ParityNonce; -mod eip1559_gas_fee; -pub(crate) use eip1559_gas_fee::FeePerGasEstimated; -use eip1559_gas_fee::{BlocknativeGasApiCaller, FeePerGasSimpleEstimator, GasApiConfig, GasApiProvider, - InfuraGasApiCaller}; +pub mod fee_estimation; +use fee_estimation::eip1559::{block_native::BlocknativeGasApiCaller, infura::InfuraGasApiCaller, + simple::FeePerGasSimpleEstimator, FeePerGasEstimated, GasApiConfig, GasApiProvider}; pub(crate) mod eth_swap_v2; /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol @@ -380,7 +379,8 @@ impl TryFrom for PayForGasOption { type GasDetails = (U256, PayForGasOption); -#[derive(Debug, Display, EnumFromStringify)] +#[derive(Debug, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] pub enum Web3RpcError { #[display(fmt = "Transport: {}", _0)] Transport(String), @@ -614,29 +614,6 @@ impl From for EthPrivKeyBuildPolicy { } } -/// Gas fee estimator loop context, runs a loop to estimate max fee and max priority fee per gas according to EIP-1559 for the next block -/// -/// This FeeEstimatorContext handles rpc requests which start and stop gas fee estimation loop and handles the loop itself. -/// FeeEstimatorContext keeps the latest estimated gas fees to return them on rpc request -pub(crate) struct FeeEstimatorContext { - /// Latest estimated gas fee values - pub(crate) estimated_fees: Arc>, - /// Handler for estimator loop graceful shutdown - pub(crate) abort_handler: AsyncMutex>, -} - -/// Gas fee estimator creation state -pub(crate) enum FeeEstimatorState { - /// Gas fee estimation not supported for this coin - CoinNotSupported, - /// Platform coin required to be enabled for gas fee estimation for this coin - PlatformCoinRequired, - /// Fee estimator created, use simple internal estimator - Simple(AsyncMutex), - /// Fee estimator created, use provider or simple internal estimator (if provider fails) - Provider(AsyncMutex), -} - /// pImpl idiom. pub struct EthCoinImpl { ticker: String, @@ -677,8 +654,6 @@ pub struct EthCoinImpl { /// consisting of the token address and token ID, separated by a comma. This field is essential for tracking the NFT assets /// information (chain & contract type, amount etc.), where ownership and amount, in ERC1155 case, might change over time. pub nfts_infos: Arc>>, - /// Context for eth fee per gas estimator loop. Created if coin supports fee per gas estimation - pub(crate) platform_fee_estimator_state: Arc, /// Config provided gas limits for swap and send transactions pub(crate) gas_limit: EthGasLimit, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation @@ -5146,14 +5121,14 @@ impl EthCoin { } /// Get gas base fee and suggest priority tip fees for the next block (see EIP-1559) - pub async fn get_eip1559_gas_fee(&self) -> Web3RpcResult { + pub async fn get_eip1559_gas_fee(&self, use_simple: bool) -> Web3RpcResult { let coin = self.clone(); let history_estimator_fut = FeePerGasSimpleEstimator::estimate_fee_by_history(&coin); let ctx = MmArc::from_weak(&coin.ctx).ok_or_else(|| MmError::new(Web3RpcError::Internal("ctx is null".into())))?; + let gas_api_conf = ctx.conf["gas_api"].clone(); - if gas_api_conf.is_null() { - debug!("No eth gas api provider config, using only history estimator"); + if gas_api_conf.is_null() || use_simple { return history_estimator_fut .await .map_err(|e| MmError::new(Web3RpcError::Internal(e.to_string()))); @@ -5190,7 +5165,7 @@ impl EthCoin { Ok(PayForGasOption::Legacy(LegacyGasPrice { gas_price })) }, SwapTxFeePolicy::Low | SwapTxFeePolicy::Medium | SwapTxFeePolicy::High => { - let fee_per_gas = coin.get_eip1559_gas_fee().await?; + let fee_per_gas = coin.get_eip1559_gas_fee(false).await?; let pay_result = match swap_fee_policy { SwapTxFeePolicy::Low => PayForGasOption::Eip1559(Eip1559FeePerGas { max_fee_per_gas: fee_per_gas.low.max_fee_per_gas, @@ -5346,16 +5321,6 @@ impl EthCoin { Box::new(fut.boxed().compat()) } - async fn spawn_balance_stream_if_enabled(&self, ctx: &MmArc) -> Result<(), String> { - if let Some(stream_config) = &ctx.event_stream_configuration { - if let EventInitStatus::Failed(err) = EventBehaviour::spawn_if_active(self.clone(), stream_config).await { - return ERR!("Failed spawning balance events. Error: {}", err); - } - } - - Ok(()) - } - /// Requests the nonce from all available nodes and returns the highest nonce available with the list of nodes that returned the highest nonce. /// Transactions will be sent using the nodes that returned the highest nonce. pub fn get_addr_nonce( @@ -5482,7 +5447,7 @@ impl EthTxFeeDetails { impl MmCoin for EthCoin { fn is_asset_chain(&self) -> bool { false } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(get_raw_transaction_impl(self.clone(), req).boxed().compat()) @@ -6372,7 +6337,6 @@ pub async fn eth_coin_from_conf_and_request( // all spawned futures related to `ETH` coin will be aborted as well. let abortable_system = try_s!(ctx.abortable_system.create_subsystem()); - let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(ctx, conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(ctx, conf, &coin_type).await?; let gas_limit = extract_gas_limit_from_conf(conf)?; @@ -6399,15 +6363,11 @@ pub async fn eth_coin_from_conf_and_request( address_nonce_locks, erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), - platform_fee_estimator_state, gas_limit, abortable_system, }; - let coin = EthCoin(Arc::new(coin)); - coin.spawn_balance_stream_if_enabled(ctx).await?; - - Ok(coin) + Ok(EthCoin(Arc::new(coin))) } /// Displays the address in mixed-case checksum form @@ -7215,7 +7175,6 @@ impl EthCoin { address_nonce_locks: Arc::clone(&self.address_nonce_locks), erc20_tokens_infos: Arc::clone(&self.erc20_tokens_infos), nfts_infos: Arc::clone(&self.nfts_infos), - platform_fee_estimator_state: Arc::clone(&self.platform_fee_estimator_state), gas_limit: EthGasLimit::default(), abortable_system: self.abortable_system.create_subsystem().unwrap(), }; diff --git a/mm2src/coins/eth/eip1559_gas_fee.rs b/mm2src/coins/eth/eip1559_gas_fee.rs deleted file mode 100644 index 4d33781f39..0000000000 --- a/mm2src/coins/eth/eip1559_gas_fee.rs +++ /dev/null @@ -1,499 +0,0 @@ -//! 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 ethereum_types::U256; -use mm2_err_handle::mm_error::MmError; -use mm2_err_handle::or_mm_error::OrMmError; -use mm2_number::BigDecimal; -use num_traits::FromPrimitive; -use std::convert::TryFrom; -use url::Url; -use web3::types::BlockNumber; - -pub(crate) use gas_api::BlocknativeGasApiCaller; -pub(crate) use gas_api::InfuraGasApiCaller; - -use gas_api::{BlocknativeBlockPricesResponse, InfuraFeePerGas}; - -const FEE_PER_GAS_LEVELS: usize = 3; - -/// Indicates which provider was used to get fee per gas estimations -#[derive(Clone, Debug)] -pub enum EstimationSource { - /// filled by default values - Empty, - /// internal simple estimator - Simple, - Infura, - Blocknative, -} - -impl ToString for EstimationSource { - fn to_string(&self) -> String { - match self { - EstimationSource::Empty => "empty".into(), - EstimationSource::Simple => "simple".into(), - EstimationSource::Infura => "infura".into(), - EstimationSource::Blocknative => "blocknative".into(), - } - } -} - -impl Default for EstimationSource { - fn default() -> Self { Self::Empty } -} - -enum PriorityLevelId { - Low = 0, - Medium = 1, - High = 2, -} - -/// Supported gas api providers -#[derive(Deserialize)] -pub enum GasApiProvider { - Infura, - Blocknative, -} - -#[derive(Deserialize)] -pub struct GasApiConfig { - /// gas api provider name to use - pub provider: GasApiProvider, - /// gas api provider or proxy base url (scheme, host and port without the relative part) - pub url: Url, -} - -/// Priority level estimated max fee per gas -#[derive(Clone, Debug, Default)] -pub struct FeePerGasLevel { - /// estimated max priority tip fee per gas in wei - pub max_priority_fee_per_gas: U256, - /// estimated max fee per gas in wei - pub max_fee_per_gas: U256, - /// estimated transaction min wait time in mempool in ms for this priority level - pub min_wait_time: Option, - /// estimated transaction max wait time in mempool in ms for this priority level - pub max_wait_time: Option, -} - -/// Internal struct for estimated fee per gas for several priority levels, in wei -/// low/medium/high levels are supported -#[derive(Default, Debug, Clone)] -pub struct FeePerGasEstimated { - /// base fee for the next block in wei - pub base_fee: U256, - /// estimated low priority fee - pub low: FeePerGasLevel, - /// estimated medium priority fee - pub medium: FeePerGasLevel, - /// estimated high priority fee - pub high: FeePerGasLevel, - /// which estimator used - pub source: EstimationSource, - /// base trend (up or down) - pub base_fee_trend: String, - /// priority trend (up or down) - pub priority_fee_trend: String, -} - -impl TryFrom for FeePerGasEstimated { - type Error = MmError; - - fn try_from(infura_fees: InfuraFeePerGas) -> Result { - Ok(Self { - 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)?, - 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 - )?, - 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)?, - min_wait_time: Some(infura_fees.high.min_wait_time_estimate), - max_wait_time: Some(infura_fees.high.max_wait_time_estimate), - }, - source: EstimationSource::Infura, - base_fee_trend: infura_fees.base_fee_trend, - priority_fee_trend: infura_fees.priority_fee_trend, - }) - } -} - -impl TryFrom for FeePerGasEstimated { - type Error = MmError; - - fn try_from(block_prices: BlocknativeBlockPricesResponse) -> Result { - if block_prices.block_prices.is_empty() { - return Ok(FeePerGasEstimated::default()); - } - if block_prices.block_prices[0].estimated_prices.len() < FEE_PER_GAS_LEVELS { - return Ok(FeePerGasEstimated::default()); - } - Ok(Self { - 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_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_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_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, - }, - source: EstimationSource::Blocknative, - base_fee_trend: String::default(), - priority_fee_trend: String::default(), - }) - } -} - -/// Simple priority fee per gas estimator based on fee history -/// normally used if gas api provider is not available -pub(crate) struct FeePerGasSimpleEstimator {} - -impl FeePerGasSimpleEstimator { - // TODO: add minimal max fee and priority fee - /// depth to look for fee history to estimate priority fees - const FEE_PRIORITY_DEPTH: u64 = 5u64; - - /// percentiles to pass to eth_feeHistory - const HISTORY_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [25.0, 50.0, 75.0]; - - /// percentile to predict next base fee over historical rewards - const BASE_FEE_PERCENTILE: f64 = 75.0; - - /// percentiles to calc max priority fee over historical rewards - const PRIORITY_FEE_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [50.0, 50.0, 50.0]; - - /// adjustment for max fee per gas picked up by sampling - const ADJUST_MAX_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.1, 1.175, 1.25]; // 1.25 assures max_fee_per_gas will be over next block base_fee - - /// adjustment for max priority fee picked up by sampling - const ADJUST_MAX_PRIORITY_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.0, 1.0, 1.0]; - - /// block depth for eth_feeHistory - pub fn history_depth() -> u64 { Self::FEE_PRIORITY_DEPTH } - - /// percentiles for priority rewards obtained with eth_feeHistory - pub fn history_percentiles() -> &'static [f64] { &Self::HISTORY_PERCENTILES } - - /// percentile for vector - fn percentile_of(v: &[U256], percent: f64) -> U256 { - let mut v_mut = v.to_owned(); - v_mut.sort(); - - // validate bounds: - let percent = if percent > 100.0 { 100.0 } else { percent }; - let percent = if percent < 0.0 { 0.0 } else { percent }; - - let value_pos = ((v_mut.len() - 1) as f64 * percent / 100.0).round() as usize; - v_mut[value_pos] - } - - /// Estimate simplified gas priority fees based on fee history - pub async fn estimate_fee_by_history(coin: &EthCoin) -> Web3RpcResult { - let res: Result = coin - .eth_fee_history( - U256::from(Self::history_depth()), - BlockNumber::Latest, - Self::history_percentiles(), - ) - .await; - - match res { - Ok(fee_history) => Ok(Self::calculate_with_history(&fee_history)?), - Err(_) => MmError::err(Web3RpcError::Internal("Eth requests failed".into())), - } - } - - fn predict_base_fee(base_fees: &[U256]) -> U256 { Self::percentile_of(base_fees, Self::BASE_FEE_PERCENTILE) } - - fn priority_fee_for_level( - level: PriorityLevelId, - base_fee: BigDecimal, - fee_history: &FeeHistoryResult, - ) -> Web3RpcResult { - let level_index = level as usize; - let level_rewards = fee_history - .priority_rewards - .as_ref() - .or_mm_err(|| Web3RpcError::Internal("expected reward in eth_feeHistory".into()))? - .iter() - .map(|rewards| rewards.get(level_index).copied().unwrap_or_else(|| U256::from(0))) - .collect::>(); - - // Calculate the max priority fee per gas based on the rewards percentile. - 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)); - - // Calculate the max fee per gas by adjusting the base fee and adding the priority fee. - let adjust_max_fee = - BigDecimal::from_f64(Self::ADJUST_MAX_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); - let adjust_max_priority_fee = - BigDecimal::from_f64(Self::ADJUST_MAX_PRIORITY_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); - - // TODO: consider use checked ops - let max_fee_per_gas_dec = base_fee * adjust_max_fee + max_priority_fee_per_gas_gwei * adjust_max_priority_fee; - - Ok(FeePerGasLevel { - max_priority_fee_per_gas, - 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, - }) - } - - /// estimate priority fees by fee history - fn calculate_with_history(fee_history: &FeeHistoryResult) -> Web3RpcResult { - // For estimation of max fee and max priority fee we use latest block base_fee but adjusted. - // Apparently for this simple fee estimator for assured high priority we should assume - // that the real base_fee may go up by 1,25 (i.e. if the block is full). This is covered by high priority ADJUST_MAX_FEE multiplier - let latest_base_fee = fee_history - .base_fee_per_gas - .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)); - - // 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) - let predicted_base_fee = Self::predict_base_fee(&fee_history.base_fee_per_gas); - Ok(FeePerGasEstimated { - base_fee: predicted_base_fee, - low: Self::priority_fee_for_level(PriorityLevelId::Low, latest_base_fee_dec.clone(), fee_history)?, - medium: Self::priority_fee_for_level(PriorityLevelId::Medium, latest_base_fee_dec.clone(), fee_history)?, - high: Self::priority_fee_for_level(PriorityLevelId::High, latest_base_fee_dec, fee_history)?, - source: EstimationSource::Simple, - base_fee_trend: String::default(), - priority_fee_trend: String::default(), - }) - } -} - -mod gas_api { - use std::convert::TryInto; - - use super::FeePerGasEstimated; - use crate::eth::{Web3RpcError, Web3RpcResult}; - use http::StatusCode; - use mm2_err_handle::mm_error::MmError; - use mm2_err_handle::prelude::*; - use mm2_net::transport::slurp_url_with_headers; - use mm2_number::BigDecimal; - use serde_json::{self as json}; - use url::Url; - - lazy_static! { - /// API key for testing - static ref INFURA_GAS_API_AUTH_TEST: String = std::env::var("INFURA_GAS_API_AUTH_TEST").unwrap_or_default(); - } - - #[derive(Clone, Debug, Deserialize)] - pub(crate) struct InfuraFeePerGasLevel { - #[serde(rename = "suggestedMaxPriorityFeePerGas")] - pub suggested_max_priority_fee_per_gas: BigDecimal, - #[serde(rename = "suggestedMaxFeePerGas")] - pub suggested_max_fee_per_gas: BigDecimal, - #[serde(rename = "minWaitTimeEstimate")] - pub min_wait_time_estimate: u32, - #[serde(rename = "maxWaitTimeEstimate")] - pub max_wait_time_estimate: u32, - } - - /// Infura gas api response - /// see https://docs.infura.io/api/infura-expansion-apis/gas-api/api-reference/gasprices-type2 - #[allow(dead_code)] - #[derive(Debug, Deserialize)] - pub(crate) struct InfuraFeePerGas { - pub low: InfuraFeePerGasLevel, - pub medium: InfuraFeePerGasLevel, - pub high: InfuraFeePerGasLevel, - #[serde(rename = "estimatedBaseFee")] - pub estimated_base_fee: BigDecimal, - #[serde(rename = "networkCongestion")] - pub network_congestion: BigDecimal, - #[serde(rename = "latestPriorityFeeRange")] - pub latest_priority_fee_range: Vec, - #[serde(rename = "historicalPriorityFeeRange")] - pub historical_priority_fee_range: Vec, - #[serde(rename = "historicalBaseFeeRange")] - pub historical_base_fee_range: Vec, - #[serde(rename = "priorityFeeTrend")] - pub priority_fee_trend: String, // we are not using enum here bcz values not mentioned in docs could be received - #[serde(rename = "baseFeeTrend")] - pub base_fee_trend: String, - } - - /// Infura gas api provider caller - #[allow(dead_code)] - pub(crate) struct InfuraGasApiCaller {} - - #[allow(dead_code)] - impl InfuraGasApiCaller { - const INFURA_GAS_FEES_ENDPOINT: &'static str = "networks/1/suggestedGasFees"; // Support only main chain - - fn get_infura_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { - let mut url = base_url.clone(); - url.set_path(Self::INFURA_GAS_FEES_ENDPOINT); - let headers = vec![("Authorization", INFURA_GAS_API_AUTH_TEST.as_str())]; - (url, headers) - } - - async fn make_infura_gas_api_request( - url: &Url, - headers: Vec<(&'static str, &'static str)>, - ) -> Result> { - let resp = slurp_url_with_headers(url.as_str(), headers) - .await - .mm_err(|e| e.to_string())?; - if resp.0 != StatusCode::OK { - let error = format!("{} failed with status code {}", url, resp.0); - return MmError::err(error); - } - let estimated_fees = json::from_slice(&resp.2).map_to_mm(|e| e.to_string())?; - Ok(estimated_fees) - } - - /// Fetch fee per gas estimations from infura provider - pub async fn fetch_infura_fee_estimation(base_url: &Url) -> Web3RpcResult { - let (url, headers) = Self::get_infura_gas_api_url(base_url); - let infura_estimated_fees = Self::make_infura_gas_api_request(&url, headers) - .await - .mm_err(Web3RpcError::Transport)?; - infura_estimated_fees.try_into().mm_err(Into::into) - } - } - - lazy_static! { - /// API key for testing - static ref BLOCKNATIVE_GAS_API_AUTH_TEST: String = std::env::var("BLOCKNATIVE_GAS_API_AUTH_TEST").unwrap_or_default(); - } - - #[allow(dead_code)] - #[derive(Clone, Debug, Deserialize)] - pub(crate) struct BlocknativeBlockPrices { - #[serde(rename = "blockNumber")] - pub block_number: u32, - #[serde(rename = "estimatedTransactionCount")] - pub estimated_transaction_count: u32, - #[serde(rename = "baseFeePerGas")] - pub base_fee_per_gas: BigDecimal, - #[serde(rename = "estimatedPrices")] - pub estimated_prices: Vec, - } - - #[allow(dead_code)] - #[derive(Clone, Debug, Deserialize)] - pub(crate) struct BlocknativeEstimatedPrices { - pub confidence: u32, - pub price: BigDecimal, - #[serde(rename = "maxPriorityFeePerGas")] - pub max_priority_fee_per_gas: BigDecimal, - #[serde(rename = "maxFeePerGas")] - pub max_fee_per_gas: BigDecimal, - } - - /// Blocknative gas prices response - /// see https://docs.blocknative.com/gas-prediction/gas-platform - #[allow(dead_code)] - #[derive(Debug, Deserialize)] - pub(crate) struct BlocknativeBlockPricesResponse { - pub system: String, - pub network: String, - pub unit: String, - #[serde(rename = "maxPrice")] - pub max_price: BigDecimal, - #[serde(rename = "currentBlockNumber")] - pub current_block_number: u32, - #[serde(rename = "msSinceLastBlock")] - pub ms_since_last_block: u32, - #[serde(rename = "blockPrices")] - pub block_prices: Vec, - } - - /// Blocknative gas api provider caller - #[allow(dead_code)] - pub(crate) struct BlocknativeGasApiCaller {} - - #[allow(dead_code)] - impl BlocknativeGasApiCaller { - const BLOCKNATIVE_GAS_PRICES_ENDPOINT: &'static str = "gasprices/blockprices"; - const BLOCKNATIVE_GAS_PRICES_LOW: &'static str = "10"; - const BLOCKNATIVE_GAS_PRICES_MEDIUM: &'static str = "50"; - const BLOCKNATIVE_GAS_PRICES_HIGH: &'static str = "90"; - - fn get_blocknative_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { - let mut url = base_url.clone(); - url.set_path(Self::BLOCKNATIVE_GAS_PRICES_ENDPOINT); - url.query_pairs_mut() - .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_LOW) - .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_MEDIUM) - .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_HIGH) - .append_pair("withBaseFees", "true"); - - let headers = vec![("Authorization", BLOCKNATIVE_GAS_API_AUTH_TEST.as_str())]; - (url, headers) - } - - async fn make_blocknative_gas_api_request( - url: &Url, - headers: Vec<(&'static str, &'static str)>, - ) -> Result> { - let resp = slurp_url_with_headers(url.as_str(), headers) - .await - .mm_err(|e| e.to_string())?; - if resp.0 != StatusCode::OK { - let error = format!("{} failed with status code {}", url, resp.0); - return MmError::err(error); - } - let block_prices = json::from_slice(&resp.2).map_err(|e| e.to_string())?; - Ok(block_prices) - } - - /// Fetch fee per gas estimations from blocknative provider - pub async fn fetch_blocknative_fee_estimation(base_url: &Url) -> Web3RpcResult { - let (url, headers) = Self::get_blocknative_gas_api_url(base_url); - let block_prices = Self::make_blocknative_gas_api_request(&url, headers) - .await - .mm_err(Web3RpcError::Transport)?; - block_prices.try_into().mm_err(Into::into) - } - } -} diff --git a/mm2src/coins/eth/eth_balance_events.rs b/mm2src/coins/eth/eth_balance_events.rs index 231aa68507..b62bd62ed1 100644 --- a/mm2src/coins/eth/eth_balance_events.rs +++ b/mm2src/coins/eth/eth_balance_events.rs @@ -1,21 +1,50 @@ +use super::EthCoin; +use crate::{eth::{u256_to_big_decimal, Erc20TokenInfo}, + BalanceError, CoinWithDerivationMethod}; +use common::{executor::Timer, log, Future01CompatExt}; +use mm2_err_handle::prelude::MmError; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_number::BigDecimal; + use async_trait::async_trait; -use common::{executor::{AbortSettings, SpawnAbortable, Timer}, - log, Future01CompatExt}; use ethereum_types::Address; -use futures::{channel::oneshot::{self, Receiver, Sender}, - stream::FuturesUnordered, - StreamExt}; +use futures::{channel::oneshot, stream::FuturesUnordered, StreamExt}; use instant::Instant; -use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::MmError; -use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - ErrorEventName, Event, EventName, EventStreamConfiguration}; -use mm2_number::BigDecimal; +use serde::Deserialize; +use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; -use super::EthCoin; -use crate::{eth::{u256_to_big_decimal, Erc20TokenInfo}, - BalanceError, CoinWithDerivationMethod, MmCoin}; +#[derive(Deserialize)] +#[serde(deny_unknown_fields, default)] +struct EthBalanceStreamingConfig { + /// The time in seconds to wait before re-polling the balance and streaming. + pub stream_interval_seconds: f64, +} + +impl Default for EthBalanceStreamingConfig { + fn default() -> Self { + Self { + stream_interval_seconds: 10.0, + } + } +} + +pub struct EthBalanceEventStreamer { + /// The period in seconds between each balance check. + interval: f64, + coin: EthCoin, +} + +impl EthBalanceEventStreamer { + pub fn try_new(config: Option, coin: EthCoin) -> serde_json::Result { + let config: EthBalanceStreamingConfig = config.map(serde_json::from_value).unwrap_or(Ok(Default::default()))?; + + Ok(Self { + interval: config.stream_interval_seconds, + coin, + }) + } +} struct BalanceData { ticker: String, @@ -23,6 +52,7 @@ struct BalanceData { balance: BigDecimal, } +#[derive(Serialize)] struct BalanceFetchError { ticker: String, address: String, @@ -113,15 +143,18 @@ async fn fetch_balance( } #[async_trait] -impl EventBehaviour for EthCoin { - fn event_name() -> EventName { EventName::CoinBalance } - - fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } - - async fn handle(self, interval: f64, tx: oneshot::Sender) { - const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; - - async fn start_polling(coin: EthCoin, ctx: MmArc, interval: f64) { +impl EventStreamer for EthBalanceEventStreamer { + type DataInType = NoDataIn; + + fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + async fn start_polling(streamer_id: String, broadcaster: Broadcaster, coin: EthCoin, interval: f64) { async fn sleep_remaining_time(interval: f64, now: Instant) { // If the interval is x seconds, // our goal is to broadcast changed balances every x seconds. @@ -145,12 +178,7 @@ impl EventBehaviour for EthCoin { Err(e) => { log::error!("Failed getting addresses for {}. Error: {}", coin.ticker, e); let e = serde_json::to_value(e).expect("Serialization shouldn't fail."); - ctx.stream_channel_controller - .broadcast(Event::new( - format!("{}:{}", EthCoin::error_event_name(), coin.ticker), - e.to_string(), - )) - .await; + broadcaster.broadcast(Event::err(streamer_id.clone(), e)); sleep_remaining_time(interval, now).await; continue; }, @@ -181,60 +209,24 @@ impl EventBehaviour for EthCoin { err.address, err.error ); - let e = serde_json::to_value(err.error).expect("Serialization shouldn't fail."); - ctx.stream_channel_controller - .broadcast(Event::new( - format!("{}:{}:{}", EthCoin::error_event_name(), err.ticker, err.address), - e.to_string(), - )) - .await; + let e = serde_json::to_value(err).expect("Serialization shouldn't fail."); + broadcaster.broadcast(Event::err(streamer_id.clone(), e)); }, }; } if !balance_updates.is_empty() { - ctx.stream_channel_controller - .broadcast(Event::new( - EthCoin::event_name().to_string(), - json!(balance_updates).to_string(), - )) - .await; + broadcaster.broadcast(Event::new(streamer_id.clone(), json!(balance_updates))); } sleep_remaining_time(interval, now).await; } } - let ctx = match MmArc::from_weak(&self.ctx) { - Some(ctx) => ctx, - None => { - let msg = "MM context must have been initialized already."; - tx.send(EventInitStatus::Failed(msg.to_owned())) - .expect(RECEIVER_DROPPED_MSG); - panic!("{}", msg); - }, - }; - - tx.send(EventInitStatus::Success).expect(RECEIVER_DROPPED_MSG); + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); - start_polling(self, ctx, interval).await - } - - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - log::info!("{} event is activated for {}", Self::event_name(), self.ticker,); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = - AbortSettings::info_on_abort(format!("{} event is stopped for {}.", Self::event_name(), self.ticker)); - self.spawner().spawn_with_settings(fut, settings); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive - } + start_polling(self.streamer_id(), broadcaster, self.coin, self.interval).await } } diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index c7f1e51d13..83e642ef5a 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -216,16 +216,13 @@ fn test_withdraw_impl_manual_fee() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: "0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94".to_string(), coin: "ETH".to_string(), - max: false, fee: Some(WithdrawFee::EthGas { gas: gas_limit::ETH_MAX_TRADE_GAS, gas_price: 1.into(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; block_on_f01(coin.get_balance()).unwrap(); @@ -265,16 +262,13 @@ fn test_withdraw_impl_fee_details() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: "0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94".to_string(), coin: "JST".to_string(), - max: false, fee: Some(WithdrawFee::EthGas { gas: gas_limit::ETH_MAX_TRADE_GAS, gas_price: 1.into(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; block_on_f01(coin.get_balance()).unwrap(); diff --git a/mm2src/coins/eth/fee_estimation/eip1559/block_native.rs b/mm2src/coins/eth/fee_estimation/eip1559/block_native.rs new file mode 100644 index 0000000000..2f301e7c85 --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/eip1559/block_native.rs @@ -0,0 +1,158 @@ +use super::{EstimationSource, FeePerGasEstimated, FeePerGasLevel, FEE_PER_GAS_LEVELS}; +use crate::eth::{Web3RpcError, Web3RpcResult}; +use crate::{wei_from_gwei_decimal, NumConversError}; +use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::prelude::*; +use mm2_net::transport::slurp_url_with_headers; +use mm2_number::BigDecimal; + +use http::StatusCode; +use serde_json::{self as json}; +use std::convert::TryFrom; +use std::convert::TryInto; +use url::Url; + +lazy_static! { + /// API key for testing + static ref BLOCKNATIVE_GAS_API_AUTH_TEST: String = std::env::var("BLOCKNATIVE_GAS_API_AUTH_TEST").unwrap_or_default(); +} + +#[allow(dead_code)] +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct BlocknativeBlockPrices { + #[serde(rename = "blockNumber")] + pub block_number: u32, + #[serde(rename = "estimatedTransactionCount")] + pub estimated_transaction_count: u32, + #[serde(rename = "baseFeePerGas")] + pub base_fee_per_gas: BigDecimal, + #[serde(rename = "estimatedPrices")] + pub estimated_prices: Vec, +} + +#[allow(dead_code)] +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct BlocknativeEstimatedPrices { + pub confidence: u32, + pub price: BigDecimal, + #[serde(rename = "maxPriorityFeePerGas")] + pub max_priority_fee_per_gas: BigDecimal, + #[serde(rename = "maxFeePerGas")] + pub max_fee_per_gas: BigDecimal, +} + +/// Blocknative gas prices response +/// see https://docs.blocknative.com/gas-prediction/gas-platform +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub(crate) struct BlocknativeBlockPricesResponse { + pub system: String, + pub network: String, + pub unit: String, + #[serde(rename = "maxPrice")] + pub max_price: BigDecimal, + #[serde(rename = "currentBlockNumber")] + pub current_block_number: u32, + #[serde(rename = "msSinceLastBlock")] + pub ms_since_last_block: u32, + #[serde(rename = "blockPrices")] + pub block_prices: Vec, +} + +impl TryFrom for FeePerGasEstimated { + type Error = MmError; + + fn try_from(block_prices: BlocknativeBlockPricesResponse) -> Result { + if block_prices.block_prices.is_empty() { + return Ok(FeePerGasEstimated::default()); + } + if block_prices.block_prices[0].estimated_prices.len() < FEE_PER_GAS_LEVELS { + return Ok(FeePerGasEstimated::default()); + } + Ok(Self { + 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_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_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_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, + }, + source: EstimationSource::Blocknative, + base_fee_trend: String::default(), + priority_fee_trend: String::default(), + }) + } +} + +/// Blocknative gas api provider caller +#[allow(dead_code)] +pub(crate) struct BlocknativeGasApiCaller {} + +#[allow(dead_code)] +impl BlocknativeGasApiCaller { + const BLOCKNATIVE_GAS_PRICES_ENDPOINT: &'static str = "gasprices/blockprices"; + const BLOCKNATIVE_GAS_PRICES_LOW: &'static str = "10"; + const BLOCKNATIVE_GAS_PRICES_MEDIUM: &'static str = "50"; + const BLOCKNATIVE_GAS_PRICES_HIGH: &'static str = "90"; + + fn get_blocknative_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { + let mut url = base_url.clone(); + url.set_path(Self::BLOCKNATIVE_GAS_PRICES_ENDPOINT); + url.query_pairs_mut() + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_LOW) + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_MEDIUM) + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_HIGH) + .append_pair("withBaseFees", "true"); + + let headers = vec![("Authorization", BLOCKNATIVE_GAS_API_AUTH_TEST.as_str())]; + (url, headers) + } + + async fn make_blocknative_gas_api_request( + url: &Url, + headers: Vec<(&'static str, &'static str)>, + ) -> Result> { + let resp = slurp_url_with_headers(url.as_str(), headers) + .await + .mm_err(|e| e.to_string())?; + if resp.0 != StatusCode::OK { + let error = format!("{} failed with status code {}", url, resp.0); + return MmError::err(error); + } + let block_prices = json::from_slice(&resp.2).map_err(|e| e.to_string())?; + Ok(block_prices) + } + + /// Fetch fee per gas estimations from blocknative provider + pub async fn fetch_blocknative_fee_estimation(base_url: &Url) -> Web3RpcResult { + let (url, headers) = Self::get_blocknative_gas_api_url(base_url); + let block_prices = Self::make_blocknative_gas_api_request(&url, headers) + .await + .mm_err(Web3RpcError::Transport)?; + block_prices.try_into().mm_err(Into::into) + } +} diff --git a/mm2src/coins/eth/fee_estimation/eip1559/infura.rs b/mm2src/coins/eth/fee_estimation/eip1559/infura.rs new file mode 100644 index 0000000000..b6aa4e84d7 --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/eip1559/infura.rs @@ -0,0 +1,127 @@ +use super::{EstimationSource, FeePerGasEstimated, FeePerGasLevel}; +use crate::eth::{Web3RpcError, Web3RpcResult}; +use crate::{wei_from_gwei_decimal, NumConversError}; +use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::prelude::*; +use mm2_net::transport::slurp_url_with_headers; +use mm2_number::BigDecimal; + +use http::StatusCode; +use serde_json::{self as json}; +use std::convert::TryFrom; +use std::convert::TryInto; +use url::Url; + +lazy_static! { + /// API key for testing + static ref INFURA_GAS_API_AUTH_TEST: String = std::env::var("INFURA_GAS_API_AUTH_TEST").unwrap_or_default(); +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct InfuraFeePerGasLevel { + #[serde(rename = "suggestedMaxPriorityFeePerGas")] + pub suggested_max_priority_fee_per_gas: BigDecimal, + #[serde(rename = "suggestedMaxFeePerGas")] + pub suggested_max_fee_per_gas: BigDecimal, + #[serde(rename = "minWaitTimeEstimate")] + pub min_wait_time_estimate: u32, + #[serde(rename = "maxWaitTimeEstimate")] + pub max_wait_time_estimate: u32, +} + +/// Infura gas api response +/// see https://docs.infura.io/api/infura-expansion-apis/gas-api/api-reference/gasprices-type2 +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub(crate) struct InfuraFeePerGas { + pub low: InfuraFeePerGasLevel, + pub medium: InfuraFeePerGasLevel, + pub high: InfuraFeePerGasLevel, + #[serde(rename = "estimatedBaseFee")] + pub estimated_base_fee: BigDecimal, + #[serde(rename = "networkCongestion")] + pub network_congestion: BigDecimal, + #[serde(rename = "latestPriorityFeeRange")] + pub latest_priority_fee_range: Vec, + #[serde(rename = "historicalPriorityFeeRange")] + pub historical_priority_fee_range: Vec, + #[serde(rename = "historicalBaseFeeRange")] + pub historical_base_fee_range: Vec, + #[serde(rename = "priorityFeeTrend")] + pub priority_fee_trend: String, // we are not using enum here bcz values not mentioned in docs could be received + #[serde(rename = "baseFeeTrend")] + pub base_fee_trend: String, +} + +impl TryFrom for FeePerGasEstimated { + type Error = MmError; + + fn try_from(infura_fees: InfuraFeePerGas) -> Result { + Ok(Self { + 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)?, + 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 + )?, + 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)?, + min_wait_time: Some(infura_fees.high.min_wait_time_estimate), + max_wait_time: Some(infura_fees.high.max_wait_time_estimate), + }, + source: EstimationSource::Infura, + base_fee_trend: infura_fees.base_fee_trend, + priority_fee_trend: infura_fees.priority_fee_trend, + }) + } +} + +/// Infura gas api provider caller +#[allow(dead_code)] +pub(crate) struct InfuraGasApiCaller {} + +#[allow(dead_code)] +impl InfuraGasApiCaller { + const INFURA_GAS_FEES_ENDPOINT: &'static str = "networks/1/suggestedGasFees"; // Support only main chain + + fn get_infura_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { + let mut url = base_url.clone(); + url.set_path(Self::INFURA_GAS_FEES_ENDPOINT); + let headers = vec![("Authorization", INFURA_GAS_API_AUTH_TEST.as_str())]; + (url, headers) + } + + async fn make_infura_gas_api_request( + url: &Url, + headers: Vec<(&'static str, &'static str)>, + ) -> Result> { + let resp = slurp_url_with_headers(url.as_str(), headers) + .await + .mm_err(|e| e.to_string())?; + if resp.0 != StatusCode::OK { + let error = format!("{} failed with status code {}", url, resp.0); + return MmError::err(error); + } + let estimated_fees = json::from_slice(&resp.2).map_to_mm(|e| e.to_string())?; + Ok(estimated_fees) + } + + /// Fetch fee per gas estimations from infura provider + pub async fn fetch_infura_fee_estimation(base_url: &Url) -> Web3RpcResult { + let (url, headers) = Self::get_infura_gas_api_url(base_url); + let infura_estimated_fees = Self::make_infura_gas_api_request(&url, headers) + .await + .mm_err(Web3RpcError::Transport)?; + infura_estimated_fees.try_into().mm_err(Into::into) + } +} diff --git a/mm2src/coins/eth/fee_estimation/eip1559/mod.rs b/mm2src/coins/eth/fee_estimation/eip1559/mod.rs new file mode 100644 index 0000000000..b4c3ffbfbc --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/eip1559/mod.rs @@ -0,0 +1,89 @@ +//! Provides estimations of base and priority fee per gas or fetch estimations from a gas api provider +pub mod block_native; +pub mod infura; +pub mod simple; + +use ethereum_types::U256; +use url::Url; + +const FEE_PER_GAS_LEVELS: usize = 3; + +/// Indicates which provider was used to get fee per gas estimations +#[derive(Clone, Debug)] +pub enum EstimationSource { + /// filled by default values + Empty, + /// internal simple estimator + Simple, + Infura, + Blocknative, +} + +impl ToString for EstimationSource { + fn to_string(&self) -> String { + match self { + EstimationSource::Empty => "empty".into(), + EstimationSource::Simple => "simple".into(), + EstimationSource::Infura => "infura".into(), + EstimationSource::Blocknative => "blocknative".into(), + } + } +} + +impl Default for EstimationSource { + fn default() -> Self { Self::Empty } +} + +enum PriorityLevelId { + Low = 0, + Medium = 1, + High = 2, +} + +/// Supported gas api providers +#[derive(Clone, Deserialize)] +pub enum GasApiProvider { + Infura, + Blocknative, +} + +#[derive(Clone, Deserialize)] +pub struct GasApiConfig { + /// gas api provider name to use + pub provider: GasApiProvider, + /// gas api provider or proxy base url (scheme, host and port without the relative part) + pub url: Url, +} + +/// Priority level estimated max fee per gas +#[derive(Clone, Debug, Default)] +pub struct FeePerGasLevel { + /// estimated max priority tip fee per gas in wei + pub max_priority_fee_per_gas: U256, + /// estimated max fee per gas in wei + pub max_fee_per_gas: U256, + /// estimated transaction min wait time in mempool in ms for this priority level + pub min_wait_time: Option, + /// estimated transaction max wait time in mempool in ms for this priority level + pub max_wait_time: Option, +} + +/// Internal struct for estimated fee per gas for several priority levels, in wei +/// low/medium/high levels are supported +#[derive(Default, Debug, Clone)] +pub struct FeePerGasEstimated { + /// base fee for the next block in wei + pub base_fee: U256, + /// estimated low priority fee + pub low: FeePerGasLevel, + /// estimated medium priority fee + pub medium: FeePerGasLevel, + /// estimated high priority fee + pub high: FeePerGasLevel, + /// which estimator used + pub source: EstimationSource, + /// base trend (up or down) + pub base_fee_trend: String, + /// priority trend (up or down) + pub priority_fee_trend: String, +} diff --git a/mm2src/coins/eth/fee_estimation/eip1559/simple.rs b/mm2src/coins/eth/fee_estimation/eip1559/simple.rs new file mode 100644 index 0000000000..c6e1c3513d --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/eip1559/simple.rs @@ -0,0 +1,137 @@ +use super::{EstimationSource, FeePerGasEstimated, FeePerGasLevel, PriorityLevelId, FEE_PER_GAS_LEVELS}; +use crate::eth::web3_transport::FeeHistoryResult; +use crate::eth::{Web3RpcError, Web3RpcResult}; +use crate::{wei_from_gwei_decimal, wei_to_gwei_decimal, EthCoin}; +use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::or_mm_error::OrMmError; +use mm2_number::BigDecimal; + +use ethereum_types::U256; +use num_traits::FromPrimitive; +use web3::types::BlockNumber; + +/// Simple priority fee per gas estimator based on fee history +/// normally used if gas api provider is not available +pub(crate) struct FeePerGasSimpleEstimator {} + +impl FeePerGasSimpleEstimator { + // TODO: add minimal max fee and priority fee + /// depth to look for fee history to estimate priority fees + const FEE_PRIORITY_DEPTH: u64 = 5u64; + + /// percentiles to pass to eth_feeHistory + const HISTORY_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [25.0, 50.0, 75.0]; + + /// percentile to predict next base fee over historical rewards + const BASE_FEE_PERCENTILE: f64 = 75.0; + + /// percentiles to calc max priority fee over historical rewards + const PRIORITY_FEE_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [50.0, 50.0, 50.0]; + + /// adjustment for max fee per gas picked up by sampling + const ADJUST_MAX_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.1, 1.175, 1.25]; // 1.25 assures max_fee_per_gas will be over next block base_fee + + /// adjustment for max priority fee picked up by sampling + const ADJUST_MAX_PRIORITY_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.0, 1.0, 1.0]; + + /// block depth for eth_feeHistory + pub fn history_depth() -> u64 { Self::FEE_PRIORITY_DEPTH } + + /// percentiles for priority rewards obtained with eth_feeHistory + pub fn history_percentiles() -> &'static [f64] { &Self::HISTORY_PERCENTILES } + + /// percentile for vector + fn percentile_of(v: &[U256], percent: f64) -> U256 { + let mut v_mut = v.to_owned(); + v_mut.sort(); + + // validate bounds: + let percent = if percent > 100.0 { 100.0 } else { percent }; + let percent = if percent < 0.0 { 0.0 } else { percent }; + + let value_pos = ((v_mut.len() - 1) as f64 * percent / 100.0).round() as usize; + v_mut[value_pos] + } + + /// Estimate simplified gas priority fees based on fee history + pub async fn estimate_fee_by_history(coin: &EthCoin) -> Web3RpcResult { + let res: Result = coin + .eth_fee_history( + U256::from(Self::history_depth()), + BlockNumber::Latest, + Self::history_percentiles(), + ) + .await; + + match res { + Ok(fee_history) => Ok(Self::calculate_with_history(&fee_history)?), + Err(_) => MmError::err(Web3RpcError::Internal("Eth requests failed".into())), + } + } + + fn predict_base_fee(base_fees: &[U256]) -> U256 { Self::percentile_of(base_fees, Self::BASE_FEE_PERCENTILE) } + + fn priority_fee_for_level( + level: PriorityLevelId, + base_fee: BigDecimal, + fee_history: &FeeHistoryResult, + ) -> Web3RpcResult { + let level_index = level as usize; + let level_rewards = fee_history + .priority_rewards + .as_ref() + .or_mm_err(|| Web3RpcError::Internal("expected reward in eth_feeHistory".into()))? + .iter() + .map(|rewards| rewards.get(level_index).copied().unwrap_or_else(|| U256::from(0))) + .collect::>(); + + // Calculate the max priority fee per gas based on the rewards percentile. + 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)); + + // Calculate the max fee per gas by adjusting the base fee and adding the priority fee. + let adjust_max_fee = + BigDecimal::from_f64(Self::ADJUST_MAX_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); + let adjust_max_priority_fee = + BigDecimal::from_f64(Self::ADJUST_MAX_PRIORITY_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); + + // TODO: consider use checked ops + let max_fee_per_gas_dec = base_fee * adjust_max_fee + max_priority_fee_per_gas_gwei * adjust_max_priority_fee; + + Ok(FeePerGasLevel { + max_priority_fee_per_gas, + 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, + }) + } + + /// estimate priority fees by fee history + fn calculate_with_history(fee_history: &FeeHistoryResult) -> Web3RpcResult { + // For estimation of max fee and max priority fee we use latest block base_fee but adjusted. + // Apparently for this simple fee estimator for assured high priority we should assume + // that the real base_fee may go up by 1,25 (i.e. if the block is full). This is covered by high priority ADJUST_MAX_FEE multiplier + let latest_base_fee = fee_history + .base_fee_per_gas + .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)); + + // 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) + let predicted_base_fee = Self::predict_base_fee(&fee_history.base_fee_per_gas); + Ok(FeePerGasEstimated { + base_fee: predicted_base_fee, + low: Self::priority_fee_for_level(PriorityLevelId::Low, latest_base_fee_dec.clone(), fee_history)?, + medium: Self::priority_fee_for_level(PriorityLevelId::Medium, latest_base_fee_dec.clone(), fee_history)?, + high: Self::priority_fee_for_level(PriorityLevelId::High, latest_base_fee_dec, fee_history)?, + source: EstimationSource::Simple, + base_fee_trend: String::default(), + priority_fee_trend: String::default(), + }) + } +} diff --git a/mm2src/coins/eth/fee_estimation/eth_fee_events.rs b/mm2src/coins/eth/fee_estimation/eth_fee_events.rs new file mode 100644 index 0000000000..8f49ebf72b --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/eth_fee_events.rs @@ -0,0 +1,95 @@ +use super::ser::FeePerGasEstimated; +use crate::eth::EthCoin; +use common::executor::Timer; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; + +use async_trait::async_trait; +use futures::channel::oneshot; +use instant::Instant; +use serde::Deserialize; +use std::convert::TryFrom; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +/// Types of estimators available. +/// Simple - simple internal gas price estimator based on historical data. +/// Provider - gas price estimator using external provider (using gas api). +pub enum EstimatorType { + Simple, + Provider, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields, default)] +pub struct EthFeeStreamingConfig { + /// The time in seconds to wait before re-estimating the gas fees. + pub estimate_every: f64, + /// The type of the estimator to use. + pub estimator_type: EstimatorType, +} + +impl Default for EthFeeStreamingConfig { + fn default() -> Self { + Self { + // FIXME: https://github.com/KomodoPlatform/komodo-defi-framework/pull/2172#discussion_r1785054117 + estimate_every: 15.0, + estimator_type: EstimatorType::Simple, + } + } +} + +pub struct EthFeeEventStreamer { + config: EthFeeStreamingConfig, + coin: EthCoin, +} + +impl EthFeeEventStreamer { + #[inline(always)] + pub fn new(config: EthFeeStreamingConfig, coin: EthCoin) -> Self { Self { config, coin } } +} + +#[async_trait] +impl EventStreamer for EthFeeEventStreamer { + type DataInType = NoDataIn; + + fn streamer_id(&self) -> String { format!("FEE_ESTIMATION:{}", self.coin.ticker) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + let use_simple = matches!(self.config.estimator_type, EstimatorType::Simple); + loop { + let now = Instant::now(); + match self + .coin + .get_eip1559_gas_fee(use_simple) + .await + .map(FeePerGasEstimated::try_from) + { + Ok(Ok(fee)) => { + let fee = serde_json::to_value(fee).expect("Serialization shouldn't fail"); + broadcaster.broadcast(Event::new(self.streamer_id(), fee)); + }, + Ok(Err(err)) => { + let err = json!({ "error": err.to_string() }); + broadcaster.broadcast(Event::err(self.streamer_id(), err)); + }, + Err(err) => { + let err = serde_json::to_value(err).expect("Serialization shouldn't fail"); + broadcaster.broadcast(Event::err(self.streamer_id(), err)); + }, + } + let sleep_time = self.config.estimate_every - now.elapsed().as_secs_f64(); + if sleep_time >= 0.1 { + Timer::sleep(sleep_time).await; + } + } + } +} diff --git a/mm2src/coins/eth/fee_estimation/mod.rs b/mm2src/coins/eth/fee_estimation/mod.rs new file mode 100644 index 0000000000..ffe9683acd --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod eip1559; +pub mod eth_fee_events; +pub mod rpc; +mod ser; diff --git a/mm2src/coins/eth/fee_estimation/rpc.rs b/mm2src/coins/eth/fee_estimation/rpc.rs new file mode 100644 index 0000000000..6fb3b84498 --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/rpc.rs @@ -0,0 +1,54 @@ +use super::eth_fee_events::EstimatorType; +use super::ser::FeePerGasEstimated; +use crate::{lp_coinfind, MmCoinEnum}; +use common::HttpStatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::mm_error::{MmError, MmResult}; + +use http::StatusCode; +use std::convert::TryFrom; + +#[derive(Deserialize)] +pub struct GetFeeEstimationRequest { + coin: String, + estimator_type: EstimatorType, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetFeeEstimationRequestError { + CoinNotFound, + Internal(String), + CoinNotSupported, +} + +impl HttpStatusCode for GetFeeEstimationRequestError { + fn status_code(&self) -> StatusCode { + match self { + GetFeeEstimationRequestError::CoinNotFound => StatusCode::NOT_FOUND, + GetFeeEstimationRequestError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + GetFeeEstimationRequestError::CoinNotSupported => StatusCode::NOT_IMPLEMENTED, + } + } +} + +pub async fn get_eth_estimated_fee_per_gas( + ctx: MmArc, + req: GetFeeEstimationRequest, +) -> MmResult { + match lp_coinfind(&ctx, &req.coin).await { + Ok(Some(MmCoinEnum::EthCoin(coin))) => { + let use_simple = matches!(req.estimator_type, EstimatorType::Simple); + let fee = coin + .get_eip1559_gas_fee(use_simple) + .await + .map_err(|e| GetFeeEstimationRequestError::Internal(e.to_string()))?; + let ser_fee = + FeePerGasEstimated::try_from(fee).map_err(|e| GetFeeEstimationRequestError::Internal(e.to_string()))?; + Ok(ser_fee) + }, + Ok(Some(_)) => MmError::err(GetFeeEstimationRequestError::CoinNotSupported), + Ok(None) => MmError::err(GetFeeEstimationRequestError::CoinNotFound), + Err(e) => MmError::err(GetFeeEstimationRequestError::Internal(e)), + } +} diff --git a/mm2src/coins/eth/fee_estimation/ser.rs b/mm2src/coins/eth/fee_estimation/ser.rs new file mode 100644 index 0000000000..62331e0f13 --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/ser.rs @@ -0,0 +1,80 @@ +//! Serializable version of fee estimation data. +use crate::eth::fee_estimation::eip1559; +use crate::{wei_to_gwei_decimal, NumConversError}; +use mm2_err_handle::mm_error::MmError; +use mm2_number::BigDecimal; + +use std::convert::TryFrom; + +/// Estimated fee per gas units +#[derive(Serialize)] +pub enum EstimationUnits { + Gwei, +} + +/// Priority level estimated max fee per gas +#[derive(Serialize)] +pub struct FeePerGasLevel { + /// estimated max priority tip fee per gas in gwei + pub max_priority_fee_per_gas: BigDecimal, + /// estimated max fee per gas in gwei + pub max_fee_per_gas: BigDecimal, + /// estimated transaction min wait time in mempool in ms for this priority level + pub min_wait_time: Option, + /// estimated transaction max wait time in mempool in ms for this priority level + pub max_wait_time: Option, +} + +/// External struct for estimated fee per gas for several priority levels, in gwei +/// low/medium/high levels are supported +#[derive(Serialize)] +pub struct FeePerGasEstimated { + /// base fee for the next block in gwei + pub base_fee: BigDecimal, + /// estimated low priority fee + pub low: FeePerGasLevel, + /// estimated medium priority fee + pub medium: FeePerGasLevel, + /// estimated high priority fee + pub high: FeePerGasLevel, + /// which estimator used + pub source: String, + /// base trend (up or down) + pub base_fee_trend: String, + /// priority trend (up or down) + pub priority_fee_trend: String, + /// fee units + pub units: EstimationUnits, +} + +impl TryFrom for FeePerGasEstimated { + type Error = MmError; + + fn try_from(fees: eip1559::FeePerGasEstimated) -> Result { + Ok(Self { + 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)?, + 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)?, + 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)?, + min_wait_time: fees.high.min_wait_time, + max_wait_time: fees.high.max_wait_time, + }, + source: fees.source.to_string(), + base_fee_trend: fees.base_fee_trend, + priority_fee_trend: fees.priority_fee_trend, + units: EstimationUnits::Gwei, + }) + } +} diff --git a/mm2src/coins/eth/for_tests.rs b/mm2src/coins/eth/for_tests.rs index d3b8ece3ac..23a59c163d 100644 --- a/mm2src/coins/eth/for_tests.rs +++ b/mm2src/coins/eth/for_tests.rs @@ -75,7 +75,6 @@ pub(crate) fn eth_coin_from_keypair( max_eth_tx_type: None, erc20_tokens_infos: Default::default(), nfts_infos: Arc::new(Default::default()), - platform_fee_estimator_state: Arc::new(FeeEstimatorState::CoinNotSupported), gas_limit, abortable_system: AbortableQueue::default(), })); diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 576920b030..4637967c5b 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -415,7 +415,6 @@ impl EthCoin { platform: protocol.platform, token_addr: protocol.token_addr, }; - let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &conf, &coin_type).await?; let gas_limit = extract_gas_limit_from_conf(&conf) .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; @@ -446,7 +445,6 @@ impl EthCoin { address_nonce_locks: self.address_nonce_locks.clone(), erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), - platform_fee_estimator_state, gas_limit, abortable_system, }; @@ -504,7 +502,6 @@ impl EthCoin { let coin_type = EthCoinType::Nft { platform: self.ticker.clone(), }; - let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &conf, &coin_type).await?; let gas_limit = extract_gas_limit_from_conf(&conf) .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; @@ -532,7 +529,6 @@ impl EthCoin { address_nonce_locks: self.address_nonce_locks.clone(), erc20_tokens_infos: Default::default(), nfts_infos: Arc::new(AsyncMutex::new(nft_infos)), - platform_fee_estimator_state, gas_limit, abortable_system, }; @@ -637,7 +633,6 @@ pub async fn eth_coin_from_conf_and_request_v2( // all spawned futures related to `ETH` coin will be aborted as well. let abortable_system = ctx.abortable_system.create_subsystem()?; let coin_type = EthCoinType::Eth; - let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(ctx, conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(ctx, conf, &coin_type).await?; let gas_limit = extract_gas_limit_from_conf(conf) .map_to_mm(|e| EthActivationV2Error::InternalError(format!("invalid gas_limit config {}", e)))?; @@ -665,17 +660,11 @@ pub async fn eth_coin_from_conf_and_request_v2( address_nonce_locks, erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), - platform_fee_estimator_state, gas_limit, abortable_system, }; - let coin = EthCoin(Arc::new(coin)); - coin.spawn_balance_stream_if_enabled(ctx) - .await - .map_err(EthActivationV2Error::FailedSpawningBalanceEvents)?; - - Ok(coin) + Ok(EthCoin(Arc::new(coin))) } /// Processes the given `priv_key_policy` and generates corresponding `KeyPair`. diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index ce97c96cb3..af2354edf0 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -15,8 +15,8 @@ use crate::lightning::ln_utils::{filter_channels, pay_invoice_with_max_total_clt use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::utxo::utxo_common::{big_decimal_from_sat, big_decimal_from_sat_unsigned}; use crate::utxo::{sat_from_big_decimal, utxo_common, BlockchainNetwork}; -use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DexFee, - FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, FeeApproxStage, + FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, @@ -27,7 +27,8 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, C ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFut, + WithdrawRequest}; use async_trait::async_trait; use bitcoin::bech32::ToBase32; use bitcoin::hashes::Hash; @@ -1266,7 +1267,7 @@ struct LightningProtocolInfo { impl MmCoin for LightningCoin { fn is_asset_chain(&self) -> bool { false } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.platform.abortable_system) } + fn spawner(&self) -> WeakSpawner { self.platform.abortable_system.weak_spawner() } fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { let fut = async move { diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs index 59e3e19488..93cdc8dbe2 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -6,7 +6,7 @@ use crate::utxo::rpc_clients::{BlockHashOrHeight, ConfirmedTransactionInfo, Elec use crate::utxo::spv::SimplePaymentVerification; use crate::utxo::utxo_standard::UtxoStandardCoin; use crate::utxo::GetConfirmedTxError; -use crate::{CoinFutSpawner, MarketCoinOps, MmCoin, WaitForHTLCTxSpendArgs}; +use crate::{MarketCoinOps, MmCoin, WaitForHTLCTxSpendArgs, WeakSpawner}; use bitcoin::blockdata::block::BlockHeader; use bitcoin::blockdata::script::Script; use bitcoin::blockdata::transaction::Transaction; @@ -216,7 +216,7 @@ impl Platform { #[inline] fn rpc_client(&self) -> &UtxoRpcClientEnum { &self.coin.as_ref().rpc_client } - pub fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + pub fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } pub async fn set_latest_fees(&self) -> UtxoRpcResult<()> { let platform_coin = &self.coin; diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 6c7fea7b26..83509cada7 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -45,8 +45,7 @@ use async_trait::async_trait; use base58::FromBase58Error; use bip32::ExtendedPrivateKey; use common::custom_futures::timeout::TimeoutError; -use common::executor::{abortable_queue::{AbortableQueue, WeakSpawner}, - AbortSettings, AbortedError, SpawnAbortable, SpawnFuture}; +use common::executor::{abortable_queue::WeakSpawner, AbortedError, SpawnFuture}; use common::log::{warn, LogOnError}; use common::{calc_total_pages, now_sec, ten, HttpStatusCode}; use crypto::{derive_secp256k1_secret, Bip32Error, Bip44Chain, CryptoCtx, CryptoCtxError, DerivationPath, @@ -75,7 +74,6 @@ use serde_json::{self as json, Value as Json}; use std::cmp::Ordering; use std::collections::hash_map::{HashMap, RawEntryMut}; use std::collections::HashSet; -use std::future::Future as Future03; use std::num::{NonZeroUsize, TryFromIntError}; use std::ops::{Add, AddAssign, Deref}; use std::str::FromStr; @@ -2108,7 +2106,7 @@ pub trait GetWithdrawSenderAddress { /// Instead, accept a generic type from withdraw implementations. /// This way we won't have to update the payload for every platform when /// one of them requires specific addition. -#[derive(Clone, Deserialize)] +#[derive(Clone, Default, Deserialize)] pub struct WithdrawRequest { coin: String, from: Option, @@ -2169,15 +2167,9 @@ impl WithdrawRequest { pub fn new_max(coin: String, to: String) -> WithdrawRequest { WithdrawRequest { coin, - from: None, to, - amount: 0.into(), max: true, - fee: None, - memo: None, - ibc_source_channel: None, - #[cfg(target_arch = "wasm32")] - broadcast: false, + ..Default::default() } } } @@ -3283,8 +3275,8 @@ pub trait MmCoin: /// /// # Note /// - /// `CoinFutSpawner` doesn't prevent the spawned futures from being aborted. - fn spawner(&self) -> CoinFutSpawner; + /// `WeakSpawner` doesn't prevent the spawned futures from being aborted. + fn spawner(&self) -> WeakSpawner; fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut; @@ -3410,43 +3402,6 @@ pub trait MmCoin: fn on_token_deactivated(&self, ticker: &str); } -/// The coin futures spawner. It's used to spawn futures that can be aborted immediately or after a timeout -/// on the the coin deactivation. -/// -/// # Note -/// -/// `CoinFutSpawner` doesn't prevent the spawned futures from being aborted. -#[derive(Clone)] -pub struct CoinFutSpawner { - inner: WeakSpawner, -} - -impl CoinFutSpawner { - pub fn new(system: &AbortableQueue) -> CoinFutSpawner { - CoinFutSpawner { - inner: system.weak_spawner(), - } - } -} - -impl SpawnFuture for CoinFutSpawner { - fn spawn(&self, f: F) - where - F: Future03 + Send + 'static, - { - self.inner.spawn(f) - } -} - -impl SpawnAbortable for CoinFutSpawner { - fn spawn_with_settings(&self, fut: F, settings: AbortSettings) - where - F: Future03 + Send + 'static, - { - self.inner.spawn_with_settings(fut, settings) - } -} - #[derive(Clone)] #[allow(clippy::large_enum_variant)] pub enum MmCoinEnum { @@ -3700,11 +3655,11 @@ impl CoinsContext { platform_coin_tokens: PaMutex::new(HashMap::new()), coins: AsyncMutex::new(HashMap::new()), balance_update_handlers: AsyncMutex::new(vec![]), - account_balance_task_manager: AccountBalanceTaskManager::new_shared(), - create_account_manager: CreateAccountTaskManager::new_shared(), - get_new_address_manager: GetNewAddressTaskManager::new_shared(), - scan_addresses_manager: ScanAddressesTaskManager::new_shared(), - withdraw_task_manager: WithdrawTaskManager::new_shared(), + account_balance_task_manager: AccountBalanceTaskManager::new_shared(ctx.event_stream_manager.clone()), + create_account_manager: CreateAccountTaskManager::new_shared(ctx.event_stream_manager.clone()), + get_new_address_manager: GetNewAddressTaskManager::new_shared(ctx.event_stream_manager.clone()), + scan_addresses_manager: ScanAddressesTaskManager::new_shared(ctx.event_stream_manager.clone()), + withdraw_task_manager: WithdrawTaskManager::new_shared(ctx.event_stream_manager.clone()), #[cfg(target_arch = "wasm32")] tx_history_db: ConstructibleDb::new(ctx).into_shared(), #[cfg(target_arch = "wasm32")] @@ -5571,7 +5526,7 @@ pub mod for_tests { use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmResult; use mm2_number::BigDecimal; - use rpc_task::RpcTaskStatus; + use rpc_task::{RpcInitReq, RpcTaskStatus}; use std::str::FromStr; /// Helper to call init_withdraw and wait for completion @@ -5583,17 +5538,18 @@ pub mod for_tests { from_derivation_path: Option<&str>, fee: Option, ) -> MmResult { - let withdraw_req = WithdrawRequest { - amount: BigDecimal::from_str(amount).unwrap(), - from: from_derivation_path.map(|from_derivation_path| WithdrawFrom::DerivationPath { - derivation_path: from_derivation_path.to_owned(), - }), - to: to.to_owned(), - coin: ticker.to_owned(), - max: false, - fee, - memo: None, - ibc_source_channel: None, + let withdraw_req = RpcInitReq { + client_id: 0, + inner: WithdrawRequest { + amount: BigDecimal::from_str(amount).unwrap(), + from: from_derivation_path.map(|from_derivation_path| WithdrawFrom::DerivationPath { + derivation_path: from_derivation_path.to_owned(), + }), + to: to.to_owned(), + coin: ticker.to_owned(), + fee, + ..Default::default() + }, }; let init = init_withdraw(ctx.clone(), withdraw_req).await.unwrap(); let timeout = wait_until_ms(150000); diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 3f86b78539..7bfa092ab5 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -16,20 +16,21 @@ use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, AddrFromStrError, Broadca UnsupportedAddr, UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFromLegacyReqErr, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; -use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, - DexFee, Eip1559Ops, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, - MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, - PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, - RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, - RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, SwapTxFeePolicy, - TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionData, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, - TransactionResult, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, - ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, Eip1559Ops, + FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, MarketCoinOps, + MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, + PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, + RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, + SignatureResult, SpendPaymentArgs, SwapOps, SwapTxFeePolicy, TakerSwapMakerCoin, TradeFee, + TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, + TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, + WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, + WithdrawResult}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; @@ -1299,7 +1300,7 @@ impl MarketCoinOps for Qrc20Coin { impl MmCoin for Qrc20Coin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo) } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn spawner(&self) -> WeakSpawner { self.as_ref().abortable_system.weak_spawner() } fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { Box::new(qrc20_withdraw(self.clone(), req).boxed().compat()) diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 930f078c53..d481580bdf 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -88,13 +88,9 @@ fn test_withdraw_to_p2sh_address_should_fail() { let req = WithdrawRequest { amount: 10.into(), - from: None, to: p2sh_address.to_string(), coin: "QRC20".into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let err = block_on_f01(coin.withdraw(req)).unwrap_err().into_inner(); let expect = WithdrawError::InvalidAddress("QRC20 can be sent to P2PKH addresses only".to_owned()); @@ -132,16 +128,13 @@ fn test_withdraw_impl_fee_details() { let withdraw_req = WithdrawRequest { amount: 10.into(), - from: None, to: "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(), coin: "QRC20".into(), - max: false, fee: Some(WithdrawFee::Qrc20Gas { gas_limit: 2_500_000, gas_price: 40, }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); diff --git a/mm2src/coins/rpc_command/get_estimated_fees.rs b/mm2src/coins/rpc_command/get_estimated_fees.rs deleted file mode 100644 index b62e572756..0000000000 --- a/mm2src/coins/rpc_command/get_estimated_fees.rs +++ /dev/null @@ -1,331 +0,0 @@ -//! 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 common::executor::{spawn_abortable, Timer}; -use common::log::debug; -use common::{HttpStatusCode, StatusCode}; -use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::*; -use mm2_number::BigDecimal; -use serde::{Deserialize, Serialize}; -use serde_json::{self as json, Value as Json}; -use std::convert::{TryFrom, TryInto}; -use std::ops::Deref; -use std::sync::Arc; - -const FEE_ESTIMATOR_NAME: &str = "eth_gas_fee_estimator_loop"; - -/// Estimated fee per gas units -#[derive(Clone, Debug, Serialize)] -pub enum EstimationUnits { - Gwei, -} - -impl Default for EstimationUnits { - fn default() -> Self { Self::Gwei } -} - -/// Priority level estimated max fee per gas -#[derive(Clone, Debug, Default, Serialize)] -pub struct FeePerGasLevel { - /// estimated max priority tip fee per gas in gwei - pub max_priority_fee_per_gas: BigDecimal, - /// estimated max fee per gas in gwei - pub max_fee_per_gas: BigDecimal, - /// estimated transaction min wait time in mempool in ms for this priority level - pub min_wait_time: Option, - /// estimated transaction max wait time in mempool in ms for this priority level - pub max_wait_time: Option, -} - -/// External struct for estimated fee per gas for several priority levels, in gwei -/// low/medium/high levels are supported -#[derive(Default, Debug, Clone, Serialize)] -pub struct FeePerGasEstimatedExt { - /// base fee for the next block in gwei - pub base_fee: BigDecimal, - /// estimated low priority fee - pub low: FeePerGasLevel, - /// estimated medium priority fee - pub medium: FeePerGasLevel, - /// estimated high priority fee - pub high: FeePerGasLevel, - /// which estimator used - pub source: String, - /// base trend (up or down) - pub base_fee_trend: String, - /// priority trend (up or down) - pub priority_fee_trend: String, - /// fee units - pub units: EstimationUnits, -} - -impl TryFrom for FeePerGasEstimatedExt { - type Error = MmError; - - fn try_from(fees: FeePerGasEstimated) -> Result { - Ok(Self { - 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)?, - 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)?, - 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)?, - min_wait_time: fees.high.min_wait_time, - max_wait_time: fees.high.max_wait_time, - }, - source: fees.source.to_string(), - base_fee_trend: fees.base_fee_trend, - priority_fee_trend: fees.priority_fee_trend, - units: EstimationUnits::Gwei, - }) - } -} - -#[derive(Debug, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum FeeEstimatorError { - #[display(fmt = "No such coin {}", coin)] - NoSuchCoin { coin: String }, - #[display(fmt = "Gas fee estimation not supported for this coin")] - CoinNotSupported, - #[display(fmt = "Platform coin needs to be enabled for gas fee estimation")] - PlatformCoinRequired, - #[display(fmt = "Gas fee estimator is already started")] - AlreadyStarted, - #[display(fmt = "Transport error: {}", _0)] - Transport(String), - #[display(fmt = "Gas fee estimator is not running")] - NotRunning, - #[display(fmt = "Internal error: {}", _0)] - InternalError(String), -} - -impl HttpStatusCode for FeeEstimatorError { - fn status_code(&self) -> StatusCode { - match self { - FeeEstimatorError::NoSuchCoin { .. } - | FeeEstimatorError::CoinNotSupported - | FeeEstimatorError::PlatformCoinRequired - | FeeEstimatorError::AlreadyStarted - | FeeEstimatorError::NotRunning => StatusCode::BAD_REQUEST, - FeeEstimatorError::Transport(_) | FeeEstimatorError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for FeeEstimatorError { - fn from(e: NumConversError) -> Self { FeeEstimatorError::InternalError(e.to_string()) } -} - -impl From for FeeEstimatorError { - fn from(e: String) -> Self { FeeEstimatorError::InternalError(e) } -} - -impl From for FeeEstimatorError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => FeeEstimatorError::NoSuchCoin { coin }, - } - } -} - -/// Gas fee estimator configuration -#[derive(Deserialize)] -enum FeeEstimatorConf { - NotConfigured, - #[serde(rename = "simple")] - Simple, - #[serde(rename = "provider")] - Provider, -} - -impl Default for FeeEstimatorConf { - fn default() -> Self { Self::NotConfigured } -} - -impl FeeEstimatorState { - /// Creates gas FeeEstimatorContext if configured for this coin and chain id, otherwise returns None. - /// The created context object (or None) is wrapped into a FeeEstimatorState so a gas fee rpc caller may know the reason why it was not created - pub(crate) async fn init_fee_estimator( - ctx: &MmArc, - conf: &Json, - coin_type: &EthCoinType, - ) -> Result, String> { - let fee_estimator_json = conf["gas_fee_estimator"].clone(); - let fee_estimator_conf: FeeEstimatorConf = if !fee_estimator_json.is_null() { - try_s!(json::from_value(fee_estimator_json)) - } else { - Default::default() - }; - match (fee_estimator_conf, coin_type) { - (FeeEstimatorConf::Simple, EthCoinType::Eth) => { - let fee_estimator_state = FeeEstimatorState::Simple(FeeEstimatorContext::new()); - Ok(Arc::new(fee_estimator_state)) - }, - (FeeEstimatorConf::Provider, EthCoinType::Eth) => { - let fee_estimator_state = FeeEstimatorState::Provider(FeeEstimatorContext::new()); - Ok(Arc::new(fee_estimator_state)) - }, - (_, EthCoinType::Erc20 { platform, .. }) | (_, EthCoinType::Nft { platform, .. }) => { - let platform_coin = lp_coinfind_or_err(ctx, platform).await; - match platform_coin { - Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin.platform_fee_estimator_state.clone()), - _ => Ok(Arc::new(FeeEstimatorState::PlatformCoinRequired)), - } - }, - (FeeEstimatorConf::NotConfigured, _) => Ok(Arc::new(FeeEstimatorState::CoinNotSupported)), - } - } -} - -impl FeeEstimatorContext { - fn new() -> AsyncMutex { - AsyncMutex::new(FeeEstimatorContext { - estimated_fees: Default::default(), - abort_handler: AsyncMutex::new(None), - }) - } - - /// Fee estimation update period in secs, basically equals to eth blocktime - const fn get_refresh_interval() -> f64 { 15.0 } - - fn get_estimator_ctx(coin: &EthCoin) -> Result<&AsyncMutex, MmError> { - match coin.platform_fee_estimator_state.deref() { - FeeEstimatorState::CoinNotSupported => MmError::err(FeeEstimatorError::CoinNotSupported), - FeeEstimatorState::PlatformCoinRequired => MmError::err(FeeEstimatorError::PlatformCoinRequired), - FeeEstimatorState::Simple(fee_estimator_ctx) | FeeEstimatorState::Provider(fee_estimator_ctx) => { - Ok(fee_estimator_ctx) - }, - } - } - - async fn start_if_not_running(coin: &EthCoin) -> Result<(), MmError> { - let estimator_ctx = Self::get_estimator_ctx(coin)?; - let estimator_ctx = estimator_ctx.lock().await; - let mut handler = estimator_ctx.abort_handler.lock().await; - if handler.is_some() { - return MmError::err(FeeEstimatorError::AlreadyStarted); - } - *handler = Some(spawn_abortable(Self::fee_estimator_loop(coin.clone()))); - Ok(()) - } - - async fn request_to_stop(coin: &EthCoin) -> Result<(), MmError> { - let estimator_ctx = Self::get_estimator_ctx(coin)?; - let estimator_ctx = estimator_ctx.lock().await; - let mut handle_guard = estimator_ctx.abort_handler.lock().await; - // Handler will be dropped here, stopping the spawned loop immediately - handle_guard - .take() - .map(|_| ()) - .or_mm_err(|| FeeEstimatorError::NotRunning) - } - - async fn get_estimated_fees(coin: &EthCoin) -> Result> { - let estimator_ctx = Self::get_estimator_ctx(coin)?; - let estimator_ctx = estimator_ctx.lock().await; - let estimated_fees = estimator_ctx.estimated_fees.lock().await; - Ok(estimated_fees.clone()) - } - - async fn check_if_estimator_supported(ctx: &MmArc, ticker: &str) -> Result> { - let eth_coin = match lp_coinfind_or_err(ctx, ticker).await? { - MmCoinEnum::EthCoin(eth) => eth, - _ => return MmError::err(FeeEstimatorError::CoinNotSupported), - }; - let _ = Self::get_estimator_ctx(ð_coin)?; - Ok(eth_coin) - } - - /// Loop polling gas fee estimator - /// - /// This loop periodically calls get_eip1559_gas_fee which fetches fee per gas estimations from a gas api provider or calculates them internally - /// The retrieved data are stored in the fee estimator context - /// To connect to the chain and gas api provider the web3 instances are used from an EthCoin coin passed in the start rpc param, - /// so this coin must be enabled first. - /// Once the loop started any other EthCoin in mainnet may request fee estimations. - /// It is up to GUI to start and stop the loop when it needs it (considering that the data in context may be used - /// for any coin with Eth or Erc20 type from the mainnet). - async fn fee_estimator_loop(coin: EthCoin) { - loop { - let started = common::now_float(); - if let Ok(estimator_ctx) = Self::get_estimator_ctx(&coin) { - let estimated_fees = coin.get_eip1559_gas_fee().await.unwrap_or_default(); - let estimator_ctx = estimator_ctx.lock().await; - *estimator_ctx.estimated_fees.lock().await = estimated_fees; - } - - let elapsed = common::now_float() - started; - debug!("{FEE_ESTIMATOR_NAME} call to provider processed in {} seconds", elapsed); - - let wait_secs = FeeEstimatorContext::get_refresh_interval() - elapsed; - let wait_secs = if wait_secs < 0.0 { 0.0 } else { wait_secs }; - Timer::sleep(wait_secs).await; - } - } -} - -/// Rpc request to start or stop gas fee estimator -#[derive(Deserialize)] -pub struct FeeEstimatorStartStopRequest { - coin: String, -} - -/// Rpc response to request to start or stop gas fee estimator -#[derive(Serialize)] -pub struct FeeEstimatorStartStopResponse { - result: String, -} - -pub type FeeEstimatorStartStopResult = Result>; - -/// Rpc request to get latest estimated fee per gas -#[derive(Deserialize)] -pub struct FeeEstimatorRequest { - /// coin ticker - coin: String, -} - -pub type FeeEstimatorResult = Result>; - -/// Start gas priority fee estimator loop -pub async fn start_eth_fee_estimator(ctx: MmArc, req: FeeEstimatorStartStopRequest) -> FeeEstimatorStartStopResult { - let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; - FeeEstimatorContext::start_if_not_running(&coin).await?; - Ok(FeeEstimatorStartStopResponse { - result: "Success".to_string(), - }) -} - -/// Stop gas priority fee estimator loop -pub async fn stop_eth_fee_estimator(ctx: MmArc, req: FeeEstimatorStartStopRequest) -> FeeEstimatorStartStopResult { - let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; - FeeEstimatorContext::request_to_stop(&coin).await?; - Ok(FeeEstimatorStartStopResponse { - result: "Success".to_string(), - }) -} - -/// Get latest estimated fee per gas for a eth coin -/// -/// Estimation loop for this coin must be stated. -/// Only main chain is supported -/// -/// Returns latest estimated fee per gas for the next block -pub async fn get_eth_estimated_fee_per_gas(ctx: MmArc, req: FeeEstimatorRequest) -> FeeEstimatorResult { - let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; - let estimated_fees = FeeEstimatorContext::get_estimated_fees(&coin).await?; - estimated_fees.try_into().mm_err(Into::into) -} diff --git a/mm2src/coins/rpc_command/get_new_address.rs b/mm2src/coins/rpc_command/get_new_address.rs index 9bba74cf75..a4a1a0c5ce 100644 --- a/mm2src/coins/rpc_command/get_new_address.rs +++ b/mm2src/coins/rpc_command/get_new_address.rs @@ -16,8 +16,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, - RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, + RpcTaskStatus, RpcTaskTypes}; use std::time::Duration; pub type GetNewAddressUserAction = HwRpcTaskUserAction; @@ -379,13 +379,15 @@ pub async fn get_new_address( /// TODO remove once GUI integrates `task::get_new_address::init`. pub async fn init_get_new_address( ctx: MmArc, - req: GetNewAddressRequest, + req: RpcInitReq, ) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(GetNewAddressRpcError::Internal)?; let spawner = coin.spawner(); let task = InitGetNewAddressTask { ctx, coin, req }; - let task_id = GetNewAddressTaskManager::spawn_rpc_task(&coins_ctx.get_new_address_manager, &spawner, task)?; + let task_id = + GetNewAddressTaskManager::spawn_rpc_task(&coins_ctx.get_new_address_manager, &spawner, task, client_id)?; Ok(InitRpcTaskResponse { task_id }) } diff --git a/mm2src/coins/rpc_command/init_account_balance.rs b/mm2src/coins/rpc_command/init_account_balance.rs index 94745f65e5..287c40dcb8 100644 --- a/mm2src/coins/rpc_command/init_account_balance.rs +++ b/mm2src/coins/rpc_command/init_account_balance.rs @@ -7,7 +7,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest}; -use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes}; pub type AccountBalanceUserAction = SerdeInfallible; pub type AccountBalanceAwaitingStatus = SerdeInfallible; @@ -89,13 +90,15 @@ impl RpcTask for InitAccountBalanceTask { pub async fn init_account_balance( ctx: MmArc, - req: InitAccountBalanceRequest, + req: RpcInitReq, ) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; let spawner = coin.spawner(); let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(HDAccountBalanceRpcError::Internal)?; let task = InitAccountBalanceTask { coin, req }; - let task_id = AccountBalanceTaskManager::spawn_rpc_task(&coins_ctx.account_balance_task_manager, &spawner, task)?; + let task_id = + AccountBalanceTaskManager::spawn_rpc_task(&coins_ctx.account_balance_task_manager, &spawner, task, client_id)?; Ok(InitRpcTaskResponse { task_id }) } diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index 012009d04b..ae139f09a5 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -14,8 +14,8 @@ use mm2_err_handle::prelude::*; use parking_lot::Mutex as PaMutex; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, - RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, + RpcTaskStatus, RpcTaskTypes}; use std::sync::Arc; use std::time::Duration; @@ -329,8 +329,9 @@ impl RpcTask for InitCreateAccountTask { pub async fn init_create_new_account( ctx: MmArc, - req: CreateNewAccountRequest, + req: RpcInitReq, ) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(CreateAccountRpcError::Internal)?; let spawner = coin.spawner(); @@ -340,7 +341,8 @@ pub async fn init_create_new_account( req, task_state: CreateAccountState::default(), }; - let task_id = CreateAccountTaskManager::spawn_rpc_task(&coins_ctx.create_account_manager, &spawner, task)?; + let task_id = + CreateAccountTaskManager::spawn_rpc_task(&coins_ctx.create_account_manager, &spawner, task, client_id)?; Ok(InitRpcTaskResponse { task_id }) } diff --git a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs index 4eabda46e9..5195248b7d 100644 --- a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs +++ b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs @@ -8,7 +8,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest}; -use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes}; pub type ScanAddressesUserAction = SerdeInfallible; pub type ScanAddressesAwaitingStatus = SerdeInfallible; @@ -108,13 +109,15 @@ impl RpcTask for InitScanAddressesTask { pub async fn init_scan_for_new_addresses( ctx: MmArc, - req: ScanAddressesRequest, + req: RpcInitReq, ) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; let spawner = coin.spawner(); let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(HDAccountBalanceRpcError::Internal)?; let task = InitScanAddressesTask { req, coin }; - let task_id = ScanAddressesTaskManager::spawn_rpc_task(&coins_ctx.scan_addresses_manager, &spawner, task)?; + let task_id = + ScanAddressesTaskManager::spawn_rpc_task(&coins_ctx.scan_addresses_manager, &spawner, task, client_id)?; Ok(InitRpcTaskResponse { task_id }) } diff --git a/mm2src/coins/rpc_command/init_withdraw.rs b/mm2src/coins/rpc_command/init_withdraw.rs index 43b86cf19a..e82ccd4d63 100644 --- a/mm2src/coins/rpc_command/init_withdraw.rs +++ b/mm2src/coins/rpc_command/init_withdraw.rs @@ -7,7 +7,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; -use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatusAlias, RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatusAlias, + RpcTaskTypes}; pub type WithdrawAwaitingStatus = HwRpcTaskAwaitingStatus; pub type WithdrawUserAction = HwRpcTaskUserAction; @@ -32,7 +33,11 @@ pub trait CoinWithdrawInit { ) -> WithdrawInitResult; } -pub async fn init_withdraw(ctx: MmArc, request: WithdrawRequest) -> WithdrawInitResult { +pub async fn init_withdraw( + ctx: MmArc, + request: RpcInitReq, +) -> WithdrawInitResult { + let (client_id, request) = (request.client_id, request.inner); let coin = lp_coinfind_or_err(&ctx, &request.coin).await?; let spawner = coin.spawner(); let task = WithdrawTask { @@ -41,7 +46,7 @@ pub async fn init_withdraw(ctx: MmArc, request: WithdrawRequest) -> WithdrawInit request, }; let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(WithdrawError::InternalError)?; - let task_id = WithdrawTaskManager::spawn_rpc_task(&coins_ctx.withdraw_task_manager, &spawner, task)?; + let task_id = WithdrawTaskManager::spawn_rpc_task(&coins_ctx.withdraw_task_manager, &spawner, task, client_id)?; Ok(InitWithdrawResponse { task_id }) } diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs index 0bec5ef493..c401853b2d 100644 --- a/mm2src/coins/rpc_command/mod.rs +++ b/mm2src/coins/rpc_command/mod.rs @@ -1,7 +1,6 @@ pub mod account_balance; pub mod get_current_mtp; pub mod get_enabled_coins; -pub mod get_estimated_fees; pub mod get_new_address; pub mod hd_account_balance_rpc_error; pub mod init_account_balance; diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index 1bd8ef6c2d..a7f6ba5fa8 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -1,17 +1,16 @@ use super::{BalanceError, CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, SwapOps, TradeFee, TransactionEnum, TransactionFut}; -use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, - ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, PrivKeyPolicy, RawTransactionResult, RefundPaymentArgs, RefundResult, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, - SignatureResult, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, ValidatePaymentResult, ValidateWatcherSpendInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, - WithdrawRequest}; +use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, + DexFee, FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, + PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicy, + RawTransactionResult, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, + SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, + TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, ValidatePaymentResult, ValidateWatcherSpendInput, VerificationResult, + WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WeakSpawner, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use common::executor::AbortedError; pub use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature}; @@ -213,7 +212,7 @@ pub struct SiaCoinProtocolInfo; impl MmCoin for SiaCoin { fn is_asset_chain(&self) -> bool { false } - fn spawner(&self) -> CoinFutSpawner { unimplemented!() } + fn spawner(&self) -> WeakSpawner { unimplemented!() } fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index 78009b5db8..9f9a5b29a9 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -6,7 +6,7 @@ pub(crate) mod ethermint_account; pub mod htlc; mod ibc; mod rpc; -mod tendermint_balance_events; +pub mod tendermint_balance_events; mod tendermint_coin; mod tendermint_token; pub mod tendermint_tx_history_v2; diff --git a/mm2src/coins/tendermint/tendermint_balance_events.rs b/mm2src/coins/tendermint/tendermint_balance_events.rs index c512cf8277..eed451b5dd 100644 --- a/mm2src/coins/tendermint/tendermint_balance_events.rs +++ b/mm2src/coins/tendermint/tendermint_balance_events.rs @@ -1,26 +1,44 @@ use async_trait::async_trait; -use common::{executor::{AbortSettings, SpawnAbortable}, - http_uri_to_ws_address, log, PROXY_REQUEST_EXPIRATION_SEC}; -use futures::channel::oneshot::{self, Receiver, Sender}; +use common::{http_uri_to_ws_address, log, PROXY_REQUEST_EXPIRATION_SEC}; +use futures::channel::oneshot; use futures_util::{SinkExt, StreamExt}; use jsonrpc_core::{Id as RpcId, Params as RpcParams, Value as RpcValue, Version as RpcVersion}; -use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - ErrorEventName, Event, EventName, EventStreamConfiguration}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; use mm2_number::BigDecimal; use proxy_signature::RawMessage; use std::collections::{HashMap, HashSet}; use super::TendermintCoin; -use crate::{tendermint::TendermintCommons, utxo::utxo_common::big_decimal_from_sat_unsigned, MarketCoinOps, MmCoin}; +use crate::{tendermint::TendermintCommons, utxo::utxo_common::big_decimal_from_sat_unsigned, MarketCoinOps}; -#[async_trait] -impl EventBehaviour for TendermintCoin { - fn event_name() -> EventName { EventName::CoinBalance } +pub struct TendermintBalanceEventStreamer { + coin: TendermintCoin, +} + +impl TendermintBalanceEventStreamer { + pub fn new(coin: TendermintCoin) -> Self { Self { coin } } +} - fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } +#[async_trait] +impl EventStreamer for TendermintBalanceEventStreamer { + type DataInType = NoDataIn; + + fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker()) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + let streamer_id = self.streamer_id(); + let coin = self.coin; + let account_id = coin.account_id.to_string(); + let mut current_balances: HashMap = HashMap::new(); - async fn handle(self, _interval: f64, tx: oneshot::Sender) { fn generate_subscription_query( query_filter: String, proxy_sign_keypair: &Option, @@ -48,24 +66,8 @@ impl EventBehaviour for TendermintCoin { serde_json::to_string(&q).expect("This should never happen") } - let ctx = match MmArc::from_weak(&self.ctx) { - Some(ctx) => ctx, - None => { - let msg = "MM context must have been initialized already."; - tx.send(EventInitStatus::Failed(msg.to_owned())) - .expect("Receiver is dropped, which should never happen."); - panic!("{}", msg); - }, - }; - - let account_id = self.account_id.to_string(); - let mut current_balances: HashMap = HashMap::new(); - - tx.send(EventInitStatus::Success) - .expect("Receiver is dropped, which should never happen."); - loop { - let client = match self.rpc_client().await { + let client = match coin.rpc_client().await { Ok(client) => client, Err(e) => { log::error!("{e}"); @@ -139,18 +141,13 @@ impl EventBehaviour for TendermintCoin { let mut balance_updates = vec![]; for denom in denoms { - if let Some((ticker, decimals)) = self.active_ticker_and_decimals_from_denom(&denom) { - let balance_denom = match self.account_balance_for_denom(&self.account_id, denom).await { + if let Some((ticker, decimals)) = coin.active_ticker_and_decimals_from_denom(&denom) { + let balance_denom = match coin.account_balance_for_denom(&coin.account_id, denom).await { Ok(balance_denom) => balance_denom, Err(e) => { log::error!("Failed getting balance for '{ticker}'. Error: {e}"); let e = serde_json::to_value(e).expect("Serialization should't fail."); - ctx.stream_channel_controller - .broadcast(Event::new( - format!("{}:{}", Self::error_event_name(), ticker), - e.to_string(), - )) - .await; + broadcaster.broadcast(Event::err(streamer_id.clone(), e)); continue; }, @@ -180,41 +177,10 @@ impl EventBehaviour for TendermintCoin { } if !balance_updates.is_empty() { - ctx.stream_channel_controller - .broadcast(Event::new( - Self::event_name().to_string(), - json!(balance_updates).to_string(), - )) - .await; + broadcaster.broadcast(Event::new(streamer_id.clone(), json!(balance_updates))); } } } } } - - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - log::info!( - "{} event is activated for {}. `stream_interval_seconds`({}) has no effect on this.", - Self::event_name(), - self.ticker(), - event.stream_interval_seconds - ); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = AbortSettings::info_on_abort(format!( - "{} event is stopped for {}.", - Self::event_name(), - self.ticker() - )); - self.spawner().spawn_with_settings(fut, settings); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive - } - } } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 5b4e7953a7..323c9da755 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -14,8 +14,8 @@ use crate::tendermint::ibc::IBC_OUT_SOURCE_PORT; use crate::utxo::sat_from_big_decimal; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, - CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, - HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, + CoinBalance, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, + MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, RpcCommonOps, @@ -27,7 +27,7 @@ use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; + WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; use bip32::DerivationPath; @@ -373,7 +373,7 @@ pub struct TendermintCoinImpl { pub(crate) history_sync_state: Mutex, client: TendermintRpcClient, pub(crate) chain_registry_name: Option, - pub(crate) ctx: MmWeak, + pub ctx: MmWeak, pub(crate) is_keplr_from_ledger: bool, } @@ -2167,7 +2167,7 @@ impl MmCoin for TendermintCoin { wallet_only_conf || self.is_keplr_from_ledger } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { let coin = self.clone(); diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index a2df5bc117..1bb63dd0eb 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -6,18 +6,19 @@ use super::{create_withdraw_msg_as_any, TendermintCoin, TendermintFeeDetails, GA use crate::coin_errors::ValidatePaymentResult; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFut, BigDecimal, - CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, - FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MyAddressError, - NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, RawTransactionError, - RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, - RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, - TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, - TransactionErr, TransactionFut, TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, + CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, + HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MyAddressError, NegotiateSwapContractAddrErr, + PaymentInstructions, PaymentInstructionsErr, RawTransactionError, RawTransactionFut, + RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, + SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageFut, + TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, + TransactionFut, TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFut, + WithdrawRequest}; use crate::{DexFee, MmCoinEnum, PaymentInstructionArgs, ValidateWatcherSpendInput, WatcherReward, WatcherRewardError}; use async_trait::async_trait; use bitcrypto::sha256; @@ -494,7 +495,7 @@ impl MmCoin for TendermintToken { wallet_only_conf || self.platform_coin.is_keplr_from_ledger } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { let platform = self.platform_coin.clone(); diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 1c8adf8de3..a227cc96ad 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -4,6 +4,7 @@ use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHisto use crate::tendermint::htlc::CustomTendermintMsgType; use crate::tendermint::TendermintFeeDetails; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; +use crate::utxo::tx_history_events::TxHistoryEventStreamer; use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; use crate::{HistorySyncState, MarketCoinOps, MmCoin, TransactionData, TransactionDetails, TransactionType, TxFeeDetails}; @@ -17,6 +18,7 @@ use cosmrs::tendermint::abci::{Code as TxCode, EventAttribute}; use cosmrs::tx::Fee; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmResult; +use mm2_event_stream::StreamingManager; use mm2_number::BigDecimal; use mm2_state_machine::prelude::*; use mm2_state_machine::state_machine::StateMachineTrait; @@ -126,6 +128,7 @@ impl CoinWithTxHistoryV2 for TendermintToken { struct TendermintTxHistoryStateMachine { coin: Coin, storage: Storage, + streaming_manager: StreamingManager, balances: AllBalancesResult, last_received_page: u32, last_spent_page: u32, @@ -571,6 +574,7 @@ where address: String, coin: &Coin, storage: &Storage, + streaming_manager: &StreamingManager, query: String, from_height: u64, page: &mut u32, @@ -754,6 +758,12 @@ where log::debug!("Tx '{}' successfully parsed.", tx.hash); } + streaming_manager + .send_fn(&TxHistoryEventStreamer::derive_streamer_id(coin.ticker()), || { + tx_details.clone() + }) + .ok(); + try_or_return_stopped_as_err!( storage .add_transactions_to_history(&coin.history_wallet_id(), tx_details) @@ -779,6 +789,7 @@ where self.address.clone(), &ctx.coin, &ctx.storage, + &ctx.streaming_manager, q, self.from_block_height, &mut ctx.last_spent_page, @@ -801,6 +812,7 @@ where self.address.clone(), &ctx.coin, &ctx.storage, + &ctx.streaming_manager, q, self.from_block_height, &mut ctx.last_received_page, @@ -914,7 +926,7 @@ fn get_value_from_event_attributes(events: &[EventAttribute], tag: &str, base64_ pub async fn tendermint_history_loop( coin: TendermintCoin, storage: impl TxHistoryStorage, - _ctx: MmArc, + ctx: MmArc, _current_balance: Option, ) { let balances = match coin.get_all_balances().await { @@ -928,6 +940,7 @@ pub async fn tendermint_history_loop( let mut state_machine = TendermintTxHistoryStateMachine { coin, storage, + streaming_manager: ctx.event_stream_manager.clone(), balances, last_received_page: 1, last_spent_page: 1, diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 8f1dd0a508..0669d7e014 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -4,20 +4,20 @@ use super::{CoinBalance, CommonSwapOpsV2, FundingTxSpend, HistorySyncState, Mark RawTransactionRequest, RefundTakerPaymentArgs, SearchForFundingSpendErr, SwapOps, TradeFee, TransactionEnum, TransactionFut, WaitForTakerPaymentSpendError}; use crate::coin_errors::ValidatePaymentResult; -use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, - ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, - GenTakerPaymentSpendArgs, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, - ParseCoinAssocTypes, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - RawTransactionResult, RefundFundingSecretArgs, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, - SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionRequest, - SignatureResult, SpendPaymentArgs, TakerCoinSwapOpsV2, TakerSwapMakerCoin, TradePreimageFut, - TradePreimageResult, TradePreimageValue, Transaction, TransactionErr, TransactionResult, TxMarshalingErr, - TxPreimageWithSig, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, ValidateSwapV2TxResult, ValidateTakerFundingArgs, - ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, VerificationResult, - WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; +use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, + FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, + MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, ParseCoinAssocTypes, PaymentInstructionArgs, + PaymentInstructions, PaymentInstructionsErr, RawTransactionResult, RefundFundingSecretArgs, + RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, + SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, + TakerCoinSwapOpsV2, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, + Transaction, TransactionErr, TransactionResult, TxMarshalingErr, TxPreimageWithSig, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + ValidateSwapV2TxResult, ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageResult, + ValidateTakerPaymentSpendPreimageResult, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, + WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WeakSpawner, WithdrawFut, WithdrawRequest}; use crate::{DexFee, ToBytes, ValidateWatcherSpendInput}; use async_trait::async_trait; use common::executor::AbortedError; @@ -343,7 +343,7 @@ impl WatcherOps for TestCoin { impl MmCoin for TestCoin { fn is_asset_chain(&self) -> bool { unimplemented!() } - fn spawner(&self) -> CoinFutSpawner { unimplemented!() } + fn spawner(&self) -> WeakSpawner { unimplemented!() } fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 6d98451c7f..49c6070707 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -32,6 +32,7 @@ pub mod rpc_clients; pub mod slp; pub mod spv; pub mod swap_proto_v2_scripts; +pub mod tx_history_events; pub mod utxo_balance_events; pub mod utxo_block_header_storage; pub mod utxo_builder; @@ -56,7 +57,7 @@ use common::{now_sec, now_sec_u32}; use crypto::{DerivationPath, HDPathToCoin, Secp256k1ExtendedPublicKey}; use derive_more::Display; #[cfg(not(target_arch = "wasm32"))] use dirs::home_dir; -use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender, UnboundedReceiver, UnboundedSender}; +use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender}; use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures01::Future; @@ -102,12 +103,12 @@ use utxo_signer::{TxProvider, TxProviderError, UtxoSignTxError, UtxoSignTxResult use self::rpc_clients::{electrum_script_hash, ElectrumClient, ElectrumConnectionSettings, EstimateFeeMethod, EstimateFeeMode, NativeClient, UnspentInfo, UnspentMap, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; -use super::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BalanceResult, CoinBalance, CoinFutSpawner, - CoinsContext, DerivationMethod, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, KmdRewardsDetails, - MarketCoinOps, MmCoin, NumConversError, NumConversResult, PrivKeyActivationPolicy, PrivKeyPolicy, +use super::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BalanceResult, CoinBalance, CoinsContext, + DerivationMethod, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, KmdRewardsDetails, MarketCoinOps, + MmCoin, NumConversError, NumConversResult, PrivKeyActivationPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, Transaction, TransactionDetails, TransactionEnum, TransactionErr, - UnexpectedDerivationMethod, VerificationError, WithdrawError, WithdrawRequest}; + UnexpectedDerivationMethod, VerificationError, WeakSpawner, WithdrawError, WithdrawRequest}; use crate::coin_balance::{EnableCoinScanPolicy, EnabledCoinBalanceParams, HDAddressBalanceScanner}; use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDPathAccountToAddressId, HDWalletCoinOps, HDWalletOps}; use crate::utxo::tx_cache::UtxoVerboseCacheShared; @@ -144,9 +145,6 @@ pub enum ScripthashNotification { SubscribeToAddresses(HashSet
), } -pub type ScripthashNotificationSender = Option>; -type ScripthashNotificationHandler = Option>>>; - #[cfg(windows)] #[cfg(not(target_arch = "wasm32"))] fn get_special_folder_path() -> PathBuf { @@ -604,14 +602,13 @@ pub struct UtxoCoinFields { /// The watcher/receiver of the block headers synchronization status, /// initialized only for non-native mode if spv is enabled for the coin. pub block_headers_status_watcher: Option>>, + /// A weak reference to the MM context we are running on top of. + /// + /// This faciliates access to global MM state and fields (e.g. event streaming manager). + pub ctx: MmWeak, /// This abortable system is used to spawn coin's related futures that should be aborted on coin deactivation /// and on [`MmArc::stop`]. pub abortable_system: AbortableQueue, - pub(crate) ctx: MmWeak, - /// This is used for balance event streaming implementation for UTXOs. - /// If balance event streaming isn't enabled, this value will always be `None`; otherwise, - /// it will be used for receiving scripthash notifications to re-fetch balances. - scripthash_notification_handler: ScripthashNotificationHandler, } #[derive(Debug, Display)] diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 5ef27ae662..ec61314ec6 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -45,8 +45,8 @@ pub type BchUnspentMap = HashMap; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BchActivationRequest { #[serde(default)] - allow_slp_unsafe_conf: bool, - bchd_urls: Vec, + pub allow_slp_unsafe_conf: bool, + pub bchd_urls: Vec, #[serde(flatten)] pub utxo_params: UtxoActivationParams, } @@ -83,6 +83,10 @@ pub struct BchCoin { slp_tokens_infos: Arc>>, } +impl From for UtxoArc { + fn from(coin: BchCoin) -> Self { coin.utxo_arc } +} + #[allow(clippy::large_enum_variant)] pub enum IsSlpUtxoError { Rpc(UtxoRpcError), @@ -158,6 +162,15 @@ impl From for IsSlpUtxoError { } impl BchCoin { + pub fn new(utxo_arc: UtxoArc, slp_addr_prefix: CashAddrPrefix, bchd_urls: Vec) -> Self { + BchCoin { + utxo_arc, + slp_addr_prefix, + bchd_urls, + slp_tokens_infos: Arc::new(Mutex::new(HashMap::new())), + } + } + pub fn slp_prefix(&self) -> &CashAddrPrefix { &self.slp_addr_prefix } pub fn slp_address(&self, address: &Address) -> Result { @@ -627,15 +640,7 @@ pub async fn bch_coin_with_policy( } let bchd_urls = params.bchd_urls; - let slp_tokens_infos = Arc::new(Mutex::new(HashMap::new())); - let constructor = { - move |utxo_arc| BchCoin { - utxo_arc, - slp_addr_prefix: slp_addr_prefix.clone(), - bchd_urls: bchd_urls.clone(), - slp_tokens_infos: slp_tokens_infos.clone(), - } - }; + let constructor = { move |utxo_arc| BchCoin::new(utxo_arc, slp_addr_prefix.clone(), bchd_urls.clone()) }; let coin = try_s!( UtxoArcBuilder::new(ctx, ticker, conf, ¶ms.utxo_params, priv_key_policy, constructor) @@ -1283,7 +1288,7 @@ impl MarketCoinOps for BchCoin { impl MmCoin for BchCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn spawner(&self) -> WeakSpawner { self.as_ref().abortable_system.weak_spawner() } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 8dd554bb53..e7d9455cde 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -903,7 +903,7 @@ impl MarketCoinOps for QtumCoin { impl MmCoin for QtumCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn spawner(&self) -> WeakSpawner { self.as_ref().abortable_system.weak_spawner() } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) diff --git a/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs index ce0498cc31..84e39997e4 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs @@ -5,7 +5,7 @@ use super::connection_manager::ConnectionManager; use super::constants::{BLOCKCHAIN_HEADERS_SUB_ID, BLOCKCHAIN_SCRIPTHASH_SUB_ID, ELECTRUM_REQUEST_TIMEOUT, NO_FORCE_CONNECT_METHODS, SEND_TO_ALL_METHODS}; use super::electrum_script_hash; -use super::event_handlers::{ElectrumConnectionManagerNotifier, ElectrumScriptHashNotificationBridge}; +use super::event_handlers::ElectrumConnectionManagerNotifier; use super::rpc_responses::*; use crate::utxo::rpc_clients::ConcurrentRequestMap; @@ -43,14 +43,15 @@ use std::ops::Deref; use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; use std::sync::Arc; +use crate::utxo::utxo_balance_events::UtxoBalanceEventStreamer; use async_trait::async_trait; -use futures::channel::mpsc::UnboundedSender; use futures::compat::Future01CompatExt; use futures::future::{join_all, FutureExt, TryFutureExt}; use futures::stream::FuturesUnordered; use futures::StreamExt; use futures01::Future; use itertools::Itertools; +use mm2_event_stream::StreamingManager; use serde_json::{self as json, Value as Json}; type ElectrumTxHistory = Vec; @@ -85,7 +86,8 @@ pub struct ElectrumClientImpl { /// in an `Arc` since they are shared outside `ElectrumClientImpl`. They are handed to each active /// `ElectrumConnection` to notify them about the events. event_handlers: Arc>>, - pub scripthash_notification_sender: Option>, + /// A streaming manager instance used to notify for Utxo balance events streamer. + streaming_manager: StreamingManager, abortable_system: AbortableQueue, } @@ -98,18 +100,10 @@ impl ElectrumClientImpl { fn try_new( client_settings: ElectrumClientSettings, block_headers_storage: BlockHeaderStorage, + streaming_manager: StreamingManager, abortable_system: AbortableQueue, mut event_handlers: Vec>, - scripthash_notification_sender: Option>, ) -> Result { - // This is used for balance event streaming implementation for UTXOs. - // Will be used for sending scripthash messages to trigger re-connections, re-fetching the balances, etc. - if let Some(scripthash_notification_sender) = scripthash_notification_sender.clone() { - event_handlers.push(Box::new(ElectrumScriptHashNotificationBridge { - scripthash_notification_sender, - })); - } - let connection_manager = ConnectionManager::try_new( client_settings.servers, client_settings.spawn_ping, @@ -132,7 +126,7 @@ impl ElectrumClientImpl { list_unspent_concurrent_map: ConcurrentRequestMap::new(), block_headers_storage, abortable_system, - scripthash_notification_sender, + streaming_manager, event_handlers: Arc::new(event_handlers), }) } @@ -142,16 +136,16 @@ impl ElectrumClientImpl { pub fn try_new_arc( client_settings: ElectrumClientSettings, block_headers_storage: BlockHeaderStorage, + streaming_manager: StreamingManager, abortable_system: AbortableQueue, event_handlers: Vec>, - scripthash_notification_sender: Option>, ) -> Result, String> { let client_impl = Arc::new(ElectrumClientImpl::try_new( client_settings, block_headers_storage, + streaming_manager, abortable_system, event_handlers, - scripthash_notification_sender, )?); // Initialize the connection manager. client_impl @@ -185,12 +179,25 @@ impl ElectrumClientImpl { /// Sends a list of addresses through the scripthash notification sender to subscribe to their scripthash notifications. pub fn subscribe_addresses(&self, addresses: HashSet
) -> Result<(), String> { - if let Some(sender) = &self.scripthash_notification_sender { - sender - .unbounded_send(ScripthashNotification::SubscribeToAddresses(addresses)) - .map_err(|e| ERRL!("Failed sending scripthash message. {}", e))?; - } + self.streaming_manager + .send( + &UtxoBalanceEventStreamer::derive_streamer_id(&self.coin_ticker), + ScripthashNotification::SubscribeToAddresses(addresses), + ) + .map_err(|e| ERRL!("Failed sending scripthash message. {:?}", e))?; + Ok(()) + } + /// Notifies the Utxo balance streamer of a new script hash balance change. + /// + /// The streamer will figure out which address this scripthash belongs to and will broadcast an notification to clients. + pub fn notify_triggered_hash(&self, script_hash: String) -> Result<(), String> { + self.streaming_manager + .send( + &UtxoBalanceEventStreamer::derive_streamer_id(&self.coin_ticker), + ScripthashNotification::Triggered(script_hash), + ) + .map_err(|e| ERRL!("Failed sending scripthash message. {:?}", e))?; Ok(()) } @@ -203,9 +210,9 @@ impl ElectrumClientImpl { pub fn with_protocol_version( client_settings: ElectrumClientSettings, block_headers_storage: BlockHeaderStorage, + streaming_manager: StreamingManager, abortable_system: AbortableQueue, event_handlers: Vec>, - scripthash_notification_sender: Option>, protocol_version: OrdRange, ) -> Result, String> { let client_impl = Arc::new(ElectrumClientImpl { @@ -213,9 +220,9 @@ impl ElectrumClientImpl { ..ElectrumClientImpl::try_new( client_settings, block_headers_storage, + streaming_manager, abortable_system, event_handlers, - scripthash_notification_sender, )? }); // Initialize the connection manager. @@ -272,15 +279,15 @@ impl ElectrumClient { client_settings: ElectrumClientSettings, event_handlers: Vec>, block_headers_storage: BlockHeaderStorage, + streaming_manager: StreamingManager, abortable_system: AbortableQueue, - scripthash_notification_sender: Option>, ) -> Result { let client = ElectrumClient(ElectrumClientImpl::try_new_arc( client_settings, block_headers_storage, + streaming_manager, abortable_system, event_handlers, - scripthash_notification_sender, )?); Ok(client) diff --git a/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection.rs index 2b9a3ada48..691a76dd85 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection.rs @@ -275,9 +275,9 @@ impl ElectrumConnection { } /// Process an incoming JSONRPC response from the electrum server. - fn process_electrum_response(&self, bytes: &[u8], event_handlers: &Vec>) { + fn process_electrum_response(&self, bytes: &[u8], client: &ElectrumClient) { // Inform the event handlers. - event_handlers.on_incoming_response(bytes); + client.event_handlers().on_incoming_response(bytes); // detect if we got standard JSONRPC response or subscription response as JSONRPC request #[derive(Deserialize)] @@ -308,8 +308,14 @@ impl ElectrumConnection { ElectrumRpcResponseEnum::BatchResponses(batch) => JsonRpcResponseEnum::Batch(batch), ElectrumRpcResponseEnum::SubscriptionNotification(req) => { match req.method.as_str() { - // NOTE: Sending a script hash notification is handled in it's own event handler. - BLOCKCHAIN_SCRIPTHASH_SUB_ID | BLOCKCHAIN_HEADERS_SUB_ID => {}, + BLOCKCHAIN_SCRIPTHASH_SUB_ID => { + if let Some(scripthash) = req.params.first().and_then(|s| s.as_str()) { + client.notify_triggered_hash(scripthash.to_string()).ok(); + } else { + error!("Notification must contain the script hash value, got: {req:?}"); + } + }, + BLOCKCHAIN_HEADERS_SUB_ID => {}, _ => { error!("Unexpected notification method: {}", req.method); }, @@ -329,18 +335,14 @@ impl ElectrumConnection { /// Process a bulk response from the electrum server. /// /// A bulk response is a response that contains multiple JSONRPC responses. - fn process_electrum_bulk_response( - &self, - bulk_response: &[u8], - event_handlers: &Vec>, - ) { + fn process_electrum_bulk_response(&self, bulk_response: &[u8], client: &ElectrumClient) { // We should split the received response because we can get several responses in bulk. let responses = bulk_response.split(|item| *item == b'\n'); for response in responses { // `split` returns empty slice if it ends with separator which is our case. if !response.is_empty() { - self.process_electrum_response(response, event_handlers) + self.process_electrum_response(response, client) } } } @@ -536,7 +538,7 @@ impl ElectrumConnection { #[cfg(not(target_arch = "wasm32"))] async fn recv_loop( connection: Arc, - event_handlers: Arc>>, + client: ElectrumClient, read: ReadHalf, last_response: Arc, ) -> ElectrumConnectionErr { @@ -555,7 +557,7 @@ impl ElectrumConnection { }; last_response.store(now_ms(), AtomicOrdering::Relaxed); - connection.process_electrum_bulk_response(buffer.as_bytes(), &event_handlers); + connection.process_electrum_bulk_response(buffer.as_bytes(), &client); buffer.clear(); } } @@ -563,7 +565,7 @@ impl ElectrumConnection { #[cfg(target_arch = "wasm32")] async fn recv_loop( connection: Arc, - event_handlers: Arc>>, + client: ElectrumClient, mut read: WsIncomingReceiver, last_response: Arc, ) -> ElectrumConnectionErr { @@ -572,7 +574,7 @@ impl ElectrumConnection { match response { Ok(bytes) => { last_response.store(now_ms(), AtomicOrdering::Relaxed); - connection.process_electrum_response(&bytes, &event_handlers); + connection.process_electrum_response(&bytes, &client); }, Err(e) => { error!("{address} error: {e:?}"); @@ -674,7 +676,7 @@ impl ElectrumConnection { let (read, write) = tokio::io::split(stream); #[cfg(target_arch = "wasm32")] let (read, write) = stream; - let recv_branch = Self::recv_loop(connection.clone(), event_handlers.clone(), read, last_response).boxed(); + let recv_branch = Self::recv_loop(connection.clone(), client.clone(), read, last_response).boxed(); // Branch 3: Send outgoing requests to the server. let (tx, rx) = mpsc::channel(0); diff --git a/mm2src/coins/utxo/rpc_clients/electrum_rpc/event_handlers.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/event_handlers.rs index 27bd74b4d9..9db2ab93ec 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/event_handlers.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/event_handlers.rs @@ -1,51 +1,6 @@ use super::connection_manager::ConnectionManager; -use super::constants::BLOCKCHAIN_SCRIPTHASH_SUB_ID; -use crate::utxo::ScripthashNotification; use crate::RpcTransportEventHandler; -use common::jsonrpc_client::JsonRpcRequest; -use common::log::{error, warn}; - -use futures::channel::mpsc::UnboundedSender; -use serde_json::{self as json, Value as Json}; - -/// An `RpcTransportEventHandler` that forwards `ScripthashNotification`s to trigger balance updates. -/// -/// This handler hooks in `on_incoming_response` and looks for an electrum script hash notification to forward it. -pub struct ElectrumScriptHashNotificationBridge { - pub scripthash_notification_sender: UnboundedSender, -} - -impl RpcTransportEventHandler for ElectrumScriptHashNotificationBridge { - fn debug_info(&self) -> String { "ElectrumScriptHashNotificationBridge".into() } - - fn on_incoming_response(&self, data: &[u8]) { - if let Ok(raw_json) = json::from_slice::(data) { - // Try to parse the notification. A notification is sent as a JSON-RPC request. - if let Ok(notification) = json::from_value::(raw_json) { - // Only care about `BLOCKCHAIN_SCRIPTHASH_SUB_ID` notifications. - if notification.method.as_str() == BLOCKCHAIN_SCRIPTHASH_SUB_ID { - if let Some(scripthash) = notification.params.first().and_then(|s| s.as_str()) { - if let Err(e) = self - .scripthash_notification_sender - .unbounded_send(ScripthashNotification::Triggered(scripthash.to_string())) - { - error!("Failed sending script hash message. {e:?}"); - } - } else { - warn!("Notification must contain the script hash value, got: {notification:?}"); - } - }; - } - } - } - - fn on_connected(&self, _address: &str) -> Result<(), String> { Ok(()) } - - fn on_disconnected(&self, _address: &str) -> Result<(), String> { Ok(()) } - - fn on_outgoing_request(&self, _data: &[u8]) {} -} /// An `RpcTransportEventHandler` that notifies the `ConnectionManager` upon connections and disconnections. /// diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index b1e7f1ef4c..e200923887 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -13,9 +13,9 @@ use crate::utxo::utxo_common::{self, big_decimal_from_sat_unsigned, payment_scri use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualTxFee, AdditionalTxData, BroadcastTxErr, FeePolicy, GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, UtxoCommonOps, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps}; -use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DerivationMethod, - DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, - MmCoinEnum, NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DerivationMethod, DexFee, + FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, + NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, @@ -25,7 +25,7 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, C UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use base64::engine::general_purpose::STANDARD; @@ -1600,7 +1600,7 @@ impl From for TxFeeDetails { impl MmCoin for SlpToken { fn is_asset_chain(&self) -> bool { false } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.conf.abortable_system) } + fn spawner(&self) -> WeakSpawner { self.conf.abortable_system.weak_spawner() } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new( diff --git a/mm2src/coins/utxo/tx_history_events.rs b/mm2src/coins/utxo/tx_history_events.rs new file mode 100644 index 0000000000..c336e6fbb0 --- /dev/null +++ b/mm2src/coins/utxo/tx_history_events.rs @@ -0,0 +1,43 @@ +use crate::TransactionDetails; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; + +use async_trait::async_trait; +use futures::channel::oneshot; +use futures::StreamExt; + +pub struct TxHistoryEventStreamer { + coin: String, +} + +impl TxHistoryEventStreamer { + #[inline(always)] + pub fn new(coin: String) -> Self { Self { coin } } + + #[inline(always)] + pub fn derive_streamer_id(coin: &str) -> String { format!("TX_HISTORY:{coin}") } +} + +#[async_trait] +impl EventStreamer for TxHistoryEventStreamer { + type DataInType = Vec; + + fn streamer_id(&self) -> String { Self::derive_streamer_id(&self.coin) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + while let Some(new_txs) = data_rx.next().await { + for new_tx in new_txs { + let tx_details = serde_json::to_value(new_tx).expect("Serialization should't fail."); + broadcaster.broadcast(Event::new(self.streamer_id(), tx_details)); + } + } + } +} diff --git a/mm2src/coins/utxo/utxo_balance_events.rs b/mm2src/coins/utxo/utxo_balance_events.rs index ec1de7aa40..5de90555e4 100644 --- a/mm2src/coins/utxo/utxo_balance_events.rs +++ b/mm2src/coins/utxo/utxo_balance_events.rs @@ -1,19 +1,18 @@ -use super::utxo_standard::UtxoStandardCoin; +use super::{utxo_standard::UtxoStandardCoin, UtxoArc}; + use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::{utxo::{output_script, rpc_clients::electrum_script_hash, utxo_common::{address_balance, address_to_scripthash}, ScripthashNotification, UtxoCoinFields}, - CoinWithDerivationMethod, MarketCoinOps, MmCoin}; + CoinWithDerivationMethod, MarketCoinOps}; + use async_trait::async_trait; -use common::{executor::{AbortSettings, SpawnAbortable}, - log}; -use futures::channel::oneshot::{self, Receiver, Sender}; -use futures_util::StreamExt; +use common::log; +use futures::channel::oneshot; +use futures::StreamExt; use keys::Address; -use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - ErrorEventName, Event, EventName, EventStreamConfiguration}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; use std::collections::{BTreeMap, HashMap, HashSet}; macro_rules! try_or_continue { @@ -28,14 +27,37 @@ macro_rules! try_or_continue { }; } +pub struct UtxoBalanceEventStreamer { + coin: UtxoStandardCoin, +} + +impl UtxoBalanceEventStreamer { + pub fn new(utxo_arc: UtxoArc) -> Self { + Self { + // We wrap the UtxoArc in a UtxoStandardCoin for easier method accessibility. + // The UtxoArc might belong to a different coin type though. + coin: UtxoStandardCoin::from(utxo_arc), + } + } + + pub fn derive_streamer_id(coin: &str) -> String { format!("BALANCE:{coin}") } +} + #[async_trait] -impl EventBehaviour for UtxoStandardCoin { - fn event_name() -> EventName { EventName::CoinBalance } +impl EventStreamer for UtxoBalanceEventStreamer { + type DataInType = ScripthashNotification; - fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } + fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker()) } - async fn handle(self, _interval: f64, tx: oneshot::Sender) { + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; + let streamer_id = self.streamer_id(); + let coin = self.coin; async fn subscribe_to_addresses( utxo: &UtxoCoinFields, @@ -66,44 +88,25 @@ impl EventBehaviour for UtxoStandardCoin { } } - let ctx = match MmArc::from_weak(&self.as_ref().ctx) { - Some(ctx) => ctx, - None => { - let msg = "MM context must have been initialized already."; - tx.send(EventInitStatus::Failed(msg.to_owned())) - .expect(RECEIVER_DROPPED_MSG); - panic!("{}", msg); - }, - }; - - let scripthash_notification_handler = match self.as_ref().scripthash_notification_handler.as_ref() { - Some(t) => t, - None => { - let e = "Scripthash notification receiver can not be empty."; - tx.send(EventInitStatus::Failed(e.to_string())) - .expect(RECEIVER_DROPPED_MSG); - panic!("{}", e); - }, - }; + if coin.as_ref().rpc_client.is_native() { + let msg = "Native RPC client is not supported for UtxoBalanceEventStreamer."; + ready_tx.send(Err(msg.to_string())).expect(RECEIVER_DROPPED_MSG); + panic!("{}", msg); + } - tx.send(EventInitStatus::Success).expect(RECEIVER_DROPPED_MSG); + ready_tx.send(Ok(())).expect(RECEIVER_DROPPED_MSG); let mut scripthash_to_address_map = BTreeMap::default(); - while let Some(message) = scripthash_notification_handler.lock().await.next().await { + while let Some(message) = data_rx.next().await { let notified_scripthash = match message { ScripthashNotification::Triggered(t) => t, ScripthashNotification::SubscribeToAddresses(addresses) => { - match subscribe_to_addresses(self.as_ref(), addresses).await { + match subscribe_to_addresses(coin.as_ref(), addresses).await { Ok(map) => scripthash_to_address_map.extend(map), Err(e) => { log::error!("{e}"); - ctx.stream_channel_controller - .broadcast(Event::new( - format!("{}:{}", Self::error_event_name(), self.ticker()), - json!({ "error": e }).to_string(), - )) - .await; + broadcaster.broadcast(Event::err(streamer_id.clone(), json!({ "error": e }))); }, }; @@ -113,7 +116,7 @@ impl EventBehaviour for UtxoStandardCoin { let address = match scripthash_to_address_map.get(¬ified_scripthash) { Some(t) => Some(t.clone()), - None => try_or_continue!(self.all_addresses().await) + None => try_or_continue!(coin.all_addresses().await) .into_iter() .find_map(|addr| { let script = match output_script(&addr) { @@ -146,62 +149,26 @@ impl EventBehaviour for UtxoStandardCoin { }, }; - let balance = match address_balance(&self, &address).await { + let balance = match address_balance(&coin, &address).await { Ok(t) => t, Err(e) => { - let ticker = self.ticker(); + let ticker = coin.ticker(); log::error!("Failed getting balance for '{ticker}'. Error: {e}"); let e = serde_json::to_value(e).expect("Serialization should't fail."); - ctx.stream_channel_controller - .broadcast(Event::new( - format!("{}:{}", Self::error_event_name(), ticker), - e.to_string(), - )) - .await; + broadcaster.broadcast(Event::err(streamer_id.clone(), e)); continue; }, }; let payload = json!({ - "ticker": self.ticker(), + "ticker": coin.ticker(), "address": address.to_string(), "balance": { "spendable": balance.spendable, "unspendable": balance.unspendable } }); - ctx.stream_channel_controller - .broadcast(Event::new( - Self::event_name().to_string(), - json!(vec![payload]).to_string(), - )) - .await; - } - } - - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - log::info!( - "{} event is activated for {}. `stream_interval_seconds`({}) has no effect on this.", - Self::event_name(), - self.ticker(), - event.stream_interval_seconds - ); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = AbortSettings::info_on_abort(format!( - "{} event is stopped for {}.", - Self::event_name(), - self.ticker() - )); - self.spawner().spawn_with_settings(fut, settings); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive + broadcaster.broadcast(Event::new(streamer_id.clone(), json!(vec![payload]))); } } } diff --git a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs index f8e16a6089..65bd34103f 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs @@ -1,9 +1,9 @@ use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientImpl, UtxoJsonRpcClientInfo, UtxoRpcClientEnum}; + use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; -use crate::utxo::utxo_standard::UtxoStandardCoin; use crate::utxo::{generate_and_send_tx, FeePolicy, GetUtxoListOps, UtxoArc, UtxoCommonOps, UtxoSyncStatusLoopHandle, UtxoWeak}; use crate::{DerivationMethod, PrivKeyBuildPolicy, UtxoActivationParams}; @@ -14,7 +14,6 @@ use common::log::{debug, error, info, warn}; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; #[cfg(test)] use mocktopus::macros::*; use rand::Rng; use script::Builder; @@ -109,7 +108,6 @@ where let utxo = self.build_utxo_fields().await?; let sync_status_loop_handle = utxo.block_headers_status_notifier.clone(); let spv_conf = utxo.conf.spv_conf.clone(); - let (is_native_mode, mode) = (utxo.rpc_client.is_native(), utxo.rpc_client.to_string()); let utxo_arc = UtxoArc::new(utxo); self.spawn_merge_utxo_loop_if_required(&utxo_arc, self.constructor.clone()); @@ -121,18 +119,6 @@ where spawn_block_header_utxo_loop(self.ticker, &utxo_arc, sync_handle, spv_conf); } - if let Some(stream_config) = &self.ctx().event_stream_configuration { - if is_native_mode { - return MmError::err(UtxoCoinBuildError::UnsupportedModeForBalanceEvents { mode }); - } - - if let EventInitStatus::Failed(err) = - EventBehaviour::spawn_if_active(UtxoStandardCoin::from(utxo_arc), stream_config).await - { - return MmError::err(UtxoCoinBuildError::FailedSpawningBalanceEvents(err)); - } - } - Ok(result_coin) } } diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 15a699c2f1..b916afc232 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -5,9 +5,8 @@ use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientSettings, ElectrumC use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::utxo_conf_builder::{UtxoConfBuilder, UtxoConfError}; -use crate::utxo::{output_script, ElectrumBuilderArgs, RecentlySpentOutPoints, ScripthashNotification, - ScripthashNotificationSender, TxFee, UtxoCoinConf, UtxoCoinFields, UtxoHDWallet, UtxoRpcMode, - UtxoSyncStatus, UtxoSyncStatusLoopHandle, UTXO_DUST_AMOUNT}; +use crate::utxo::{output_script, ElectrumBuilderArgs, RecentlySpentOutPoints, TxFee, UtxoCoinConf, UtxoCoinFields, + UtxoHDWallet, UtxoRpcMode, UtxoSyncStatus, UtxoSyncStatusLoopHandle, UTXO_DUST_AMOUNT}; use crate::{BlockchainNetwork, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, SharableRpcTransportEventHandler, UtxoActivationParams}; @@ -17,7 +16,7 @@ use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, Aborted use common::now_sec; use crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, StandardHDPathError}; use derive_more::Display; -use futures::channel::mpsc::{channel, Receiver as AsyncReceiver, UnboundedReceiver, UnboundedSender}; +use futures::channel::mpsc::{channel, Receiver as AsyncReceiver}; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use keys::bytes::Bytes; @@ -30,7 +29,6 @@ use serde_json::{self as json, Value as Json}; use spv_validation::conf::SPVConf; use spv_validation::helpers_validation::SPVError; use spv_validation::storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; -use std::sync::Arc; use std::sync::Mutex; cfg_native! { @@ -38,6 +36,7 @@ cfg_native! { use crate::utxo::rpc_clients::{ConcurrentRequestMap, NativeClient, NativeClientImpl}; use dirs::home_dir; use std::path::{Path, PathBuf}; + use std::sync::Arc; } /// Number of seconds in a day (24 hours * 60 * 60) @@ -227,22 +226,6 @@ pub trait UtxoFieldsWithGlobalHDBuilder: UtxoCoinBuilderCommonOps { fn gap_limit(&self) -> u32 { self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) } } -// The return type is one-time used only. No need to create a type for it. -#[allow(clippy::type_complexity)] -fn get_scripthash_notification_handlers( - ctx: &MmArc, -) -> Option<( - UnboundedSender, - Arc>>, -)> { - if ctx.event_stream_configuration.is_some() { - let (sender, receiver) = futures::channel::mpsc::unbounded(); - Some((sender, Arc::new(AsyncMutex::new(receiver)))) - } else { - None - } -} - async fn build_utxo_coin_fields_with_conf_and_policy( builder: &Builder, conf: UtxoCoinConf, @@ -266,19 +249,11 @@ where let my_script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; - let (scripthash_notification_sender, scripthash_notification_handler) = - match get_scripthash_notification_handlers(builder.ctx()) { - Some((sender, receiver)) => (Some(sender), Some(receiver)), - None => (None, None), - }; - // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, // all spawned futures related to this `UTXO` coin will be aborted as well. let abortable_system: AbortableQueue = builder.ctx().abortable_system.create_subsystem()?; - let rpc_client = builder - .rpc_client(scripthash_notification_sender, abortable_system.create_subsystem()?) - .await?; + let rpc_client = builder.rpc_client(abortable_system.create_subsystem()?).await?; let tx_fee = builder.tx_fee(&rpc_client).await?; let decimals = builder.decimals(&rpc_client).await?; let dust_amount = builder.dust_amount(); @@ -305,9 +280,8 @@ where check_utxo_maturity, block_headers_status_notifier, block_headers_status_watcher, + ctx: builder.ctx().clone().weak(), abortable_system, - scripthash_notification_handler, - ctx: builder.ctx().weak(), }; Ok(coin) @@ -353,19 +327,11 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { address_format, }; - let (scripthash_notification_sender, scripthash_notification_handler) = - match get_scripthash_notification_handlers(self.ctx()) { - Some((sender, receiver)) => (Some(sender), Some(receiver)), - None => (None, None), - }; - // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, // all spawned futures related to this `UTXO` coin will be aborted as well. let abortable_system: AbortableQueue = self.ctx().abortable_system.create_subsystem()?; - let rpc_client = self - .rpc_client(scripthash_notification_sender, abortable_system.create_subsystem()?) - .await?; + let rpc_client = self.rpc_client(abortable_system.create_subsystem()?).await?; let tx_fee = self.tx_fee(&rpc_client).await?; let decimals = self.decimals(&rpc_client).await?; let dust_amount = self.dust_amount(); @@ -392,9 +358,8 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { check_utxo_maturity, block_headers_status_notifier, block_headers_status_watcher, + ctx: self.ctx().clone().weak(), abortable_system, - scripthash_notification_handler, - ctx: self.ctx().weak(), }; Ok(coin) } @@ -529,11 +494,7 @@ pub trait UtxoCoinBuilderCommonOps { } } - async fn rpc_client( - &self, - scripthash_notification_sender: ScripthashNotificationSender, - abortable_system: AbortableQueue, - ) -> UtxoCoinBuildResult { + async fn rpc_client(&self, abortable_system: AbortableQueue) -> UtxoCoinBuildResult { match self.activation_params().mode.clone() { UtxoRpcMode::Native => { #[cfg(target_arch = "wasm32")] @@ -557,7 +518,6 @@ pub trait UtxoCoinBuilderCommonOps { ElectrumBuilderArgs::default(), servers, (min_connected, max_connected), - scripthash_notification_sender, ) .await?; Ok(UtxoRpcClientEnum::Electrum(electrum)) @@ -573,7 +533,6 @@ pub trait UtxoCoinBuilderCommonOps { args: ElectrumBuilderArgs, servers: Vec, (min_connected, max_connected): (Option, Option), - scripthash_notification_sender: ScripthashNotificationSender, ) -> UtxoCoinBuildResult { let coin_ticker = self.ticker().to_owned(); let ctx = self.ctx(); @@ -610,8 +569,8 @@ pub trait UtxoCoinBuilderCommonOps { client_settings, event_handlers, block_headers_storage, + ctx.event_stream_manager.clone(), abortable_system, - scripthash_notification_sender, ) .map_to_mm(UtxoCoinBuildError::Internal) } @@ -768,7 +727,7 @@ pub trait UtxoCoinBuilderCommonOps { }; let secs_since_date = current_time_sec - date_s; - let days_since_date = (secs_since_date / DAY_IN_SECONDS) - 1; + let days_since_date = (secs_since_date / DAY_IN_SECONDS).max(1) - 1; let blocks_to_sync = (days_since_date * blocks_per_day) + blocks_per_day; if current_block_height < blocks_to_sync { diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index 03fea53699..d432207d8e 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -147,9 +147,8 @@ pub(super) fn utxo_coin_fields_for_test( check_utxo_maturity: false, block_headers_status_notifier: None, block_headers_status_watcher: None, + ctx: MmWeak::default(), abortable_system: AbortableQueue::default(), - scripthash_notification_handler: None, - ctx: Default::default(), } } @@ -271,6 +270,7 @@ pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { hd_accounts.insert(0, hd_account_for_test); let mut fields = utxo_coin_fields_for_test(rpc_client.into(), None, false); + fields.ctx = ctx.weak(); fields.conf.ticker = "DOC".to_string(); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { inner: HDWallet { @@ -291,6 +291,7 @@ pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { coin.clone(), storage, ctx.metrics.clone(), + ctx.event_stream_manager.clone(), current_balances.clone(), )); @@ -316,6 +317,7 @@ pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { coin.clone(), storage, ctx.metrics.clone(), + ctx.event_stream_manager.clone(), current_balances, )); diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index e2c2a7b740..0abf28e35b 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -975,7 +975,7 @@ impl MarketCoinOps for UtxoStandardCoin { impl MmCoin for UtxoStandardCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn spawner(&self) -> WeakSpawner { self.as_ref().abortable_system.weak_spawner() } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 8148cd4ce9..0f376620db 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -42,6 +42,7 @@ use futures::channel::mpsc::channel; use futures::future::{join_all, Either, FutureExt, TryFutureExt}; use keys::prefixes::*; use mm2_core::mm_ctx::MmCtxBuilder; +use mm2_event_stream::StreamingManager; use mm2_number::bigdecimal::{BigDecimal, Signed}; use mm2_test_helpers::electrums::doc_electrums; use mm2_test_helpers::for_tests::{electrum_servers_rpc, mm_ctx_with_custom_db, DOC_ELECTRUM_ADDRS, @@ -85,7 +86,7 @@ pub fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let servers = servers.into_iter().map(|s| json::from_value(s).unwrap()).collect(); let abortable_system = AbortableQueue::default(); - block_on(builder.electrum_client(abortable_system, args, servers, (None, None), None)).unwrap() + block_on(builder.electrum_client(abortable_system, args, servers, (None, None))).unwrap() } /// Returned client won't work by default, requires some mocks to be usable @@ -481,8 +482,8 @@ fn test_wait_for_payment_spend_timeout_electrum() { client_settings, Default::default(), block_headers_storage, + StreamingManager::default(), abortable_system, - None, ) .expect("Expected electrum_client_impl constructed without a problem"); let client = UtxoRpcClientEnum::Electrum(client); @@ -603,15 +604,12 @@ fn test_withdraw_impl_set_fixed_fee() { let withdraw_req = WithdrawRequest { amount: 1u64.into(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), - max: false, fee: Some(WithdrawFee::UtxoFixed { amount: "0.1".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; let expected = Some( UtxoFeeDetails { @@ -652,15 +650,12 @@ fn test_withdraw_impl_sat_per_kb_fee() { let withdraw_req = WithdrawRequest { amount: 1u64.into(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), - max: false, fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; // The resulting transaction size might be 244 or 245 bytes depending on signature size // MM2 always expects the worst case during fee calculation @@ -704,15 +699,12 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { let withdraw_req = WithdrawRequest { amount: "9.9789".parse().unwrap(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), - max: false, fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); // The resulting transaction size might be 210 or 211 bytes depending on signature size @@ -758,15 +750,12 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee() let withdraw_req = WithdrawRequest { amount: "9.9789".parse().unwrap(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), - max: false, fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.09999999".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); // The resulting transaction size might be 210 or 211 bytes depending on signature size @@ -812,15 +801,12 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() { let withdraw_req = WithdrawRequest { amount: "9.97939455".parse().unwrap(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), - max: false, fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; block_on_f01(coin.withdraw(withdraw_req)).unwrap_err(); } @@ -853,15 +839,13 @@ fn test_withdraw_impl_sat_per_kb_fee_max() { let withdraw_req = WithdrawRequest { amount: 0u64.into(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), max: true, fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; // The resulting transaction size might be 210 or 211 bytes depending on signature size // MM2 always expects the worst case during fee calculation @@ -922,13 +906,9 @@ fn test_withdraw_kmd_rewards_impl( let withdraw_req = WithdrawRequest { amount: BigDecimal::from_str("0.00001").unwrap(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: "KMD".to_owned(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some("KMD".into()), @@ -1004,13 +984,9 @@ fn test_withdraw_rick_rewards_none() { let withdraw_req = WithdrawRequest { amount: BigDecimal::from_str("0.00001").unwrap(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: "RICK".to_owned(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -1543,13 +1519,13 @@ fn test_network_info_negative_time_offset() { #[test] fn test_unavailable_electrum_proto_version() { ElectrumClientImpl::try_new_arc.mock_safe( - |client_settings, block_headers_storage, abortable_system, event_handlers, scripthash_notification_sender| { + |client_settings, block_headers_storage, streaming_manager, abortable_system, event_handlers| { MockResult::Return(ElectrumClientImpl::with_protocol_version( client_settings, block_headers_storage, + streaming_manager, abortable_system, event_handlers, - scripthash_notification_sender, OrdRange::new(1.8, 1.9).unwrap(), )) }, @@ -1624,13 +1600,13 @@ fn test_spam_rick() { fn test_one_unavailable_electrum_proto_version() { // Patch the electurm client construct to require protocol version 1.4 only. ElectrumClientImpl::try_new_arc.mock_safe( - |client_settings, block_headers_storage, abortable_system, event_handlers, scripthash_notification_sender| { + |client_settings, block_headers_storage, streaming_manager, abortable_system, event_handlers| { MockResult::Return(ElectrumClientImpl::with_protocol_version( client_settings, block_headers_storage, + streaming_manager, abortable_system, event_handlers, - scripthash_notification_sender, OrdRange::new(1.4, 1.4).unwrap(), )) }, @@ -3204,13 +3180,9 @@ fn test_withdraw_to_p2pk_fails() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: "03f8f8fa2062590ba9a0a7a86f937de22f540c015864aad35a2a9f6766de906265".to_string(), coin: TEST_COIN_NAME.into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; assert!(matches!( @@ -3262,13 +3234,9 @@ fn test_withdraw_to_p2pkh() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: p2pkh_address.to_string(), coin: TEST_COIN_NAME.into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); @@ -3322,13 +3290,9 @@ fn test_withdraw_to_p2sh() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: p2sh_address.to_string(), coin: TEST_COIN_NAME.into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); @@ -3382,13 +3346,9 @@ fn test_withdraw_to_p2wpkh() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: p2wpkh_address.to_string(), coin: TEST_COIN_NAME.into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); @@ -3437,13 +3397,9 @@ fn test_withdraw_p2pk_balance() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: my_p2pkh_address.to_string(), coin: TEST_COIN_NAME.into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); @@ -4075,8 +4031,6 @@ fn test_scan_for_new_addresses() { let client = NativeClient(Arc::new(NativeClientImpl::default())); let mut fields = utxo_coin_fields_for_test(UtxoRpcClientEnum::Native(client), None, false); - let ctx = MmCtxBuilder::new().into_mm_arc(); - fields.ctx = ctx.weak(); let mut hd_accounts = HDAccountsMap::new(); hd_accounts.insert(0, UtxoHDAccount { account_id: 0, @@ -4219,8 +4173,6 @@ fn test_get_new_address() { let client = NativeClient(Arc::new(NativeClientImpl::default())); let mut fields = utxo_coin_fields_for_test(UtxoRpcClientEnum::Native(client), None, false); - let ctx = MmCtxBuilder::new().into_mm_arc(); - fields.ctx = ctx.weak(); let mut hd_accounts = HDAccountsMap::new(); let hd_account_for_test = UtxoHDAccount { account_id: 0, diff --git a/mm2src/coins/utxo/utxo_tx_history_v2.rs b/mm2src/coins/utxo/utxo_tx_history_v2.rs index 698a9bf0b6..3f44593643 100644 --- a/mm2src/coins/utxo/utxo_tx_history_v2.rs +++ b/mm2src/coins/utxo/utxo_tx_history_v2.rs @@ -4,16 +4,18 @@ use crate::my_tx_history_v2::{CoinWithTxHistoryV2, DisplayAddress, TxHistoryStor use crate::tx_history_storage::FilteringAddresses; use crate::utxo::bch::BchCoin; use crate::utxo::slp::ParseSlpScriptError; +use crate::utxo::tx_history_events::TxHistoryEventStreamer; use crate::utxo::{utxo_common, AddrFromStrError, GetBlockHeaderError}; use crate::{BalanceError, BalanceResult, BlockHeightAndTime, CoinWithDerivationMethod, HistorySyncState, - MarketCoinOps, NumConversError, ParseBigDecimalError, TransactionDetails, UnexpectedDerivationMethod, - UtxoRpcError, UtxoTx}; + MarketCoinOps, MmCoin, NumConversError, ParseBigDecimalError, TransactionDetails, + UnexpectedDerivationMethod, UtxoRpcError, UtxoTx}; use async_trait::async_trait; use common::executor::Timer; use common::log::{error, info}; use derive_more::Display; use keys::Address; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use mm2_state_machine::prelude::*; @@ -104,7 +106,7 @@ pub struct UtxoTxDetailsParams<'a, Storage> { #[async_trait] pub trait UtxoTxHistoryOps: - CoinWithTxHistoryV2 + CoinWithDerivationMethod + MarketCoinOps + Send + Sync + 'static + CoinWithTxHistoryV2 + CoinWithDerivationMethod + MarketCoinOps + MmCoin + Send + Sync + 'static { /// Returns addresses for those we need to request Transaction history. async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError>; @@ -145,6 +147,8 @@ struct UtxoTxHistoryStateMachine`] everywhere. balances: HashMap, @@ -620,6 +624,12 @@ where }, }; + ctx.streaming_manager + .send_fn(&TxHistoryEventStreamer::derive_streamer_id(ctx.coin.ticker()), || { + tx_details.clone() + }) + .ok(); + if let Err(e) = ctx.storage.add_transactions_to_history(&wallet_id, tx_details).await { return Self::change_state(Stopped::storage_error(e)); } @@ -707,6 +717,7 @@ pub async fn bch_and_slp_history_loop( coin: BchCoin, storage: impl TxHistoryStorage, metrics: MetricsArc, + streaming_manager: StreamingManager, current_balance: Option, ) { let balances = match current_balance { @@ -743,6 +754,7 @@ pub async fn bch_and_slp_history_loop( coin, storage, metrics, + streaming_manager, balances, }; state_machine @@ -755,6 +767,7 @@ pub async fn utxo_history_loop( coin: Coin, storage: Storage, metrics: MetricsArc, + streaming_manager: StreamingManager, current_balances: HashMap, ) where Coin: UtxoTxHistoryOps, @@ -764,6 +777,7 @@ pub async fn utxo_history_loop( coin, storage, metrics, + streaming_manager, balances: current_balances, }; state_machine diff --git a/mm2src/coins/utxo/utxo_wasm_tests.rs b/mm2src/coins/utxo/utxo_wasm_tests.rs index bd059c8627..2241f40781 100644 --- a/mm2src/coins/utxo/utxo_wasm_tests.rs +++ b/mm2src/coins/utxo/utxo_wasm_tests.rs @@ -42,7 +42,7 @@ pub async fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let servers = servers.into_iter().map(|s| json::from_value(s).unwrap()).collect(); let abortable_system = AbortableQueue::default(); builder - .electrum_client(abortable_system, args, servers, (None, None), None) + .electrum_client(abortable_system, args, servers, (None, None)) .await .unwrap() } diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 07462d2a07..1e4ef60567 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1,7 +1,10 @@ pub mod storage; -mod z_balance_streaming; +pub mod tx_history_events; +#[cfg_attr(not(target_arch = "wasm32"), cfg(test))] +mod tx_streaming_tests; +pub mod z_balance_streaming; mod z_coin_errors; -#[cfg(all(test, feature = "zhtlc-native-tests"))] +#[cfg(all(test, not(target_arch = "wasm32"), feature = "zhtlc-native-tests"))] mod z_coin_native_tests; mod z_htlc; mod z_rpc; @@ -24,11 +27,11 @@ use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxDa UtxoCommonOps, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; use crate::utxo::{UnsupportedAddr, UtxoFeeDetails}; use crate::z_coin::storage::{BlockDbImpl, WalletDbShared}; -use crate::z_coin::z_balance_streaming::ZBalanceEventHandler; + use crate::z_coin::z_tx_history::{fetch_tx_history_from_db, ZCoinTxHistoryItem}; -use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, - DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, - MmCoinEnum, NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, + FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, + NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyActivationPolicy, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, @@ -39,15 +42,14 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, Coi ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; + WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use bitcrypto::dhash256; use chain::constants::SEQUENCE_FINAL; use chain::{Transaction as UtxoTx, TransactionOutput}; -use common::calc_total_pages; use common::executor::{AbortableSystem, AbortedError}; -use common::{log, one_thousand_u32}; +use common::{calc_total_pages, log}; use crypto::privkey::{key_pair_from_secret, secp_privkey_from_hash}; use crypto::HDPathToCoin; use crypto::{Bip32DerPathOps, GlobalHDAccountArc}; @@ -59,7 +61,6 @@ use keys::hash::H256; use keys::{KeyPair, Message, Public}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; use primitives::bytes::Bytes; @@ -70,6 +71,7 @@ use serialization::CoinVariant; use std::collections::{HashMap, HashSet}; use std::convert::TryInto; use std::iter; +use std::num::NonZeroU32; use std::num::TryFromIntError; use std::path::PathBuf; use std::sync::Arc; @@ -209,7 +211,6 @@ pub struct ZCoinFields { light_wallet_db: WalletDbShared, consensus_params: ZcoinConsensusParams, sync_state_connector: AsyncMutex, - z_balance_event_handler: Option, } impl Transaction for ZTransaction { @@ -520,7 +521,7 @@ impl ZCoin { fn tx_details_from_db_item( &self, tx_item: ZCoinTxHistoryItem, - transactions: &mut HashMap, + transactions: &HashMap, prev_transactions: &HashMap, current_block: u64, ) -> Result> { @@ -533,7 +534,7 @@ impl ZCoin { let mut transparent_input_amount = Amount::zero(); let hash = H256Json::from(tx_item.tx_hash.as_slice()); - let z_tx = transactions.remove(&hash).or_mm_err(|| NoInfoAboutTx(hash))?; + let z_tx = transactions.get(&hash).or_mm_err(|| NoInfoAboutTx(hash))?; for input in z_tx.vin.iter() { let mut hash = H256Json::from(*input.prevout.hash()); hash.0.reverse(); @@ -624,7 +625,7 @@ impl ZCoin { .iter() .map(|item| H256Json::from(item.tx_hash.as_slice())) .collect(); - let mut transactions = self.z_transactions_from_cache_or_rpc(hashes_for_verbose).await?; + let transactions = self.z_transactions_from_cache_or_rpc(hashes_for_verbose).await?; let prev_tx_hashes: HashSet<_> = transactions .iter() @@ -641,9 +642,7 @@ impl ZCoin { let transactions = req_result .transactions .into_iter() - .map(|sql_item| { - self.tx_details_from_db_item(sql_item, &mut transactions, &prev_transactions, current_block) - }) + .map(|sql_item| self.tx_details_from_db_item(sql_item, &transactions, &prev_transactions, current_block)) .collect::>()?; Ok(MyTxHistoryResponseV2 { @@ -660,17 +659,6 @@ impl ZCoin { paging_options: request.paging_options, }) } - - async fn spawn_balance_stream_if_enabled(&self, ctx: &MmArc) -> Result<(), String> { - let coin = self.clone(); - if let Some(stream_config) = &ctx.event_stream_configuration { - if let EventInitStatus::Failed(err) = EventBehaviour::spawn_if_active(coin, stream_config).await { - return ERR!("Failed spawning zcoin balance event with error: {}", err); - } - } - - Ok(()) - } } impl AsRef for ZCoin { @@ -768,19 +756,38 @@ pub enum ZcoinRpcMode { } #[derive(Clone, Deserialize)] +#[serde(default)] pub struct ZcoinActivationParams { pub mode: ZcoinRpcMode, pub required_confirmations: Option, pub requires_notarization: Option, pub zcash_params_path: Option, - #[serde(default = "one_thousand_u32")] - pub scan_blocks_per_iteration: u32, - #[serde(default)] + pub scan_blocks_per_iteration: NonZeroU32, pub scan_interval_ms: u64, - #[serde(default)] pub account: u32, } +impl Default for ZcoinActivationParams { + fn default() -> Self { + Self { + mode: ZcoinRpcMode::Light { + electrum_servers: Vec::new(), + min_connected: None, + max_connected: None, + light_wallet_d_servers: Vec::new(), + sync_params: None, + skip_sync_params: None, + }, + required_confirmations: None, + requires_notarization: None, + zcash_params_path: None, + scan_blocks_per_iteration: NonZeroU32::new(1000).expect("1000 is a valid value"), + scan_interval_ms: Default::default(), + account: Default::default(), + } + } +} + pub async fn z_coin_from_conf_and_params( ctx: &MmArc, ticker: &str, @@ -897,24 +904,11 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { ); let blocks_db = self.init_blocks_db().await?; - let (z_balance_event_sender, z_balance_event_handler) = if self.ctx.event_stream_configuration.is_some() { - let (sender, receiver) = futures::channel::mpsc::unbounded(); - (Some(sender), Some(Arc::new(AsyncMutex::new(receiver)))) - } else { - (None, None) - }; let (sync_state_connector, light_wallet_db) = match &self.z_coin_params.mode { #[cfg(not(target_arch = "wasm32"))] ZcoinRpcMode::Native => { - init_native_client( - &self, - self.native_client()?, - blocks_db, - &z_spending_key, - z_balance_event_sender, - ) - .await? + init_native_client(&self, self.native_client()?, blocks_db, &z_spending_key).await? }, ZcoinRpcMode::Light { light_wallet_d_servers, @@ -929,7 +923,6 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { sync_params, skip_sync_params.unwrap_or_default(), &z_spending_key, - z_balance_event_sender, ) .await? }, @@ -945,16 +938,9 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { light_wallet_db, consensus_params: self.protocol_info.consensus_params, sync_state_connector, - z_balance_event_handler, }); - let zcoin = ZCoin { utxo_arc, z_fields }; - zcoin - .spawn_balance_stream_if_enabled(self.ctx) - .await - .map_to_mm(ZCoinBuildError::FailedSpawningBalanceEvents)?; - - Ok(zcoin) + Ok(ZCoin { utxo_arc, z_fields }) } } @@ -1676,7 +1662,7 @@ impl WatcherOps for ZCoin { impl MmCoin for ZCoin { fn is_asset_chain(&self) -> bool { self.utxo_arc.conf.asset_chain } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn spawner(&self) -> WeakSpawner { self.as_ref().abortable_system.weak_spawner() } fn withdraw(&self, _req: WithdrawRequest) -> WithdrawFut { Box::new(futures01::future::err(MmError::new(WithdrawError::InternalError( diff --git a/mm2src/coins/z_coin/storage.rs b/mm2src/coins/z_coin/storage.rs index b3c2c108c4..e2534281b7 100644 --- a/mm2src/coins/z_coin/storage.rs +++ b/mm2src/coins/z_coin/storage.rs @@ -1,4 +1,5 @@ use crate::z_coin::{ValidateBlocksError, ZcoinConsensusParams, ZcoinStorageError}; +use mm2_event_stream::StreamingManager; pub mod blockdb; pub use blockdb::*; @@ -10,7 +11,6 @@ pub(crate) use z_params::ZcashParamsWasmImpl; pub use walletdb::*; -use crate::z_coin::z_balance_streaming::ZBalanceEventSender; use mm2_err_handle::mm_error::MmResult; #[cfg(target_arch = "wasm32")] use walletdb::wasm::storage::DataConnStmtCacheWasm; @@ -60,7 +60,7 @@ pub struct CompactBlockRow { #[derive(Clone)] pub enum BlockProcessingMode { Validate, - Scan(DataConnStmtCacheWrapper, Option), + Scan(DataConnStmtCacheWrapper, StreamingManager), } /// Checks that the scanned blocks in the data database, when combined with the recent @@ -119,7 +119,7 @@ pub async fn scan_cached_block( params: &ZcoinConsensusParams, block: &CompactBlock, last_height: &mut BlockHeight, -) -> Result { +) -> Result>, ValidateBlocksError> { let mut data_guard = data.inner().clone(); // Fetch the ExtendedFullViewingKeys we are tracking let extfvks = data_guard.get_extended_full_viewing_keys().await?; @@ -184,9 +184,7 @@ pub async fn scan_cached_block( ); witnesses.extend(new_witnesses); - *last_height = current_height; - // If there are any transactions in the block, return the transaction count - Ok(txs.len()) + Ok(txs) } diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs index cccf8cc0a9..826ed52bdd 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs @@ -1,9 +1,10 @@ use crate::z_coin::storage::{scan_cached_block, validate_chain, BlockDbImpl, BlockProcessingMode, CompactBlockRow, ZcoinConsensusParams, ZcoinStorageRes}; +use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; +use crate::z_coin::z_balance_streaming::ZCoinBalanceEventStreamer; use crate::z_coin::z_coin_errors::ZcoinStorageError; use async_trait::async_trait; -use futures_util::SinkExt; use mm2_core::mm_ctx::MmArc; use mm2_db::indexed_db::{BeBigUint, ConstructibleDb, DbIdentifier, DbInstance, DbLocked, DbUpgrader, IndexedDb, IndexedDbBuilder, InitDbResult, MultiIndex, OnUpgradeResult, TableSignature}; @@ -221,6 +222,7 @@ impl BlockDbImpl { validate_from: Option<(BlockHeight, BlockHash)>, limit: Option, ) -> ZcoinStorageRes<()> { + let ticker = self.ticker.to_owned(); let mut from_height = match &mode { BlockProcessingMode::Validate => validate_from .map(|(height, _)| height) @@ -241,7 +243,7 @@ impl BlockDbImpl { if block.height() != cbr.height { return MmError::err(ZcoinStorageError::CorruptedData(format!( - "Block height {} did not match row's height field value {}", + "{ticker}, Block height {} did not match row's height field value {}", block.height(), cbr.height ))); @@ -251,14 +253,17 @@ impl BlockDbImpl { BlockProcessingMode::Validate => { validate_chain(block, &mut prev_height, &mut prev_hash).await?; }, - BlockProcessingMode::Scan(data, z_balance_change_sender) => { - let tx_size = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; - // If there is/are transactions present in the current scanned block(s), - // we trigger a `Triggered` event to update the balance change. - if tx_size > 0 { - if let Some(mut sender) = z_balance_change_sender.clone() { - sender.send(()).await.expect("No receiver is available/dropped"); - }; + BlockProcessingMode::Scan(data, streaming_manager) => { + let txs = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; + if !txs.is_empty() { + // Stream out the new transactions. + streaming_manager + .send(&ZCoinTxHistoryEventStreamer::derive_streamer_id(&ticker), txs) + .ok(); + // And also stream balance changes. + streaming_manager + .send(&ZCoinBalanceEventStreamer::derive_streamer_id(&ticker), ()) + .ok(); }; }, } 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..e8ccdd2554 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs @@ -1,12 +1,13 @@ use crate::z_coin::storage::{scan_cached_block, validate_chain, BlockDbImpl, BlockProcessingMode, CompactBlockRow, ZcoinStorageRes}; +use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; +use crate::z_coin::z_balance_streaming::ZCoinBalanceEventStreamer; use crate::z_coin::z_coin_errors::ZcoinStorageError; use crate::z_coin::ZcoinConsensusParams; use common::async_blocking; use db_common::sqlite::rusqlite::{params, Connection}; use db_common::sqlite::{query_single_row, run_optimization_pragmas, rusqlite}; -use futures_util::SinkExt; use itertools::Itertools; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -225,14 +226,17 @@ impl BlockDbImpl { BlockProcessingMode::Validate => { validate_chain(block, &mut prev_height, &mut prev_hash).await?; }, - BlockProcessingMode::Scan(data, z_balance_change_sender) => { - let tx_size = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; - // If there are transactions present in the current scanned block, - // we send a `Triggered` event to update the balance change. - if tx_size > 0 { - if let Some(mut sender) = z_balance_change_sender.clone() { - sender.send(()).await.expect("No receiver is available/dropped"); - }; + BlockProcessingMode::Scan(data, streaming_manager) => { + let txs = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; + if !txs.is_empty() { + // Stream out the new transactions. + streaming_manager + .send(&ZCoinTxHistoryEventStreamer::derive_streamer_id(&ticker), txs) + .ok(); + // And also stream balance changes. + streaming_manager + .send(&ZCoinBalanceEventStreamer::derive_streamer_id(&ticker), ()) + .ok(); }; }, } diff --git a/mm2src/coins/z_coin/storage/blockdb/mod.rs b/mm2src/coins/z_coin/storage/blockdb/mod.rs index 7e2ef49fe7..1b3676dbbd 100644 --- a/mm2src/coins/z_coin/storage/blockdb/mod.rs +++ b/mm2src/coins/z_coin/storage/blockdb/mod.rs @@ -18,7 +18,6 @@ pub struct BlockDbImpl { pub db: Arc>, #[cfg(target_arch = "wasm32")] pub db: SharedDb, - #[allow(unused)] ticker: String, } diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs index 4c68fec22c..c1ffdfb0a2 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs @@ -71,6 +71,7 @@ mod wasm_test { use crate::z_coin::{ValidateBlocksError, ZcoinConsensusParams, ZcoinStorageError}; use crate::ZcoinProtocolInfo; use mm2_core::mm_ctx::MmArc; + use mm2_event_stream::StreamingManager; use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; use protobuf::Message; use std::path::PathBuf; @@ -255,7 +256,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -300,7 +301,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -359,7 +360,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -453,7 +454,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -542,7 +543,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -572,7 +573,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -611,7 +612,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -629,7 +630,7 @@ mod wasm_test { let scan = blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -653,7 +654,7 @@ mod wasm_test { assert!(blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None ) @@ -697,7 +698,7 @@ mod wasm_test { assert!(blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None ) @@ -718,7 +719,7 @@ mod wasm_test { assert!(blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None ) @@ -760,7 +761,7 @@ mod wasm_test { assert!(blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None ) @@ -790,7 +791,7 @@ mod wasm_test { let scan = blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -832,7 +833,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .is_ok()); // @@ -852,7 +853,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .is_ok()); // @@ -897,7 +898,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .is_ok()); // @@ -928,7 +929,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .is_ok()); // @@ -1098,7 +1099,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .unwrap(); // assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value); @@ -1155,7 +1156,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .unwrap(); // @@ -1191,7 +1192,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .unwrap(); // diff --git a/mm2src/coins/z_coin/tx_history_events.rs b/mm2src/coins/z_coin/tx_history_events.rs new file mode 100644 index 0000000000..beb57a2b1a --- /dev/null +++ b/mm2src/coins/z_coin/tx_history_events.rs @@ -0,0 +1,118 @@ +use super::z_tx_history::fetch_txs_from_db; +use super::{NoInfoAboutTx, ZCoin, ZTxHistoryError, ZcoinTxDetails}; +use crate::utxo::rpc_clients::UtxoRpcError; +use crate::MarketCoinOps; +use common::log; +use mm2_err_handle::prelude::MmError; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use rpc::v1::types::H256 as H256Json; + +use async_trait::async_trait; +use futures::channel::oneshot; +use futures::compat::Future01CompatExt; +use futures::StreamExt; +use zcash_client_backend::wallet::WalletTx; +use zcash_primitives::sapling::Nullifier; + +pub struct ZCoinTxHistoryEventStreamer { + coin: ZCoin, +} + +impl ZCoinTxHistoryEventStreamer { + #[inline(always)] + pub fn new(coin: ZCoin) -> Self { Self { coin } } + + #[inline(always)] + pub fn derive_streamer_id(coin: &str) -> String { format!("TX_HISTORY:{coin}") } +} + +#[async_trait] +impl EventStreamer for ZCoinTxHistoryEventStreamer { + type DataInType = Vec>; + + fn streamer_id(&self) -> String { Self::derive_streamer_id(self.coin.ticker()) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + while let Some(new_txs) = data_rx.next().await { + let new_txs_details = match get_tx_details(&self.coin, new_txs).await { + Ok(tx_details) => tx_details, + Err(e) => { + broadcaster.broadcast(Event::err(self.streamer_id(), json!({ "error": e.to_string() }))); + log::error!("Failed to get tx details in streamer {}: {e:?}", self.streamer_id()); + continue; + }, + }; + for tx_details in new_txs_details { + let tx_details = serde_json::to_value(tx_details).expect("Serialization should't fail."); + broadcaster.broadcast(Event::new(self.streamer_id(), tx_details)); + } + } + } +} + +/// Errors that can occur while getting transaction details for some tx hashes. +/// +/// The error implements `Display` trait, so it can be easily converted `.to_string`. +#[derive(Debug, derive_more::Display)] +enum GetTxDetailsError { + #[display(fmt = "RPC Error: {_0:?}")] + UtxoRpcError(UtxoRpcError), + #[display(fmt = "DB Error: {_0:?}")] + DbError(String), + #[display(fmt = "Internal Error: {_0:?}")] + Internal(NoInfoAboutTx), +} + +impl From> for GetTxDetailsError { + fn from(e: MmError) -> Self { GetTxDetailsError::UtxoRpcError(e.into_inner()) } +} + +impl From> for GetTxDetailsError { + fn from(e: MmError) -> Self { GetTxDetailsError::DbError(e.to_string()) } +} + +impl From> for GetTxDetailsError { + fn from(e: MmError) -> Self { GetTxDetailsError::Internal(e.into_inner()) } +} + +async fn get_tx_details(coin: &ZCoin, txs: Vec>) -> Result, GetTxDetailsError> { + let current_block = coin.utxo_rpc_client().get_block_count().compat().await?; + let txs_from_db = { + let tx_ids = txs.iter().map(|tx| tx.txid).collect(); + fetch_txs_from_db(coin, tx_ids).await? + }; + + let hashes_for_verbose = txs_from_db + .iter() + .map(|item| H256Json::from(item.tx_hash.as_slice())) + .collect(); + let transactions = coin.z_transactions_from_cache_or_rpc(hashes_for_verbose).await?; + + let prev_tx_hashes = transactions + .iter() + .flat_map(|(_, tx)| { + tx.vin.iter().map(|vin| { + let mut hash = *vin.prevout.hash(); + hash.reverse(); + H256Json::from(hash) + }) + }) + .collect(); + let prev_transactions = coin.z_transactions_from_cache_or_rpc(prev_tx_hashes).await?; + + let txs_details = txs_from_db + .into_iter() + .map(|tx_item| coin.tx_details_from_db_item(tx_item, &transactions, &prev_transactions, current_block)) + .collect::>()?; + + Ok(txs_details) +} diff --git a/mm2src/coins/z_coin/tx_streaming_tests/mod.rs b/mm2src/coins/z_coin/tx_streaming_tests/mod.rs new file mode 100644 index 0000000000..457162c80f --- /dev/null +++ b/mm2src/coins/z_coin/tx_streaming_tests/mod.rs @@ -0,0 +1,30 @@ +#[cfg(not(target_arch = "wasm32"))] mod native; +#[cfg(target_arch = "wasm32")] mod wasm; + +use common::now_sec; +use mm2_test_helpers::for_tests::{PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS}; + +use crate::utxo::rpc_clients::ElectrumConnectionSettings; +use crate::z_coin::{ZcoinActivationParams, ZcoinRpcMode}; + +fn light_zcoin_activation_params() -> ZcoinActivationParams { + ZcoinActivationParams { + mode: ZcoinRpcMode::Light { + electrum_servers: PIRATE_ELECTRUMS + .iter() + .map(|s| ElectrumConnectionSettings { + url: s.to_string(), + protocol: Default::default(), + disable_cert_verification: Default::default(), + timeout_sec: None, + }) + .collect(), + min_connected: None, + max_connected: None, + light_wallet_d_servers: PIRATE_LIGHTWALLETD_URLS.iter().map(|s| s.to_string()).collect(), + sync_params: Some(crate::z_coin::SyncStartPoint::Date(now_sec() - 24 * 60 * 60)), + skip_sync_params: None, + }, + ..Default::default() + } +} diff --git a/mm2src/coins/z_coin/tx_streaming_tests/native.rs b/mm2src/coins/z_coin/tx_streaming_tests/native.rs new file mode 100644 index 0000000000..cc5ecc5812 --- /dev/null +++ b/mm2src/coins/z_coin/tx_streaming_tests/native.rs @@ -0,0 +1,73 @@ +use common::custom_futures::timeout::FutureTimerExt; +use common::{block_on, Future01CompatExt}; +use mm2_core::mm_ctx::MmCtxBuilder; +use mm2_test_helpers::for_tests::{pirate_conf, ARRR}; +use std::time::Duration; + +use super::light_zcoin_activation_params; +use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; +use crate::z_coin::z_coin_from_conf_and_params; +use crate::z_coin::z_htlc::z_send_dex_fee; +use crate::{CoinProtocol, MarketCoinOps, MmCoin, PrivKeyBuildPolicy}; + +#[test] +#[ignore] // Ignored because we don't have zcash params in CI. TODO: Why not download them on demand like how we do in wasm (see download_and_save_params). +fn test_zcoin_tx_streaming() { + let ctx = MmCtxBuilder::default().into_mm_arc(); + let conf = pirate_conf(); + let params = light_zcoin_activation_params(); + // Address: RQX5MnqnxEk6P33LSEAxC2vqA7DfSdWVyH + // Or: zs1n2azlwcj9pvl2eh36qvzgeukt2cpzmw44hya8wyu52j663d0dfs4d5hjx6tr04trz34jxyy433j + let priv_key_policy = + PrivKeyBuildPolicy::IguanaPrivKey("6d862798ef956fb60fb17bcc417dd6d44bfff066a4a49301cd2528e41a4a3e45".into()); + let protocol_info = match serde_json::from_value::(conf["protocol"].clone()).unwrap() { + CoinProtocol::ZHTLC(protocol_info) => protocol_info, + other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), + }; + + let coin = block_on(z_coin_from_conf_and_params( + &ctx, + ARRR, + &conf, + ¶ms, + protocol_info, + priv_key_policy, + )) + .unwrap(); + + // Wait till we are synced with the sapling state. + while !block_on(coin.is_sapling_state_synced()) { + std::thread::sleep(Duration::from_secs(1)); + } + + // Query the block height to make sure our electrums are actually connected. + log!("current block = {:?}", block_on(coin.current_block().compat()).unwrap()); + + // Add a new client to use it for listening to tx history events. + let client_id = 1; + let mut event_receiver = ctx.event_stream_manager.new_client(client_id).unwrap(); + // Add the streamer that will stream the tx history events. + let streamer = ZCoinTxHistoryEventStreamer::new(coin.clone()); + // Subscribe the client to the streamer. + block_on(ctx.event_stream_manager.add(client_id, streamer, coin.spawner())).unwrap(); + + // Send a tx to have it in the tx history. + let tx = block_on(z_send_dex_fee(&coin, "0.0001".parse().unwrap(), &[1; 16])).unwrap(); + + // Wait for the tx history event (should be streamed next block). + let event = block_on(Box::pin(event_receiver.recv()).timeout_secs(120.)) + .expect("timed out waiting for tx to showup") + .expect("tx history sender shutdown"); + + log!("{:?}", event.get()); + let (event_type, event_data) = event.get(); + // Make sure this is not an error event, + assert!(!event_type.starts_with("ERROR_")); + // from the expected streamer, + assert_eq!( + event_type, + ZCoinTxHistoryEventStreamer::derive_streamer_id(coin.ticker()) + ); + // and has the expected data. + assert_eq!(event_data["tx_hash"].as_str().unwrap(), tx.txid().to_string()); +} diff --git a/mm2src/coins/z_coin/tx_streaming_tests/wasm.rs b/mm2src/coins/z_coin/tx_streaming_tests/wasm.rs new file mode 100644 index 0000000000..192db3c7a9 --- /dev/null +++ b/mm2src/coins/z_coin/tx_streaming_tests/wasm.rs @@ -0,0 +1,74 @@ +use common::custom_futures::timeout::FutureTimerExt; +use common::{executor::Timer, Future01CompatExt}; +use mm2_core::mm_ctx::MmCtxBuilder; +use mm2_test_helpers::for_tests::{pirate_conf, ARRR}; +use wasm_bindgen_test::*; + +use super::light_zcoin_activation_params; +use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; +use crate::z_coin::z_coin_from_conf_and_params; +use crate::z_coin::z_htlc::z_send_dex_fee; +use crate::PrivKeyBuildPolicy; +use crate::{CoinProtocol, MarketCoinOps, MmCoin}; + +#[wasm_bindgen_test] +async fn test_zcoin_tx_streaming() { + let ctx = MmCtxBuilder::default().into_mm_arc(); + let conf = pirate_conf(); + let params = light_zcoin_activation_params(); + // Address: RQX5MnqnxEk6P33LSEAxC2vqA7DfSdWVyH + // Or: zs1n2azlwcj9pvl2eh36qvzgeukt2cpzmw44hya8wyu52j663d0dfs4d5hjx6tr04trz34jxyy433j + let priv_key_policy = + PrivKeyBuildPolicy::IguanaPrivKey("6d862798ef956fb60fb17bcc417dd6d44bfff066a4a49301cd2528e41a4a3e45".into()); + let protocol_info = match serde_json::from_value::(conf["protocol"].clone()).unwrap() { + CoinProtocol::ZHTLC(protocol_info) => protocol_info, + other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), + }; + + let coin = z_coin_from_conf_and_params(&ctx, ARRR, &conf, ¶ms, protocol_info, priv_key_policy) + .await + .unwrap(); + + // Wait till we are synced with the sapling state. + while !coin.is_sapling_state_synced().await { + Timer::sleep(1.).await; + } + + // Query the block height to make sure our electrums are actually connected. + log!("current block = {:?}", coin.current_block().compat().await.unwrap()); + + // Add a new client to use it for listening to tx history events. + let client_id = 1; + let mut event_receiver = ctx.event_stream_manager.new_client(client_id).unwrap(); + // Add the streamer that will stream the tx history events. + let streamer = ZCoinTxHistoryEventStreamer::new(coin.clone()); + // Subscribe the client to the streamer. + ctx.event_stream_manager + .add(client_id, streamer, coin.spawner()) + .await + .unwrap(); + + // Send a tx to have it in the tx history. + let tx = z_send_dex_fee(&coin, "0.0001".parse().unwrap(), &[1; 16]) + .await + .unwrap(); + + // Wait for the tx history event (should be streamed next block). + let event = Box::pin(event_receiver.recv()) + .timeout_secs(120.) + .await + .expect("timed out waiting for tx to showup") + .expect("tx history sender shutdown"); + + log!("{:?}", event.get()); + let (event_type, event_data) = event.get(); + // Make sure this is not an error event, + assert!(!event_type.starts_with("ERROR_")); + // from the expected streamer, + assert_eq!( + event_type, + ZCoinTxHistoryEventStreamer::derive_streamer_id(coin.ticker()) + ); + // and has the expected data. + assert_eq!(event_data["tx_hash"].as_str().unwrap(), tx.txid().to_string()); +} diff --git a/mm2src/coins/z_coin/z_balance_streaming.rs b/mm2src/coins/z_coin/z_balance_streaming.rs index 5f6d3e590a..0760bfc929 100644 --- a/mm2src/coins/z_coin/z_balance_streaming.rs +++ b/mm2src/coins/z_coin/z_balance_streaming.rs @@ -1,114 +1,63 @@ use crate::common::Future01CompatExt; use crate::z_coin::ZCoin; -use crate::{MarketCoinOps, MmCoin}; +use crate::MarketCoinOps; use async_trait::async_trait; -use common::executor::{AbortSettings, SpawnAbortable}; -use common::log::{error, info}; -use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use common::log::error; use futures::channel::oneshot; -use futures::channel::oneshot::{Receiver, Sender}; -use futures::lock::Mutex as AsyncMutex; use futures_util::StreamExt; -use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; -use mm2_event_stream::{ErrorEventName, Event, EventName, EventStreamConfiguration}; -use std::sync::Arc; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; -pub type ZBalanceEventSender = UnboundedSender<()>; -pub type ZBalanceEventHandler = Arc>>; - -#[async_trait] -impl EventBehaviour for ZCoin { - fn event_name() -> EventName { EventName::CoinBalance } - - fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } - - async fn handle(self, _interval: f64, tx: Sender) { - const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; - - macro_rules! send_status_on_err { - ($match: expr, $sender: tt, $msg: literal) => { - match $match { - Some(t) => t, - None => { - $sender - .send(EventInitStatus::Failed($msg.to_owned())) - .expect(RECEIVER_DROPPED_MSG); - panic!("{}", $msg); - }, - } - }; - } +pub struct ZCoinBalanceEventStreamer { + coin: ZCoin, +} - let ctx = send_status_on_err!( - MmArc::from_weak(&self.as_ref().ctx), - tx, - "MM context must have been initialized already." - ); - let z_balance_change_handler = send_status_on_err!( - self.z_fields.z_balance_event_handler.as_ref(), - tx, - "Z balance change receiver can not be empty." - ); +impl ZCoinBalanceEventStreamer { + #[inline(always)] + pub fn new(coin: ZCoin) -> Self { Self { coin } } - tx.send(EventInitStatus::Success).expect(RECEIVER_DROPPED_MSG); + #[inline(always)] + pub fn derive_streamer_id(coin: &str) -> String { format!("BALANCE:{coin}") } +} - // Locks the balance change handler, iterates through received events, and updates balance changes accordingly. - let mut bal = z_balance_change_handler.lock().await; - while (bal.next().await).is_some() { - match self.my_balance().compat().await { +#[async_trait] +impl EventStreamer for ZCoinBalanceEventStreamer { + type DataInType = (); + + fn streamer_id(&self) -> String { Self::derive_streamer_id(self.coin.ticker()) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput<()>, + ) { + let streamer_id = self.streamer_id(); + let coin = self.coin; + + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + // Iterates through received events, and updates balance changes accordingly. + while (data_rx.next().await).is_some() { + match coin.my_balance().compat().await { Ok(balance) => { let payload = json!({ - "ticker": self.ticker(), - "address": self.my_z_address_encoded(), + "ticker": coin.ticker(), + "address": coin.my_z_address_encoded(), "balance": { "spendable": balance.spendable, "unspendable": balance.unspendable } }); - ctx.stream_channel_controller - .broadcast(Event::new(Self::event_name().to_string(), payload.to_string())) - .await; + broadcaster.broadcast(Event::new(streamer_id.clone(), payload)); }, Err(err) => { - let ticker = self.ticker(); + let ticker = coin.ticker(); error!("Failed getting balance for '{ticker}'. Error: {err}"); let e = serde_json::to_value(err).expect("Serialization should't fail."); - return ctx - .stream_channel_controller - .broadcast(Event::new( - format!("{}:{}", Self::error_event_name(), ticker), - e.to_string(), - )) - .await; + return broadcaster.broadcast(Event::err(streamer_id.clone(), e)); }, }; } } - - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - info!( - "{} event is activated for {} address {}. `stream_interval_seconds`({}) has no effect on this.", - Self::event_name(), - self.ticker(), - self.my_z_address_encoded(), - event.stream_interval_seconds - ); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = AbortSettings::info_on_abort(format!( - "{} event is stopped for {}.", - Self::event_name(), - self.ticker() - )); - self.spawner().spawn_with_settings(fut, settings); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive - } - } } diff --git a/mm2src/coins/z_coin/z_coin_errors.rs b/mm2src/coins/z_coin/z_coin_errors.rs index 7fcf06cb12..55e9ccc633 100644 --- a/mm2src/coins/z_coin/z_coin_errors.rs +++ b/mm2src/coins/z_coin/z_coin_errors.rs @@ -301,6 +301,7 @@ impl From for ZTxHistoryError { fn from(err: CursorError) -> Self { ZTxHistoryError::IndexedDbError(err.to_string()) } } +#[derive(Debug)] pub(super) struct NoInfoAboutTx(pub(super) H256Json); impl From for MyTxHistoryErrorV2 { diff --git a/mm2src/coins/z_coin/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs index f554d7c5d3..0cde681ee8 100644 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ b/mm2src/coins/z_coin/z_coin_native_tests.rs @@ -13,11 +13,18 @@ use crate::DexFee; use crate::{CoinProtocol, SwapTxTypeWithSecretHash}; use mm2_number::MmNumber; +fn native_zcoin_activation_params() -> ZcoinActivationParams { + ZcoinActivationParams { + mode: ZcoinRpcMode::Native, + ..Default::default() + } +} + #[test] fn zombie_coin_send_and_refund_maker_payment() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); - let params = default_zcoin_activation_params(); + let params = native_zcoin_activation_params(); let pk_data = [1; 32]; let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); @@ -76,7 +83,7 @@ fn zombie_coin_send_and_refund_maker_payment() { fn zombie_coin_send_and_spend_maker_payment() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); - let params = default_zcoin_activation_params(); + let params = native_zcoin_activation_params(); let pk_data = [1; 32]; let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); @@ -138,7 +145,7 @@ fn zombie_coin_send_and_spend_maker_payment() { fn zombie_coin_send_dex_fee() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); - let params = default_zcoin_activation_params(); + let params = native_zcoin_activation_params(); let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); @@ -167,7 +174,7 @@ fn zombie_coin_send_dex_fee() { fn prepare_zombie_sapling_cache() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); - let params = default_zcoin_activation_params(); + let params = native_zcoin_activation_params(); let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); @@ -197,7 +204,7 @@ fn prepare_zombie_sapling_cache() { fn zombie_coin_validate_dex_fee() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); - let params = default_zcoin_activation_params(); + let params = native_zcoin_activation_params(); let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); @@ -280,15 +287,3 @@ fn zombie_coin_validate_dex_fee() { }; block_on(coin.validate_fee(validate_fee_args)).unwrap(); } - -fn default_zcoin_activation_params() -> ZcoinActivationParams { - ZcoinActivationParams { - mode: ZcoinRpcMode::Native, - required_confirmations: None, - requires_notarization: None, - zcash_params_path: None, - scan_blocks_per_iteration: 0, - scan_interval_ms: 0, - account: 0, - } -} diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index bd71d554c6..7bfd299bdc 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -17,6 +17,7 @@ use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures::StreamExt; use hex::{FromHex, FromHexError}; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use parking_lot::Mutex; use prost::Message; use rpc::v1::types::{Bytes, H256 as H256Json}; @@ -33,7 +34,6 @@ use zcash_primitives::zip32::ExtendedSpendingKey; pub(crate) mod z_coin_grpc { tonic::include_proto!("pirate.wallet.sdk.rpc"); } -use crate::z_coin::z_balance_streaming::ZBalanceEventSender; use z_coin_grpc::compact_tx_streamer_client::CompactTxStreamerClient; use z_coin_grpc::{ChainSpec, CompactBlock as TonicCompactBlock}; @@ -509,7 +509,6 @@ pub(super) async fn init_light_client<'a>( sync_params: &Option, skip_sync_params: bool, z_spending_key: &ExtendedSpendingKey, - z_balance_event_sender: Option, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); @@ -568,10 +567,10 @@ pub(super) async fn init_light_client<'a>( main_sync_state_finished: false, on_tx_gen_watcher, watch_for_tx: None, - scan_blocks_per_iteration: builder.z_coin_params.scan_blocks_per_iteration, + scan_blocks_per_iteration: builder.z_coin_params.scan_blocks_per_iteration.into(), scan_interval_ms: builder.z_coin_params.scan_interval_ms, first_sync_block: first_sync_block.clone(), - z_balance_event_sender, + streaming_manager: builder.ctx.event_stream_manager.clone(), }; let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(light_rpc_clients))); @@ -588,7 +587,6 @@ pub(super) async fn init_native_client<'a>( native_client: NativeClient, blocks_db: BlockDbImpl, z_spending_key: &ExtendedSpendingKey, - z_balance_event_sender: Option, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); @@ -616,10 +614,10 @@ pub(super) async fn init_native_client<'a>( main_sync_state_finished: false, on_tx_gen_watcher, watch_for_tx: None, - scan_blocks_per_iteration: builder.z_coin_params.scan_blocks_per_iteration, + scan_blocks_per_iteration: builder.z_coin_params.scan_blocks_per_iteration.into(), scan_interval_ms: builder.z_coin_params.scan_interval_ms, first_sync_block: first_sync_block.clone(), - z_balance_event_sender, + streaming_manager: builder.ctx.event_stream_manager.clone(), }; let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(native_client))); @@ -718,7 +716,8 @@ pub struct SaplingSyncLoopHandle { scan_blocks_per_iteration: u32, scan_interval_ms: u64, first_sync_block: FirstSyncBlock, - z_balance_event_sender: Option, + /// A copy of the streaming manager to send notifications to the streamers upon new txs, balance change, etc... + streaming_manager: StreamingManager, } impl SaplingSyncLoopHandle { @@ -842,7 +841,7 @@ impl SaplingSyncLoopHandle { blocks_db .process_blocks_with_mode( self.consensus_params.clone(), - BlockProcessingMode::Scan(scan, self.z_balance_event_sender.clone()), + BlockProcessingMode::Scan(scan, self.streaming_manager.clone()), None, Some(self.scan_blocks_per_iteration), ) diff --git a/mm2src/coins/z_coin/z_tx_history.rs b/mm2src/coins/z_coin/z_tx_history.rs index 57eb2fdb4c..2125a23c7d 100644 --- a/mm2src/coins/z_coin/z_tx_history.rs +++ b/mm2src/coins/z_coin/z_tx_history.rs @@ -1,11 +1,16 @@ +use std::collections::HashSet; + use crate::z_coin::{ZCoin, ZTxHistoryError}; use common::PagingOptionsEnum; use mm2_err_handle::prelude::MmError; +use zcash_primitives::transaction::TxId; cfg_wasm32!( use crate::z_coin::storage::wasm::tables::{WalletDbBlocksTable, WalletDbReceivedNotesTable, WalletDbTransactionsTable}; use crate::MarketCoinOps; use mm2_number::BigInt; + use mm2_db::indexed_db::cursor_prelude::CursorError; + use mm2_err_handle::prelude::MapToMmResult; use num_traits::ToPrimitive; ); @@ -220,3 +225,153 @@ pub(crate) async fn fetch_tx_history_from_db( }) .await } + +#[cfg(target_arch = "wasm32")] +pub(crate) async fn fetch_txs_from_db( + z: &ZCoin, + tx_hashes: HashSet, +) -> Result, MmError> { + let wallet_db = z.z_fields.light_wallet_db.clone(); + let wallet_db = wallet_db.db.lock_db().await.unwrap(); + let db_transaction = wallet_db.get_inner().transaction().await?; + let tx_table = db_transaction.table::().await?; + + let limit = tx_hashes.len(); + let condition = { + // Convert TxIds to Vecs for comparison. + let tx_hashes: HashSet<_> = tx_hashes.into_iter().map(|txid| txid.0.to_vec()).collect(); + move |tx| { + let tx = serde_json::from_value::(tx) + .map_to_mm(|err| CursorError::ErrorDeserializingItem(err.to_string()))?; + Ok(tx_hashes.contains(&tx.txid)) + } + }; + + // Fetch transactions + let txs = tx_table + .cursor_builder() + .only("ticker", z.ticker())? + // We need to explicitly set a limit since `where_` implicitly sets a limit of 1 if no limit is set. + // TODO: Remove when `where_` doesn't exhibit this behavior. + .limit(limit) + .where_(condition) + .reverse() + .open_cursor("ticker") + .await? + .collect() + .await?; + + // Fetch received notes + let rn_table = db_transaction.table::().await?; + let received_notes = rn_table + .cursor_builder() + .only("ticker", z.ticker())? + .open_cursor("ticker") + .await? + .collect() + .await?; + + // Fetch blocks + let blocks_table = db_transaction.table::().await?; + let blocks = blocks_table + .cursor_builder() + .only("ticker", z.ticker())? + .open_cursor("ticker") + .await? + .collect() + .await?; + + // Process transactions and construct tx_details + let mut transactions = Vec::new(); + for (tx_id, tx) in txs { + if let Some((_, WalletDbBlocksTable { height, time, .. })) = blocks + .iter() + .find(|(_, block)| tx.block.map(|b| b == block.height).unwrap_or_default()) + { + let internal_id = tx_id; + let mut received_amount = 0; + let mut spent_amount = 0; + + for (_, note) in &received_notes { + if internal_id == note.tx { + received_amount += note.value.to_u64().ok_or_else(|| { + ZTxHistoryError::IndexedDbError("Number is too large to fit in a u64".to_string()) + })? as i64; + } + + // detecting spent amount by "spent" field in received_notes table + if let Some(spent) = ¬e.spent { + if &BigInt::from(internal_id) == spent { + spent_amount += note.value.to_u64().ok_or_else(|| { + ZTxHistoryError::IndexedDbError("Number is too large to fit in a u64".to_string()) + })? as i64; + } + } + } + + let mut tx_hash = tx.txid; + tx_hash.reverse(); + + transactions.push(ZCoinTxHistoryItem { + tx_hash, + internal_id: internal_id as i64, + height: *height as i64, + timestamp: *time as i64, + received_amount, + spent_amount, + }); + } + } + + Ok(transactions) +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) async fn fetch_txs_from_db( + z: &ZCoin, + tx_hashes: HashSet, +) -> Result, MmError> { + let wallet_db = z.z_fields.light_wallet_db.clone(); + async_blocking(move || { + let sql_query = SqlBuilder::select_from(name!(TRANSACTIONS_TABLE; "txes")) + .field("txes.txid as tx_hash") + .field("txes.id_tx as internal_id") + .field("txes.block as block") + .field("blocks.time") + .field("COALESCE(rn.received_amount, 0)") + .field("COALESCE(sn.sent_amount, 0)") + .and_where_in_quoted( + // Make sure the tx hash from the DB is lowercase, + "lower(hex(tx_hash))", + &tx_hashes + .iter() + // as well as the tx hashes we are looking for. + .map(|tx_hash| hex::encode(tx_hash.0).to_lowercase()) + .collect::>(), + ) + .left() + .join("(SELECT tx, SUM(value) as received_amount FROM received_notes GROUP BY tx) as rn") + .on("txes.id_tx = rn.tx") + .join("(SELECT spent, SUM(value) as sent_amount FROM received_notes GROUP BY spent) as sn") + .on("txes.id_tx = sn.spent") + .join(BLOCKS_TABLE) + .on("txes.block = blocks.height") + .group_by("internal_id") + .order_by("block", true) + .order_by("internal_id", false) + .sql() + .expect("valid query"); + + let txs = wallet_db + .db + .inner() + .lock() + .unwrap() + .sql_conn() + .prepare(&sql_query)? + .query_map([], ZCoinTxHistoryItem::try_from_sql_row)? + .collect::, _>>()?; + Ok(txs) + }) + .await +} diff --git a/mm2src/coins_activation/src/bch_with_tokens_activation.rs b/mm2src/coins_activation/src/bch_with_tokens_activation.rs index ebda8efcba..b38c1bee36 100644 --- a/mm2src/coins_activation/src/bch_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/bch_with_tokens_activation.rs @@ -19,7 +19,6 @@ use common::{drop_mutability, true_f}; use crypto::CryptoCtxError; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::EventStreamConfiguration; use mm2_number::BigDecimal; use rpc_task::RpcTaskHandleShared; use serde_derive::{Deserialize, Serialize}; @@ -347,19 +346,18 @@ impl PlatformCoinWithTokensActivationOps for BchCoin { storage: impl TxHistoryStorage + Send + 'static, initial_balance: Option, ) { - let fut = bch_and_slp_history_loop(self.clone(), storage, ctx.metrics.clone(), initial_balance); + let fut = bch_and_slp_history_loop( + self.clone(), + storage, + ctx.metrics.clone(), + ctx.event_stream_manager.clone(), + initial_balance, + ); let settings = AbortSettings::info_on_abort(format!("bch_and_slp_history_loop stopped for {}", self.ticker())); self.spawner().spawn_with_settings(fut, settings); } - async fn handle_balance_streaming( - &self, - _config: &EventStreamConfiguration, - ) -> Result<(), MmError> { - Ok(()) - } - fn rpc_task_manager( _activation_ctx: &CoinsActivationContext, ) -> &InitPlatformCoinWithTokensTaskManagerShared { diff --git a/mm2src/coins_activation/src/context.rs b/mm2src/coins_activation/src/context.rs index 5ae19eb60e..dca33f40c8 100644 --- a/mm2src/coins_activation/src/context.rs +++ b/mm2src/coins_activation/src/context.rs @@ -4,7 +4,8 @@ use crate::init_erc20_token_activation::Erc20TokenTaskManagerShared; use crate::lightning_activation::LightningTaskManagerShared; #[cfg(feature = "enable-sia")] use crate::sia_coin_activation::SiaCoinTaskManagerShared; -use crate::utxo_activation::{QtumTaskManagerShared, UtxoStandardTaskManagerShared}; +use crate::tendermint_with_assets_activation::TendermintCoinTaskManagerShared; +use crate::utxo_activation::{BchTaskManagerShared, QtumTaskManagerShared, UtxoStandardTaskManagerShared}; use crate::z_coin_activation::ZcoinTaskManagerShared; use mm2_core::mm_ctx::{from_ctx, MmArc}; use rpc_task::RpcTaskManager; @@ -12,12 +13,14 @@ use std::sync::Arc; pub struct CoinsActivationContext { pub(crate) init_utxo_standard_task_manager: UtxoStandardTaskManagerShared, + pub(crate) init_bch_task_manager: BchTaskManagerShared, pub(crate) init_qtum_task_manager: QtumTaskManagerShared, #[cfg(feature = "enable-sia")] pub(crate) init_sia_task_manager: SiaCoinTaskManagerShared, pub(crate) init_z_coin_task_manager: ZcoinTaskManagerShared, pub(crate) init_eth_task_manager: EthTaskManagerShared, pub(crate) init_erc20_token_task_manager: Erc20TokenTaskManagerShared, + pub(crate) init_tendermint_coin_task_manager: TendermintCoinTaskManagerShared, #[cfg(not(target_arch = "wasm32"))] pub(crate) init_lightning_task_manager: LightningTaskManagerShared, } @@ -28,14 +31,16 @@ impl CoinsActivationContext { from_ctx(&ctx.coins_activation_ctx, move || { Ok(CoinsActivationContext { #[cfg(feature = "enable-sia")] - init_sia_task_manager: RpcTaskManager::new_shared(), - init_utxo_standard_task_manager: RpcTaskManager::new_shared(), - init_qtum_task_manager: RpcTaskManager::new_shared(), - init_z_coin_task_manager: RpcTaskManager::new_shared(), - init_eth_task_manager: RpcTaskManager::new_shared(), - init_erc20_token_task_manager: RpcTaskManager::new_shared(), + init_sia_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_utxo_standard_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_bch_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_qtum_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_z_coin_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_eth_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_erc20_token_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_tendermint_coin_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), #[cfg(not(target_arch = "wasm32"))] - init_lightning_task_manager: RpcTaskManager::new_shared(), + init_lightning_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), }) }) } diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 62e8fe4c4c..4b236be290 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -25,7 +25,6 @@ use common::{drop_mutability, true_f}; use crypto::HwRpcError; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::EventStreamConfiguration; #[cfg(target_arch = "wasm32")] use mm2_metamask::MetamaskRpcError; use mm2_number::BigDecimal; @@ -441,13 +440,6 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { ) { } - async fn handle_balance_streaming( - &self, - _config: &EventStreamConfiguration, - ) -> Result<(), MmError> { - Ok(()) - } - fn rpc_task_manager( activation_ctx: &CoinsActivationContext, ) -> &InitPlatformCoinWithTokensTaskManagerShared { diff --git a/mm2src/coins_activation/src/init_token.rs b/mm2src/coins_activation/src/init_token.rs index dbc03b1754..58d9ed9180 100644 --- a/mm2src/coins_activation/src/init_token.rs +++ b/mm2src/coins_activation/src/init_token.rs @@ -15,8 +15,8 @@ use mm2_err_handle::mm_error::{MmError, MmResult, NotEqual, NotMmError}; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError, RpcTaskUserActionRequest}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, - RpcTaskTypes, TaskId}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, + RpcTaskStatus, RpcTaskTypes, TaskId}; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; use std::time::Duration; @@ -45,7 +45,7 @@ pub struct InitTokenReq { pub trait InitTokenActivationOps: Into + TokenOf + Clone + Send + Sync + 'static { type ActivationRequest: Clone + Send + Sync; type ProtocolInfo: TokenProtocolParams + TryFromCoinProtocol + Clone + Send + Sync; - type ActivationResult: serde::Serialize + Clone + CurrentBlock + Send + Sync; + type ActivationResult: CurrentBlock + serde::Serialize + Clone + Send + Sync; type ActivationError: From + Into + NotEqual @@ -53,8 +53,8 @@ pub trait InitTokenActivationOps: Into + TokenOf + Clone + Send + Sy + Clone + Send + Sync; - type InProgressStatus: InitTokenInitialStatus + Clone + Send + Sync; - type AwaitingStatus: Clone + Send + Sync; + type InProgressStatus: InitTokenInitialStatus + serde::Serialize + Clone + Send + Sync; + type AwaitingStatus: serde::Serialize + Clone + Send + Sync; type UserAction: NotMmError + Send + Sync; /// Getter for the token initialization task manager. @@ -82,7 +82,7 @@ pub trait InitTokenActivationOps: Into + TokenOf + Clone + Send + Sy /// Implementation of the init token RPC command. pub async fn init_token( ctx: MmArc, - request: InitTokenReq, + request: RpcInitReq>, ) -> MmResult where Token: InitTokenActivationOps + Send + Sync + 'static, @@ -90,6 +90,7 @@ where InitTokenError: From, (Token::ActivationError, InitTokenError): NotEqual, { + let (client_id, request) = (request.client_id, request.inner); if let Ok(Some(_)) = lp_coinfind(&ctx, &request.ticker).await { return MmError::err(InitTokenError::TokenIsAlreadyActivated { ticker: request.ticker }); } @@ -116,7 +117,7 @@ where }; let task_manager = Token::rpc_task_manager(&coins_act_ctx); - let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task, client_id) .mm_err(|e| InitTokenError::Internal(e.to_string()))?; Ok(InitTokenResponse { task_id }) diff --git a/mm2src/coins_activation/src/l2/init_l2.rs b/mm2src/coins_activation/src/l2/init_l2.rs index 20e66ebbde..14aee68afe 100644 --- a/mm2src/coins_activation/src/l2/init_l2.rs +++ b/mm2src/coins_activation/src/l2/init_l2.rs @@ -10,7 +10,8 @@ use common::SuccessResponse; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusRequest, RpcTaskUserActionRequest}; -use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes}; use serde_derive::Deserialize; use serde_json::Value as Json; @@ -39,8 +40,8 @@ pub trait InitL2ActivationOps: Into + Send + Sync + 'static { type CoinConf: Clone + Send + Sync; type ActivationResult: serde::Serialize + Clone + Send + Sync; type ActivationError: From + NotEqual + SerMmErrorType + Clone + Send + Sync; - type InProgressStatus: InitL2InitialStatus + Clone + Send + Sync; - type AwaitingStatus: Clone + Send + Sync; + type InProgressStatus: InitL2InitialStatus + serde::Serialize + Clone + Send + Sync; + type AwaitingStatus: serde::Serialize + Clone + Send + Sync; type UserAction: NotMmError + Send + Sync; fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &InitL2TaskManagerShared; @@ -67,13 +68,14 @@ pub trait InitL2ActivationOps: Into + Send + Sync + 'static { pub async fn init_l2( ctx: MmArc, - req: InitL2Req, + req: RpcInitReq>, ) -> Result> where L2: InitL2ActivationOps, InitL2Error: From, (L2::ActivationError, InitL2Error): NotEqual, { + let (client_id, req) = (req.client_id, req.inner); let ticker = req.ticker.clone(); if let Ok(Some(_)) = lp_coinfind(&ctx, &ticker).await { return MmError::err(InitL2Error::L2IsAlreadyActivated(ticker)); @@ -108,7 +110,7 @@ where }; let task_manager = L2::rpc_task_manager(&coins_act_ctx); - let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task, client_id) .mm_err(|e| InitL2Error::Internal(e.to_string()))?; Ok(InitL2Response { task_id }) diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index 051dd22fc3..ed72183116 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -13,12 +13,11 @@ use crypto::CryptoCtxError; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::EventStreamConfiguration; use mm2_number::BigDecimal; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError, RpcTaskUserActionRequest}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, - RpcTaskTypes, TaskId}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, + RpcTaskStatus, RpcTaskTypes, TaskId}; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; use serde_json::Value as Json; @@ -172,8 +171,8 @@ pub trait PlatformCoinWithTokensActivationOps: Into + Clone + Send + + Send + Sync; - type InProgressStatus: InitPlatformCoinWithTokensInitialStatus + Clone + Send + Sync; - type AwaitingStatus: Clone + Send + Sync; + type InProgressStatus: InitPlatformCoinWithTokensInitialStatus + serde::Serialize + Clone + Send + Sync; + type AwaitingStatus: serde::Serialize + Clone + Send + Sync; type UserAction: NotMmError + Send + Sync; /// Initializes the platform coin itself @@ -214,11 +213,6 @@ pub trait PlatformCoinWithTokensActivationOps: Into + Clone + Send + initial_balance: Option, ); - async fn handle_balance_streaming( - &self, - config: &EventStreamConfiguration, - ) -> Result<(), MmError>; - fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &InitPlatformCoinWithTokensTaskManagerShared where EnablePlatformCoinWithTokensError: From; @@ -481,10 +475,6 @@ where ); } - if let Some(config) = &ctx.event_stream_configuration { - platform_coin.handle_balance_streaming(config).await?; - } - let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); coins_ctx .add_platform_with_tokens(platform_coin.into(), mm_tokens, nft_global) @@ -557,7 +547,7 @@ impl InitPlatformCoinWithTokensInitialStatus for InitPlatformCoinWithTokensInPro /// Implementation of the init platform coin with tokens RPC command. pub async fn init_platform_coin_with_tokens( ctx: MmArc, - request: EnablePlatformCoinWithTokensReq, + request: RpcInitReq>, ) -> MmResult where Platform: PlatformCoinWithTokensActivationOps + Send + Sync + 'static + Clone, @@ -565,6 +555,7 @@ where EnablePlatformCoinWithTokensError: From, (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, { + let (client_id, request) = (request.client_id, request.inner); if let Ok(Some(_)) = lp_coinfind(&ctx, &request.ticker).await { return MmError::err(EnablePlatformCoinWithTokensError::PlatformIsAlreadyActivated( request.ticker, @@ -577,7 +568,7 @@ where let task = InitPlatformCoinWithTokensTask:: { ctx, request }; let task_manager = Platform::rpc_task_manager(&coins_act_ctx); - let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task, client_id) .mm_err(|e| EnablePlatformCoinWithTokensError::Internal(e.to_string()))?; Ok(EnablePlatformCoinWithTokensResponse { task_id }) @@ -661,7 +652,7 @@ pub mod for_tests { use common::{executor::Timer, now_ms, wait_until_ms}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmResult; - use rpc_task::RpcTaskStatus; + use rpc_task::{RpcInitReq, RpcTaskStatus}; use super::{init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, EnablePlatformCoinWithTokensError, EnablePlatformCoinWithTokensReq, @@ -679,6 +670,10 @@ pub mod for_tests { EnablePlatformCoinWithTokensError: From, (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, { + let request = RpcInitReq { + client_id: 0, + inner: request, + }; let init_result = init_platform_coin_with_tokens::(ctx.clone(), request) .await .unwrap(); diff --git a/mm2src/coins_activation/src/prelude.rs b/mm2src/coins_activation/src/prelude.rs index d000170fa3..0fd890fa64 100644 --- a/mm2src/coins_activation/src/prelude.rs +++ b/mm2src/coins_activation/src/prelude.rs @@ -1,5 +1,6 @@ #[cfg(feature = "enable-sia")] use coins::siacoin::SiaCoinActivationParams; +use coins::utxo::bch::BchActivationRequest; use coins::utxo::UtxoActivationParams; use coins::z_coin::ZcoinActivationParams; use coins::{coin_conf, CoinBalance, CoinProtocol, DerivationMethodResponse, MmCoinEnum}; @@ -22,6 +23,10 @@ impl TxHistory for UtxoActivationParams { fn tx_history(&self) -> bool { self.tx_history } } +impl TxHistory for BchActivationRequest { + fn tx_history(&self) -> bool { self.utxo_params.tx_history } +} + #[cfg(feature = "enable-sia")] impl TxHistory for SiaCoinActivationParams { fn tx_history(&self) -> bool { self.tx_history } diff --git a/mm2src/coins_activation/src/sia_coin_activation.rs b/mm2src/coins_activation/src/sia_coin_activation.rs index 11c72955ab..110f8bbb7b 100644 --- a/mm2src/coins_activation/src/sia_coin_activation.rs +++ b/mm2src/coins_activation/src/sia_coin_activation.rs @@ -17,6 +17,7 @@ use derive_more::Display; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use rpc_task::RpcTaskError; @@ -25,6 +26,7 @@ use serde_derive::Serialize; use serde_json::Value as Json; use std::collections::HashMap; use std::time::Duration; + pub type SiaCoinTaskManagerShared = InitStandaloneCoinTaskManagerShared; pub type SiaCoinRpcTaskHandleShared = InitStandaloneCoinTaskHandleShared; pub type SiaCoinAwaitingStatus = HwRpcTaskAwaitingStatus; @@ -237,6 +239,7 @@ impl InitStandaloneCoinActivationOps for SiaCoin { &self, _metrics: MetricsArc, _storage: impl TxHistoryStorage, + _streaming_manager: StreamingManager, _current_balances: HashMap, ) { } diff --git a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs index 314f3066b4..f1dbea7b81 100644 --- a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs +++ b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs @@ -10,10 +10,12 @@ use coins::{lp_coinfind, lp_register_coin, CoinsContext, MmCoinEnum, RegisterCoi use common::{log, SuccessResponse}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use rpc_task::rpc_common::{CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusRequest, RpcTaskUserActionRequest}; -use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes}; use serde_derive::Deserialize; use serde_json::Value as Json; use std::collections::HashMap; @@ -72,13 +74,14 @@ pub trait InitStandaloneCoinActivationOps: Into + Send + Sync + 'sta &self, metrics: MetricsArc, storage: impl TxHistoryStorage, + streaming_manager: StreamingManager, current_balances: HashMap, ); } pub async fn init_standalone_coin( ctx: MmArc, - request: InitStandaloneCoinReq, + request: RpcInitReq>, ) -> MmResult where Standalone: InitStandaloneCoinActivationOps + Send + Sync + 'static, @@ -86,6 +89,7 @@ where InitStandaloneCoinError: From, (Standalone::ActivationError, InitStandaloneCoinError): NotEqual, { + let (client_id, request) = (request.client_id, request.inner); if let Ok(Some(_)) = lp_coinfind(&ctx, &request.ticker).await { return MmError::err(InitStandaloneCoinError::CoinIsAlreadyActivated { ticker: request.ticker }); } @@ -102,7 +106,7 @@ where }; let task_manager = Standalone::rpc_task_manager(&coins_act_ctx); - let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task, client_id) .mm_err(|e| InitStandaloneCoinError::Internal(e.to_string()))?; Ok(InitStandaloneCoinResponse { task_id }) @@ -215,6 +219,7 @@ where coin.start_history_background_fetching( self.ctx.metrics.clone(), TxHistoryStorageBuilder::new(&self.ctx).build()?, + self.ctx.event_stream_manager.clone(), current_balances, ); } diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 349e37b23d..d62c9ebd8b 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -20,14 +20,14 @@ use common::executor::{AbortSettings, SpawnAbortable}; use common::{true_f, Future01CompatExt}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; -use mm2_event_stream::EventStreamConfiguration; use mm2_number::BigDecimal; use rpc_task::RpcTaskHandleShared; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; +pub type TendermintCoinTaskManagerShared = InitPlatformCoinWithTokensTaskManagerShared; + impl TokenOf for TendermintToken { type PlatformCoin = TendermintCoin; } @@ -368,22 +368,7 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { self.spawner().spawn_with_settings(fut, settings); } - async fn handle_balance_streaming( - &self, - config: &EventStreamConfiguration, - ) -> Result<(), MmError> { - if let EventInitStatus::Failed(err) = EventBehaviour::spawn_if_active(self.clone(), config).await { - return MmError::err(TendermintInitError { - ticker: self.ticker().to_owned(), - kind: TendermintInitErrorKind::BalanceStreamInitError(err), - }); - } - Ok(()) - } - - fn rpc_task_manager( - _activation_ctx: &CoinsActivationContext, - ) -> &InitPlatformCoinWithTokensTaskManagerShared { - unimplemented!() + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &TendermintCoinTaskManagerShared { + &activation_ctx.init_tendermint_coin_task_manager } } diff --git a/mm2src/coins_activation/src/utxo_activation/common_impl.rs b/mm2src/coins_activation/src/utxo_activation/common_impl.rs index 8e3d071025..edba083c49 100644 --- a/mm2src/coins_activation/src/utxo_activation/common_impl.rs +++ b/mm2src/coins_activation/src/utxo_activation/common_impl.rs @@ -8,13 +8,14 @@ use coins::hd_wallet::RpcTaskXPubExtractor; use coins::my_tx_history_v2::TxHistoryStorage; use coins::utxo::utxo_tx_history_v2::{utxo_history_loop, UtxoTxHistoryOps}; use coins::utxo::{UtxoActivationParams, UtxoCoinFields}; -use coins::{CoinBalance, CoinFutSpawner, MarketCoinOps, PrivKeyActivationPolicy, PrivKeyBuildPolicy}; +use coins::{CoinBalance, MarketCoinOps, PrivKeyActivationPolicy, PrivKeyBuildPolicy}; use common::executor::{AbortSettings, SpawnAbortable}; use crypto::hw_rpc_task::HwConnectStatuses; use crypto::{CryptoCtxError, HwRpcError}; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use std::collections::HashMap; @@ -99,14 +100,15 @@ pub(crate) fn start_history_background_fetching( coin: Coin, metrics: MetricsArc, storage: impl TxHistoryStorage, + streaming_manager: StreamingManager, current_balances: HashMap, ) where Coin: AsRef + UtxoTxHistoryOps, { - let spawner = CoinFutSpawner::new(&coin.as_ref().abortable_system); + let spawner = coin.as_ref().abortable_system.weak_spawner(); let msg = format!("'utxo_history_loop' has been aborted for {}", coin.ticker()); - let fut = utxo_history_loop(coin, storage, metrics, current_balances); + let fut = utxo_history_loop(coin, storage, metrics, streaming_manager, current_balances); let settings = AbortSettings::info_on_abort(msg); spawner.spawn_with_settings(fut, settings); diff --git a/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs new file mode 100644 index 0000000000..8c27226959 --- /dev/null +++ b/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs @@ -0,0 +1,118 @@ +use crate::context::CoinsActivationContext; +use crate::prelude::TryFromCoinProtocol; +use crate::standalone_coin::{InitStandaloneCoinActivationOps, InitStandaloneCoinTaskHandleShared, + InitStandaloneCoinTaskManagerShared}; +use crate::utxo_activation::common_impl::{get_activation_result, priv_key_build_policy, + start_history_background_fetching}; +use crate::utxo_activation::init_utxo_standard_activation_error::InitUtxoStandardError; +use crate::utxo_activation::init_utxo_standard_statuses::{UtxoStandardAwaitingStatus, UtxoStandardInProgressStatus, + UtxoStandardUserAction}; +use crate::utxo_activation::utxo_standard_activation_result::UtxoStandardActivationResult; +use async_trait::async_trait; +use coins::my_tx_history_v2::TxHistoryStorage; +use coins::utxo::bch::CashAddrPrefix; +use coins::utxo::bch::{BchActivationRequest, BchCoin}; +use coins::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; +use coins::CoinProtocol; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; +use serde_json::Value as Json; +use std::collections::HashMap; +use std::str::FromStr; + +pub type BchTaskManagerShared = InitStandaloneCoinTaskManagerShared; +pub type BchRpcTaskHandleShared = InitStandaloneCoinTaskHandleShared; + +#[derive(Clone)] +pub struct BchProtocolInfo { + slp_prefix: String, +} + +impl TryFromCoinProtocol for BchProtocolInfo { + fn try_from_coin_protocol(proto: CoinProtocol) -> Result> + where + Self: Sized, + { + match proto { + CoinProtocol::BCH { slp_prefix } => Ok(BchProtocolInfo { slp_prefix }), + protocol => MmError::err(protocol), + } + } +} + +#[async_trait] +impl InitStandaloneCoinActivationOps for BchCoin { + type ActivationRequest = BchActivationRequest; + type StandaloneProtocol = BchProtocolInfo; + type ActivationResult = UtxoStandardActivationResult; + type ActivationError = InitUtxoStandardError; + type InProgressStatus = UtxoStandardInProgressStatus; + type AwaitingStatus = UtxoStandardAwaitingStatus; + type UserAction = UtxoStandardUserAction; + + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &BchTaskManagerShared { + &activation_ctx.init_bch_task_manager + } + + async fn init_standalone_coin( + ctx: MmArc, + ticker: String, + coin_conf: Json, + activation_request: &Self::ActivationRequest, + protocol_info: Self::StandaloneProtocol, + _task_handle: BchRpcTaskHandleShared, + ) -> Result> { + if activation_request.bchd_urls.is_empty() && !activation_request.allow_slp_unsafe_conf { + Err(InitUtxoStandardError::CoinCreationError { + ticker: ticker.clone(), + error: "Using empty bchd_urls is unsafe for SLP users!".into(), + })?; + } + let prefix = CashAddrPrefix::from_str(&protocol_info.slp_prefix).map_err(|e| { + InitUtxoStandardError::CoinCreationError { + ticker: ticker.clone(), + error: format!("Couldn't parse cash address prefix: {e:?}"), + } + })?; + let priv_key_policy = priv_key_build_policy(&ctx, activation_request.utxo_params.priv_key_policy)?; + + let bchd_urls = activation_request.bchd_urls.clone(); + let constructor = { move |utxo_arc| BchCoin::new(utxo_arc, prefix.clone(), bchd_urls.clone()) }; + + let coin = UtxoArcBuilder::new( + &ctx, + &ticker, + &coin_conf, + &activation_request.utxo_params, + priv_key_policy, + constructor, + ) + .build() + .await + .mm_err(|e| InitUtxoStandardError::from_build_err(e, ticker.clone()))?; + + Ok(coin) + } + + async fn get_activation_result( + &self, + ctx: MmArc, + task_handle: BchRpcTaskHandleShared, + activation_request: &Self::ActivationRequest, + ) -> MmResult { + get_activation_result(&ctx, self, task_handle, &activation_request.utxo_params).await + } + + fn start_history_background_fetching( + &self, + metrics: MetricsArc, + storage: impl TxHistoryStorage, + streaming_manager: StreamingManager, + current_balances: HashMap, + ) { + start_history_background_fetching(self.clone(), metrics, storage, streaming_manager, current_balances) + } +} diff --git a/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs index ae8cdec6ce..a6644f3275 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs @@ -16,6 +16,7 @@ use coins::utxo::UtxoActivationParams; use coins::CoinProtocol; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use serde_json::Value as Json; @@ -83,8 +84,9 @@ impl InitStandaloneCoinActivationOps for QtumCoin { &self, metrics: MetricsArc, storage: impl TxHistoryStorage, + streaming_manager: StreamingManager, current_balances: HashMap, ) { - start_history_background_fetching(self.clone(), metrics, storage, current_balances) + start_history_background_fetching(self.clone(), metrics, storage, streaming_manager, current_balances) } } diff --git a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs index 206c750f15..10715e2f0e 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs @@ -17,6 +17,7 @@ use coins::CoinProtocol; use futures::StreamExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use serde_json::Value as Json; @@ -124,8 +125,9 @@ impl InitStandaloneCoinActivationOps for UtxoStandardCoin { &self, metrics: MetricsArc, storage: impl TxHistoryStorage, + streaming_manager: StreamingManager, current_balances: HashMap, ) { - start_history_background_fetching(self.clone(), metrics, storage, current_balances) + start_history_background_fetching(self.clone(), metrics, storage, streaming_manager, current_balances) } } diff --git a/mm2src/coins_activation/src/utxo_activation/mod.rs b/mm2src/coins_activation/src/utxo_activation/mod.rs index 5ef6021199..42764e5c93 100644 --- a/mm2src/coins_activation/src/utxo_activation/mod.rs +++ b/mm2src/coins_activation/src/utxo_activation/mod.rs @@ -1,10 +1,12 @@ mod common_impl; +mod init_bch_activation; mod init_qtum_activation; mod init_utxo_standard_activation; mod init_utxo_standard_activation_error; mod init_utxo_standard_statuses; mod utxo_standard_activation_result; +pub use init_bch_activation::BchTaskManagerShared; pub use init_qtum_activation::QtumTaskManagerShared; pub use init_utxo_standard_activation::UtxoStandardTaskManagerShared; @@ -14,7 +16,7 @@ pub mod for_tests { use common::{executor::Timer, now_ms, wait_until_ms}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::{MmResult, NotEqual}; - use rpc_task::RpcTaskStatus; + use rpc_task::{RpcInitReq, RpcTaskStatus}; use crate::{init_standalone_coin, init_standalone_coin_status, standalone_coin::{InitStandaloneCoinActivationOps, InitStandaloneCoinError, @@ -32,6 +34,10 @@ pub mod for_tests { InitStandaloneCoinError: From, (Standalone::ActivationError, InitStandaloneCoinError): NotEqual, { + let request = RpcInitReq { + client_id: 0, + inner: request, + }; let init_result = init_standalone_coin::(ctx.clone(), request).await.unwrap(); let timeout = wait_until_ms(150000); loop { diff --git a/mm2src/coins_activation/src/z_coin_activation.rs b/mm2src/coins_activation/src/z_coin_activation.rs index 70da5c4eae..2332710218 100644 --- a/mm2src/coins_activation/src/z_coin_activation.rs +++ b/mm2src/coins_activation/src/z_coin_activation.rs @@ -16,6 +16,7 @@ use derive_more::Display; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use rpc_task::RpcTaskError; @@ -303,6 +304,7 @@ impl InitStandaloneCoinActivationOps for ZCoin { &self, _metrics: MetricsArc, _storage: impl TxHistoryStorage, + _streaming_manager: StreamingManager, _current_balances: HashMap, ) { } diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index de201856d8..54a9a37d40 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -131,6 +131,7 @@ pub mod custom_iter; pub mod expirable_map; pub mod notifier; pub mod number_type_casting; +pub mod on_drop_callback; pub mod password_policy; pub mod seri; pub mod time_cache; diff --git a/mm2src/common/on_drop_callback.rs b/mm2src/common/on_drop_callback.rs new file mode 100644 index 0000000000..a454cc9554 --- /dev/null +++ b/mm2src/common/on_drop_callback.rs @@ -0,0 +1,19 @@ +/// Runs some function when this object is dropped. +/// +/// We wrap the callback function in an `Option` so that we can exercise the less strict `FnOnce` bound +/// (`FnOnce` is less strict than `Fn`). This way we can take out the function and execute it when dropping. +/// We also implement this with `Box` instead of generics so not to force users to use generics if +/// this callback handle is stored in some struct. +pub struct OnDropCallback(Option>); + +impl OnDropCallback { + pub fn new(f: impl FnOnce() + Send + 'static) -> Self { Self(Some(Box::new(f))) } +} + +impl Drop for OnDropCallback { + fn drop(&mut self) { + if let Some(func) = self.0.take() { + func() + } + } +} diff --git a/mm2src/mm2_core/src/data_asker.rs b/mm2src/mm2_core/src/data_asker.rs index 7f32f93365..90970af7d4 100644 --- a/mm2src/mm2_core/src/data_asker.rs +++ b/mm2src/mm2_core/src/data_asker.rs @@ -1,3 +1,4 @@ +use common::custom_futures::timeout::FutureTimerExt; use common::expirable_map::ExpirableMap; use common::{HttpStatusCode, StatusCode}; use derive_more::Display; @@ -26,11 +27,12 @@ pub struct DataAsker { #[derive(Debug, Display)] pub enum AskForDataError { #[display( - fmt = "Expected JSON data, but given(from data provider) one was not deserializable: {:?}", + fmt = "Expected JSON data, but the received data (from data provider) was not deserializable: {:?}", _0 )] DeserializationError(serde_json::Error), Internal(String), + Timeout, } impl MmCtx { @@ -68,18 +70,18 @@ impl MmCtx { "data": data }); - self.stream_channel_controller - .broadcast(Event::new(format!("{EVENT_NAME}:{data_type}"), input.to_string())) - .await; + self.event_stream_manager + .broadcast_all(Event::new(format!("{EVENT_NAME}:{data_type}"), input)); - match receiver.await { - Ok(response) => match serde_json::from_value::(response) { + match receiver.timeout(timeout).await { + Ok(Ok(response)) => match serde_json::from_value::(response) { Ok(value) => Ok(value), Err(error) => MmError::err(AskForDataError::DeserializationError(error)), }, - Err(error) => MmError::err(AskForDataError::Internal(format!( - "Sender channel is not alive. {error}" + Ok(Err(error)) => MmError::err(AskForDataError::Internal(format!( + "Receiver channel is not alive. {error}" ))), + Err(_) => MmError::err(AskForDataError::Timeout), } } } diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 8c417f2ce1..82006d57dc 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -3,14 +3,14 @@ use common::executor::Timer; use common::log::{self, LogLevel, LogOnError, LogState}; use common::{cfg_native, cfg_wasm32, small_rng}; use common::{executor::{abortable_queue::{AbortableQueue, WeakSpawner}, - graceful_shutdown, AbortSettings, AbortableSystem, SpawnAbortable, SpawnFuture}, + graceful_shutdown, AbortableSystem}, expirable_map::ExpirableMap}; use futures::channel::oneshot; use futures::lock::Mutex as AsyncMutex; use gstuff::{try_s, Constructible, ERR, ERRL}; use lazy_static::lazy_static; use libp2p::PeerId; -use mm2_event_stream::{controller::Controller, Event, EventStreamConfiguration}; +use mm2_event_stream::{EventStreamingConfiguration, StreamingManager}; use mm2_metrics::{MetricsArc, MetricsOps}; use primitives::hash::H160; use rand::Rng; @@ -20,7 +20,6 @@ use std::any::Any; use std::collections::hash_map::{Entry, HashMap}; use std::collections::HashSet; use std::fmt; -use std::future::Future; use std::ops::Deref; use std::sync::{Arc, Mutex}; @@ -79,12 +78,10 @@ pub struct MmCtx { pub initialized: Constructible, /// True if the RPC HTTP server was started. pub rpc_started: Constructible, - /// 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. pub(crate) data_asker: DataAsker, - /// Configuration of event streaming used for SSE. - pub event_stream_configuration: Option, + /// A manager for the event streaming system. To be used to start/stop/communicate with event streamers. + pub event_stream_manager: StreamingManager, /// True if the MarketMaker instance needs to stop. pub stop: Constructible, /// Unique context identifier, allowing us to more easily pass the context through the FFI boundaries. @@ -157,9 +154,8 @@ impl MmCtx { metrics: MetricsArc::new(), initialized: Constructible::default(), rpc_started: Constructible::default(), - stream_channel_controller: Controller::new(), data_asker: DataAsker::default(), - event_stream_configuration: None, + event_stream_manager: Default::default(), stop: Constructible::default(), ffi_handle: Constructible::default(), ordermatch_ctx: Mutex::new(None), @@ -342,8 +338,13 @@ impl MmCtx { /// Returns whether node is configured to use [Upgraded Trading Protocol](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895) pub fn use_trading_proto_v2(&self) -> bool { self.conf["use_trading_proto_v2"].as_bool().unwrap_or_default() } - /// Returns the cloneable `MmFutSpawner`. - pub fn spawner(&self) -> MmFutSpawner { MmFutSpawner::new(&self.abortable_system) } + /// Returns the event streaming configuration in use. + pub fn event_streaming_configuration(&self) -> Option { + serde_json::from_value(self.conf["event_streaming_configuration"].clone()).ok() + } + + /// Returns the cloneable `WeakSpawner`. + pub fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } /// True if the MarketMaker instance needs to stop. pub fn is_stopping(&self) -> bool { self.stop.copy_or(false) } @@ -656,44 +657,6 @@ impl MmArc { } } -/// The futures spawner pinned to the `MmCtx` context. -/// It's used to spawn futures that can be aborted immediately or after a timeout -/// on the [`MmArc::stop`] function call. -/// -/// # Note -/// -/// `MmFutSpawner` doesn't prevent the spawned futures from being aborted. -#[derive(Clone)] -pub struct MmFutSpawner { - inner: WeakSpawner, -} - -impl MmFutSpawner { - pub fn new(system: &AbortableQueue) -> MmFutSpawner { - MmFutSpawner { - inner: system.weak_spawner(), - } - } -} - -impl SpawnFuture for MmFutSpawner { - fn spawn(&self, f: F) - where - F: Future + Send + 'static, - { - self.inner.spawn(f) - } -} - -impl SpawnAbortable for MmFutSpawner { - fn spawn_with_settings(&self, fut: F, settings: AbortSettings) - where - F: Future + Send + 'static, - { - self.inner.spawn_with_settings(fut, settings) - } -} - /// Helps getting a crate context from a corresponding `MmCtx` field. /// /// * `ctx_field` - A dedicated crate context field in `MmCtx`, such as the `MmCtx::portfolio_ctx`. @@ -779,14 +742,6 @@ impl MmCtxBuilder { if let Some(conf) = self.conf { ctx.conf = conf; - - let event_stream_configuration = &ctx.conf["event_stream_configuration"]; - if !event_stream_configuration.is_null() { - let event_stream_configuration: EventStreamConfiguration = - json::from_value(event_stream_configuration.clone()) - .expect("Invalid json value in 'event_stream_configuration'."); - ctx.event_stream_configuration = Some(event_stream_configuration); - } } #[cfg(target_arch = "wasm32")] diff --git a/mm2src/mm2_event_stream/Cargo.toml b/mm2src/mm2_event_stream/Cargo.toml index adf20e7ee2..5b1677fa0e 100644 --- a/mm2src/mm2_event_stream/Cargo.toml +++ b/mm2src/mm2_event_stream/Cargo.toml @@ -10,10 +10,11 @@ common = { path = "../common" } futures = { version = "0.3", default-features = false } parking_lot = "0.12" serde = { version = "1", features = ["derive", "rc"] } -tokio = { version = "1", features = ["sync"] } +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +tokio = "1.20" [dev-dependencies] -tokio = { version = "1", features = ["sync", "macros", "time", "rt"] } +tokio = { version = "1.20", features = ["macros"] } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-test = { version = "0.3.2" } diff --git a/mm2src/mm2_event_stream/src/behaviour.rs b/mm2src/mm2_event_stream/src/behaviour.rs deleted file mode 100644 index ff2cfbefa9..0000000000 --- a/mm2src/mm2_event_stream/src/behaviour.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{ErrorEventName, EventName, EventStreamConfiguration}; -use async_trait::async_trait; -use futures::channel::oneshot; - -#[derive(Clone, Debug)] -pub enum EventInitStatus { - Inactive, - Success, - Failed(String), -} - -#[async_trait] -pub trait EventBehaviour { - /// Returns the unique name of the event as an EventName enum variant. - fn event_name() -> EventName; - - /// Returns the name of the error event as an ErrorEventName enum variant. - /// By default, it returns `ErrorEventName::GenericError,` which shows as "ERROR" in the event stream. - fn error_event_name() -> ErrorEventName { ErrorEventName::GenericError } - - /// Event handler that is responsible for broadcasting event data to the streaming channels. - async fn handle(self, interval: f64, tx: oneshot::Sender); - - /// Spawns the `Self::handle` in a separate thread if the event is active according to the mm2 configuration. - /// Does nothing if the event is not active. - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus; -} diff --git a/mm2src/mm2_event_stream/src/configuration.rs b/mm2src/mm2_event_stream/src/configuration.rs new file mode 100644 index 0000000000..590665d581 --- /dev/null +++ b/mm2src/mm2_event_stream/src/configuration.rs @@ -0,0 +1,19 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(default)] +/// The network-related configuration of the event streaming interface. +// TODO: This better fits in mm2_net but then we would have circular dependency error trying to import it in mm2_core. +pub struct EventStreamingConfiguration { + pub worker_path: String, + pub access_control_allow_origin: String, +} + +impl Default for EventStreamingConfiguration { + fn default() -> Self { + Self { + worker_path: "event_streaming_worker.js".to_string(), + access_control_allow_origin: "*".to_string(), + } + } +} diff --git a/mm2src/mm2_event_stream/src/controller.rs b/mm2src/mm2_event_stream/src/controller.rs deleted file mode 100644 index 72870308b4..0000000000 --- a/mm2src/mm2_event_stream/src/controller.rs +++ /dev/null @@ -1,199 +0,0 @@ -use parking_lot::Mutex; -use std::{collections::HashMap, sync::Arc}; -use tokio::sync::mpsc::{self, Receiver, Sender}; - -type ChannelId = u64; - -/// Root controller of streaming channels -pub struct Controller(Arc>>); - -impl Clone for Controller { - fn clone(&self) -> Self { Self(Arc::clone(&self.0)) } -} - -/// Inner part of the controller -pub struct ChannelsInner { - last_id: u64, - channels: HashMap>, -} - -struct Channel { - tx: Sender>, -} - -/// guard to trace channels disconnection -pub struct ChannelGuard { - channel_id: ChannelId, - controller: Controller, -} - -/// Receiver to cleanup resources on `Drop` -pub struct GuardedReceiver { - rx: Receiver>, - #[allow(dead_code)] - guard: ChannelGuard, -} - -impl Controller { - /// Creates a new channels controller - pub fn new() -> Self { Default::default() } - - /// Creates a new channel and returns it's events receiver - pub fn create_channel(&mut self, concurrency: usize) -> GuardedReceiver { - let (tx, rx) = mpsc::channel::>(concurrency); - let channel = Channel { tx }; - - let mut inner = self.0.lock(); - let channel_id = inner.last_id.overflowing_add(1).0; - inner.channels.insert(channel_id, channel); - inner.last_id = channel_id; - - let guard = ChannelGuard::new(channel_id, self.clone()); - GuardedReceiver { rx, guard } - } - - /// Returns number of active channels - pub fn num_connections(&self) -> usize { self.0.lock().channels.len() } - - /// Broadcast message to all channels - pub async fn broadcast(&self, message: M) { - let msg = Arc::new(message); - for rx in self.all_senders() { - rx.send(Arc::clone(&msg)).await.ok(); - } - } - - /// Removes the channel from the controller - fn remove_channel(&mut self, channel_id: &ChannelId) { - let mut inner = self.0.lock(); - inner.channels.remove(channel_id); - } - - /// Returns all the active channels - fn all_senders(&self) -> Vec>> { self.0.lock().channels.values().map(|c| c.tx.clone()).collect() } -} - -impl Default for Controller { - fn default() -> Self { - let inner = ChannelsInner { - last_id: 0, - channels: HashMap::new(), - }; - Self(Arc::new(Mutex::new(inner))) - } -} - -impl ChannelGuard { - fn new(channel_id: ChannelId, controller: Controller) -> Self { Self { channel_id, controller } } -} - -impl Drop for ChannelGuard { - fn drop(&mut self) { - common::log::debug!("Dropping event channel with id: {}", self.channel_id); - - self.controller.remove_channel(&self.channel_id); - } -} - -impl GuardedReceiver { - /// Receives the next event from the channel - pub async fn recv(&mut self) -> Option> { self.rx.recv().await } -} - -#[cfg(any(test, target_arch = "wasm32"))] -mod tests { - use super::*; - use common::cross_test; - - common::cfg_wasm32! { - use wasm_bindgen_test::*; - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - } - - cross_test!(test_create_channel_and_broadcast, { - let mut controller = Controller::new(); - let mut guard_receiver = controller.create_channel(1); - - controller.broadcast("Message".to_string()).await; - - let received_msg = guard_receiver.recv().await.unwrap(); - assert_eq!(*received_msg, "Message".to_string()); - }); - - cross_test!(test_multiple_channels_and_broadcast, { - let mut controller = Controller::new(); - - let mut receivers = Vec::new(); - for _ in 0..3 { - receivers.push(controller.create_channel(1)); - } - - controller.broadcast("Message".to_string()).await; - - for receiver in &mut receivers { - let received_msg = receiver.recv().await.unwrap(); - assert_eq!(*received_msg, "Message".to_string()); - } - }); - - cross_test!(test_channel_cleanup_on_drop, { - let mut controller: Controller<()> = Controller::new(); - let guard_receiver = controller.create_channel(1); - - assert_eq!(controller.num_connections(), 1); - - drop(guard_receiver); - - common::executor::Timer::sleep(0.1).await; // Give time for the drop to execute - - assert_eq!(controller.num_connections(), 0); - }); - - cross_test!(test_broadcast_across_channels, { - let mut controller = Controller::new(); - - let mut receivers = Vec::new(); - for _ in 0..3 { - receivers.push(controller.create_channel(1)); - } - - controller.broadcast("Message".to_string()).await; - - for receiver in &mut receivers { - let received_msg = receiver.recv().await.unwrap(); - assert_eq!(*received_msg, "Message".to_string()); - } - }); - - cross_test!(test_multiple_messages_and_drop, { - let mut controller = Controller::new(); - let mut guard_receiver = controller.create_channel(6); - - controller.broadcast("Message 1".to_string()).await; - controller.broadcast("Message 2".to_string()).await; - controller.broadcast("Message 3".to_string()).await; - controller.broadcast("Message 4".to_string()).await; - controller.broadcast("Message 5".to_string()).await; - controller.broadcast("Message 6".to_string()).await; - - let mut received_msgs = Vec::new(); - for _ in 0..6 { - let received_msg = guard_receiver.recv().await.unwrap(); - received_msgs.push(received_msg); - } - - assert_eq!(*received_msgs[0], "Message 1".to_string()); - assert_eq!(*received_msgs[1], "Message 2".to_string()); - assert_eq!(*received_msgs[2], "Message 3".to_string()); - assert_eq!(*received_msgs[3], "Message 4".to_string()); - assert_eq!(*received_msgs[4], "Message 5".to_string()); - assert_eq!(*received_msgs[5], "Message 6".to_string()); - - // Consume the GuardedReceiver to trigger drop and channel cleanup - drop(guard_receiver); - - common::executor::Timer::sleep(0.1).await; // Give time for the drop to execute - - assert_eq!(controller.num_connections(), 0); - }); -} diff --git a/mm2src/mm2_event_stream/src/event.rs b/mm2src/mm2_event_stream/src/event.rs new file mode 100644 index 0000000000..306bbc9e49 --- /dev/null +++ b/mm2src/mm2_event_stream/src/event.rs @@ -0,0 +1,47 @@ +use serde_json::Value as Json; + +// Note `Event` shouldn't be `Clone`able, but rather Arc/Rc wrapped and then shared. +// This is only for testing. +/// Multi-purpose/generic event type that can easily be used over the event streaming +#[cfg_attr(any(test, target_arch = "wasm32"), derive(Clone, Debug, PartialEq))] +#[derive(Default)] +pub struct Event { + /// The type of the event (balance, network, swap, etc...). + event_type: String, + /// The message to be sent to the client. + message: Json, + /// Indicating whether this event is an error event or a normal one. + error: bool, +} + +impl Event { + /// Creates a new `Event` instance with the specified event type and message. + #[inline(always)] + pub fn new(streamer_id: String, message: Json) -> Self { + Self { + event_type: streamer_id, + message, + error: false, + } + } + + /// Create a new error `Event` instance with the specified error event type and message. + #[inline(always)] + pub fn err(streamer_id: String, message: Json) -> Self { + Self { + event_type: streamer_id, + message, + error: true, + } + } + + /// Returns the `event_type` (the ID of the streamer firing this event). + #[inline(always)] + pub fn origin(&self) -> &str { &self.event_type } + + /// Returns the event type and message as a pair. + pub fn get(&self) -> (String, &Json) { + let prefix = if self.error { "ERROR:" } else { "" }; + (format!("{prefix}{}", self.event_type), &self.message) + } +} diff --git a/mm2src/mm2_event_stream/src/lib.rs b/mm2src/mm2_event_stream/src/lib.rs index 1dc15bcd53..db4587a77a 100644 --- a/mm2src/mm2_event_stream/src/lib.rs +++ b/mm2src/mm2_event_stream/src/lib.rs @@ -1,125 +1,10 @@ -use serde::Deserialize; -use std::collections::HashMap; -use std::fmt; -#[cfg(target_arch = "wasm32")] use std::path::PathBuf; - -#[cfg(target_arch = "wasm32")] -const DEFAULT_WORKER_PATH: &str = "event_streaming_worker.js"; - -/// Multi-purpose/generic event type that can easily be used over the event streaming -pub struct Event { - _type: String, - message: String, -} - -impl Event { - /// Creates a new `Event` instance with the specified event type and message. - #[inline] - pub fn new(event_type: String, message: String) -> Self { - Self { - _type: event_type, - message, - } - } - - /// Gets the event type. - #[inline] - pub fn event_type(&self) -> &str { &self._type } - - /// Gets the event message. - #[inline] - pub fn message(&self) -> &str { &self.message } -} - -/// Event types streamed to clients through channels like Server-Sent Events (SSE). -#[derive(Deserialize, Eq, Hash, PartialEq)] -pub enum EventName { - /// Indicates a change in the balance of a coin. - #[serde(rename = "COIN_BALANCE")] - CoinBalance, - /// Event triggered at regular intervals to indicate that the system is operational. - HEARTBEAT, - /// Returns p2p network information at a regular interval. - NETWORK, -} - -impl fmt::Display for EventName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::CoinBalance => write!(f, "COIN_BALANCE"), - Self::HEARTBEAT => write!(f, "HEARTBEAT"), - Self::NETWORK => write!(f, "NETWORK"), - } - } -} - -/// Error event types used to indicate various kinds of errors to clients through channels like Server-Sent Events (SSE). -pub enum ErrorEventName { - /// A generic error that doesn't fit any other specific categories. - GenericError, - /// Signifies an error related to fetching or calculating the balance of a coin. - CoinBalanceError, -} - -impl fmt::Display for ErrorEventName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::GenericError => write!(f, "ERROR"), - Self::CoinBalanceError => write!(f, "COIN_BALANCE_ERROR"), - } - } -} - -/// Configuration for event streaming -#[derive(Deserialize)] -pub struct EventStreamConfiguration { - /// The value to set for the `Access-Control-Allow-Origin` header. - #[serde(default)] - pub access_control_allow_origin: String, - #[serde(default)] - active_events: HashMap, - /// The path to the worker script for event streaming. - #[cfg(target_arch = "wasm32")] - #[serde(default = "default_worker_path")] - pub worker_path: PathBuf, -} - -#[cfg(target_arch = "wasm32")] -#[inline] -fn default_worker_path() -> PathBuf { PathBuf::from(DEFAULT_WORKER_PATH) } - -/// Represents the configuration for a specific event within the event stream. -#[derive(Clone, Default, Deserialize)] -pub struct EventConfig { - /// The interval in seconds at which the event should be streamed. - #[serde(default = "default_stream_interval")] - pub stream_interval_seconds: f64, -} - -const fn default_stream_interval() -> f64 { 5. } - -impl Default for EventStreamConfiguration { - fn default() -> Self { - Self { - access_control_allow_origin: String::from("*"), - active_events: Default::default(), - #[cfg(target_arch = "wasm32")] - worker_path: default_worker_path(), - } - } -} - -impl EventStreamConfiguration { - /// Retrieves the configuration for a specific event by its name. - #[inline] - pub fn get_event(&self, event_name: &EventName) -> Option { - self.active_events.get(event_name).cloned() - } - - /// Gets the total number of active events in the configuration. - #[inline] - pub fn total_active_events(&self) -> usize { self.active_events.len() } -} - -pub mod behaviour; -pub mod controller; +pub mod configuration; +pub mod event; +pub mod manager; +pub mod streamer; + +// Re-export important types. +pub use configuration::EventStreamingConfiguration; +pub use event::Event; +pub use manager::{StreamingManager, StreamingManagerError}; +pub use streamer::{Broadcaster, EventStreamer, NoDataIn, StreamHandlerInput}; diff --git a/mm2src/mm2_event_stream/src/manager.rs b/mm2src/mm2_event_stream/src/manager.rs new file mode 100644 index 0000000000..f9c895a2d1 --- /dev/null +++ b/mm2src/mm2_event_stream/src/manager.rs @@ -0,0 +1,543 @@ +use std::any::Any; +use std::collections::{HashMap, HashSet}; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; + +use crate::streamer::spawn; +use crate::{Event, EventStreamer}; +use common::executor::abortable_queue::WeakSpawner; +use common::log::{error, LogOnError}; + +use common::on_drop_callback::OnDropCallback; +use futures::channel::mpsc::UnboundedSender; +use futures::channel::oneshot; +use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use tokio::sync::mpsc; + +/// The errors that could originate from the streaming manager. +#[derive(Debug)] +pub enum StreamingManagerError { + /// There is no streamer with the given ID. + StreamerNotFound, + /// Couldn't send the data to the streamer. + SendError(String), + /// The streamer doesn't accept an input. + NoDataIn, + /// Couldn't spawn the streamer. + SpawnError(String), + /// The client is not known/registered. + UnknownClient, + /// A client with the same ID already exists. + ClientExists, + /// The client is already listening to the streamer. + ClientAlreadyListening, +} + +#[derive(Debug)] +struct StreamerInfo { + /// The communication channel to the streamer. + data_in: Option>>, + /// Clients the streamer is serving for. + clients: HashSet, + /// The shutdown handle of the streamer. + shutdown: oneshot::Sender<()>, +} + +impl StreamerInfo { + fn new(data_in: Option>>, shutdown: oneshot::Sender<()>) -> Self { + Self { + data_in, + clients: HashSet::new(), + shutdown, + } + } + + fn add_client(&mut self, client_id: u64) { self.clients.insert(client_id); } + + fn remove_client(&mut self, client_id: &u64) { self.clients.remove(client_id); } + + fn is_down(&self) -> bool { self.shutdown.is_canceled() } +} + +#[derive(Debug)] +struct ClientInfo { + /// The streamers the client is listening to. + listening_to: HashSet, + /// The communication/stream-out channel to the client. + // NOTE: Here we are using `tokio`'s `mpsc` because the one in `futures` have some extra feature + // (ref: https://users.rust-lang.org/t/why-does-try-send-from-crate-futures-require-mut-self/100389). + // This feature is aimed towards the multi-producer case (which we don't use) and requires a mutable + // reference on `try_send` calls. This will require us to put the channel in a mutex and degrade the + // broadcasting performance. + channel: mpsc::Sender>, +} + +impl ClientInfo { + fn new(channel: mpsc::Sender>) -> Self { + Self { + listening_to: HashSet::new(), + channel, + } + } + + fn add_streamer(&mut self, streamer_id: String) { self.listening_to.insert(streamer_id); } + + fn remove_streamer(&mut self, streamer_id: &str) { self.listening_to.remove(streamer_id); } + + fn listens_to(&self, streamer_id: &str) -> bool { self.listening_to.contains(streamer_id) } + + fn send_event(&self, event: Arc) { + // Only `try_send` here. If the channel is full (client is slow), the message + // will be dropped and the client won't receive it. + // This avoids blocking the broadcast to other receivers. + self.channel.try_send(event).error_log(); + } +} + +#[derive(Default, Debug)] +struct StreamingManagerInner { + /// A map from streamer IDs to their communication channels (if present) and shutdown handles. + streamers: HashMap, + /// An inverse map from client IDs to the streamers they are listening to and the communication channel with the client. + clients: HashMap, +} + +#[derive(Clone, Default, Debug)] +pub struct StreamingManager(Arc>); + +impl StreamingManager { + /// Returns a read guard over the streaming manager. + fn read(&self) -> RwLockReadGuard { self.0.read() } + + /// Returns a write guard over the streaming manager. + fn write(&self) -> RwLockWriteGuard { self.0.write() } + + /// Spawns and adds a new streamer `streamer` to the manager. + pub async fn add( + &self, + client_id: u64, + streamer: impl EventStreamer, + spawner: WeakSpawner, + ) -> Result { + let streamer_id = streamer.streamer_id(); + // Remove the streamer if it died for some reason. + self.remove_streamer_if_down(&streamer_id); + + // Pre-checks before spawning the streamer. Inside another scope to drop the lock early. + { + let mut this = self.write(); + match this.clients.get(&client_id) { + // We don't know that client. We don't have a connection to it. + None => return Err(StreamingManagerError::UnknownClient), + // The client is already listening to that streamer. + Some(client_info) if client_info.listens_to(&streamer_id) => { + return Err(StreamingManagerError::ClientAlreadyListening); + }, + _ => (), + } + + // If a streamer is already up and running, we won't spawn another one. + if let Some(streamer_info) = this.streamers.get_mut(&streamer_id) { + // Register the client as a listener to the streamer. + streamer_info.add_client(client_id); + // Register the streamer as listened-to by the client. + if let Some(client_info) = this.clients.get_mut(&client_id) { + client_info.add_streamer(streamer_id.clone()); + } + return Ok(streamer_id); + } + } + + // Spawn a new streamer. + let (shutdown, data_in) = spawn(streamer, spawner, self.clone()) + .await + .map_err(StreamingManagerError::SpawnError)?; + let streamer_info = StreamerInfo::new(data_in, shutdown); + + // Note that we didn't hold the lock while spawning the streamer (potentially a long operation). + // This means we can't assume either that the client still exists at this point or + // that the streamer still doesn't exist. + let mut this = self.write(); + if let Some(client_info) = this.clients.get_mut(&client_id) { + client_info.add_streamer(streamer_id.clone()); + this.streamers + .entry(streamer_id.clone()) + .or_insert(streamer_info) + .add_client(client_id); + } else { + // The client was removed while we were spawning the streamer. + // We no longer have a connection for it. + return Err(StreamingManagerError::UnknownClient); + } + Ok(streamer_id) + } + + /// Sends data to a streamer with `streamer_id`. + pub fn send(&self, streamer_id: &str, data: T) -> Result<(), StreamingManagerError> { + let this = self.read(); + let streamer_info = this + .streamers + .get(streamer_id) + .ok_or(StreamingManagerError::StreamerNotFound)?; + let data_in = streamer_info.data_in.as_ref().ok_or(StreamingManagerError::NoDataIn)?; + data_in + .unbounded_send(Box::new(data)) + .map_err(|e| StreamingManagerError::SendError(e.to_string())) + } + + /// Same as `StreamingManager::send`, but computes that data to send to a streamer using a closure, + /// thus avoiding computations & cloning if the intended streamer isn't running (more like the + /// laziness of `*_or_else()` functions). + /// + /// `data_fn` will only be evaluated if the streamer is found and accepts an input. + pub fn send_fn( + &self, + streamer_id: &str, + data_fn: impl FnOnce() -> T, + ) -> Result<(), StreamingManagerError> { + let this = self.read(); + let streamer_info = this + .streamers + .get(streamer_id) + .ok_or(StreamingManagerError::StreamerNotFound)?; + let data_in = streamer_info.data_in.as_ref().ok_or(StreamingManagerError::NoDataIn)?; + data_in + .unbounded_send(Box::new(data_fn())) + .map_err(|e| StreamingManagerError::SendError(e.to_string())) + } + + /// Stops streaming from the streamer with `streamer_id` to the client with `client_id`. + pub fn stop(&self, client_id: u64, streamer_id: &str) -> Result<(), StreamingManagerError> { + let mut this = self.write(); + let client_info = this + .clients + .get_mut(&client_id) + .ok_or(StreamingManagerError::UnknownClient)?; + client_info.remove_streamer(streamer_id); + + this.streamers + .get_mut(streamer_id) + .ok_or(StreamingManagerError::StreamerNotFound)? + .remove_client(&client_id); + + // If there are no more listening clients, terminate the streamer. + if this.streamers.get(streamer_id).map(|info| info.clients.len()) == Some(0) { + this.streamers.remove(streamer_id); + } + Ok(()) + } + + /// Broadcasts some event to clients listening to it. + /// + /// In contrast to `StreamingManager::send`, which sends some data to a streamer, + /// this method broadcasts an event to the listening *clients* directly, independently + /// of any streamer (i.e. bypassing any streamer). + pub fn broadcast(&self, event: Event) { + let event = Arc::new(event); + let this = self.read(); + if let Some(client_ids) = this.streamers.get(event.origin()).map(|info| &info.clients) { + client_ids.iter().for_each(|client_id| { + if let Some(info) = this.clients.get(client_id) { + info.send_event(event.clone()); + } + }); + }; + } + + /// Broadcasts (actually just *sends* in this case) some event to a specific client. + /// + /// Could be used in case we have a single known client and don't want to spawn up a streamer just for that. + pub fn broadcast_to(&self, event: Event, client_id: u64) -> Result<(), StreamingManagerError> { + let event = Arc::new(event); + self.read() + .clients + .get(&client_id) + .map(|info| info.send_event(event)) + .ok_or(StreamingManagerError::UnknownClient) + } + + /// Forcefully broadcasts an event to all known clients even if they are not listening for such an event. + pub fn broadcast_all(&self, event: Event) { + let event = Arc::new(event); + self.read().clients.values().for_each(|info| { + info.send_event(event.clone()); + }); + } + + /// Creates a new client and returns the event receiver for this client. + pub fn new_client(&self, client_id: u64) -> Result { + let mut this = self.write(); + if this.clients.contains_key(&client_id) { + return Err(StreamingManagerError::ClientExists); + } + // Note that events queued in the channel are `Arc<` shared. + // So a 1024 long buffer isn't actually heavy on memory. + let (tx, rx) = mpsc::channel(1024); + let client_info = ClientInfo::new(tx); + this.clients.insert(client_id, client_info); + let manager = self.clone(); + Ok(ClientHandle { + rx, + _on_drop_callback: OnDropCallback::new(move || { + manager.remove_client(client_id).ok(); + }), + }) + } + + /// Removes a client from the manager. + pub fn remove_client(&self, client_id: u64) -> Result<(), StreamingManagerError> { + let mut this = self.write(); + // Remove the client from our known-clients map. + let client_info = this + .clients + .remove(&client_id) + .ok_or(StreamingManagerError::UnknownClient)?; + // Remove the client from all the streamers it was listening to. + for streamer_id in client_info.listening_to { + if let Some(streamer_info) = this.streamers.get_mut(&streamer_id) { + streamer_info.remove_client(&client_id); + } else { + error!("Client {client_id} was listening to a non-existent streamer {streamer_id}. This is a bug!"); + } + // If there are no more listening clients, terminate the streamer. + if this.streamers.get(&streamer_id).map(|info| info.clients.len()) == Some(0) { + this.streamers.remove(&streamer_id); + } + } + Ok(()) + } + + /// Removes a streamer if it is no longer running. + /// + /// Aside from us shutting down a streamer when all its clients are disconnected, + /// the streamer might die by itself (e.g. the spawner it was spawned with aborted). + /// In this case, we need to remove the streamer and de-list it from all clients. + fn remove_streamer_if_down(&self, streamer_id: &str) { + let mut this = self.write(); + let Some(streamer_info) = this.streamers.get(streamer_id) else { return }; + if !streamer_info.is_down() { + return; + } + // Remove the streamer from our registry. + let Some(streamer_info) = this.streamers.remove(streamer_id) else { return }; + // And remove the streamer from all clients listening to it. + for client_id in streamer_info.clients { + if let Some(info) = this.clients.get_mut(&client_id) { + info.remove_streamer(streamer_id); + } + } + } +} + +/// A handle that is returned on [`StreamingManager::new_client`] calls that will auto remove +/// the client when dropped. +/// So this handle must live as long as the client is connected. +pub struct ClientHandle { + rx: mpsc::Receiver>, + _on_drop_callback: OnDropCallback, +} + +/// Deref the handle to the receiver inside for ease of use. +impl Deref for ClientHandle { + type Target = mpsc::Receiver>; + fn deref(&self) -> &Self::Target { &self.rx } +} + +/// Also DerefMut since the receiver inside is mutated when consumed. +impl DerefMut for ClientHandle { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.rx } +} + +#[cfg(any(test, target_arch = "wasm32"))] +mod tests { + use super::*; + use crate::streamer::test_utils::{InitErrorStreamer, PeriodicStreamer, ReactiveStreamer}; + + use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, Timer}; + use common::{cfg_wasm32, cross_test}; + use serde_json::json; + cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + + cross_test!(test_add_remove_client, { + let manager = StreamingManager::default(); + let client_id1 = 1; + let client_id2 = 2; + let client_id3 = 3; + + let c1_handle = manager.new_client(client_id1); + assert!(matches!(c1_handle, Ok(..))); + // Adding the same client again should fail. + assert!(matches!( + manager.new_client(client_id1), + Err(StreamingManagerError::ClientExists) + )); + // Adding a different new client should be OK. + let c2_handle = manager.new_client(client_id2); + assert!(matches!(c2_handle, Ok(..))); + + assert!(matches!(manager.remove_client(client_id1), Ok(()))); + // Removing a removed client should fail. + assert!(matches!( + manager.remove_client(client_id1), + Err(StreamingManagerError::UnknownClient) + )); + // Same as removing a non-existent client. + assert!(matches!( + manager.remove_client(client_id3), + Err(StreamingManagerError::UnknownClient) + )); + }); + + cross_test!(test_broadcast_all, { + // Create a manager and add register two clients with it. + let manager = StreamingManager::default(); + let mut client1 = manager.new_client(1).unwrap(); + let mut client2 = manager.new_client(2).unwrap(); + let event = Event::new("test".to_string(), json!("test")); + + // Broadcast the event to all clients. + manager.broadcast_all(event.clone()); + + // The clients should receive the events. + assert_eq!(*client1.try_recv().unwrap(), event); + assert_eq!(*client2.try_recv().unwrap(), event); + + // Remove the clients. + manager.remove_client(1).unwrap(); + manager.remove_client(2).unwrap(); + + // `recv` shouldn't work at this point since the client is removed. + assert!(client1.try_recv().is_err()); + assert!(client2.try_recv().is_err()); + }); + + cross_test!(test_periodic_streamer, { + let manager = StreamingManager::default(); + let system = AbortableQueue::default(); + let (client_id1, client_id2) = (1, 2); + // Register a new client with the manager. + let mut client1 = manager.new_client(client_id1).unwrap(); + // Another client whom we won't have it subscribe to the streamer. + let mut client2 = manager.new_client(client_id2).unwrap(); + // Subscribe the new client to PeriodicStreamer. + let streamer_id = manager + .add(client_id1, PeriodicStreamer, system.weak_spawner()) + .await + .unwrap(); + + // We should be hooked now. try to receive some events from the streamer. + for _ in 0..3 { + // The streamer should send an event every 0.1s. Wait for 0.15s for safety. + Timer::sleep(0.15).await; + let event = client1.try_recv().unwrap(); + assert_eq!(event.origin(), streamer_id); + } + + // The other client shouldn't have received any events. + assert!(client2.try_recv().is_err()); + }); + + cross_test!(test_reactive_streamer, { + let manager = StreamingManager::default(); + let system = AbortableQueue::default(); + let (client_id1, client_id2) = (1, 2); + // Register a new client with the manager. + let mut client1 = manager.new_client(client_id1).unwrap(); + // Another client whom we won't have it subscribe to the streamer. + let mut client2 = manager.new_client(client_id2).unwrap(); + // Subscribe the new client to ReactiveStreamer. + let streamer_id = manager + .add(client_id1, ReactiveStreamer, system.weak_spawner()) + .await + .unwrap(); + + // We should be hooked now. try to receive some events from the streamer. + for i in 1..=3 { + let msg = format!("send{}", i); + manager.send(&streamer_id, msg.clone()).unwrap(); + // Wait for a little bit to make sure the streamer received the data we sent. + Timer::sleep(0.1).await; + // The streamer should broadcast some event to the subscribed clients. + let event = client1.try_recv().unwrap(); + assert_eq!(event.origin(), streamer_id); + // It's an echo streamer, so the message should be the same. + assert_eq!(event.get().1, &json!(msg)); + } + + // If we send the wrong datatype (void here instead of String), the streamer should ignore it. + manager.send(&streamer_id, ()).unwrap(); + Timer::sleep(0.1).await; + assert!(client1.try_recv().is_err()); + + // The other client shouldn't have received any events. + assert!(client2.try_recv().is_err()); + }); + + cross_test!(test_erroring_streamer, { + let manager = StreamingManager::default(); + let system = AbortableQueue::default(); + let client_id = 1; + // Register a new client with the manager. + let _client = manager.new_client(client_id).unwrap(); + // Subscribe the new client to InitErrorStreamer. + let error = manager + .add(client_id, InitErrorStreamer, system.weak_spawner()) + .await + .unwrap_err(); + + assert!(matches!(error, StreamingManagerError::SpawnError(..))); + }); + + cross_test!(test_remove_streamer_if_down, { + let manager = StreamingManager::default(); + let system = AbortableQueue::default(); + let client_id = 1; + // Register a new client with the manager. + let _client = manager.new_client(client_id).unwrap(); + // Subscribe the new client to PeriodicStreamer. + let streamer_id = manager + .add(client_id, PeriodicStreamer, system.weak_spawner()) + .await + .unwrap(); + + // The streamer is up and streaming to `client_id`. + assert!(manager + .0 + .read() + .streamers + .get(&streamer_id) + .unwrap() + .clients + .contains(&client_id)); + + // The client should be registered and listening to `streamer_id`. + assert!(manager + .0 + .read() + .clients + .get(&client_id) + .unwrap() + .listens_to(&streamer_id)); + + // Abort the system to kill the streamer. + system.abort_all().unwrap(); + // Wait a little bit since the abortion doesn't take effect immediately (the aborted task needs to yield first). + Timer::sleep(0.1).await; + + manager.remove_streamer_if_down(&streamer_id); + + // The streamer should be removed. + assert!(manager.read().streamers.get(&streamer_id).is_none()); + // And the client is no more listening to it. + assert!(!manager + .0 + .read() + .clients + .get(&client_id) + .unwrap() + .listens_to(&streamer_id)); + }); +} diff --git a/mm2src/mm2_event_stream/src/streamer.rs b/mm2src/mm2_event_stream/src/streamer.rs new file mode 100644 index 0000000000..6c319cb89c --- /dev/null +++ b/mm2src/mm2_event_stream/src/streamer.rs @@ -0,0 +1,233 @@ +use std::any::{self, Any}; + +use crate::{Event, StreamingManager}; +use common::executor::{abortable_queue::WeakSpawner, AbortSettings, SpawnAbortable}; +use common::log::{error, info}; + +use async_trait::async_trait; +use futures::channel::{mpsc, oneshot}; +use futures::{future, select, FutureExt, Stream, StreamExt}; + +/// A marker to indicate that the event streamer doesn't take any input data. +pub struct NoDataIn; + +/// A mixture trait combining `Stream`, `Send` & `Unpin` together (to avoid confusing annotation). +pub trait StreamHandlerInput: Stream + Send + Unpin {} +/// Implement the trait for all types `T` that implement `Stream + Send + Unpin` for any `D`. +impl StreamHandlerInput for T where T: Stream + Send + Unpin {} + +#[async_trait] +pub trait EventStreamer +where + Self: Sized + Send + 'static, +{ + type DataInType: Send; + + /// Returns a human readable unique identifier for the event streamer. + /// No other event streamer should have the same identifier. + fn streamer_id(&self) -> String; + + /// Event handler that is responsible for broadcasting event data to the streaming channels. + /// + /// `ready_tx` is a oneshot sender that is used to send the initialization status of the event. + /// `data_rx` is a receiver that the streamer *could* use to receive data from the outside world. + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + data_rx: impl StreamHandlerInput, + ); +} + +/// Spawns the [`EventStreamer::handle`] in a separate task using [`WeakSpawner`]. +/// +/// Returns a [`oneshot::Sender`] to shutdown the handler and an optional [`mpsc::UnboundedSender`] +/// to send data to the handler. +pub(crate) async fn spawn( + streamer: S, + spawner: WeakSpawner, + streaming_manager: StreamingManager, +) -> Result<(oneshot::Sender<()>, Option>>), String> +where + S: EventStreamer, +{ + let streamer_id = streamer.streamer_id(); + info!("Spawning event streamer: {streamer_id}"); + + // A oneshot channel to receive the initialization status of the handler through. + let (tx_ready, ready_rx) = oneshot::channel(); + // A oneshot channel to shutdown the handler. + let (tx_shutdown, rx_shutdown) = oneshot::channel::<()>(); + // An unbounded channel to send data to the handler. + let (any_data_sender, any_data_receiver) = mpsc::unbounded::>(); + // A middleware to cast the data of type `Box` to the actual input datatype of this streamer. + let data_receiver = any_data_receiver.filter_map({ + let streamer_id = streamer_id.clone(); + move |any_input_data| { + let streamer_id = streamer_id.clone(); + future::ready( + any_input_data + .downcast() + .map(|input_data| *input_data) + .map_err(|_| { + error!("Couldn't downcast a received message to {}. This message wasn't intended to be sent to this streamer ({streamer_id}).", any::type_name::()); + }) + .ok(), + ) + } + }); + + let handler_with_shutdown = { + let streamer_id = streamer_id.clone(); + async move { + select! { + _ = rx_shutdown.fuse() => { + info!("Manually shutting down event streamer: {streamer_id}.") + } + _ = streamer.handle(Broadcaster::new(streaming_manager), tx_ready, data_receiver).fuse() => {} + } + } + }; + let settings = AbortSettings::info_on_abort(format!("{streamer_id} streamer has stopped.")); + spawner.spawn_with_settings(handler_with_shutdown, settings); + + ready_rx.await.unwrap_or_else(|e| { + Err(format!( + "The handler was aborted before sending event initialization status: {e}" + )) + })?; + + // If the handler takes no input data, return `None` for the data sender. + if any::TypeId::of::() == any::TypeId::of::() { + Ok((tx_shutdown, None)) + } else { + Ok((tx_shutdown, Some(any_data_sender))) + } +} + +/// A wrapper around `StreamingManager` to only expose the `broadcast` method. +pub struct Broadcaster(StreamingManager); + +impl Broadcaster { + pub fn new(inner: StreamingManager) -> Self { Self(inner) } + + pub fn broadcast(&self, event: Event) { self.0.broadcast(event); } +} + +#[cfg(any(test, target_arch = "wasm32"))] +pub mod test_utils { + use super::*; + + use common::executor::Timer; + use serde_json::json; + + /// A test event streamer that broadcasts an event periodically. + /// Broadcasts `json!("hello")` every tenth of a second. + pub struct PeriodicStreamer; + + #[async_trait] + impl EventStreamer for PeriodicStreamer { + type DataInType = NoDataIn; + + fn streamer_id(&self) -> String { "periodic_streamer".to_string() } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + ready_tx.send(Ok(())).unwrap(); + loop { + broadcaster.broadcast(Event::new(self.streamer_id(), json!("hello"))); + Timer::sleep(0.1).await; + } + } + } + + /// A test event streamer that broadcasts an event whenever it receives a new message through `data_rx`. + pub struct ReactiveStreamer; + + #[async_trait] + impl EventStreamer for ReactiveStreamer { + type DataInType = String; + + fn streamer_id(&self) -> String { "reactive_streamer".to_string() } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + ready_tx.send(Ok(())).unwrap(); + while let Some(msg) = data_rx.next().await { + // Just echo back whatever we receive. + broadcaster.broadcast(Event::new(self.streamer_id(), json!(msg))); + } + } + } + + /// A test event streamer that fails upon initialization. + pub struct InitErrorStreamer; + + #[async_trait] + impl EventStreamer for InitErrorStreamer { + type DataInType = NoDataIn; + + fn streamer_id(&self) -> String { "init_error_streamer".to_string() } + + async fn handle( + self, + _: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + // Fail the initialization and stop. + ready_tx.send(Err("error".to_string())).unwrap(); + } + } +} + +#[cfg(any(test, target_arch = "wasm32"))] +mod tests { + use super::test_utils::{InitErrorStreamer, PeriodicStreamer, ReactiveStreamer}; + use super::*; + + use common::executor::abortable_queue::AbortableQueue; + use common::{cfg_wasm32, cross_test}; + cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + + cross_test!(test_spawn_periodic_streamer, { + let system = AbortableQueue::default(); + // Spawn the periodic streamer. + let (_, data_in) = spawn(PeriodicStreamer, system.weak_spawner(), StreamingManager::default()) + .await + .unwrap(); + // Periodic streamer shouldn't be ingesting any input. + assert!(data_in.is_none()); + }); + + cross_test!(test_spawn_reactive_streamer, { + let system = AbortableQueue::default(); + // Spawn the reactive streamer. + let (_, data_in) = spawn(ReactiveStreamer, system.weak_spawner(), StreamingManager::default()) + .await + .unwrap(); + // Reactive streamer should be ingesting some input. + assert!(data_in.is_some()); + }); + + cross_test!(test_spawn_erroring_streamer, { + let system = AbortableQueue::default(); + // Try to spawn the erroring streamer. + let err = spawn(InitErrorStreamer, system.weak_spawner(), StreamingManager::default()) + .await + .unwrap_err(); + // The streamer should return an error. + assert_eq!(err, "error"); + }); +} diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 4fa36b0f82..78a52125f1 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -65,7 +65,7 @@ mm2_gui_storage = { path = "../mm2_gui_storage" } mm2_io = { path = "../mm2_io" } mm2_libp2p = { path = "../mm2_p2p", package = "mm2_p2p", features = ["application"] } mm2_metrics = { path = "../mm2_metrics" } -mm2_net = { path = "../mm2_net" } +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" } diff --git a/mm2src/mm2_main/src/heartbeat_event.rs b/mm2src/mm2_main/src/heartbeat_event.rs index 6c4d19d77b..a2c46f2fb6 100644 --- a/mm2src/mm2_main/src/heartbeat_event.rs +++ b/mm2src/mm2_main/src/heartbeat_event.rs @@ -1,52 +1,50 @@ use async_trait::async_trait; -use common::{executor::{SpawnFuture, Timer}, - log::info}; -use futures::channel::oneshot::{self, Receiver, Sender}; -use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - Event, EventName, EventStreamConfiguration}; +use common::executor::Timer; +use futures::channel::oneshot; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields, default)] +pub struct HeartbeatEventConfig { + /// The time in seconds to wait before sending another ping event. + pub stream_interval_seconds: f64, +} + +impl Default for HeartbeatEventConfig { + fn default() -> Self { + Self { + stream_interval_seconds: 5.0, + } + } +} pub struct HeartbeatEvent { - ctx: MmArc, + config: HeartbeatEventConfig, } impl HeartbeatEvent { - pub fn new(ctx: MmArc) -> Self { Self { ctx } } + pub fn new(config: HeartbeatEventConfig) -> Self { Self { config } } } #[async_trait] -impl EventBehaviour for HeartbeatEvent { - fn event_name() -> EventName { EventName::HEARTBEAT } +impl EventStreamer for HeartbeatEvent { + type DataInType = NoDataIn; - async fn handle(self, interval: f64, tx: oneshot::Sender) { - tx.send(EventInitStatus::Success).unwrap(); + fn streamer_id(&self) -> String { "HEARTBEAT".to_string() } - loop { - self.ctx - .stream_channel_controller - .broadcast(Event::new(Self::event_name().to_string(), json!({}).to_string())) - .await; + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + ready_tx.send(Ok(())).unwrap(); - Timer::sleep(interval).await; - } - } + loop { + broadcaster.broadcast(Event::new(self.streamer_id(), json!({}))); - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - info!( - "{} event is activated with {} seconds interval.", - Self::event_name(), - event.stream_interval_seconds - ); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - self.ctx.spawner().spawn(self.handle(event.stream_interval_seconds, tx)); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive + Timer::sleep(self.config.stream_interval_seconds).await; } } } diff --git a/mm2src/mm2_main/src/lp_init/init_context.rs b/mm2src/mm2_main/src/lp_init/init_context.rs index 8b03751b69..a260b4ab67 100644 --- a/mm2src/mm2_main/src/lp_init/init_context.rs +++ b/mm2src/mm2_main/src/lp_init/init_context.rs @@ -16,9 +16,9 @@ impl MmInitContext { pub fn from_ctx(ctx: &MmArc) -> Result, String> { from_ctx(&ctx.mm_init_ctx, move || { Ok(MmInitContext { - init_hw_task_manager: RpcTaskManager::new_shared(), + init_hw_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), #[cfg(target_arch = "wasm32")] - init_metamask_manager: RpcTaskManager::new_shared(), + init_metamask_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), }) }) } diff --git a/mm2src/mm2_main/src/lp_init/init_hw.rs b/mm2src/mm2_main/src/lp_init/init_hw.rs index b9d0c67664..6148a44f53 100644 --- a/mm2src/mm2_main/src/lp_init/init_hw.rs +++ b/mm2src/mm2_main/src/lp_init/init_hw.rs @@ -12,8 +12,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, - RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, + RpcTaskStatus, RpcTaskTypes}; use std::sync::Arc; use std::time::Duration; @@ -165,7 +165,8 @@ impl RpcTask for InitHwTask { } } -pub async fn init_trezor(ctx: MmArc, req: InitHwRequest) -> MmResult { +pub async fn init_trezor(ctx: MmArc, req: RpcInitReq) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let init_ctx = MmInitContext::from_ctx(&ctx).map_to_mm(InitHwError::Internal)?; let spawner = ctx.spawner(); let task = InitHwTask { @@ -173,7 +174,7 @@ pub async fn init_trezor(ctx: MmArc, req: InitHwRequest) -> MmResult; pub type InitMetamaskStatus = @@ -132,12 +133,13 @@ impl RpcTask for InitMetamaskTask { pub async fn connect_metamask( ctx: MmArc, - req: InitMetamaskRequest, + req: RpcInitReq, ) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let init_ctx = MmInitContext::from_ctx(&ctx).map_to_mm(InitMetamaskError::Internal)?; let spawner = ctx.spawner(); let task = InitMetamaskTask { ctx, req }; - let task_id = RpcTaskManager::spawn_rpc_task(&init_ctx.init_metamask_manager, &spawner, task)?; + let task_id = RpcTaskManager::spawn_rpc_task(&init_ctx.init_metamask_manager, &spawner, task, client_id)?; Ok(InitRpcTaskResponse { task_id }) } diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index 8e1e91ec13..9bfd13b922 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -28,8 +28,6 @@ use enum_derives::EnumFromTrait; use mm2_core::mm_ctx::{MmArc, MmCtx}; use mm2_err_handle::common_errors::InternalError; use mm2_err_handle::prelude::*; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; -use mm2_libp2p::application::network_event::NetworkEvent; use mm2_libp2p::behaviours::atomicdex::{generate_ed25519_keypair, GossipsubConfig, DEPRECATED_NETID_LIST}; use mm2_libp2p::p2p_ctx::P2PContext; use mm2_libp2p::{spawn_gossipsub, AdexBehaviourError, NodeType, RelayAddress, RelayAddressError, SeedNodeInfo, @@ -46,7 +44,6 @@ use std::{fs, usize}; #[cfg(not(target_arch = "wasm32"))] use crate::database::init_and_migrate_sql_db; -use crate::heartbeat_event::HeartbeatEvent; use crate::lp_healthcheck::peer_healthcheck_topic; use crate::lp_message_service::{init_message_service, InitMessageServiceError}; use crate::lp_network::{lp_network_ports, p2p_event_process_loop, subscribe_to_topic, NetIdError}; @@ -67,7 +64,7 @@ cfg_native! { #[path = "lp_init/init_hw.rs"] pub mod init_hw; cfg_wasm32! { - use mm2_net::wasm_event_stream::handle_worker_stream; + use mm2_net::event_streaming::wasm_event_stream::handle_worker_stream; #[path = "lp_init/init_metamask.rs"] pub mod init_metamask; @@ -205,10 +202,8 @@ pub enum MmInitError { OrdersKickStartError(String), #[display(fmt = "Error initializing wallet: {}", _0)] WalletInitError(String), - #[display(fmt = "NETWORK event initialization failed: {}", _0)] - NetworkEventInitFailed(String), - #[display(fmt = "HEARTBEAT event initialization failed: {}", _0)] - HeartbeatEventInitFailed(String), + #[display(fmt = "Event streamer initialization failed: {}", _0)] + EventStreamerInitFailed(String), #[from_trait(WithHwRpcError::hw_rpc_error)] #[display(fmt = "{}", _0)] HwError(HwRpcError), @@ -427,25 +422,11 @@ fn migrate_db(ctx: &MmArc) -> MmInitResult<()> { #[cfg(not(target_arch = "wasm32"))] fn migration_1(_ctx: &MmArc) {} -async fn init_event_streaming(ctx: &MmArc) -> MmInitResult<()> { - // This condition only executed if events were enabled in mm2 configuration. - if let Some(config) = &ctx.event_stream_configuration { - if let EventInitStatus::Failed(err) = NetworkEvent::new(ctx.clone()).spawn_if_active(config).await { - return MmError::err(MmInitError::NetworkEventInitFailed(err)); - } - - if let EventInitStatus::Failed(err) = HeartbeatEvent::new(ctx.clone()).spawn_if_active(config).await { - return MmError::err(MmInitError::HeartbeatEventInitFailed(err)); - } - } - - Ok(()) -} - #[cfg(target_arch = "wasm32")] fn init_wasm_event_streaming(ctx: &MmArc) { - if ctx.event_stream_configuration.is_some() { - ctx.spawner().spawn(handle_worker_stream(ctx.clone())); + if let Some(event_streaming_config) = ctx.event_streaming_configuration() { + ctx.spawner() + .spawn(handle_worker_stream(ctx.clone(), event_streaming_config.worker_path)); } } @@ -482,8 +463,6 @@ pub async fn lp_init_continue(ctx: MmArc) -> MmInitResult<()> { // an order and start new swap that might get started 2 times because of kick-start kick_start(ctx.clone()).await?; - init_event_streaming(&ctx).await?; - ctx.spawner().spawn(lp_ordermatch_loop(ctx.clone())); ctx.spawner().spawn(broadcast_maker_orders_keep_alive_loop(ctx.clone())); diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 620cb79bfb..fca69372c1 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -41,6 +41,7 @@ use http::Response; use keys::{AddressFormat, KeyPair}; use mm2_core::mm_ctx::{from_ctx, MmArc, MmWeak}; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_libp2p::application::request_response::ordermatch::OrdermatchRequest; use mm2_libp2p::application::request_response::P2PRequest; use mm2_libp2p::{decode_signed, encode_and_sign, encode_message, pub_sub_topic, PublicKey, TopicHash, TopicPrefix, @@ -55,6 +56,8 @@ use my_orders_storage::{delete_my_maker_order, delete_my_taker_order, save_maker save_my_new_maker_order, save_my_new_taker_order, MyActiveOrders, MyOrdersFilteringHistory, MyOrdersHistory, MyOrdersStorage}; use num_traits::identities::Zero; +use order_events::{OrderStatusEvent, OrderStatusStreamer}; +use orderbook_events::{OrderbookItemChangeEvent, OrderbookStreamer}; use parking_lot::Mutex as PaMutex; use rpc::v1::types::H256 as H256Json; use serde_json::{self as json, Value as Json}; @@ -102,8 +105,10 @@ pub use lp_bot::{start_simple_market_maker_bot, stop_simple_market_maker_bot, St mod my_orders_storage; mod new_protocol; +pub(crate) mod order_events; mod order_requests_tracker; mod orderbook_depth; +pub(crate) mod orderbook_events; mod orderbook_rpc; #[cfg(all(test, not(target_arch = "wasm32")))] #[path = "ordermatch_tests.rs"] @@ -2482,6 +2487,8 @@ struct Orderbook { /// MemoryDB instance to store Patricia Tries data memory_db: MemoryDB, my_p2p_pubkeys: HashSet, + /// A copy of the streaming manager to stream orderbook events out. + streaming_manager: StreamingManager, } impl Default for Orderbook { @@ -2497,6 +2504,7 @@ impl Default for Orderbook { topics_subscribed_to: HashMap::default(), memory_db: MemoryDB::default(), my_p2p_pubkeys: HashSet::default(), + streaming_manager: Default::default(), } } } @@ -2504,6 +2512,13 @@ impl Default for Orderbook { fn hashed_null_node() -> TrieHash { ::hashed_null_node() } impl Orderbook { + fn new(streaming_manager: StreamingManager) -> Orderbook { + Orderbook { + streaming_manager, + ..Default::default() + } + } + fn find_order_by_uuid_and_pubkey(&self, uuid: &Uuid, from_pubkey: &str) -> Option { self.order_set.get(uuid).and_then(|order| { if order.pubkey == from_pubkey { @@ -2607,6 +2622,11 @@ impl Orderbook { .or_insert_with(HashSet::new) .insert(order.uuid); + self.streaming_manager + .send_fn(&OrderbookStreamer::derive_streamer_id(&order.base, &order.rel), || { + OrderbookItemChangeEvent::NewOrUpdatedItem(Box::new(order.clone().into())) + }) + .ok(); self.order_set.insert(order.uuid, order); } @@ -2664,6 +2684,12 @@ impl Orderbook { next_root: *pair_state, }); } + + self.streaming_manager + .send_fn(&OrderbookStreamer::derive_streamer_id(&order.base, &order.rel), || { + OrderbookItemChangeEvent::RemovedItem(order.uuid) + }) + .ok(); Some(order) } @@ -2765,7 +2791,7 @@ pub fn init_ordermatch_context(ctx: &MmArc) -> OrdermatchInitResult<()> { let ordermatch_context = OrdermatchContext { maker_orders_ctx: PaMutex::new(MakerOrdersContext::new(ctx)?), my_taker_orders: Default::default(), - orderbook: Default::default(), + orderbook: PaMutex::new(Orderbook::new(ctx.event_stream_manager.clone())), pending_maker_reserved: Default::default(), orderbook_tickers, original_tickers, @@ -2795,7 +2821,7 @@ impl OrdermatchContext { Ok(OrdermatchContext { maker_orders_ctx: PaMutex::new(try_s!(MakerOrdersContext::new(ctx))), my_taker_orders: Default::default(), - orderbook: Default::default(), + orderbook: PaMutex::new(Orderbook::new(ctx.event_stream_manager.clone())), pending_maker_reserved: Default::default(), orderbook_tickers: Default::default(), original_tickers: Default::default(), @@ -3569,6 +3595,13 @@ async fn process_maker_reserved(ctx: MmArc, from_pubkey: H256Json, reserved_msg: connected: None, last_updated: now_ms(), }; + + ctx.event_stream_manager + .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + OrderStatusEvent::TakerMatch(taker_match.clone()) + }) + .ok(); + my_order .matches .insert(taker_match.reserved.maker_order_uuid, taker_match); @@ -3617,6 +3650,13 @@ async fn process_maker_connected(ctx: MmArc, from_pubkey: PublicKey, connected: error!("Connected message sender pubkey != reserved message sender pubkey"); return; } + + ctx.event_stream_manager + .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + OrderStatusEvent::TakerConnected(order_match.clone()) + }) + .ok(); + // alice lp_connected_alice( ctx.clone(), @@ -3723,6 +3763,13 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: connected: None, last_updated: now_ms(), }; + + ctx.event_stream_manager + .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + OrderStatusEvent::MakerMatch(maker_match.clone()) + }) + .ok(); + order.matches.insert(maker_match.request.uuid, maker_match); storage .update_active_maker_order(&order) @@ -3787,6 +3834,13 @@ async fn process_taker_connect(ctx: MmArc, sender_pubkey: PublicKey, connect_msg order_match.connect = Some(connect_msg); order_match.connected = Some(connected.clone()); let order_match = order_match.clone(); + + ctx.event_stream_manager + .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + OrderStatusEvent::MakerConnected(order_match.clone()) + }) + .ok(); + my_order.started_swaps.push(order_match.request.uuid); lp_connect_start_bob(ctx.clone(), order_match, my_order.clone(), sender_pubkey); let topic = my_order.orderbook_topic(); @@ -3870,7 +3924,7 @@ pub async fn sell(ctx: MmArc, req: Json) -> Result>, String> { /// Created when maker order is matched with taker request #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -struct MakerMatch { +pub struct MakerMatch { request: TakerRequest, reserved: MakerReserved, connect: Option, @@ -3880,7 +3934,7 @@ struct MakerMatch { /// Created upon taker request broadcast #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -struct TakerMatch { +pub struct TakerMatch { reserved: MakerReserved, connect: TakerConnect, connected: Option, @@ -3981,7 +4035,7 @@ pub async fn lp_auto_buy( /// Orderbook Item P2P message /// DO NOT CHANGE - it will break backwards compatibility #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -struct OrderbookP2PItem { +pub struct OrderbookP2PItem { pubkey: String, base: String, rel: String, diff --git a/mm2src/mm2_main/src/lp_ordermatch/order_events.rs b/mm2src/mm2_main/src/lp_ordermatch/order_events.rs new file mode 100644 index 0000000000..547ee7df4e --- /dev/null +++ b/mm2src/mm2_main/src/lp_ordermatch/order_events.rs @@ -0,0 +1,49 @@ +use super::{MakerMatch, TakerMatch}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; + +use async_trait::async_trait; +use futures::channel::oneshot; +use futures::StreamExt; + +pub struct OrderStatusStreamer; + +impl OrderStatusStreamer { + #[inline(always)] + pub fn new() -> Self { Self } + + #[inline(always)] + pub const fn derive_streamer_id() -> &'static str { "ORDER_STATUS" } +} + +#[derive(Serialize)] +#[serde(tag = "order_type", content = "order_data")] +pub enum OrderStatusEvent { + MakerMatch(MakerMatch), + TakerMatch(TakerMatch), + MakerConnected(MakerMatch), + TakerConnected(TakerMatch), +} + +#[async_trait] +impl EventStreamer for OrderStatusStreamer { + type DataInType = OrderStatusEvent; + + fn streamer_id(&self) -> String { Self::derive_streamer_id().to_string() } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + while let Some(order_data) = data_rx.next().await { + let event_data = serde_json::to_value(order_data).expect("Serialization shouldn't fail."); + let event = Event::new(self.streamer_id(), event_data); + broadcaster.broadcast(event); + } + } +} diff --git a/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs b/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs new file mode 100644 index 0000000000..b1954ac57f --- /dev/null +++ b/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs @@ -0,0 +1,93 @@ +use super::{orderbook_topic_from_base_rel, subscribe_to_orderbook_topic, OrderbookP2PItem}; +use coins::{is_wallet_only_ticker, lp_coinfind}; +use mm2_core::mm_ctx::MmArc; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; + +use async_trait::async_trait; +use futures::channel::oneshot; +use futures::StreamExt; +use uuid::Uuid; + +pub struct OrderbookStreamer { + ctx: MmArc, + base: String, + rel: String, +} + +impl OrderbookStreamer { + pub fn new(ctx: MmArc, base: String, rel: String) -> Self { Self { ctx, base, rel } } + + pub fn derive_streamer_id(base: &str, rel: &str) -> String { + format!("ORDERBOOK_UPDATE/{}", orderbook_topic_from_base_rel(base, rel)) + } +} + +#[derive(Serialize)] +#[serde(tag = "order_type", content = "order_data")] +pub enum OrderbookItemChangeEvent { + // NOTE(clippy): This is box-ed due to in-balance of the size of enum variants. + /// New or updated orderbook item. + NewOrUpdatedItem(Box), + /// Removed orderbook item (only UUID is relevant in this case). + RemovedItem(Uuid), +} + +#[async_trait] +impl EventStreamer for OrderbookStreamer { + type DataInType = OrderbookItemChangeEvent; + + fn streamer_id(&self) -> String { Self::derive_streamer_id(&self.base, &self.rel) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; + if let Err(err) = sanity_checks(&self.ctx, &self.base, &self.rel).await { + ready_tx.send(Err(err.clone())).expect(RECEIVER_DROPPED_MSG); + panic!("{}", err); + } + // We need to subscribe to the orderbook, otherwise we won't get any updates from the P2P network. + if let Err(err) = subscribe_to_orderbook_topic(&self.ctx, &self.base, &self.rel, false).await { + let err = format!("Subscribing to orderbook topic failed: {err:?}"); + ready_tx.send(Err(err.clone())).expect(RECEIVER_DROPPED_MSG); + panic!("{}", err); + } + ready_tx.send(Ok(())).expect(RECEIVER_DROPPED_MSG); + + while let Some(orderbook_update) = data_rx.next().await { + let event_data = serde_json::to_value(orderbook_update).expect("Serialization shouldn't fail."); + let event = Event::new(self.streamer_id(), event_data); + broadcaster.broadcast(event); + } + } +} + +async fn sanity_checks(ctx: &MmArc, base: &str, rel: &str) -> Result<(), String> { + // TODO: This won't work with no-login mode. + lp_coinfind(ctx, base) + .await + .map_err(|e| format!("Coin {base} not found: {e}"))?; + if is_wallet_only_ticker(ctx, base) { + return Err(format!("Coin {base} is wallet-only.")); + } + lp_coinfind(ctx, rel) + .await + .map_err(|e| format!("Coin {base} not found: {e}"))?; + if is_wallet_only_ticker(ctx, rel) { + return Err(format!("Coin {rel} is wallet-only.")); + } + Ok(()) +} + +impl Drop for OrderbookStreamer { + fn drop(&mut self) { + // FIXME(discuss): Do we want to unsubscribe from the orderbook topic when streaming is dropped? + + // Note that the client enables orderbook streaming for all enabled coins when they query for best + // orders. These could potentially be a lot of pairs! + // Also, in the dev branch, we seem to never unsubscribe from an orderbook topic after doing an orderbook RPC! + } +} diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 7692503c18..90d227d50e 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -106,6 +106,7 @@ mod swap_lock; #[path = "lp_swap/komodefi.swap_v2.pb.rs"] #[rustfmt::skip] mod swap_v2_pb; +pub(crate) mod swap_events; mod swap_v2_common; pub(crate) mod swap_v2_rpcs; pub(crate) mod swap_watcher; diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index f9a729e537..21010d110a 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -13,6 +13,7 @@ use super::{broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_msg_e use crate::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::lp_network::subscribe_to_topic; use crate::lp_ordermatch::MakerOrderBuilder; +use crate::lp_swap::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use crate::lp_swap::swap_v2_common::mark_swap_as_finished; use crate::lp_swap::{broadcast_swap_message, taker_payment_spend_duration, MAX_STARTED_AT_DIFF}; use coins::lp_price::fetch_swap_coins_price; @@ -2102,11 +2103,9 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { swap_ctx.running_swaps.lock().unwrap().push(weak_ref); let mut swap_fut = Box::pin( async move { - let mut events; loop { let res = running_swap.handle_command(command).await.expect("!handle_command"); - events = res.1; - for event in events { + for event in res.1 { let to_save = MakerSavedEvent { timestamp: now_ms(), event: event.clone(), @@ -2119,6 +2118,13 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { .dispatch_async(ctx.clone(), LpEvents::MakerSwapStatusChanged(event_to_send)) .await; drop(dispatcher); + // Send a notification to the swap status streamer about a new event. + ctx.event_stream_manager + .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::MakerV1 { + uuid: running_swap.uuid, + event: to_save.clone(), + }) + .ok(); save_my_maker_swap_event(&ctx, &running_swap, to_save) .await .expect("!save_my_maker_swap_event"); diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs index 2a5fd662ac..ba2137dd53 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -1,3 +1,4 @@ +use super::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use super::swap_v2_common::*; use super::{swap_v2_topic, LockedAmount, LockedAmountInfo, SavedTradeFee, SwapsContext, NEGOTIATE_SEND_INTERVAL, NEGOTIATION_TIMEOUT_SEC}; @@ -46,7 +47,7 @@ cfg_wasm32!( #[allow(unused_imports)] use prost::Message; /// Negotiation data representation to be stored in DB. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct StoredNegotiationData { taker_payment_locktime: u64, taker_funding_locktime: u64, @@ -58,7 +59,7 @@ pub struct StoredNegotiationData { } /// Represents events produced by maker swap states. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "event_type", content = "event_data")] pub enum MakerSwapEvent { /// Swap has been successfully initialized. @@ -718,12 +719,17 @@ impl (), } + // Send a notification to the swap status streamer about a new event. + self.ctx + .event_stream_manager + .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::MakerV2 { + uuid: self.uuid, + event: event.clone(), + }) + .ok(); } - fn on_kickstart_event( - &mut self, - event: <::DbRepr as StateMachineDbRepr>::Event, - ) { + fn on_kickstart_event(&mut self, event: MakerSwapEvent) { match event { MakerSwapEvent::Initialized { maker_payment_trade_fee, diff --git a/mm2src/mm2_main/src/lp_swap/swap_events.rs b/mm2src/mm2_main/src/lp_swap/swap_events.rs new file mode 100644 index 0000000000..7f4aaa90eb --- /dev/null +++ b/mm2src/mm2_main/src/lp_swap/swap_events.rs @@ -0,0 +1,53 @@ +use super::maker_swap::MakerSavedEvent; +use super::maker_swap_v2::MakerSwapEvent; +use super::taker_swap::TakerSavedEvent; +use super::taker_swap_v2::TakerSwapEvent; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; + +use async_trait::async_trait; +use futures::channel::oneshot; +use futures::StreamExt; +use uuid::Uuid; + +pub struct SwapStatusStreamer; + +impl SwapStatusStreamer { + #[inline(always)] + pub fn new() -> Self { Self } + + #[inline(always)] + pub const fn derive_streamer_id() -> &'static str { "SWAP_STATUS" } +} + +#[derive(Serialize)] +#[serde(tag = "swap_type", content = "swap_data")] +pub enum SwapStatusEvent { + MakerV1 { uuid: Uuid, event: MakerSavedEvent }, + TakerV1 { uuid: Uuid, event: TakerSavedEvent }, + MakerV2 { uuid: Uuid, event: MakerSwapEvent }, + TakerV2 { uuid: Uuid, event: TakerSwapEvent }, +} + +#[async_trait] +impl EventStreamer for SwapStatusStreamer { + type DataInType = SwapStatusEvent; + + fn streamer_id(&self) -> String { Self::derive_streamer_id().to_string() } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + while let Some(swap_data) = data_rx.next().await { + let event_data = serde_json::to_value(swap_data).expect("Serialization shouldn't fail."); + let event = Event::new(self.streamer_id(), event_data); + broadcaster.broadcast(event); + } + } +} diff --git a/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs b/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs index 686d263d5a..ea459082a0 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs @@ -37,7 +37,7 @@ pub struct ActiveSwapV2Info { } /// DB representation of tx preimage with signature -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct StoredTxPreimage { pub preimage: BytesJson, pub signature: BytesJson, diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 4fe453c457..e38d2e02e5 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -12,6 +12,7 @@ use super::{broadcast_my_swap_status, broadcast_swap_message, broadcast_swap_msg SwapsContext, TransactionIdentifier, INCLUDE_REFUND_FEE, NO_REFUND_FEE, WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_network::subscribe_to_topic; use crate::lp_ordermatch::TakerOrderBuilder; +use crate::lp_swap::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use crate::lp_swap::swap_v2_common::mark_swap_as_finished; use crate::lp_swap::taker_restart::get_command_based_on_maker_or_watcher_activity; use crate::lp_swap::{broadcast_p2p_tx_msg, broadcast_swap_msg_every_delayed, tx_helper_topic, @@ -152,7 +153,7 @@ async fn save_my_taker_swap_event(ctx: &MmArc, swap: &TakerSwap, event: TakerSav } } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct TakerSavedEvent { pub timestamp: u64, pub event: TakerSwapEvent, @@ -478,6 +479,13 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { event: event.clone(), }; + // Send a notification to the swap status streamer about a new event. + ctx.event_stream_manager + .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::TakerV1 { + uuid: running_swap.uuid, + event: to_save.clone(), + }) + .ok(); save_my_taker_swap_event(&ctx, &running_swap, to_save) .await .expect("!save_my_taker_swap_event"); diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs index 224ce38090..0fd8ac5218 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs @@ -1,3 +1,4 @@ +use super::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use super::swap_v2_common::*; use super::{LockedAmount, LockedAmountInfo, SavedTradeFee, SwapsContext, TakerSwapPreparedParams, NEGOTIATE_SEND_INTERVAL, NEGOTIATION_TIMEOUT_SEC}; @@ -47,7 +48,7 @@ cfg_wasm32!( #[allow(unused_imports)] use prost::Message; /// Negotiation data representation to be stored in DB. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct StoredNegotiationData { maker_payment_locktime: u64, maker_secret_hash: BytesJson, @@ -59,7 +60,7 @@ pub struct StoredNegotiationData { } /// Represents events produced by taker swap states. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "event_type", content = "event_data")] pub enum TakerSwapEvent { /// Swap has been successfully initialized. @@ -838,12 +839,17 @@ impl (), } + // Send a notification to the swap status streamer about a new event. + self.ctx + .event_stream_manager + .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::TakerV2 { + uuid: self.uuid, + event: event.clone(), + }) + .ok(); } - fn on_kickstart_event( - &mut self, - event: <::DbRepr as StateMachineDbRepr>::Event, - ) { + fn on_kickstart_event(&mut self, event: TakerSwapEvent) { match event { TakerSwapEvent::Initialized { taker_payment_fee, .. } | TakerSwapEvent::Negotiated { taker_payment_fee, .. } => { diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index 85b61db612..76adc524a3 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -38,7 +38,7 @@ use std::net::SocketAddr; cfg_native! { use hyper::{self, Body, Server}; use futures::channel::oneshot; - use mm2_net::sse_handler::{handle_sse, SSE_ENDPOINT}; + use mm2_net::event_streaming::sse_handler::{handle_sse, SSE_ENDPOINT}; } #[path = "rpc/dispatcher/dispatcher.rs"] mod dispatcher; @@ -48,8 +48,9 @@ mod dispatcher_legacy; #[path = "rpc/lp_commands/lp_commands_legacy.rs"] pub mod lp_commands_legacy; mod rate_limiter; +mod streaming_activations; -/// Lists the RPC method not requiring the "userpass" authentication. +/// Lists the RPC method not requiring the "userpass" authentication. /// None is also public to skip auth and display proper error in case of method is missing const PUBLIC_METHODS: &[Option<&str>] = &[ // Sorted alphanumerically (on the first letter) for readability. @@ -339,13 +340,13 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { req: Request, remote_addr: SocketAddr, ctx_h: u32, - is_event_stream_enabled: bool, ) -> Result, Infallible> { let (tx, rx) = oneshot::channel(); // We execute the request in a separate task to avoid it being left uncompleted if the client disconnects. - // So what's inside the spawn here will complete till completion (or panic). + // So what's inside the spawn here will run till completion (or panic). common::executor::spawn(async move { - if is_event_stream_enabled && req.uri().path() == SSE_ENDPOINT { + if req.uri().path() == SSE_ENDPOINT { + // FIXME: THIS SHOULD BE AUTHENTICATED!!! tx.send(handle_sse(req, ctx_h).await).ok(); } else { tx.send(rpc_service(req, ctx_h, remote_addr).await).ok(); @@ -369,7 +370,6 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { // cf. https://github.com/hyperium/hyper/pull/1640. let ctx = MmArc::from_ffi_handle(ctx_h).expect("No context"); - let is_event_stream_enabled = ctx.event_stream_configuration.is_some(); //The `make_svc` macro creates a `make_service_fn` for a specified socket type. // `$socket_type`: The socket type with a `remote_addr` method that returns a `SocketAddr`. @@ -379,7 +379,7 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { let remote_addr = socket.remote_addr(); async move { Ok::<_, Infallible>(service_fn(move |req: Request| { - handle_request(req, remote_addr, ctx_h, is_event_stream_enabled) + handle_request(req, remote_addr, ctx_h) })) } }) diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index ba99379892..3fbb0a5215 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -1,3 +1,4 @@ +use super::streaming_activations; use super::{DispatcherError, DispatcherResult, PUBLIC_METHODS}; use crate::lp_healthcheck::peer_connection_healthcheck_rpc; use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor_status, init_trezor_user_action}; @@ -12,14 +13,13 @@ use crate::{lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, stop_version_stat_collection, update_version_stat_collection}, lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}, rpc::lp_commands::{get_public_key, get_public_key_hash, get_shared_db_id, trezor_connection_status}}; +use coins::eth::fee_estimation::rpc::get_eth_estimated_fee_per_gas; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, get_enabled_coins::get_enabled_coins, - get_estimated_fees::{get_eth_estimated_fee_per_gas, start_eth_fee_estimator, - stop_eth_fee_estimator}, get_new_address::{cancel_get_new_address, get_new_address, init_get_new_address, init_get_new_address_status, init_get_new_address_user_action}, init_account_balance::{cancel_account_balance, init_account_balance, @@ -139,6 +139,10 @@ async fn auth(request: &MmRpcRequest, ctx: &MmArc, client: &SocketAddr) -> Dispa } async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult>> { + if let Some(streaming_request) = request.method.strip_prefix("stream::") { + let streaming_request = streaming_request.to_string(); + return rpc_streaming_dispatcher(request, ctx, streaming_request).await; + } if let Some(task_method) = request.method.strip_prefix("task::") { let task_method = task_method.to_string(); return rpc_task_dispatcher(request, ctx, task_method).await; @@ -189,6 +193,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, my_recent_swaps_rpc).await, "my_swap_status" => handle_mmrpc(ctx, request, my_swap_status_rpc).await, "my_tx_history" => handle_mmrpc(ctx, request, my_tx_history_v2_rpc).await, + "z_coin_tx_history" => handle_mmrpc(ctx, request, coins::my_tx_history_v2::z_coin_tx_history_rpc).await, "orderbook" => handle_mmrpc(ctx, request, orderbook_rpc_v2).await, "recreate_swap_data" => handle_mmrpc(ctx, request, recreate_swap_data).await, "refresh_nft_metadata" => handle_mmrpc(ctx, request, refresh_nft_metadata).await, @@ -210,13 +215,10 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, ibc_transfer_channels).await, "peer_connection_healthcheck" => handle_mmrpc(ctx, request, peer_connection_healthcheck_rpc).await, "withdraw_nft" => handle_mmrpc(ctx, request, withdraw_nft).await, - "start_eth_fee_estimator" => handle_mmrpc(ctx, request, start_eth_fee_estimator).await, - "stop_eth_fee_estimator" => handle_mmrpc(ctx, request, stop_eth_fee_estimator).await, "get_eth_estimated_fee_per_gas" => handle_mmrpc(ctx, request, get_eth_estimated_fee_per_gas).await, "get_swap_transaction_fee_policy" => handle_mmrpc(ctx, request, get_swap_transaction_fee_policy).await, "set_swap_transaction_fee_policy" => 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, _ => MmError::err(DispatcherError::NoSuchMethod), } } @@ -241,6 +243,10 @@ async fn rpc_task_dispatcher( "create_new_account::init" => handle_mmrpc(ctx, request, init_create_new_account).await, "create_new_account::status" => handle_mmrpc(ctx, request, init_create_new_account_status).await, "create_new_account::user_action" => handle_mmrpc(ctx, request, init_create_new_account_user_action).await, + "enable_bch::cancel" => handle_mmrpc(ctx, request, cancel_init_standalone_coin::).await, + "enable_bch::init" => handle_mmrpc(ctx, request, init_standalone_coin::).await, + "enable_bch::status" => handle_mmrpc(ctx, request, init_standalone_coin_status::).await, + "enable_bch::user_action" => handle_mmrpc(ctx, request, init_standalone_coin_user_action::).await, "enable_qtum::cancel" => handle_mmrpc(ctx, request, cancel_init_standalone_coin::).await, "enable_qtum::init" => handle_mmrpc(ctx, request, init_standalone_coin::).await, "enable_qtum::status" => handle_mmrpc(ctx, request, init_standalone_coin_status::).await, @@ -261,6 +267,28 @@ async fn rpc_task_dispatcher( "enable_erc20::init" => handle_mmrpc(ctx, request, init_token::).await, "enable_erc20::status" => handle_mmrpc(ctx, request, init_token_status::).await, "enable_erc20::user_action" => handle_mmrpc(ctx, request, init_token_user_action::).await, + "enable_tendermint::cancel" => { + handle_mmrpc(ctx, request, cancel_init_platform_coin_with_tokens::).await + }, + "enable_tendermint::init" => handle_mmrpc(ctx, request, init_platform_coin_with_tokens::).await, + "enable_tendermint::status" => { + handle_mmrpc(ctx, request, init_platform_coin_with_tokens_status::).await + }, + "enable_tendermint::user_action" => { + handle_mmrpc( + ctx, + request, + init_platform_coin_with_tokens_user_action::, + ) + .await + }, + // // TODO: tendermint tokens + // "enable_tendermint_token::cancel" => handle_mmrpc(ctx, request, cancel_init_token::).await, + // "enable_tendermint_token::init" => handle_mmrpc(ctx, request, init_token::).await, + // "enable_tendermint_token::status" => handle_mmrpc(ctx, request, init_token_status::).await, + // "enable_tendermint_token::user_action" => { + // handle_mmrpc(ctx, request, init_token_user_action::).await + // }, "get_new_address::cancel" => handle_mmrpc(ctx, request, cancel_get_new_address).await, "get_new_address::init" => handle_mmrpc(ctx, request, init_get_new_address).await, "get_new_address::status" => handle_mmrpc(ctx, request, init_get_new_address_status).await, @@ -304,6 +332,25 @@ async fn rpc_task_dispatcher( } } +async fn rpc_streaming_dispatcher( + request: MmRpcRequest, + ctx: MmArc, + streaming_request: String, +) -> DispatcherResult>> { + match streaming_request.as_str() { + "balance::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_balance).await, + "network::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_network).await, + "heartbeat::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_heartbeat).await, + "fee_estimator::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_fee_estimation).await, + "swap_status::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_swap_status).await, + "order_status::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_order_status).await, + "tx_history::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_tx_history).await, + "orderbook::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_orderbook).await, + "disable" => handle_mmrpc(ctx, request, streaming_activations::disable_streamer).await, + _ => MmError::err(DispatcherError::NoSuchMethod), + } +} + /// `gui_storage` dispatcher. /// /// # Note diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/balance.rs b/mm2src/mm2_main/src/rpc/streaming_activations/balance.rs new file mode 100644 index 0000000000..76f9d594e1 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/balance.rs @@ -0,0 +1,100 @@ +//! RPC activation and deactivation for different balance event streamers. +use super::{EnableStreamingRequest, EnableStreamingResponse}; + +use coins::eth::eth_balance_events::EthBalanceEventStreamer; +use coins::tendermint::tendermint_balance_events::TendermintBalanceEventStreamer; +use coins::utxo::utxo_balance_events::UtxoBalanceEventStreamer; +use coins::z_coin::z_balance_streaming::ZCoinBalanceEventStreamer; +use coins::{lp_coinfind, MmCoin, MmCoinEnum}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +use serde_json::Value as Json; + +#[derive(Deserialize)] +pub struct EnableBalanceStreamingRequest { + pub coin: String, + pub config: Option, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum BalanceStreamingRequestError { + EnableError(String), + CoinNotFound, + CoinNotSupported, + Internal(String), +} + +impl HttpStatusCode for BalanceStreamingRequestError { + fn status_code(&self) -> StatusCode { + match self { + BalanceStreamingRequestError::EnableError(_) => StatusCode::BAD_REQUEST, + BalanceStreamingRequestError::CoinNotFound => StatusCode::NOT_FOUND, + BalanceStreamingRequestError::CoinNotSupported => StatusCode::NOT_IMPLEMENTED, + BalanceStreamingRequestError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn enable_balance( + ctx: MmArc, + req: EnableStreamingRequest, +) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); + let coin = lp_coinfind(&ctx, &req.coin) + .await + .map_err(BalanceStreamingRequestError::Internal)? + .ok_or(BalanceStreamingRequestError::CoinNotFound)?; + + match coin { + MmCoinEnum::EthCoin(_) => (), + MmCoinEnum::ZCoin(_) + | MmCoinEnum::UtxoCoin(_) + | MmCoinEnum::Bch(_) + | MmCoinEnum::QtumCoin(_) + | MmCoinEnum::Tendermint(_) => { + if req.config.is_some() { + Err(BalanceStreamingRequestError::EnableError( + "Invalid config provided. No config needed".to_string(), + ))? + } + }, + _ => Err(BalanceStreamingRequestError::CoinNotSupported)?, + } + + let enable_result = match coin { + MmCoinEnum::UtxoCoin(coin) => { + let streamer = UtxoBalanceEventStreamer::new(coin.clone().into()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::Bch(coin) => { + let streamer = UtxoBalanceEventStreamer::new(coin.clone().into()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::QtumCoin(coin) => { + let streamer = UtxoBalanceEventStreamer::new(coin.clone().into()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::EthCoin(coin) => { + let streamer = EthBalanceEventStreamer::try_new(req.config, coin.clone()) + .map_to_mm(|e| BalanceStreamingRequestError::EnableError(format!("{e:?}")))?; + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::ZCoin(coin) => { + let streamer = ZCoinBalanceEventStreamer::new(coin.clone()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::Tendermint(coin) => { + let streamer = TendermintBalanceEventStreamer::new(coin.clone()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + _ => Err(BalanceStreamingRequestError::CoinNotSupported)?, + }; + + enable_result + .map(EnableStreamingResponse::new) + .map_to_mm(|e| BalanceStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/disable.rs b/mm2src/mm2_main/src/rpc/streaming_activations/disable.rs new file mode 100644 index 0000000000..9643e9e652 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/disable.rs @@ -0,0 +1,50 @@ +//! The module for handling any event streaming deactivation requests. +//! +//! All event streamers are deactivated using the streamer ID only. + +use common::HttpStatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +use http::StatusCode; + +/// The request used for any event streaming deactivation. +#[derive(Deserialize)] +pub struct DisableStreamingRequest { + pub client_id: u64, + pub streamer_id: String, +} + +/// The success/ok response for any event streaming deactivation request. +#[derive(Serialize)] +pub struct DisableStreamingResponse { + result: &'static str, +} + +impl DisableStreamingResponse { + fn new() -> Self { Self { result: "Success" } } +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +/// The error response for any event streaming deactivation request. +pub enum DisableStreamingRequestError { + DisableError(String), +} + +impl HttpStatusCode for DisableStreamingRequestError { + fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } +} + +/// Disables a streamer. +/// +/// This works for any streamer regarding of their type/usage. +pub async fn disable_streamer( + ctx: MmArc, + req: DisableStreamingRequest, +) -> MmResult { + ctx.event_stream_manager + .stop(req.client_id, &req.streamer_id) + .map_to_mm(|e| DisableStreamingRequestError::DisableError(format!("{e:?}")))?; + Ok(DisableStreamingResponse::new()) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/fee_estimation.rs b/mm2src/mm2_main/src/rpc/streaming_activations/fee_estimation.rs new file mode 100644 index 0000000000..ef36542716 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/fee_estimation.rs @@ -0,0 +1,58 @@ +//! RPC activation and deactivation for different fee estimation streamers. +use super::{EnableStreamingRequest, EnableStreamingResponse}; + +use coins::eth::fee_estimation::eth_fee_events::{EthFeeEventStreamer, EthFeeStreamingConfig}; +use coins::{lp_coinfind, MmCoin, MmCoinEnum}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +#[derive(Deserialize)] +pub struct EnableFeeStreamingRequest { + pub coin: String, + pub config: EthFeeStreamingConfig, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum FeeStreamingRequestError { + EnableError(String), + CoinNotFound, + CoinNotSupported, + Internal(String), +} + +impl HttpStatusCode for FeeStreamingRequestError { + fn status_code(&self) -> StatusCode { + match self { + FeeStreamingRequestError::EnableError(_) => StatusCode::BAD_REQUEST, + FeeStreamingRequestError::CoinNotFound => StatusCode::NOT_FOUND, + FeeStreamingRequestError::CoinNotSupported => StatusCode::NOT_IMPLEMENTED, + FeeStreamingRequestError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn enable_fee_estimation( + ctx: MmArc, + req: EnableStreamingRequest, +) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); + let coin = lp_coinfind(&ctx, &req.coin) + .await + .map_err(FeeStreamingRequestError::Internal)? + .ok_or(FeeStreamingRequestError::CoinNotFound)?; + + match coin { + MmCoinEnum::EthCoin(coin) => { + let eth_fee_estimator_streamer = EthFeeEventStreamer::new(req.config, coin.clone()); + ctx.event_stream_manager + .add(client_id, eth_fee_estimator_streamer, coin.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| FeeStreamingRequestError::EnableError(format!("{e:?}"))) + }, + _ => Err(FeeStreamingRequestError::CoinNotSupported)?, + } +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/heartbeat.rs b/mm2src/mm2_main/src/rpc/streaming_activations/heartbeat.rs new file mode 100644 index 0000000000..e3f4d06c5e --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/heartbeat.rs @@ -0,0 +1,36 @@ +//! RPC activation and deactivation for the heartbeats. +use super::{EnableStreamingRequest, EnableStreamingResponse}; + +use crate::heartbeat_event::{HeartbeatEvent, HeartbeatEventConfig}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +#[derive(Deserialize)] +pub struct EnableHeartbeatRequest { + pub config: HeartbeatEventConfig, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum HeartbeatRequestError { + EnableError(String), +} + +impl HttpStatusCode for HeartbeatRequestError { + fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } +} + +pub async fn enable_heartbeat( + ctx: MmArc, + req: EnableStreamingRequest, +) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); + let heartbeat_streamer = HeartbeatEvent::new(req.config); + ctx.event_stream_manager + .add(client_id, heartbeat_streamer, ctx.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| HeartbeatRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/mod.rs b/mm2src/mm2_main/src/rpc/streaming_activations/mod.rs new file mode 100644 index 0000000000..05d2848f97 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/mod.rs @@ -0,0 +1,45 @@ +mod balance; +mod disable; +mod fee_estimation; +mod heartbeat; +mod network; +mod orderbook; +mod orders; +mod swaps; +mod tx_history; + +// Re-exports +pub use balance::*; +pub use disable::*; +pub use fee_estimation::*; +pub use heartbeat::*; +pub use network::*; +pub use orderbook::*; +pub use orders::*; +pub use swaps::*; +pub use tx_history::*; + +/// The general request for enabling any streamer. +/// `client_id` is common in each request, other data is request-specific. +#[derive(Deserialize)] +pub struct EnableStreamingRequest { + // If the client ID isn't included, assume it's 0. + #[serde(default)] + pub client_id: u64, + #[serde(flatten)] + inner: T, +} + +/// The success/ok response for any event streaming activation request. +#[derive(Serialize)] +pub struct EnableStreamingResponse { + pub streamer_id: String, + // TODO: If the the streamer was already running, it is probably running with different configuration. + // We might want to inform the client that the configuration they asked for wasn't applied and return + // the active configuration instead? + // pub config: Json, +} + +impl EnableStreamingResponse { + fn new(streamer_id: String) -> Self { Self { streamer_id } } +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/network.rs b/mm2src/mm2_main/src/rpc/streaming_activations/network.rs new file mode 100644 index 0000000000..11f9d0ed3b --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/network.rs @@ -0,0 +1,36 @@ +//! RPC activation and deactivation for the network event streamer. +use super::{EnableStreamingRequest, EnableStreamingResponse}; + +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; +use mm2_libp2p::application::network_event::{NetworkEvent, NetworkEventConfig}; + +#[derive(Deserialize)] +pub struct EnableNetworkStreamingRequest { + pub config: NetworkEventConfig, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum NetworkStreamingRequestError { + EnableError(String), +} + +impl HttpStatusCode for NetworkStreamingRequestError { + fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } +} + +pub async fn enable_network( + ctx: MmArc, + req: EnableStreamingRequest, +) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); + let network_steamer = NetworkEvent::new(req.config, ctx.clone()); + ctx.event_stream_manager + .add(client_id, network_steamer, ctx.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| NetworkStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/orderbook.rs b/mm2src/mm2_main/src/rpc/streaming_activations/orderbook.rs new file mode 100644 index 0000000000..60805c4a54 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/orderbook.rs @@ -0,0 +1,37 @@ +//! RPC activation and deactivation of the orderbook streamer. +use super::EnableStreamingResponse; +use crate::lp_ordermatch::orderbook_events::OrderbookStreamer; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +use common::HttpStatusCode; +use http::StatusCode; + +#[derive(Deserialize)] +pub struct EnableOrderbookStreamingRequest { + pub client_id: u64, + pub base: String, + pub rel: String, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum OrderbookStreamingRequestError { + EnableError(String), +} + +impl HttpStatusCode for OrderbookStreamingRequestError { + fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } +} + +pub async fn enable_orderbook( + ctx: MmArc, + req: EnableOrderbookStreamingRequest, +) -> MmResult { + let order_status_streamer = OrderbookStreamer::new(ctx.clone(), req.base, req.rel); + ctx.event_stream_manager + .add(req.client_id, order_status_streamer, ctx.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| OrderbookStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/orders.rs b/mm2src/mm2_main/src/rpc/streaming_activations/orders.rs new file mode 100644 index 0000000000..08fd0a959a --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/orders.rs @@ -0,0 +1,30 @@ +//! RPC activation and deactivation of the order status streamer. +use super::{EnableStreamingRequest, EnableStreamingResponse}; +use crate::lp_ordermatch::order_events::OrderStatusStreamer; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +use common::HttpStatusCode; +use http::StatusCode; + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum OrderStatusStreamingRequestError { + EnableError(String), +} + +impl HttpStatusCode for OrderStatusStreamingRequestError { + fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } +} + +pub async fn enable_order_status( + ctx: MmArc, + req: EnableStreamingRequest<()>, +) -> MmResult { + let order_status_streamer = OrderStatusStreamer::new(); + ctx.event_stream_manager + .add(req.client_id, order_status_streamer, ctx.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| OrderStatusStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/swaps.rs b/mm2src/mm2_main/src/rpc/streaming_activations/swaps.rs new file mode 100644 index 0000000000..3d4aa2b93e --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/swaps.rs @@ -0,0 +1,33 @@ +//! RPC activation and deactivation of the swap status streamer. +use super::{EnableStreamingRequest, EnableStreamingResponse}; +use crate::lp_swap::swap_events::SwapStatusStreamer; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +use common::HttpStatusCode; +use http::StatusCode; + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum SwapStatusStreamingRequestError { + EnableError(String), +} + +impl HttpStatusCode for SwapStatusStreamingRequestError { + fn status_code(&self) -> StatusCode { + match self { + SwapStatusStreamingRequestError::EnableError(_) => StatusCode::BAD_REQUEST, + } + } +} + +pub async fn enable_swap_status( + ctx: MmArc, + req: EnableStreamingRequest<()>, +) -> MmResult { + ctx.event_stream_manager + .add(req.client_id, SwapStatusStreamer::new(), ctx.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| SwapStatusStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/tx_history.rs b/mm2src/mm2_main/src/rpc/streaming_activations/tx_history.rs new file mode 100644 index 0000000000..ac37ca21b5 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/tx_history.rs @@ -0,0 +1,76 @@ +//! RPC activation and deactivation for Tx history event streamers. +use super::{EnableStreamingRequest, EnableStreamingResponse}; + +use coins::utxo::tx_history_events::TxHistoryEventStreamer; +use coins::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; +use coins::{lp_coinfind, MmCoin, MmCoinEnum}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +#[derive(Deserialize)] +pub struct EnableTxHistoryStreamingRequest { + pub coin: String, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum TxHistoryStreamingRequestError { + EnableError(String), + CoinNotFound, + CoinNotSupported, + Internal(String), +} + +impl HttpStatusCode for TxHistoryStreamingRequestError { + fn status_code(&self) -> StatusCode { + match self { + TxHistoryStreamingRequestError::EnableError(_) => StatusCode::BAD_REQUEST, + TxHistoryStreamingRequestError::CoinNotFound => StatusCode::NOT_FOUND, + TxHistoryStreamingRequestError::CoinNotSupported => StatusCode::NOT_IMPLEMENTED, + TxHistoryStreamingRequestError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn enable_tx_history( + ctx: MmArc, + req: EnableStreamingRequest, +) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); + let coin = lp_coinfind(&ctx, &req.coin) + .await + .map_err(TxHistoryStreamingRequestError::Internal)? + .ok_or(TxHistoryStreamingRequestError::CoinNotFound)?; + + let enable_result = match coin { + MmCoinEnum::UtxoCoin(coin) => { + let streamer = TxHistoryEventStreamer::new(req.coin); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::Bch(coin) => { + let streamer = TxHistoryEventStreamer::new(req.coin); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::QtumCoin(coin) => { + let streamer = TxHistoryEventStreamer::new(req.coin); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::Tendermint(coin) => { + // The tx history streamer is very primitive reactive streamer that only emits new txs. + // it's logic is exactly the same for utxo coins and tendermint coins as well. + let streamer = TxHistoryEventStreamer::new(req.coin); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::ZCoin(coin) => { + let streamer = ZCoinTxHistoryEventStreamer::new(coin.clone()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + _ => Err(TxHistoryStreamingRequestError::CoinNotSupported)?, + }; + + enable_result + .map(EnableStreamingResponse::new) + .map_to_mm(|e| TxHistoryStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 906af55887..2366cb9680 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -6031,7 +6031,7 @@ mod trezor_tests { withdraw_status, MarketMakerIt, Mm2TestConf, ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT}; use mm2_test_helpers::structs::{InitTaskResult, RpcV2Response, TransactionDetails, WithdrawStatus}; - use rpc_task::{rpc_common::RpcTaskStatusRequest, RpcTaskStatus}; + use rpc_task::{rpc_common::RpcTaskStatusRequest, RpcInitReq, RpcTaskStatus}; use serde_json::{self as json, json, Value as Json}; use std::io::{stdin, stdout, BufRead, Write}; @@ -6048,7 +6048,7 @@ mod trezor_tests { let ctx = mm_ctx_with_custom_db_with_conf(Some(conf)); CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "123456").unwrap(); // for now we need passphrase seed for init - let req: InitHwRequest = serde_json::from_value(json!({ "device_pubkey": null })).unwrap(); + let req: RpcInitReq = serde_json::from_value(json!({ "device_pubkey": null })).unwrap(); let res = match init_trezor(ctx.clone(), req).await { Ok(res) => res, _ => { diff --git a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs index 004ee27cac..d99290c3dc 100644 --- a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs @@ -17,8 +17,8 @@ use std::str::FromStr; use std::thread; use std::time::Duration; -const ZOMBIE_TEST_BIP39_ACTIVATION_SEED: &str = "course flock lucky cereal hamster novel team never metal bean behind cute cruel matrix symptom fault harsh fashion impact prison glove then tree chef"; -const ZOMBIE_TEST_BALANCE_SEED: &str = "zombie test seed"; +const ARRR_TEST_BIP39_ACTIVATION_SEED: &str = "course flock lucky cereal hamster novel team never metal bean behind cute cruel matrix symptom fault harsh fashion impact prison glove then tree chef"; +const ARRR_TEST_BALANCE_SEED: &str = "zombie test seed"; const ARRR_TEST_ACTIVATION_SEED: &str = "arrr test activation seed"; const ZOMBIE_TEST_HISTORY_SEED: &str = "zombie test history seed"; const ZOMBIE_TEST_WITHDRAW_SEED: &str = "zombie withdraw test seed"; @@ -48,16 +48,16 @@ async fn withdraw(mm: &MarketMakerIt, coin: &str, to: &str, amount: &str) -> Tra #[test] fn activate_z_coin_light() { - let coins = json!([zombie_conf()]); + let coins = json!([pirate_conf()]); - let conf = Mm2TestConf::seednode(ZOMBIE_TEST_BALANCE_SEED, &coins); + let conf = Mm2TestConf::seednode(ARRR_TEST_BALANCE_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let activation_result = block_on(enable_z_coin_light( &mm, - ZOMBIE_TICKER, - ZOMBIE_ELECTRUMS, - ZOMBIE_LIGHTWALLETD_URLS, + ARRR, + PIRATE_ELECTRUMS, + PIRATE_LIGHTWALLETD_URLS, None, None, )); @@ -71,16 +71,16 @@ fn activate_z_coin_light() { #[test] fn activate_z_coin_light_with_changing_height() { - let coins = json!([zombie_conf()]); + let coins = json!([pirate_conf()]); - let conf = Mm2TestConf::seednode_with_hd_account(ZOMBIE_TEST_BIP39_ACTIVATION_SEED, &coins); + let conf = Mm2TestConf::seednode_with_hd_account(ARRR_TEST_BIP39_ACTIVATION_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let activation_result = block_on(enable_z_coin_light( &mm, - ZOMBIE_TICKER, - ZOMBIE_ELECTRUMS, - ZOMBIE_LIGHTWALLETD_URLS, + ARRR, + PIRATE_ELECTRUMS, + PIRATE_LIGHTWALLETD_URLS, None, None, )); @@ -93,7 +93,7 @@ fn activate_z_coin_light_with_changing_height() { assert_eq!(balance.balance.spendable, BigDecimal::default()); // disable coin - block_on(disable_coin(&mm, ZOMBIE_TICKER, true)); + block_on(disable_coin(&mm, ARRR, true)); // Perform activation with changed height // Calculate timestamp for 2 days ago @@ -106,9 +106,9 @@ fn activate_z_coin_light_with_changing_height() { let activation_result = block_on(enable_z_coin_light( &mm, - ZOMBIE_TICKER, - ZOMBIE_ELECTRUMS, - ZOMBIE_LIGHTWALLETD_URLS, + ARRR, + PIRATE_ELECTRUMS, + PIRATE_LIGHTWALLETD_URLS, None, Some(two_days_ago), )); @@ -132,17 +132,17 @@ fn activate_z_coin_light_with_changing_height() { #[test] fn activate_z_coin_with_hd_account() { - let coins = json!([zombie_conf()]); + let coins = json!([pirate_conf()]); let hd_account_id = 0; - let conf = Mm2TestConf::seednode_with_hd_account(ZOMBIE_TEST_BIP39_ACTIVATION_SEED, &coins); + let conf = Mm2TestConf::seednode_with_hd_account(ARRR_TEST_BIP39_ACTIVATION_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let activation_result = block_on(enable_z_coin_light( &mm, - ZOMBIE_TICKER, - ZOMBIE_ELECTRUMS, - ZOMBIE_LIGHTWALLETD_URLS, + ARRR, + PIRATE_ELECTRUMS, + PIRATE_LIGHTWALLETD_URLS, Some(hd_account_id), None, )); diff --git a/mm2src/mm2_net/src/event_streaming/mod.rs b/mm2src/mm2_net/src/event_streaming/mod.rs new file mode 100644 index 0000000000..001424f5f4 --- /dev/null +++ b/mm2src/mm2_net/src/event_streaming/mod.rs @@ -0,0 +1,2 @@ +#[cfg(not(target_arch = "wasm32"))] pub mod sse_handler; +#[cfg(target_arch = "wasm32")] pub mod wasm_event_stream; diff --git a/mm2src/mm2_net/src/event_streaming/sse_handler.rs b/mm2src/mm2_net/src/event_streaming/sse_handler.rs new file mode 100644 index 0000000000..cd5ba585b3 --- /dev/null +++ b/mm2src/mm2_net/src/event_streaming/sse_handler.rs @@ -0,0 +1,70 @@ +use http::header::{ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_TYPE}; +use hyper::{body::Bytes, Body, Request, Response}; +use mm2_core::mm_ctx::MmArc; +use serde_json::json; + +pub const SSE_ENDPOINT: &str = "/event-stream"; + +/// Handles broadcasted messages from `mm2_event_stream` continuously. +pub async fn handle_sse(request: Request, ctx_h: u32) -> Response { + let ctx = match MmArc::from_ffi_handle(ctx_h) { + Ok(ctx) => ctx, + Err(err) => return handle_internal_error(err).await, + }; + + let Some(event_streaming_config) = ctx.event_streaming_configuration() else { + return handle_internal_error("Event streaming is disabled".to_string()).await; + }; + + let client_id = match request.uri().query().and_then(|query| { + query + .split('&') + .find(|param| param.starts_with("id=")) + .map(|id_param| id_param.trim_start_matches("id=").parse::()) + }) { + Some(Ok(id)) => id, + // Default to zero when client ID isn't passed, most of the cases we will have a single user/client. + _ => 0, + }; + + let event_stream_manager = ctx.event_stream_manager.clone(); + let Ok(mut rx) = event_stream_manager.new_client(client_id) else { + return handle_internal_error("ID already in use".to_string()).await + }; + let body = Body::wrap_stream(async_stream::stream! { + while let Some(event) = rx.recv().await { + // The event's filter will decide whether to expose the event data to this client or not. + // This happens based on the events that this client has subscribed to. + let (event_type, message) = event.get(); + let data = json!({ + "_type": event_type, + "message": message, + }); + + yield Ok::<_, hyper::Error>(Bytes::from(format!("data: {data} \n\n"))); + } + }); + + let response = Response::builder() + .status(200) + .header(CONTENT_TYPE, "text/event-stream") + .header(CACHE_CONTROL, "no-cache") + .header( + ACCESS_CONTROL_ALLOW_ORIGIN, + event_streaming_config.access_control_allow_origin, + ) + .body(body); + + match response { + Ok(res) => res, + Err(err) => handle_internal_error(err.to_string()).await, + } +} + +/// Fallback function for handling errors in SSE connections +async fn handle_internal_error(message: String) -> Response { + Response::builder() + .status(500) + .body(Body::from(message)) + .expect("Returning 500 should never fail.") +} diff --git a/mm2src/mm2_net/src/wasm_event_stream.rs b/mm2src/mm2_net/src/event_streaming/wasm_event_stream.rs similarity index 67% rename from mm2src/mm2_net/src/wasm_event_stream.rs rename to mm2src/mm2_net/src/event_streaming/wasm_event_stream.rs index dcd6da33e2..c10f838a70 100644 --- a/mm2src/mm2_net/src/wasm_event_stream.rs +++ b/mm2src/mm2_net/src/event_streaming/wasm_event_stream.rs @@ -11,21 +11,9 @@ struct SendableMessagePort(web_sys::MessagePort); unsafe impl Send for SendableMessagePort {} /// Handles broadcasted messages from `mm2_event_stream` continuously for WASM. -pub async fn handle_worker_stream(ctx: MmArc) { - let config = ctx - .event_stream_configuration - .as_ref() - .expect("Event stream configuration couldn't be found. This should never happen."); - - let mut channel_controller = ctx.stream_channel_controller.clone(); - let mut rx = channel_controller.create_channel(config.total_active_events()); - - let worker_path = config - .worker_path - .to_str() - .expect("worker_path contains invalid UTF-8 characters"); +pub async fn handle_worker_stream(ctx: MmArc, worker_path: String) { let worker = SendableSharedWorker( - SharedWorker::new(worker_path).unwrap_or_else(|_| { + SharedWorker::new(&worker_path).unwrap_or_else(|_| { panic!( "Failed to create a new SharedWorker with path '{}'.\n\ This could be due to the file missing or the browser being incompatible.\n\ @@ -38,13 +26,18 @@ pub async fn handle_worker_stream(ctx: MmArc) { let port = SendableMessagePort(worker.0.port()); port.0.start(); + let event_stream_manager = ctx.event_stream_manager.clone(); + let mut rx = event_stream_manager + .new_client(0) + .expect("A different wasm client is already listening. Only one client is allowed at a time."); + while let Some(event) = rx.recv().await { + let (event_type, message) = event.get(); let data = json!({ - "_type": event.event_type(), - "message": event.message(), + "_type": event_type, + "message": message, }); let message_js = wasm_bindgen::JsValue::from_str(&data.to_string()); - port.0.post_message(&message_js) .expect("Failed to post a message to the SharedWorker.\n\ This could be due to the browser being incompatible.\n\ diff --git a/mm2src/mm2_net/src/lib.rs b/mm2src/mm2_net/src/lib.rs index 4ae26ca182..28293f9864 100644 --- a/mm2src/mm2_net/src/lib.rs +++ b/mm2src/mm2_net/src/lib.rs @@ -1,9 +1,7 @@ +pub mod event_streaming; pub mod grpc_web; -pub mod transport; - #[cfg(not(target_arch = "wasm32"))] pub mod ip_addr; #[cfg(not(target_arch = "wasm32"))] pub mod native_http; #[cfg(not(target_arch = "wasm32"))] pub mod native_tls; -#[cfg(not(target_arch = "wasm32"))] pub mod sse_handler; +pub mod transport; #[cfg(target_arch = "wasm32")] pub mod wasm; -#[cfg(target_arch = "wasm32")] pub mod wasm_event_stream; diff --git a/mm2src/mm2_net/src/sse_handler.rs b/mm2src/mm2_net/src/sse_handler.rs deleted file mode 100644 index 568bfc98c0..0000000000 --- a/mm2src/mm2_net/src/sse_handler.rs +++ /dev/null @@ -1,75 +0,0 @@ -use hyper::{body::Bytes, Body, Request, Response}; -use mm2_core::mm_ctx::MmArc; -use serde_json::json; - -pub const SSE_ENDPOINT: &str = "/event-stream"; - -/// Handles broadcasted messages from `mm2_event_stream` continuously. -pub async fn handle_sse(request: Request, ctx_h: u32) -> Response { - // This is only called once for per client on the initialization, - // meaning this is not a resource intensive computation. - let ctx = match MmArc::from_ffi_handle(ctx_h) { - Ok(ctx) => ctx, - Err(err) => return handle_internal_error(err).await, - }; - - let config = match &ctx.event_stream_configuration { - Some(config) => config, - None => { - return handle_internal_error( - "Event stream configuration couldn't be found. This should never happen.".to_string(), - ) - .await - }, - }; - - let filtered_events = request - .uri() - .query() - .and_then(|query| { - query - .split('&') - .find(|param| param.starts_with("filter=")) - .map(|param| param.trim_start_matches("filter=")) - }) - .map_or(Vec::new(), |events_param| { - events_param.split(',').map(|event| event.to_string()).collect() - }); - - let mut channel_controller = ctx.stream_channel_controller.clone(); - let mut rx = channel_controller.create_channel(config.total_active_events()); - let body = Body::wrap_stream(async_stream::stream! { - while let Some(event) = rx.recv().await { - // If there are no filtered events, that means we want to - // stream out all the events. - if filtered_events.is_empty() || filtered_events.contains(&event.event_type().to_owned()) { - let data = json!({ - "_type": event.event_type(), - "message": event.message(), - }); - - yield Ok::<_, hyper::Error>(Bytes::from(format!("data: {data} \n\n"))); - } - } - }); - - let response = Response::builder() - .status(200) - .header("Content-Type", "text/event-stream") - .header("Cache-Control", "no-cache") - .header("Access-Control-Allow-Origin", &config.access_control_allow_origin) - .body(body); - - match response { - Ok(res) => res, - Err(err) => handle_internal_error(err.to_string()).await, - } -} - -/// Fallback function for handling errors in SSE connections -async fn handle_internal_error(message: String) -> Response { - Response::builder() - .status(500) - .body(Body::from(message)) - .expect("Returning 500 should never fail.") -} diff --git a/mm2src/mm2_p2p/src/application/network_event.rs b/mm2src/mm2_p2p/src/application/network_event.rs index c3c0a0eb5c..fa152469d1 100644 --- a/mm2src/mm2_p2p/src/application/network_event.rs +++ b/mm2src/mm2_p2p/src/application/network_event.rs @@ -1,30 +1,55 @@ -use async_trait::async_trait; -use common::{executor::{SpawnFuture, Timer}, - log::info}; -use futures::channel::oneshot::{self, Receiver, Sender}; - +use common::executor::Timer; use mm2_core::mm_ctx::MmArc; -pub use mm2_event_stream::behaviour::EventBehaviour; -use mm2_event_stream::{behaviour::EventInitStatus, Event, EventName, EventStreamConfiguration}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; + +use async_trait::async_trait; +use futures::channel::oneshot; +use serde::Deserialize; use serde_json::json; +#[derive(Deserialize)] +#[serde(deny_unknown_fields, default)] +pub struct NetworkEventConfig { + /// The time in seconds to wait after sending network info before sending another one. + pub stream_interval_seconds: f64, + /// Always (force) send network info data, even if it's the same as the previous one sent. + pub always_send: bool, +} + +impl Default for NetworkEventConfig { + fn default() -> Self { + Self { + stream_interval_seconds: 5.0, + always_send: false, + } + } +} + pub struct NetworkEvent { + config: NetworkEventConfig, ctx: MmArc, } impl NetworkEvent { - pub fn new(ctx: MmArc) -> Self { Self { ctx } } + pub fn new(config: NetworkEventConfig, ctx: MmArc) -> Self { Self { config, ctx } } } #[async_trait] -impl EventBehaviour for NetworkEvent { - fn event_name() -> EventName { EventName::NETWORK } +impl EventStreamer for NetworkEvent { + type DataInType = NoDataIn; - async fn handle(self, interval: f64, tx: oneshot::Sender) { + fn streamer_id(&self) -> String { "NETWORK".to_string() } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { let p2p_ctx = crate::p2p_ctx::P2PContext::fetch_from_mm_arc(&self.ctx); let mut previously_sent = json!({}); - tx.send(EventInitStatus::Success).unwrap(); + ready_tx.send(Ok(())).unwrap(); loop { let p2p_cmd_tx = p2p_ctx.cmd_tx.lock().clone(); @@ -43,34 +68,13 @@ impl EventBehaviour for NetworkEvent { "relay_mesh": relay_mesh, }); - if previously_sent != event_data { - self.ctx - .stream_channel_controller - .broadcast(Event::new(Self::event_name().to_string(), event_data.to_string())) - .await; + if previously_sent != event_data || self.config.always_send { + broadcaster.broadcast(Event::new(self.streamer_id(), event_data.clone())); previously_sent = event_data; } - Timer::sleep(interval).await; - } - } - - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - info!( - "NETWORK event is activated with {} seconds interval.", - event.stream_interval_seconds - ); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - self.ctx.spawner().spawn(self.handle(event.stream_interval_seconds, tx)); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive + Timer::sleep(self.config.stream_interval_seconds).await; } } } diff --git a/mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs b/mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs index d585424c57..a14f5edc13 100644 --- a/mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs +++ b/mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs @@ -11,23 +11,17 @@ pub struct UtxoMergeParams { } #[allow(clippy::upper_case_acronyms)] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] /// Deserializable Electrum protocol representation for RPC -#[derive(Default)] pub enum ElectrumProtocol { /// TCP - #[default] + #[cfg_attr(not(target_arch = "wasm32"), default)] TCP, /// SSL/TLS SSL, /// Insecure WebSocket. WS, /// Secure WebSocket. + #[cfg_attr(target_arch = "wasm32", default)] WSS, } - -#[cfg(not(target_arch = "wasm32"))] -#[cfg(target_arch = "wasm32")] -impl Default for ElectrumProtocol { - fn default() -> Self { ElectrumProtocol::WS } -} diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 25ebef2df5..de26d80b44 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -199,21 +199,21 @@ pub const MARTY_ELECTRUM_ADDRS: &[&str] = &[ "electrum3.cipig.net:10021", ]; pub const ZOMBIE_TICKER: &str = "ZOMBIE"; +#[cfg(not(target_arch = "wasm32"))] +pub const ZOMBIE_ELECTRUMS: &[&str] = &["zombie.dragonhound.info:10033", "zombie.dragonhound.info:10133"]; +#[cfg(target_arch = "wasm32")] +pub const ZOMBIE_ELECTRUMS: &[&str] = &["zombie.dragonhound.info:30058", "zombie.dragonhound.info:30059"]; +pub const ZOMBIE_LIGHTWALLETD_URLS: &[&str] = &[ + "https://zombie.dragonhound.info:443", + "https://zombie.dragonhound.info:1443", +]; pub const ARRR: &str = "ARRR"; -pub const ZOMBIE_ELECTRUMS: &[&str] = &[ +#[cfg(not(target_arch = "wasm32"))] +pub const PIRATE_ELECTRUMS: &[&str] = &[ "electrum1.cipig.net:10008", "electrum2.cipig.net:10008", "electrum3.cipig.net:10008", ]; -pub const ZOMBIE_LIGHTWALLETD_URLS: &[&str] = &[ - "https://lightd1.pirate.black:443", - "https://piratelightd1.cryptoforge.cc:443", - "https://piratelightd2.cryptoforge.cc:443", - "https://piratelightd3.cryptoforge.cc:443", - "https://piratelightd4.cryptoforge.cc:443", -]; -#[cfg(not(target_arch = "wasm32"))] -pub const PIRATE_ELECTRUMS: &[&str] = &["node1.chainkeeper.pro:10132"]; #[cfg(target_arch = "wasm32")] pub const PIRATE_ELECTRUMS: &[&str] = &[ "electrum3.cipig.net:30008", @@ -221,7 +221,13 @@ pub const PIRATE_ELECTRUMS: &[&str] = &[ "electrum2.cipig.net:30008", ]; #[cfg(not(target_arch = "wasm32"))] -pub const PIRATE_LIGHTWALLETD_URLS: &[&str] = &["http://node1.chainkeeper.pro:443"]; +pub const PIRATE_LIGHTWALLETD_URLS: &[&str] = &[ + "https://lightd1.pirate.black:443", + "https://piratelightd1.cryptoforge.cc:443", + "https://piratelightd2.cryptoforge.cc:443", + "https://piratelightd3.cryptoforge.cc:443", + "https://piratelightd4.cryptoforge.cc:443", +]; #[cfg(target_arch = "wasm32")] pub const PIRATE_LIGHTWALLETD_URLS: &[&str] = &["https://pirate.battlefield.earth:8581"]; pub const DEFAULT_RPC_PASSWORD: &str = "pass"; @@ -517,9 +523,11 @@ pub fn pirate_conf() -> Json { "b58_pubkey_address_prefix": [ 28, 184 ], "b58_script_address_prefix": [ 28, 189 ] }, + "z_derivation_path": "m/32'/133'", } }, - "required_confirmations":0 + "required_confirmations":0, + "derivation_path": "m/44'/133'", }) } diff --git a/mm2src/rpc_task/Cargo.toml b/mm2src/rpc_task/Cargo.toml index 4bf524f86a..c542159f25 100644 --- a/mm2src/rpc_task/Cargo.toml +++ b/mm2src/rpc_task/Cargo.toml @@ -10,9 +10,11 @@ doctest = false async-trait = "0.1" common = { path = "../common" } mm2_err_handle = { path = "../mm2_err_handle" } +mm2_event_stream = { path = "../mm2_event_stream" } derive_more = "0.99" futures = "0.3" ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } serde = "1" serde_derive = "1" +serde_json = "1" diff --git a/mm2src/rpc_task/src/lib.rs b/mm2src/rpc_task/src/lib.rs index f5861f37cc..2e8f703d87 100644 --- a/mm2src/rpc_task/src/lib.rs +++ b/mm2src/rpc_task/src/lib.rs @@ -16,7 +16,7 @@ mod task; pub use handle::{RpcTaskHandle, RpcTaskHandleShared}; pub use manager::{RpcTaskManager, RpcTaskManagerShared}; -pub use task::{RpcTask, RpcTaskTypes}; +pub use task::{RpcInitReq, RpcTask, RpcTaskTypes}; pub type RpcTaskResult = Result>; pub type TaskId = u64; diff --git a/mm2src/rpc_task/src/manager.rs b/mm2src/rpc_task/src/manager.rs index 950eac97f4..7e8e38fc24 100644 --- a/mm2src/rpc_task/src/manager.rs +++ b/mm2src/rpc_task/src/manager.rs @@ -2,10 +2,11 @@ use crate::task::RpcTaskTypes; use crate::{AtomicTaskId, RpcTask, RpcTaskError, RpcTaskHandle, RpcTaskResult, RpcTaskStatus, RpcTaskStatusAlias, TaskAbortHandle, TaskAbortHandler, TaskId, TaskStatus, TaskStatusError, UserActionSender}; use common::executor::SpawnFuture; -use common::log::{debug, info}; +use common::log::{debug, info, warn}; use futures::channel::oneshot; use futures::future::{select, Either}; use mm2_err_handle::prelude::*; +use mm2_event_stream::{Event, StreamingManager}; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::sync::atomic::Ordering; @@ -29,26 +30,29 @@ static NEXT_RPC_TASK_ID: AtomicTaskId = AtomicTaskId::new(0); fn next_rpc_task_id() -> TaskId { NEXT_RPC_TASK_ID.fetch_add(1, Ordering::Relaxed) } pub struct RpcTaskManager { + /// A map of task IDs to their statuses and abort handlers. tasks: HashMap>, -} - -impl Default for RpcTaskManager { - fn default() -> Self { RpcTaskManager { tasks: HashMap::new() } } + /// A copy of the MM2's streaming manager to broadcast task status updates to interested parties. + streaming_manager: StreamingManager, } impl RpcTaskManager { /// Create new instance of `RpcTaskHandle` attached to the only one `RpcTask`. /// This function registers corresponding RPC task in the `RpcTaskManager` and returns the task id. - pub fn spawn_rpc_task(this: &RpcTaskManagerShared, spawner: &F, mut task: Task) -> RpcTaskResult + pub fn spawn_rpc_task( + this: &RpcTaskManagerShared, + spawner: &F, + mut task: Task, + client_id: u64, + ) -> RpcTaskResult where F: SpawnFuture, { - let initial_task_status = task.initial_status(); let (task_id, task_abort_handler) = { let mut task_manager = this .lock() .map_to_mm(|e| RpcTaskError::Internal(format!("RpcTaskManager is not available: {}", e)))?; - task_manager.register_task(initial_task_status)? + task_manager.register_task(&task, client_id)? }; let task_handle = Arc::new(RpcTaskHandle { task_manager: RpcTaskManagerShared::downgrade(this), @@ -103,10 +107,26 @@ impl RpcTaskManager { Some(rpc_status) } - pub fn new_shared() -> RpcTaskManagerShared { Arc::new(Mutex::new(Self::default())) } + pub fn new(streaming_manager: StreamingManager) -> Self { + RpcTaskManager { + tasks: HashMap::new(), + streaming_manager, + } + } + + pub fn new_shared(streaming_manager: StreamingManager) -> RpcTaskManagerShared { + Arc::new(Mutex::new(Self::new(streaming_manager))) + } pub fn contains(&self, task_id: TaskId) -> bool { self.tasks.contains_key(&task_id) } + fn get_client_id(&self, task_id: TaskId) -> Option { + self.tasks.get(&task_id).and_then(|task| match task { + TaskStatusExt::InProgress { client_id, .. } | TaskStatusExt::Awaiting { client_id, .. } => Some(*client_id), + _ => None, + }) + } + /// Cancel task if it's in progress. pub fn cancel_task(&mut self, task_id: TaskId) -> RpcTaskResult<()> { let task = self.tasks.remove(&task_id); @@ -138,18 +158,16 @@ impl RpcTaskManager { } } - pub(crate) fn register_task( - &mut self, - task_initial_in_progress_status: Task::InProgressStatus, - ) -> RpcTaskResult<(TaskId, TaskAbortHandler)> { + pub(crate) fn register_task(&mut self, task: &Task, client_id: u64) -> RpcTaskResult<(TaskId, TaskAbortHandler)> { let task_id = next_rpc_task_id(); let (abort_handle, abort_handler) = oneshot::channel(); match self.tasks.entry(task_id) { Entry::Occupied(_entry) => unexpected_task_status!(task_id, actual = InProgress, expected = Idle), Entry::Vacant(entry) => { entry.insert(TaskStatusExt::InProgress { - status: task_initial_in_progress_status, + status: task.initial_status(), abort_handle, + client_id, }); Ok((task_id, abort_handler)) }, @@ -157,7 +175,9 @@ impl RpcTaskManager { } pub(crate) fn update_task_status(&mut self, task_id: TaskId, status: TaskStatus) -> RpcTaskResult<()> { - match status { + // Get the client ID before updating the task status because not all task status variants store the ID. + let client_id = self.get_client_id(task_id); + let update_result = match status { TaskStatus::Ok(result) => self.on_task_finished(task_id, Ok(result)), TaskStatus::Error(error) => self.on_task_finished(task_id, Err(error)), TaskStatus::InProgress(in_progress) => self.update_in_progress_status(task_id, in_progress), @@ -165,7 +185,23 @@ impl RpcTaskManager { awaiting_status, user_action_tx, } => self.set_task_is_waiting_for_user_action(task_id, awaiting_status, user_action_tx), - } + }; + // If the status was updated successfully, we need to inform the client about the new status. + if update_result.is_ok() { + if let Some(client_id) = client_id { + // Note that this should really always be `Some`, since we updated the status *successfully*. + if let Some(new_status) = self.task_status(task_id, false) { + let event = Event::new( + format!("TASK:{task_id}"), + serde_json::to_value(new_status).expect("Serialization shouldn't fail."), + ); + if let Err(e) = self.streaming_manager.broadcast_to(event, client_id) { + warn!("Failed to send task status update to the client (ID={client_id}): {e:?}"); + } + }; + } + }; + update_result } pub(crate) fn on_task_cancelling_finished(&mut self, task_id: TaskId) -> RpcTaskResult<()> { @@ -196,11 +232,22 @@ impl RpcTaskManager { fn update_in_progress_status(&mut self, task_id: TaskId, status: Task::InProgressStatus) -> RpcTaskResult<()> { match self.tasks.remove(&task_id) { - Some(TaskStatusExt::InProgress { abort_handle, .. }) - | Some(TaskStatusExt::Awaiting { abort_handle, .. }) => { + Some(TaskStatusExt::InProgress { + abort_handle, + client_id, + .. + }) + | Some(TaskStatusExt::Awaiting { + abort_handle, + client_id, + .. + }) => { // Insert new in-progress status to the tasks container. - self.tasks - .insert(task_id, TaskStatusExt::InProgress { status, abort_handle }); + self.tasks.insert(task_id, TaskStatusExt::InProgress { + status, + abort_handle, + client_id, + }); Ok(()) }, Some(cancelling @ TaskStatusExt::Cancelling { .. }) => { @@ -227,13 +274,15 @@ impl RpcTaskManager { Some(TaskStatusExt::InProgress { status: next_in_progress_status, abort_handle, + client_id, }) => { // Insert new awaiting status to the tasks container. self.tasks.insert(task_id, TaskStatusExt::Awaiting { status, - abort_handle, action_sender, next_in_progress_status, + abort_handle, + client_id, }); Ok(()) }, @@ -259,8 +308,9 @@ impl RpcTaskManager { match self.tasks.remove(&task_id) { Some(TaskStatusExt::Awaiting { action_sender, - abort_handle, next_in_progress_status: status, + abort_handle, + client_id, .. }) => { let result = action_sender @@ -268,8 +318,11 @@ impl RpcTaskManager { // The task seems to be canceled/aborted for some reason. .map_to_mm(|_user_action| RpcTaskError::Cancelled); // Insert new in-progress status to the tasks container. - self.tasks - .insert(task_id, TaskStatusExt::InProgress { status, abort_handle }); + self.tasks.insert(task_id, TaskStatusExt::InProgress { + status, + abort_handle, + client_id, + }); result }, Some(unexpected) => { @@ -298,12 +351,16 @@ enum TaskStatusExt { InProgress { status: Task::InProgressStatus, abort_handle: TaskAbortHandle, + /// The ID of the client requesting the task. To stream out the updates & results for them. + client_id: u64, }, Awaiting { status: Task::AwaitingStatus, action_sender: UserActionSender, - abort_handle: TaskAbortHandle, next_in_progress_status: Task::InProgressStatus, + abort_handle: TaskAbortHandle, + /// The ID of the client requesting the task. To stream out the updates & results for them. + client_id: u64, }, /// `Cancelling` status is set on [`RpcTaskManager::cancel_task`]. /// This status is used to save the task state before it's actually canceled on [`RpcTaskHandle::on_canceled`], diff --git a/mm2src/rpc_task/src/task.rs b/mm2src/rpc_task/src/task.rs index 6c38f75050..3f5ceca1bd 100644 --- a/mm2src/rpc_task/src/task.rs +++ b/mm2src/rpc_task/src/task.rs @@ -6,8 +6,8 @@ use serde::Serialize; pub trait RpcTaskTypes { type Item: Serialize + Clone + Send + Sync + 'static; type Error: SerMmErrorType + Clone + Send + Sync + 'static; - type InProgressStatus: Clone + Send + Sync + 'static; - type AwaitingStatus: Clone + Send + Sync + 'static; + type InProgressStatus: Serialize + Clone + Send + Sync + 'static; + type AwaitingStatus: Serialize + Clone + Send + Sync + 'static; type UserAction: NotMmError + Send + Sync + 'static; } @@ -20,3 +20,16 @@ pub trait RpcTask: RpcTaskTypes + Sized + Send + 'static { async fn run(&mut self, task_handle: RpcTaskHandleShared) -> Result>; } + +/// The general request for initializing an RPC Task. +/// +/// `client_id` is used to identify the client to which the task should stream out update events +/// to and is common in each request. Other data is request-specific. +#[derive(Deserialize)] +pub struct RpcInitReq { + // If the client ID isn't included, assume it's 0. + #[serde(default)] + pub client_id: u64, + #[serde(flatten)] + pub inner: T, +}