diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 37790bddbf..ea01601730 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -991,94 +991,92 @@ fn test_get_enabled_erc20_by_contract_and_platform() { const BNB_TOKEN: &str = "1INCH-BEP20"; const ETH_TOKEN: &str = "1INCH-ERC20"; + let passphrase = "spice describe gravity federal blast come thank unfair canal monkey style afraid"; let conf = json!({ "coins": [{ - "coin": "BNB", - "name": "binancesmartchain", - "fname": "Binance Coin", - "avg_blocktime": 3, - "rpcport": 80, - "mm2": 1, - "use_access_list": true, - "max_eth_tx_type": 2, - "required_confirmations": 3, - "protocol": { - "type": "ETH", - "protocol_data": { - "chain_id": 56 - } - }, - "derivation_path": "m/44'/60'", - "trezor_coin": "Binance Smart Chain", - "links": { - "homepage": "https://www.binance.org" - } - },{ - "coin": BNB_TOKEN, - "name": "1inch_bep20", - "fname": "1Inch", - "rpcport": 80, - "mm2": 1, - "avg_blocktime": 3, - "required_confirmations": 3, - "protocol": { - "type": "ERC20", - "protocol_data": { - "platform": "BNB", - "contract_address": "0x111111111117dC0aa78b770fA6A738034120C302" - } - }, - "derivation_path": "m/44'/60'", - "use_access_list": true, - "max_eth_tx_type": 2, - "gas_limit": { - "eth_send_erc20": 60000, - "erc20_payment": 110000, - "erc20_receiver_spend": 85000, - "erc20_sender_refund": 85000 - } - },{ - "coin": "ETH", - "name": "ethereum", - "fname": "Ethereum", - "rpcport": 80, - "mm2": 1, - "sign_message_prefix": "Ethereum Signed Message:\n", - "required_confirmations": 3, - "avg_blocktime": 15, - "protocol": { - "type": "ETH", - "protocol_data": { - "chain_id": 1 - } - }, - "derivation_path": "m/44'/60'" - },{ - "coin": ETH_TOKEN, - "name": "1inch_erc20", - "fname": "1Inch", - "rpcport": 80, - "mm2": 1, - "avg_blocktime": 15, - "required_confirmations": 3, - "decimals": 18, - "protocol": { - "type": "ERC20", - "protocol_data": { - "platform": "ETH", - "contract_address": "0x111111111117dC0aa78b770fA6A738034120C302" - } - }, - "derivation_path": "m/44'/60'" - }] + "coin": "BNB", + "name": "binancesmartchain", + "fname": "Binance Coin", + "avg_blocktime": 3, + "rpcport": 80, + "mm2": 1, + "use_access_list": true, + "max_eth_tx_type": 2, + "required_confirmations": 3, + "protocol": { + "type": "ETH", + "protocol_data": { + "chain_id": 56 + } + }, + "derivation_path": "m/44'/60'", + "trezor_coin": "Binance Smart Chain", + "links": { + "homepage": "https://www.binance.org" + } + },{ + "coin": BNB_TOKEN, + "name": "1inch_bep20", + "fname": "1Inch", + "rpcport": 80, + "mm2": 1, + "avg_blocktime": 3, + "required_confirmations": 3, + "protocol": { + "type": "ERC20", + "protocol_data": { + "platform": "BNB", + "contract_address": "0x111111111117dC0aa78b770fA6A738034120C302" + } + }, + "derivation_path": "m/44'/60'", + "use_access_list": true, + "max_eth_tx_type": 2, + "gas_limit": { + "eth_send_erc20": 60000, + "erc20_payment": 110000, + "erc20_receiver_spend": 85000, + "erc20_sender_refund": 85000 + } + },{ + "coin": "ETH", + "name": "ethereum", + "fname": "Ethereum", + "rpcport": 80, + "mm2": 1, + "sign_message_prefix": "Ethereum Signed Message:\n", + "required_confirmations": 3, + "avg_blocktime": 15, + "protocol": { + "type": "ETH", + "protocol_data": { + "chain_id": 1 + } + }, + "derivation_path": "m/44'/60'" + },{ + "coin": ETH_TOKEN, + "name": "1inch_erc20", + "fname": "1Inch", + "rpcport": 80, + "mm2": 1, + "avg_blocktime": 15, + "required_confirmations": 3, + "decimals": 18, + "protocol": { + "type": "ERC20", + "protocol_data": { + "platform": "ETH", + "contract_address": "0x111111111117dC0aa78b770fA6A738034120C302" + } + }, + "derivation_path": "m/44'/60'" + }], + "passphrase": passphrase }); let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); - CryptoCtx::init_with_iguana_passphrase( - ctx.clone(), - "spice describe gravity federal blast come thank unfair canal monkey style afraid", - ) - .unwrap(); + CryptoCtx::init_with_iguana_passphrase(ctx.clone(), passphrase).unwrap(); let req_bnb_token = json!({ "urls":["https://bsc-dataseed1.binance.org","https://bsc-dataseed1.defibit.io"], @@ -1117,6 +1115,7 @@ fn test_fee_history() { fn test_gas_limit_conf() { use mm2_test_helpers::for_tests::ETH_SEPOLIA_SWAP_CONTRACT; + let passphrase = "123456"; let conf = json!({ "coins": [{ "coin": "ETH", @@ -1135,11 +1134,12 @@ fn test_gas_limit_conf() { "erc20_receiver_spend": 130000, "erc20_sender_refund": 110000 } - }] + }], + "passphrase": passphrase }); let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); - CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "123456").unwrap(); + CryptoCtx::init_with_iguana_passphrase(ctx.clone(), passphrase).unwrap(); let req = json!({ "urls":ETH_SEPOLIA_NODES, diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 2ee493ec69..5424f27f24 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -15,6 +15,7 @@ fn pass() { } async fn init_eth_coin_helper() -> Result<(MmArc, MmCoinEnum), String> { + let passphrase = "spice describe gravity federal blast come thank unfair canal monkey style afraid"; let conf = json!({ "coins": [{ "coin": "ETH", @@ -28,16 +29,13 @@ async fn init_eth_coin_helper() -> Result<(MmArc, MmCoinEnum), String> { }, "rpcport": 80, "mm2": 1, - "max_eth_tx_type": 2 - }] + "max_eth_tx_type": 2, + }], + "passphrase": passphrase, }); let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); - CryptoCtx::init_with_iguana_passphrase( - ctx.clone(), - "spice describe gravity federal blast come thank unfair canal monkey style afraid", - ) - .unwrap(); + CryptoCtx::init_with_iguana_passphrase(ctx.clone(), passphrase).unwrap(); let req = json!({ "urls":ETH_SEPOLIA_NODES, diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index dcbb2f738f..ee7ca33128 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -5137,6 +5137,10 @@ pub fn is_wallet_only_ticker(ctx: &MmArc, ticker: &str) -> bool { /// /// * `req` - Payload of the corresponding "enable" or "electrum" RPC request. pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result { + if ctx.is_no_login_mode() { + return ERR!("Cannot enable coins in no-login mode."); + } + let cctx = try_s!(CoinsContext::from_ctx(ctx)); { let coins = cctx.coins.lock().await; diff --git a/mm2src/coins/rpc_command/offline_keys.rs b/mm2src/coins/rpc_command/offline_keys.rs index 3a306503b4..bfc07c51d1 100644 --- a/mm2src/coins/rpc_command/offline_keys.rs +++ b/mm2src/coins/rpc_command/offline_keys.rs @@ -100,6 +100,8 @@ pub enum OfflineKeysError { MissingPrefixValue { ticker: String, prefix_type: String }, #[display(fmt = "Invalid parameters: start_index and end_index are only valid for HD mode")] InvalidParametersForMode, + #[display(fmt = "Cannot get private key in no-login mode.")] + NotAllowedInNoLoginMode, } #[derive(Debug, Clone)] @@ -116,9 +118,10 @@ impl HttpStatusCode for OfflineKeysError { Self::CoinConfigNotFound(_) => StatusCode::BAD_REQUEST, Self::KeyDerivationFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR, Self::InvalidHdRange { .. } => StatusCode::BAD_REQUEST, - Self::HdRangeTooLarge => StatusCode::BAD_REQUEST, Self::MissingPrefixValue { .. } => StatusCode::BAD_REQUEST, - Self::InvalidParametersForMode => StatusCode::BAD_REQUEST, + Self::HdRangeTooLarge | Self::NotAllowedInNoLoginMode | Self::InvalidParametersForMode => { + StatusCode::BAD_REQUEST + }, Self::ProtocolParseError { .. } => StatusCode::BAD_REQUEST, } } @@ -507,7 +510,9 @@ async fn offline_iguana_keys_export_internal( let prefix_values = extract_prefix_values(&ctx, &ticker, &coin_conf)?; - let passphrase = ctx.conf["passphrase"].as_str().unwrap_or(""); + let passphrase = ctx.conf["passphrase"].as_str().ok_or_else(|| { + OfflineKeysError::Internal("Couldn't derive 'passphrase' from the KDF configuration.".to_owned()) + })?; let key_pair = { match key_pair_from_seed(passphrase) { @@ -645,6 +650,10 @@ pub async fn get_private_keys( ctx: MmArc, req: GetPrivateKeysRequest, ) -> Result> { + if ctx.is_no_login_mode() { + return MmError::err(OfflineKeysError::NotAllowedInNoLoginMode); + } + let mode = req.mode.unwrap_or_else(|| { if ctx.enable_hd() { KeyExportMode::Hd @@ -965,7 +974,8 @@ mod tests { let ctx = MmCtxBuilder::new() .with_conf(json!({ "coins": [btc_conf], - "rpc_password": "test123" + "rpc_password": "test123", + "passphrase": TEST_MNEMONIC, })) .into_mm_arc(); @@ -999,7 +1009,8 @@ mod tests { let ctx = MmCtxBuilder::new() .with_conf(json!({ "coins": [btc_conf], - "rpc_password": "test123" + "rpc_password": "test123", + "passphrase": TEST_MNEMONIC, })) .into_mm_arc(); @@ -1093,7 +1104,8 @@ mod tests { let ctx = MmCtxBuilder::new() .with_conf(json!({ "coins": [arrr_conf], - "rpc_password": "test123" + "rpc_password": "test123", + "passphrase": TEST_SEED, })) .into_mm_arc(); @@ -1154,15 +1166,17 @@ mod tests { async fn test_eth_iguana_eip55_formatting() { use mm2_test_helpers::for_tests::eth_dev_conf; + let passphrase = "test_passphrase_for_eip55"; let ctx = MmCtxBuilder::new() .with_conf(json!({ "coins": [eth_dev_conf()], - "rpc_password": "test123" + "rpc_password": "test123", + "passphrase": passphrase, })) .into_mm_arc(); // Initialize with a test passphrase for Iguana mode - CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "test_passphrase_for_eip55").unwrap(); + CryptoCtx::init_with_iguana_passphrase(ctx.clone(), passphrase).unwrap(); let req = GetPrivateKeysRequest { coins: vec!["ETH".to_string()], diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index 885221cd91..51b1721c05 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -296,6 +296,8 @@ pub enum EnablePlatformCoinWithTokensError { #[display(fmt = "WalletConnect Error: {_0}")] WalletConnectError(String), PlatformCoinMismatch, + #[display(fmt = "Cannot enable coins in no-login mode.")] + CannotEnableInNoLoginMode, } impl From for EnablePlatformCoinWithTokensError { @@ -390,6 +392,7 @@ impl HttpStatusCode for EnablePlatformCoinWithTokensError { | EnablePlatformCoinWithTokensError::FailedSpawningBalanceEvents(_) | EnablePlatformCoinWithTokensError::WalletConnectError(_) | EnablePlatformCoinWithTokensError::PlatformCoinMismatch + | EnablePlatformCoinWithTokensError::CannotEnableInNoLoginMode | EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { .. } => StatusCode::BAD_REQUEST, EnablePlatformCoinWithTokensError::Transport(_) => StatusCode::BAD_GATEWAY, } @@ -441,6 +444,10 @@ where Platform: PlatformCoinWithTokensActivationOps, EnablePlatformCoinWithTokensError: From, { + if ctx.is_no_login_mode() { + return MmError::err(EnablePlatformCoinWithTokensError::CannotEnableInNoLoginMode); + } + if req.request.is_hw_policy() { return MmError::err(EnablePlatformCoinWithTokensError::UnexpectedDeviceActivationPolicy); } diff --git a/mm2src/crypto/src/crypto_ctx.rs b/mm2src/crypto/src/crypto_ctx.rs index 822b6f9298..b25b1049c4 100644 --- a/mm2src/crypto/src/crypto_ctx.rs +++ b/mm2src/crypto/src/crypto_ctx.rs @@ -31,6 +31,8 @@ pub enum CryptoInitError { #[display(fmt = "Invalid passphrase: '{_0}'")] InvalidPassphrase(PrivKeyError), Internal(String), + #[display(fmt = "Cannot initialize in no-login mode.")] + CannotInitInNoLoginMode, } impl From for CryptoInitError { @@ -54,6 +56,8 @@ pub enum CryptoCtxError { NotInitialized, #[display(fmt = "Internal error: {_0}")] Internal(String), + #[display(fmt = "Cannot use `CryptoCtx` in no-login mode.")] + CannotInitInNoLoginMode, } #[derive(Debug)] @@ -111,11 +115,16 @@ impl CryptoCtx { match CryptoCtx::from_ctx(ctx).split_mm() { Ok(_) => Ok(true), Err((CryptoCtxError::NotInitialized, _trace)) => Ok(false), + Err((CryptoCtxError::CannotInitInNoLoginMode, _trace)) => Ok(false), Err((other, trace)) => MmError::err_with_trace(InternalError(other.to_string()), trace), } } pub fn from_ctx(ctx: &MmArc) -> MmResult, CryptoCtxError> { + if ctx.is_no_login_mode() { + return MmError::err(CryptoCtxError::CannotInitInNoLoginMode); + } + let ctx_field = ctx .crypto_ctx .lock() diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 70378ddc8a..1411621a9f 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -249,6 +249,10 @@ impl MmCtx { self.conf["i_am_seed"].as_bool().unwrap_or(false) } + pub fn is_no_login_mode(&self) -> bool { + self.conf["passphrase"].is_null() && self.conf["wallet_name"].is_null() + } + #[cfg(not(target_arch = "wasm32"))] pub fn rpc_ip_port(&self) -> Result { let port = match self.conf.get("rpcport") { diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index 3b8a02da85..49d296ae8f 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -434,6 +434,10 @@ pub async fn lp_init_continue(ctx: MmArc) -> MmInitResult<()> { pub async fn lp_init(ctx: MmArc, version: String, datetime: String) -> MmInitResult<()> { info!("Version: {} DT {}", version, datetime); + if ctx.is_no_login_mode() { + info!("Running on no-login mode. Wallet operations will be unavailable."); + } + // Ensure the database root directory exists before initializing the wallet passphrase. // This is necessary to store the encrypted wallet passphrase if needed. #[cfg(not(target_arch = "wasm32"))] diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 90cb215881..5e18aeeb7b 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -541,7 +541,7 @@ where match CryptoCtx::from_ctx(ctx).split_mm() { Ok(crypto_ctx) => Ok(Some(CryptoCtx::mm2_internal_pubkey_hex(crypto_ctx.as_ref()))), Err((CryptoCtxError::NotInitialized, _)) => Ok(None), - Err((CryptoCtxError::Internal(error), trace)) => MmError::err_with_trace(err_construct(error), trace), + Err((e, trace)) => MmError::err_with_trace(err_construct(e.to_string()), trace), } } 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 71c55fec0f..95da3af278 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -5763,6 +5763,33 @@ fn test_no_login() { assert!(version.0.is_success(), "!version: {}", version.1); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_no_login_get_private_keys() { + let coins = json!([rick_conf(), morty_conf()]); + let seednode_passphrase = get_passphrase(&".env.seed", "BOB_PASSPHRASE").unwrap(); + let seednode_conf = Mm2TestConf::seednode(&seednode_passphrase, &coins); + let seednode = MarketMakerIt::start(seednode_conf.conf, seednode_conf.rpc_password, None).unwrap(); + let (_dump_log, _dump_dashboard) = seednode.mm_dump(); + log!("log path: {}", seednode.log_path.display()); + + let no_login_conf = Mm2TestConf::no_login_node(&coins, &[&seednode.ip.to_string()]); + let mm = MarketMakerIt::start(no_login_conf.conf, no_login_conf.rpc_password, None).unwrap(); + + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "get_private_keys", + "mmrpc": "2.0", + "params": { + "coins": [] + } + }))) + .unwrap(); + + assert!(rc.0.is_client_error()); + assert!(rc.1.contains("NotAllowedInNoLoginMode")); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_gui_storage_accounts_functionality() {