Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

active/passive modes for parent coins #1763

Merged
merged 15 commits into from
May 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Some enhancements were done for `enable_bch_with_tokens`,`enable_eth_with_tokens`,`enable_tendermint_with_assets` RPCs in [#1762](https://github.com/KomodoPlatform/atomicDEX-API/pull/1762)
- A new parameter `get_balances` was added to the above methods requests, when this parameter is set to `false`, balances will not be returned in the response. The default value for this parameter is `true` to ensure backward compatibility.
- Token balances requests are now performed concurrently for the above methods.
- Add passive parent coin state for keeping tokens active when platform is disabled [#1763](https://github.com/KomodoPlatform/atomicDEX-API/pull/1763)


## v1.0.3-beta - 2023-04-28
Expand Down
162 changes: 146 additions & 16 deletions mm2src/coins/lp_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ use base58::FromBase58Error;
use common::custom_futures::timeout::TimeoutError;
use common::executor::{abortable_queue::{AbortableQueue, WeakSpawner},
AbortSettings, AbortedError, SpawnAbortable, SpawnFuture};
use common::log::LogOnError;
use common::log::{warn, LogOnError};
use common::{calc_total_pages, now_ms, ten, HttpStatusCode};
use crypto::{Bip32Error, CryptoCtx, CryptoCtxError, DerivationPath, GlobalHDAccountArc, HwRpcError, KeyPairPolicy,
Secp256k1Secret, WithHwRpcError};
Expand Down Expand Up @@ -76,6 +76,8 @@ use std::future::Future as Future03;
use std::num::NonZeroUsize;
use std::ops::{Add, Deref};
use std::str::FromStr;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering as AtomicOrdering;
use std::sync::Arc;
use std::time::Duration;
use utxo_signer::with_key_pair::UtxoSignWithKeyPairError;
Expand Down Expand Up @@ -2237,7 +2239,7 @@ pub trait MmCoin:
fn on_disabled(&self) -> Result<(), AbortedError>;

/// For Handling the removal/deactivation of token on platform coin deactivation.
fn on_token_deactivated(&self, _ticker: &str);
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
Expand Down Expand Up @@ -2426,17 +2428,60 @@ impl MmCoinEnum {
}

pub fn is_eth(&self) -> bool { matches!(self, MmCoinEnum::EthCoin(_)) }

fn is_platform_coin(&self) -> bool { self.ticker() == self.platform_ticker() }
}

#[async_trait]
pub trait BalanceTradeFeeUpdatedHandler {
async fn balance_updated(&self, coin: &MmCoinEnum, new_balance: &BigDecimal);
}

#[derive(Clone)]
pub struct MmCoinStruct {
pub inner: MmCoinEnum,
is_available: Arc<AtomicBool>,
}

impl MmCoinStruct {
fn new(coin: MmCoinEnum) -> Self {
Self {
inner: coin,
is_available: AtomicBool::new(true).into(),
}
}

/// Gets the current state of the parent coin whether
/// it's available for the external requests or not.
///
/// Always `true` for child tokens.
pub fn is_available(&self) -> bool {
!self.inner.is_platform_coin() // Tokens are always active or disabled
|| self.is_available.load(AtomicOrdering::SeqCst)
}

/// Makes the coin disabled to the external requests.
/// Useful for executing `disable_coin` on parent coins
/// that have child tokens enabled.
///
/// Ineffective for child tokens.
pub fn update_is_available(&self, to: bool) {
if !self.inner.is_platform_coin() {
warn!(
"`update_is_available` is ineffective for tokens. Current token: {}",
self.inner.ticker()
);
return;
}

self.is_available.store(to, AtomicOrdering::SeqCst);
}
}

pub struct CoinsContext {
/// A map from a currency ticker symbol to the corresponding coin.
/// Similar to `LP_coins`.
coins: AsyncMutex<HashMap<String, MmCoinEnum>>,
coins: AsyncMutex<HashMap<String, MmCoinStruct>>,
balance_update_handlers: AsyncMutex<Vec<Box<dyn BalanceTradeFeeUpdatedHandler + Send + Sync>>>,
account_balance_task_manager: AccountBalanceTaskManagerShared,
create_account_manager: CreateAccountTaskManagerShared,
Expand Down Expand Up @@ -2483,6 +2528,7 @@ impl CoinsContext {
coin: coin.ticker().into(),
});
}

let ticker = coin.ticker();

let mut platform_coin_tokens = self.platform_coin_tokens.lock();
Expand All @@ -2491,7 +2537,8 @@ impl CoinsContext {
platform.insert(ticker.to_owned());
}

coins.insert(ticker.into(), coin);
coins.insert(ticker.into(), MmCoinStruct::new(coin));

Ok(())
}

Expand All @@ -2509,14 +2556,19 @@ impl CoinsContext {
let mut coins = self.coins.lock().await;
let mut platform_coin_tokens = self.platform_coin_tokens.lock();

if coins.contains_key(platform.ticker()) {
return MmError::err(PlatformIsAlreadyActivatedErr {
ticker: platform.ticker().into(),
});
}
let platform_ticker = platform.ticker().to_owned();

if let Some(coin) = coins.get(&platform_ticker) {
if coin.is_available() {
return MmError::err(PlatformIsAlreadyActivatedErr {
ticker: platform.ticker().into(),
});
}

let platform_ticker = platform.ticker().to_string();
coins.insert(platform_ticker.clone(), platform);
coin.update_is_available(true);
} else {
coins.insert(platform_ticker.clone(), MmCoinStruct::new(platform));
}

// Tokens can't be activated without platform coin so we can safely insert them without checking prior existence
let mut token_tickers = Vec::with_capacity(tokens.len());
Expand All @@ -2526,9 +2578,13 @@ impl CoinsContext {
// We try to activate ETH coin and USDT token via enable_eth_with_tokens
for token in tokens {
token_tickers.push(token.ticker().to_string());
coins.insert(token.ticker().into(), token);
coins
.entry(token.ticker().into())
.or_insert_with(|| MmCoinStruct::new(token));
}

token_tickers.dedup();

platform_coin_tokens
.entry(platform_ticker)
.or_default()
Expand All @@ -2555,6 +2611,7 @@ impl CoinsContext {
if let Some(token) = coins_storage.remove(token) {
// Abort all token related futures on token deactivation
token
.inner
.on_disabled()
.error_log_with_msg(&format!("Error aborting coin({ticker}) futures"));
}
Expand All @@ -2565,7 +2622,7 @@ impl CoinsContext {
tokens.remove(ticker);
}
if let Some(platform_coin) = coins_storage.get(platform_ticker) {
platform_coin.on_token_deactivated(ticker);
platform_coin.inner.on_token_deactivated(ticker);
}
};

Expand Down Expand Up @@ -3037,10 +3094,10 @@ pub async fn lp_register_coin(
RawEntryMut::Occupied(_oe) => {
return MmError::err(RegisterCoinError::CoinIsInitializedAlready { coin: ticker.clone() })
},
RawEntryMut::Vacant(ve) => ve.insert(ticker.clone(), coin.clone()),
RawEntryMut::Vacant(ve) => ve.insert(ticker.clone(), MmCoinStruct::new(coin.clone())),
};

if coin.ticker() == coin.platform_ticker() {
if coin.is_platform_coin() {
let mut platform_coin_tokens = cctx.platform_coin_tokens.lock();
platform_coin_tokens
.entry(coin.ticker().to_string())
Expand All @@ -3062,6 +3119,21 @@ fn lp_spawn_tx_history(ctx: MmArc, coin: MmCoinEnum) -> Result<(), String> {
pub async fn lp_coinfind(ctx: &MmArc, ticker: &str) -> Result<Option<MmCoinEnum>, String> {
let cctx = try_s!(CoinsContext::from_ctx(ctx));
let coins = cctx.coins.lock().await;

if let Some(coin) = coins.get(ticker) {
if coin.is_available() {
return Ok(Some(coin.inner.clone()));
}
};

Ok(None)
}

/// Returns coins even if they are on the passive mode
pub async fn lp_coinfind_any(ctx: &MmArc, ticker: &str) -> Result<Option<MmCoinStruct>, String> {
let cctx = try_s!(CoinsContext::from_ctx(ctx));
let coins = cctx.coins.lock().await;

Ok(coins.get(ticker).cloned())
}

Expand Down Expand Up @@ -3355,7 +3427,7 @@ pub async fn get_enabled_coins(ctx: MmArc) -> Result<Response<Vec<u8>>, String>
let enabled_coins: Vec<_> = try_s!(coins
.iter()
.map(|(ticker, coin)| {
let address = try_s!(coin.my_address());
let address = try_s!(coin.inner.my_address());
Ok(EnabledCoin {
ticker: ticker.clone(),
address,
Expand Down Expand Up @@ -3840,3 +3912,61 @@ fn coins_conf_check(ctx: &MmArc, coins_en: &Json, ticker: &str, req: Option<&Jso
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

use common::block_on;
use mm2_test_helpers::for_tests::RICK;

#[test]
fn test_lp_coinfind() {
let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc();
let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap();
let coin = MmCoinEnum::Test(TestCoin::new(RICK));

// Add test coin to coins context
common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![])).unwrap();

// Try to find RICK from coins context that was added above
let _found = common::block_on(lp_coinfind(&ctx, RICK)).unwrap();

assert!(matches!(Some(coin), _found));

block_on(coins_ctx.coins.lock())
.get(RICK)
.unwrap()
.update_is_available(false);

// Try to find RICK from coins context after making it passive
let found = common::block_on(lp_coinfind(&ctx, RICK)).unwrap();

assert!(found.is_none());
}

#[test]
fn test_lp_coinfind_any() {
let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc();
let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap();
let coin = MmCoinEnum::Test(TestCoin::new(RICK));

// Add test coin to coins context
common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![])).unwrap();

// Try to find RICK from coins context that was added above
let _found = common::block_on(lp_coinfind_any(&ctx, RICK)).unwrap();

assert!(matches!(Some(coin.clone()), _found));

block_on(coins_ctx.coins.lock())
.get(RICK)
.unwrap()
.update_is_available(false);

// Try to find RICK from coins context after making it passive
let _found = common::block_on(lp_coinfind_any(&ctx, RICK)).unwrap();

assert!(matches!(Some(coin), _found));
}
}
19 changes: 15 additions & 4 deletions mm2src/coins/test_coin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,31 @@ use mm2_number::{BigDecimal, MmNumber};
use mocktopus::macros::*;
use rpc::v1::types::Bytes as BytesJson;
use serde_json::Value as Json;
use std::ops::Deref;
use std::sync::Arc;

/// Dummy coin struct used in tests which functions are unimplemented but then mocked
/// in specific test to emulate the required behaviour
#[derive(Clone, Debug)]
pub struct TestCoin {
pub struct TestCoin(Arc<TestCoinImpl>);

impl Deref for TestCoin {
type Target = TestCoinImpl;

fn deref(&self) -> &Self::Target { &self.0 }
}

#[derive(Debug)]
pub struct TestCoinImpl {
ticker: String,
}

impl Default for TestCoin {
fn default() -> Self { TestCoin { ticker: "test".into() } }
fn default() -> Self { TestCoin(Arc::new(TestCoinImpl { ticker: "test".into() })) }
}

impl TestCoin {
pub fn new(ticker: &str) -> TestCoin { TestCoin { ticker: ticker.into() } }
pub fn new(ticker: &str) -> TestCoin { TestCoin(Arc::new(TestCoinImpl { ticker: ticker.into() })) }
}

#[mockable]
Expand All @@ -57,7 +68,7 @@ impl MarketCoinOps for TestCoin {

fn base_coin_balance(&self) -> BalanceFut<BigDecimal> { unimplemented!() }

fn platform_ticker(&self) -> &str { unimplemented!() }
fn platform_ticker(&self) -> &str { &self.ticker }

/// Receives raw transaction bytes in hexadecimal format as input and returns tx hash in hexadecimal format
fn send_raw_tx(&self, tx: &str) -> Box<dyn Future<Item = String, Error = String> + Send> { unimplemented!() }
Expand Down
11 changes: 11 additions & 0 deletions mm2src/coins_activation/src/bch_with_tokens_activation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use coins::utxo::rpc_clients::UtxoRpcError;
use coins::utxo::slp::{EnableSlpError, SlpProtocolConf, SlpToken};
use coins::utxo::utxo_tx_history_v2::bch_and_slp_history_loop;
use coins::utxo::UtxoCommonOps;
use coins::MmCoinEnum;
use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed,
UnexpectedDerivationMethod};
use common::executor::{AbortSettings, SpawnAbortable};
Expand Down Expand Up @@ -237,6 +238,16 @@ impl PlatformWithTokensActivationOps for BchCoin {
Ok(platform_coin)
}

fn try_from_mm_coin(coin: MmCoinEnum) -> Option<Self>
where
Self: Sized,
{
match coin {
MmCoinEnum::Bch(coin) => Some(coin),
_ => None,
}
}

fn token_initializers(
&self,
) -> Vec<Box<dyn TokenAsMmCoinInitializer<PlatformCoin = Self, ActivationRequest = Self::ActivationRequest>>> {
Expand Down
12 changes: 11 additions & 1 deletion mm2src/coins_activation/src/eth_with_token_activation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use crate::{platform_coin_with_tokens::{EnablePlatformCoinWithTokensError, GetPl
TokenInitializer, TokenOf},
prelude::*};
use async_trait::async_trait;
use coins::eth::v2_activation::EthPrivKeyActivationPolicy;
use coins::eth::EthPrivKeyBuildPolicy;
use coins::{eth::v2_activation::EthPrivKeyActivationPolicy, MmCoinEnum};
use coins::{eth::{v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationError,
Erc20TokenActivationRequest, EthActivationV2Error, EthActivationV2Request},
Erc20TokenInfo, EthCoin, EthCoinType},
Expand Down Expand Up @@ -193,6 +193,16 @@ impl PlatformWithTokensActivationOps for EthCoin {
Ok(platform_coin)
}

fn try_from_mm_coin(coin: MmCoinEnum) -> Option<Self>
where
Self: Sized,
{
match coin {
MmCoinEnum::EthCoin(coin) => Some(coin),
_ => None,
}
}

fn token_initializers(
&self,
) -> Vec<Box<dyn TokenAsMmCoinInitializer<PlatformCoin = Self, ActivationRequest = Self::ActivationRequest>>> {
Expand Down
Loading