diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index f01f6b9dd2..2d55a249b4 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -59,8 +59,8 @@ use super::lp_network::P2PRequestResult; use crate::lp_network::{broadcast_p2p_msg, Libp2pPeerId, P2PProcessError, P2PProcessResult, P2PRequestError}; -use crate::lp_swap::maker_swap_v2::{MakerSwapStateMachine, MakerSwapStorage}; -use crate::lp_swap::taker_swap_v2::{TakerSwapStateMachine, TakerSwapStorage}; +use crate::lp_swap::maker_swap_v2::MakerSwapStorage; +use crate::lp_swap::taker_swap_v2::TakerSwapStorage; use bitcrypto::sha256; use coins::{lp_coinfind, lp_coinfind_or_err, CoinFindError, DexFee, MmCoin, MmCoinEnum, TradeFee, TransactionEnum}; use common::log::{debug, warn}; @@ -117,7 +117,6 @@ mod trade_preimage; #[cfg(target_arch = "wasm32")] mod swap_wasm_db; pub use check_balance::{check_other_coin_balance_for_swap, CheckBalanceError, CheckBalanceResult}; -use coins::utxo::utxo_standard::UtxoStandardCoin; use crypto::secret_hash_algo::SecretHashAlgo; use crypto::CryptoCtx; use keys::{KeyPair, SECP_SIGN, SECP_VERIFY}; @@ -131,7 +130,8 @@ use pubkey_banning::BanReason; pub use pubkey_banning::{ban_pubkey_rpc, is_pubkey_banned, list_banned_pubkeys_rpc, unban_pubkeys_rpc}; pub use recreate_swap_data::recreate_swap_data; pub use saved_swap::{SavedSwap, SavedSwapError, SavedSwapIo, SavedSwapResult}; -use swap_v2_common::{get_unfinished_swaps_uuids, swap_kickstart_handler, ActiveSwapV2Info}; +use swap_v2_common::{get_unfinished_swaps_uuids, swap_kickstart_handler_for_maker, swap_kickstart_handler_for_taker, + ActiveSwapV2Info}; use swap_v2_pb::*; use swap_v2_rpcs::{get_maker_swap_data_for_rpc, get_swap_type, get_taker_swap_data_for_rpc}; pub use swap_watcher::{process_watcher_msg, watcher_topic, TakerSwapWatcherData, MAKER_PAYMENT_SPEND_FOUND_LOG, @@ -1411,12 +1411,8 @@ pub async fn swap_kick_starts(ctx: MmArc) -> Result, String> { coins.insert(maker_swap_repr.maker_coin.clone()); coins.insert(maker_swap_repr.taker_coin.clone()); - let fut = swap_kickstart_handler::>( - ctx.clone(), - maker_swap_repr, - maker_swap_storage.clone(), - maker_uuid, - ); + let fut = + swap_kickstart_handler_for_maker(ctx.clone(), maker_swap_repr, maker_swap_storage.clone(), maker_uuid); ctx.spawner().spawn(fut); } @@ -1436,12 +1432,8 @@ pub async fn swap_kick_starts(ctx: MmArc) -> Result, String> { coins.insert(taker_swap_repr.maker_coin.clone()); coins.insert(taker_swap_repr.taker_coin.clone()); - let fut = swap_kickstart_handler::>( - ctx.clone(), - taker_swap_repr, - taker_swap_storage.clone(), - taker_uuid, - ); + let fut = + swap_kickstart_handler_for_taker(ctx.clone(), taker_swap_repr, taker_swap_storage.clone(), taker_uuid); ctx.spawner().spawn(fut); } Ok(coins) 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 52f1690335..fd897507b6 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs @@ -1,8 +1,9 @@ use crate::lp_network::{subscribe_to_topic, unsubscribe_from_topic}; +use crate::lp_swap::maker_swap_v2::{MakerSwapDbRepr, MakerSwapStateMachine, MakerSwapStorage}; use crate::lp_swap::swap_lock::{SwapLock, SwapLockError, SwapLockOps}; +use crate::lp_swap::taker_swap_v2::{TakerSwapDbRepr, TakerSwapStateMachine, TakerSwapStorage}; use crate::lp_swap::{swap_v2_topic, SwapsContext}; -use coins::utxo::utxo_standard::UtxoStandardCoin; -use coins::{lp_coinfind, MmCoinEnum}; +use coins::{lp_coinfind, MakerCoinSwapOpsV2, MmCoin, MmCoinEnum, TakerCoinSwapOpsV2}; use common::executor::abortable_queue::AbortableQueue; use common::executor::{SpawnFuture, Timer}; use common::log::{error, info, warn}; @@ -292,25 +293,18 @@ pub(super) trait GetSwapCoins { fn taker_coin(&self) -> &str; } -/// Generic function for upgraded swaps kickstart handling. -/// It is implemented only for UtxoStandardCoin/UtxoStandardCoin case temporary. -pub(super) async fn swap_kickstart_handler< - T: StorableStateMachine>, ->( - ctx: MmArc, - swap_repr: ::DbRepr, - storage: T::Storage, - uuid: ::MachineId, -) where - ::MachineId: Copy + std::fmt::Display, - ::DbRepr: GetSwapCoins, - T::Error: std::fmt::Display, - T::RecreateError: std::fmt::Display, -{ +/// Attempts to find and return the maker and taker coins required for the swap to proceed. +/// If a coin is not activated, it logs the information and retries until the coin is found. +/// If an unexpected issue occurs, function logs the error and returns `None`. +pub(super) async fn swap_kickstart_coins( + ctx: &MmArc, + swap_repr: &T, + uuid: &Uuid, +) -> Option<(MmCoinEnum, MmCoinEnum)> { let taker_coin_ticker = swap_repr.taker_coin(); let taker_coin = loop { - match lp_coinfind(&ctx, taker_coin_ticker).await { + match lp_coinfind(ctx, taker_coin_ticker).await { Ok(Some(c)) => break c, Ok(None) => { info!( @@ -321,7 +315,7 @@ pub(super) async fn swap_kickstart_handler< }, Err(e) => { error!("Error {} on {} find attempt", e, taker_coin_ticker); - return; + return None; }, }; }; @@ -329,7 +323,7 @@ pub(super) async fn swap_kickstart_handler< let maker_coin_ticker = swap_repr.maker_coin(); let maker_coin = loop { - match lp_coinfind(&ctx, maker_coin_ticker).await { + match lp_coinfind(ctx, maker_coin_ticker).await { Ok(Some(c)) => break c, Ok(None) => { info!( @@ -340,23 +334,30 @@ pub(super) async fn swap_kickstart_handler< }, Err(e) => { error!("Error {} on {} find attempt", e, maker_coin_ticker); - return; + return None; }, }; }; - // TODO add ETH support - let (maker_coin, taker_coin) = match (maker_coin, taker_coin) { - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => (m, t), - _ => { - error!( - "V2 swaps are not currently supported for {}/{} pair", - maker_coin_ticker, taker_coin_ticker - ); - return; - }, - }; + Some((maker_coin, taker_coin)) +} +/// Handles the recreation and kickstart of a swap state machine. +pub(super) async fn swap_kickstart_handler< + T: StorableStateMachine>, + MakerCoin: MmCoin + MakerCoinSwapOpsV2, + TakerCoin: MmCoin + TakerCoinSwapOpsV2, +>( + swap_repr: ::DbRepr, + storage: T::Storage, + uuid: ::MachineId, + maker_coin: MakerCoin, + taker_coin: TakerCoin, +) where + ::MachineId: Copy + std::fmt::Display, + T::Error: std::fmt::Display, + T::RecreateError: std::fmt::Display, +{ let recreate_context = SwapRecreateCtx { maker_coin, taker_coin }; let (mut state_machine, state) = match T::recreate_machine(uuid, storage, swap_repr, recreate_context).await { @@ -371,3 +372,65 @@ pub(super) async fn swap_kickstart_handler< error!("Error {} on trying to run the swap {}", e, uuid); } } + +pub(super) async fn swap_kickstart_handler_for_maker( + ctx: MmArc, + swap_repr: MakerSwapDbRepr, + storage: MakerSwapStorage, + uuid: Uuid, +) { + if let Some((maker_coin, taker_coin)) = swap_kickstart_coins(&ctx, &swap_repr, &uuid).await { + match (maker_coin, taker_coin) { + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::EthCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::EthCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::UtxoCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + _ => { + error!( + "V2 swaps are not currently supported for {}/{} pair", + swap_repr.maker_coin(), + swap_repr.taker_coin() + ); + }, + } + } +} + +pub(super) async fn swap_kickstart_handler_for_taker( + ctx: MmArc, + swap_repr: TakerSwapDbRepr, + storage: TakerSwapStorage, + uuid: Uuid, +) { + if let Some((maker_coin, taker_coin)) = swap_kickstart_coins(&ctx, &swap_repr, &uuid).await { + match (maker_coin, taker_coin) { + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::EthCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::EthCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::UtxoCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + _ => { + error!( + "V2 swaps are not currently supported for {}/{} pair", + swap_repr.maker_coin(), + swap_repr.taker_coin() + ); + }, + } + } +} diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs index 3050f22826..311800bb5d 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -1313,8 +1313,7 @@ pub fn init_geth_node() { thread::sleep(Duration::from_millis(100)); } - let dex_fee_addr = addr_from_raw_pubkey(&DEX_FEE_ADDR_RAW_PUBKEY).unwrap(); - let dex_fee_addr = Token::Address(dex_fee_addr); + let dex_fee_addr = Token::Address(GETH_ACCOUNT); let params = ethabi::encode(&[dex_fee_addr]); let taker_swap_v2_data = format!("{}{}", TAKER_SWAP_V2_BYTES, hex::encode(params)); diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index d922ba8f27..8aec7ceed5 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -28,12 +28,13 @@ use coins::{CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaym use common::{block_on, block_on_f01, now_sec}; use crypto::Secp256k1Secret; use ethereum_types::U256; -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use mm2_core::mm_ctx::MmArc; use mm2_number::{BigDecimal, BigUint}; -use mm2_test_helpers::for_tests::{account_balance, disable_coin, enable_erc20_token_v2, enable_eth_with_tokens_v2, - erc20_dev_conf, eth_dev_conf, get_new_address, get_token_info, nft_dev_conf, - MarketMakerIt, Mm2TestConf}; +use mm2_test_helpers::for_tests::{account_balance, active_swaps, coins_needed_for_kickstart, disable_coin, + enable_erc20_token_v2, enable_eth_coin_v2, enable_eth_with_tokens_v2, + erc20_dev_conf, eth1_dev_conf, eth_dev_conf, get_locked_amount, get_new_address, + get_token_info, mm_dump, my_swap_status, nft_dev_conf, start_swaps, MarketMakerIt, + Mm2TestConf, SwapV2TestContracts, TestNode}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use mm2_test_helpers::for_tests::{eth_sepolia_conf, sepolia_erc20_dev_conf}; use mm2_test_helpers::structs::{Bip44Chain, EnableCoinBalanceMap, EthWithTokensActivationResult, HDAccountAddressId, @@ -43,6 +44,7 @@ use serde_json::Value as Json; use std::str::FromStr; use std::thread; use std::time::Duration; +use uuid::Uuid; use web3::contract::{Contract, Options}; use web3::ethabi::Token; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] @@ -55,6 +57,7 @@ const SEPOLIA_MAKER_PRIV: &str = "6e2f3a6223b928a05a3a3622b0c3f3573d03663b704a61 const SEPOLIA_TAKER_PRIV: &str = "e0be82dca60ff7e4c6d6db339ac9e1ae249af081dba2110bddd281e711608f16"; const NFT_ETH: &str = "NFT_ETH"; const ETH: &str = "ETH"; +const ETH1: &str = "ETH1"; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] const ERC20: &str = "ERC20DEV"; @@ -1427,15 +1430,16 @@ impl SwapAddresses { } } -#[allow(dead_code)] /// Needed for eth or erc20 v2 activation in Geth tests fn eth_coin_v2_activation_with_random_privkey( + ctx: &MmArc, ticker: &str, conf: &Json, swap_addr: SwapAddresses, erc20: bool, -) -> EthCoin { - let build_policy = EthPrivKeyBuildPolicy::IguanaPrivKey(random_secp256k1_secret()); +) -> (EthCoin, Secp256k1Secret) { + let priv_key = random_secp256k1_secret(); + let build_policy = EthPrivKeyBuildPolicy::IguanaPrivKey(priv_key); let node = EthNode { url: GETH_RPC_URL.to_string(), komodo_proxy: false, @@ -1455,7 +1459,7 @@ fn eth_coin_v2_activation_with_random_privkey( gap_limit: None, }; let coin = block_on(eth_coin_from_conf_and_request_v2( - &MM_CTX1, + ctx, ticker, conf, platform_request, @@ -1471,9 +1475,9 @@ fn eth_coin_v2_activation_with_random_privkey( token_addr: erc20_contract(), }; let coin = block_on(coin.set_coin_type(coin_type)); - return coin; + return (coin, priv_key); } - coin + (coin, priv_key) } #[cfg(feature = "sepolia-taker-swap-v2-tests")] @@ -2749,3 +2753,125 @@ fn test_enable_custom_erc20_with_duplicate_contract_in_config() { // Disable the custom token, this to check that it was enabled correctly block_on(disable_coin(&mm_hd, &config_ticker, true)); } + +#[test] +fn test_v2_eth_eth_kickstart() { + // Initialize swap addresses and configurations + let swap_addresses = SwapAddresses::init(); + let contracts = SwapV2TestContracts { + maker_swap_v2_contract: eth_addr_to_hex(&swap_addresses.swap_v2_contracts.maker_swap_v2_contract), + taker_swap_v2_contract: eth_addr_to_hex(&swap_addresses.swap_v2_contracts.taker_swap_v2_contract), + nft_maker_swap_v2_contract: eth_addr_to_hex(&swap_addresses.swap_v2_contracts.nft_maker_swap_v2_contract), + }; + let swap_contract_address = eth_addr_to_hex(&swap_addresses.swap_contract_address); + let node = TestNode { + url: GETH_RPC_URL.to_string(), + }; + + // Helper function for activating coins + let enable_coins = |mm: &MarketMakerIt, coins: &[&str]| { + for &coin in coins { + log!( + "{:?}", + block_on(enable_eth_coin_v2( + mm, + coin, + &swap_contract_address, + contracts.clone(), + None, + &[node.clone()] + )) + ); + } + }; + + // start Bob and Alice + let (_, bob_priv_key) = + eth_coin_v2_activation_with_random_privkey(&MM_CTX, ETH, ð_dev_conf(), swap_addresses, false); + let (_, alice_priv_key) = + eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ETH1, ð1_dev_conf(), swap_addresses, false); + let coins = json!([eth_dev_conf(), eth1_dev_conf()]); + + let mut bob_conf = Mm2TestConf::seednode_trade_v2(&format!("0x{}", hex::encode(bob_priv_key)), &coins); + let mut mm_bob = MarketMakerIt::start(bob_conf.conf.clone(), bob_conf.rpc_password.clone(), None).unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + log!("Bob log path: {}", mm_bob.log_path.display()); + + let mut alice_conf = + Mm2TestConf::light_node_trade_v2(&format!("0x{}", hex::encode(alice_priv_key)), &coins, &[&mm_bob + .ip + .to_string()]); + let mut mm_alice = MarketMakerIt::start(alice_conf.conf.clone(), alice_conf.rpc_password.clone(), None).unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + log!("Alice log path: {}", mm_alice.log_path.display()); + + // Enable ETH and ETH1 for both Bob and Alice + enable_coins(&mm_bob, &[ETH, ETH1]); + enable_coins(&mm_alice, &[ETH, ETH1]); + + let uuids = block_on(start_swaps(&mut mm_bob, &mut mm_alice, &[(ETH, ETH1)], 1.0, 1.0, 77.)); + log!("{:?}", uuids); + let parsed_uuids: Vec = uuids.iter().map(|u| u.parse().unwrap()).collect(); + + for uuid in uuids.iter() { + log_swap_status_before_stop(&mm_bob, uuid, "Maker"); + log_swap_status_before_stop(&mm_alice, uuid, "Taker"); + } + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); + + // Restart Bob and Alice + bob_conf.conf["dbdir"] = mm_bob.folder.join("DB").to_str().unwrap().into(); + bob_conf.conf["log"] = mm_bob.folder.join("mm2_dup.log").to_str().unwrap().into(); + + let mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + log!("Bob log path: {}", mm_bob.log_path.display()); + + alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); + alice_conf.conf["log"] = mm_alice.folder.join("mm2_dup.log").to_str().unwrap().into(); + alice_conf.conf["seednodes"] = vec![mm_bob.ip.to_string()].into(); + + let mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + log!("Alice log path: {}", mm_alice.log_path.display()); + + verify_coins_needed_for_kickstart(&mm_bob, &[ETH, ETH1]); + verify_coins_needed_for_kickstart(&mm_alice, &[ETH, ETH1]); + + enable_coins(&mm_bob, &[ETH, ETH1]); + enable_coins(&mm_alice, &[ETH, ETH1]); + + // give swaps 1 second to restart + thread::sleep(Duration::from_secs(1)); + + verify_active_swaps(&mm_bob, &parsed_uuids); + verify_active_swaps(&mm_alice, &parsed_uuids); + + // coins must be virtually locked after kickstart until swap transactions are sent + verify_locked_amount(&mm_alice, "Taker", ETH1); + verify_locked_amount(&mm_bob, "Maker", ETH); +} + +fn log_swap_status_before_stop(mm: &MarketMakerIt, uuid: &str, role: &str) { + let status = block_on(my_swap_status(mm, uuid)); + log!("{} swap {} status before stop: {:?}", role, uuid, status); +} + +fn verify_coins_needed_for_kickstart(mm: &MarketMakerIt, expected_coins: &[&str]) { + let mut coins_needed = block_on(coins_needed_for_kickstart(mm)); + coins_needed.sort(); + assert_eq!(coins_needed, expected_coins); +} + +fn verify_active_swaps(mm: &MarketMakerIt, expected_uuids: &[Uuid]) { + let active_swaps = block_on(active_swaps(mm)); + assert_eq!(active_swaps.uuids, expected_uuids); +} + +fn verify_locked_amount(mm: &MarketMakerIt, role: &str, coin: &str) { + let locked = block_on(get_locked_amount(mm, coin)); + log!("{} {} locked amount: {:?}", role, coin, locked.locked_amount); + assert_eq!(locked.coin, coin); +} diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 7592384696..6aaee66488 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -820,8 +820,16 @@ pub fn eth_testnet_conf_trezor() -> Json { /// ETH configuration used for dockerized Geth dev node pub fn eth_dev_conf() -> Json { + eth_conf("ETH") +} + +pub fn eth1_dev_conf() -> Json { + eth_conf("ETH1") +} + +fn eth_conf(coin: &str) -> Json { json!({ - "coin": "ETH", + "coin": coin, "name": "ethereum", "mm2": 1, "chain_id": 1337, @@ -2041,6 +2049,51 @@ pub async fn enable_eth_coin( json::from_str(&enable.1).unwrap() } +#[derive(Clone)] +pub struct SwapV2TestContracts { + pub maker_swap_v2_contract: String, + pub taker_swap_v2_contract: String, + pub nft_maker_swap_v2_contract: String, +} + +#[derive(Clone)] +pub struct TestNode { + pub url: String, +} + +pub async fn enable_eth_coin_v2( + mm: &MarketMakerIt, + ticker: &str, + swap_contract_address: &str, + swap_v2_contracts: SwapV2TestContracts, + fallback_swap_contract: Option<&str>, + nodes: &[TestNode], +) -> Json { + let enable = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "enable_eth_with_tokens", + "mmrpc": "2.0", + "params": { + "ticker": ticker, + "mm2": 1, + "swap_contract_address": swap_contract_address, + "swap_v2_contracts": { + "maker_swap_v2_contract": swap_v2_contracts.maker_swap_v2_contract, + "taker_swap_v2_contract": swap_v2_contracts.taker_swap_v2_contract, + "nft_maker_swap_v2_contract": swap_v2_contracts.nft_maker_swap_v2_contract + }, + "fallback_swap_contract": fallback_swap_contract, + "nodes": nodes.iter().map(|node| json!({ "url": node.url })).collect::>(), + "erc20_tokens_requests": [] + } + })) + .await + .unwrap(); + assert_eq!(enable.0, StatusCode::OK, "'enable_eth_with_tokens' failed: {}", enable.1); + json::from_str(&enable.1).unwrap() +} + pub async fn enable_slp(mm: &MarketMakerIt, coin: &str) -> Json { let enable = mm .rpc(&json!({