diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 1626e9d8af..419ab55c2a 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -211,6 +211,7 @@ use coin_balance::{AddressBalanceStatus, HDAddressBalance, HDWalletBalanceOps}; pub mod lp_price; pub mod watcher_common; +pub mod priv_key; pub mod coin_errors; use coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentFut, diff --git a/mm2src/coins/priv_key.rs b/mm2src/coins/priv_key.rs new file mode 100644 index 0000000000..b966579d69 --- /dev/null +++ b/mm2src/coins/priv_key.rs @@ -0,0 +1,233 @@ +use crate::hd_wallet::{HDAccountOps, HDWalletOps}; +use crate::{CoinWithDerivationMethod, CoinWithPrivKeyPolicy, DerivationMethod, MmCoin, MmCoinEnum, PrivKeyPolicy}; +use bip32::ChildNumber; +use common::HttpStatusCode; +use crypto::Bip44Chain; +use derive_more::Display; +use http::StatusCode; +use keys::{KeyPair, Private}; +use mm2_err_handle::prelude::*; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DerivedPrivKey { + pub coin: String, + pub address: String, + pub derivation_path: String, + pub priv_key: String, + pub pub_key: String, +} + +#[derive(Debug, Deserialize)] +pub struct DerivePrivKeyReq { + pub account_id: u32, + pub chain: Option, + pub address_id: u32, +} + +#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum DerivePrivKeyError { + #[display(fmt = "No such coin: {}", ticker)] + NoSuchCoin { + ticker: String, + }, + #[display(fmt = "Coin {} doesn't support HD wallet derivation", ticker)] + CoinDoesntSupportDerivation { + ticker: String, + }, + #[display(fmt = "Hardware/remote wallet doesn't allow exporting private keys")] + HwWalletNotAllowed, + #[display(fmt = "Internal error: {}", reason)] + Internal { + reason: String, + }, +} + +impl HttpStatusCode for DerivePrivKeyError { + fn status_code(&self) -> StatusCode { + match self { + DerivePrivKeyError::NoSuchCoin { .. } => StatusCode::NOT_FOUND, + DerivePrivKeyError::CoinDoesntSupportDerivation { .. } => StatusCode::BAD_REQUEST, + DerivePrivKeyError::HwWalletNotAllowed => StatusCode::FORBIDDEN, + DerivePrivKeyError::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn derive_priv_key(coin: MmCoinEnum, req: &DerivePrivKeyReq) -> Result> { + match coin { + MmCoinEnum::UtxoCoin(c) => derive_priv_key_for_utxo_coin(c, req).await, + MmCoinEnum::Bch(c) => derive_priv_key_for_utxo_coin(c, req).await, + MmCoinEnum::QtumCoin(c) => derive_priv_key_for_utxo_coin(c, req).await, + MmCoinEnum::EthCoin(c) => derive_priv_key_for_eth_coin(c, req).await, + _ => MmError::err(DerivePrivKeyError::CoinDoesntSupportDerivation { + ticker: coin.ticker().to_string(), + }), + } +} + +async fn derive_priv_key_for_utxo_coin( + coin: impl MmCoin + CoinWithPrivKeyPolicy + CoinWithDerivationMethod + AsRef, + req: &DerivePrivKeyReq, +) -> Result> { + let coin_fields = coin.as_ref(); + + match coin.priv_key_policy() { + PrivKeyPolicy::Iguana(_) => MmError::err(DerivePrivKeyError::CoinDoesntSupportDerivation { + ticker: coin.ticker().to_string(), + }), + PrivKeyPolicy::Trezor | PrivKeyPolicy::WalletConnect { .. } => { + MmError::err(DerivePrivKeyError::HwWalletNotAllowed) + }, + PrivKeyPolicy::HDWallet { .. } => { + let hd_wallet = match coin.derivation_method() { + DerivationMethod::HDWallet(hd_wallet) => hd_wallet, + _ => { + return MmError::err(DerivePrivKeyError::CoinDoesntSupportDerivation { + ticker: coin.ticker().to_string(), + }) + }, + }; + + let account = hd_wallet + .get_account(req.account_id) + .await + .ok_or_else(|| DerivePrivKeyError::Internal { + reason: format!("Account {} not found", req.account_id), + })?; + + let mut path_to_address = account.account_derivation_path(); + path_to_address.push(req.chain.unwrap_or(Bip44Chain::External).to_child_number()); + path_to_address.push(ChildNumber::new(req.address_id, false).expect("non-hardened")); + + let secret_key = coin + .priv_key_policy() + .hd_wallet_derived_priv_key_or_err(&path_to_address) + .map_err(|e| DerivePrivKeyError::Internal { + reason: format!("Error deriving secret key: {}", e), + })?; + + let private = Private { + prefix: coin_fields.conf.wif_prefix, + secret: secret_key.into(), + compressed: true, + checksum_type: coin_fields.conf.checksum_type, + }; + + let key_pair = KeyPair::from_private(private) + .map_err(|e| DerivePrivKeyError::Internal { + reason: format!("Error creating key pair from secret: {}", e), + })?; + + let pubkey_slice = key_pair.public_slice(); + let pubkey: [u8; 33] = pubkey_slice + .try_into() + .map_err(|_| DerivePrivKeyError::Internal { + reason: "Error converting pubkey slice to array".to_string(), + })?; + + let address = coin + .address_from_pubkey(&pubkey.into()) + .map_err(|e| DerivePrivKeyError::Internal { + reason: format!("Error getting address from pubkey: {}", e), + })?; + + let priv_key_wif = key_pair.private().to_string(); + let pub_key_hex = hex::encode(pubkey); + + let response = DerivedPrivKey { + coin: coin.ticker().to_string(), + address: address.to_string(), + derivation_path: path_to_address.to_string(), + priv_key: priv_key_wif, + pub_key: pub_key_hex, + }; + Ok(response) + }, + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => MmError::err(DerivePrivKeyError::HwWalletNotAllowed), + } +} + +async fn derive_priv_key_for_eth_coin( + coin: impl MmCoin + CoinWithPrivKeyPolicy + CoinWithDerivationMethod, + req: &DerivePrivKeyReq, +) -> Result> { + match coin.priv_key_policy() { + PrivKeyPolicy::Iguana(_) => MmError::err(DerivePrivKeyError::CoinDoesntSupportDerivation { + ticker: coin.ticker().to_string(), + }), + PrivKeyPolicy::Trezor | PrivKeyPolicy::WalletConnect { .. } => { + MmError::err(DerivePrivKeyError::HwWalletNotAllowed) + }, + PrivKeyPolicy::HDWallet { .. } => { + let hd_wallet = match coin.derivation_method() { + DerivationMethod::HDWallet(hd_wallet) => hd_wallet, + _ => { + return MmError::err(DerivePrivKeyError::CoinDoesntSupportDerivation { + ticker: coin.ticker().to_string(), + }) + }, + }; + + let account = hd_wallet + .get_account(req.account_id) + .await + .ok_or_else(|| DerivePrivKeyError::Internal { + reason: format!("Account {} not found", req.account_id), + })?; + + let mut path_to_address = account.account_derivation_path(); + path_to_address.push(req.chain.unwrap_or(Bip44Chain::External).to_child_number()); + path_to_address.push(ChildNumber::new(req.address_id, false).expect("non-hardened")); + + let secret_key = coin + .priv_key_policy() + .hd_wallet_derived_priv_key_or_err(&path_to_address) + .map_err(|e| DerivePrivKeyError::Internal { + reason: format!("Error deriving secret key: {}", e), + })?; + + let private = Private { + prefix: 0, // ETH doesn't use WIF format + secret: secret_key.into(), + compressed: true, + checksum_type: Default::default(), + }; + + let key_pair = KeyPair::from_private(private) + .map_err(|e| DerivePrivKeyError::Internal { + reason: format!("Error creating key pair from secret: {}", e), + })?; + + let pubkey_slice = key_pair.public_slice(); + let pubkey: [u8; 33] = pubkey_slice + .try_into() + .map_err(|_| DerivePrivKeyError::Internal { + reason: "Error converting pubkey slice to array".to_string(), + })?; + + let address = coin + .address_from_pubkey(&pubkey.into()) + .map_err(|e| DerivePrivKeyError::Internal { + reason: format!("Error getting address from pubkey: {}", e), + })?; + + let priv_key_hex = format!("0x{}", hex::encode(key_pair.private_bytes())); + let pub_key_hex = format!("0x{}", hex::encode(pubkey)); + + let response = DerivedPrivKey { + coin: coin.ticker().to_string(), + address: address.to_string(), + derivation_path: path_to_address.to_string(), + priv_key: priv_key_hex, + pub_key: pub_key_hex, + }; + Ok(response) + }, + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => MmError::err(DerivePrivKeyError::HwWalletNotAllowed), + } +} diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 6e55fc0e65..c70d89ea56 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -20,6 +20,7 @@ use crate::rpc::lp_commands::one_inch::rpcs::{one_inch_v6_0_classic_swap_contrac one_inch_v6_0_classic_swap_liquidity_sources_rpc, one_inch_v6_0_classic_swap_quote_rpc, one_inch_v6_0_classic_swap_tokens_rpc}; +use crate::rpc::lp_commands::priv_key::derive_priv_key_rpc; use crate::rpc::lp_commands::pubkey::*; use crate::rpc::lp_commands::tokens::get_token_info; use crate::rpc::lp_commands::tokens::{approve_token_rpc, get_token_allowance_rpc}; @@ -220,6 +221,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, enable_token::).await, "get_current_mtp" => handle_mmrpc(ctx, request, get_current_mtp_rpc).await, "get_enabled_coins" => handle_mmrpc(ctx, request, get_enabled_coins_rpc).await, + "derive_priv_key" => handle_mmrpc(ctx, request, derive_priv_key_rpc).await, "get_locked_amount" => handle_mmrpc(ctx, request, get_locked_amount_rpc).await, "get_mnemonic" => handle_mmrpc(ctx, request, get_mnemonic_rpc).await, "get_my_address" => handle_mmrpc(ctx, request, get_my_address).await, diff --git a/mm2src/mm2_main/src/rpc/lp_commands/mod.rs b/mm2src/mm2_main/src/rpc/lp_commands/mod.rs index 0826bbb648..c6c5f94fb2 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/mod.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod db_id; pub mod legacy; pub(crate) mod lr_swap; pub(crate) mod one_inch; +pub mod priv_key; pub(crate) mod pubkey; pub(crate) mod tokens; pub(crate) mod trezor; diff --git a/mm2src/mm2_main/src/rpc/lp_commands/priv_key.rs b/mm2src/mm2_main/src/rpc/lp_commands/priv_key.rs new file mode 100644 index 0000000000..71e324c819 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/priv_key.rs @@ -0,0 +1,35 @@ +use coins::lp_coinfind_any; +use coins::priv_key::{derive_priv_key, DerivePrivKeyError, DerivePrivKeyReq, DerivedPrivKey}; +use crypto::Bip44Chain; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct DerivePrivKeyRequest { + pub coin: String, + pub account_id: u32, + pub address_id: u32, + #[serde(default)] + pub chain: Option, +} + +pub async fn derive_priv_key_rpc(ctx: MmArc, req: DerivePrivKeyRequest) -> MmResult { + let coin = lp_coinfind_any(&ctx, &req.coin) + .await + .map_err(|e| DerivePrivKeyError::Internal { + reason: e.to_string(), + })? + .ok_or_else(|| DerivePrivKeyError::NoSuchCoin { + ticker: req.coin.clone(), + })? + .inner; + + let req = DerivePrivKeyReq { + account_id: req.account_id, + chain: req.chain, + address_id: req.address_id, + }; + + derive_priv_key(coin, &req).await +} 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 439467c334..34ff9d55fa 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -13,11 +13,11 @@ use mm2_test_helpers::electrums::*; #[cfg(all(not(target_arch = "wasm32"), not(feature = "zhtlc-native-tests")))] use mm2_test_helpers::for_tests::wait_check_stats_swap_status; use mm2_test_helpers::for_tests::{account_balance, btc_segwit_conf, btc_with_spv_conf, btc_with_sync_starting_header, - check_recent_swaps, delete_wallet, enable_qrc20, enable_utxo_v2_electrum, - eth_dev_conf, find_metrics_in_json, from_env_file, get_new_address, - get_shared_db_id, get_wallet_names, mm_spat, morty_conf, my_balance, rick_conf, - sign_message, start_swaps, tbtc_conf, tbtc_segwit_conf, tbtc_with_spv_conf, - test_qrc20_history_impl, tqrc20_conf, verify_message, + check_recent_swaps, delete_wallet, enable_eth_with_tokens_v2, enable_qrc20, + enable_utxo_v2_electrum, eth_dev_conf, find_metrics_in_json, from_env_file, + get_new_address, get_shared_db_id, get_wallet_names, mm_spat, morty_conf, + my_balance, rick_conf, sign_message, start_swaps, tbtc_conf, tbtc_segwit_conf, + tbtc_with_spv_conf, test_qrc20_history_impl, tqrc20_conf, verify_message, wait_for_swaps_finish_and_check_status, wait_till_history_has_records, MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, RaiiDump, DOC_ELECTRUM_ADDRS, ETH_MAINNET_NODES, ETH_MAINNET_SWAP_CONTRACT, ETH_SEPOLIA_NODES, @@ -1996,6 +1996,99 @@ fn test_show_priv_key() { ); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_derive_priv_key() { + let coins = json!([rick_conf(), eth_dev_conf()]); + + let mm = MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9998, + "myipaddr": env::var("BOB_TRADE_IP").unwrap(), + "rpcip": env::var("BOB_TRADE_IP").unwrap(), + "canbind": env::var("BOB_TRADE_PORT").unwrap().parse::().unwrap(), + "passphrase": "february soldier message acid member jump shadow walk novel impose puppy tornado", + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true, + "enable_hd": true + }), + "pass".into(), + None, + ) + .unwrap(); + + let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("Log path: {}", mm.log_path.display()); + + let enable_rick_res = block_on(enable_utxo_v2_electrum(&mm, "RICK", doc_electrums(), None, 60, None)); + log!("enable RICK: {:?}", enable_rick_res); + + let enable_eth_res = block_on(enable_eth_with_tokens_v2( + &mm, + "ETH", + &[], + ETH_SEPOLIA_SWAP_CONTRACT, + ETH_SEPOLIA_NODES, + 60, + None, + )); + log!("enable ETH: {:?}", enable_eth_res); + + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "derive_priv_key", + "params": { + "coin": "RICK", + "account_id": 0, + "address_id": 12 + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "!derive_priv_key: {}", rc.1); + let privkey: Json = json::from_str(&rc.1).unwrap(); + assert_eq!(privkey["result"]["coin"], "RICK"); + assert_eq!(privkey["result"]["address"], "RXJDtxUcmSZ8MQpFW7GMm8McMkK7349zV6"); + assert_eq!(privkey["result"]["derivation_path"], "m/44'/141'/0'/0/12"); + assert_eq!( + privkey["result"]["priv_key"], + "UrerqiGFWB9obJnKuDdscisN7feGcvGQG67MfUD1ni4VYMjXpvkJ" + ); + assert_eq!( + privkey["result"]["pub_key"], + "02a478f38a006e89f9667b3a6bf93c011ba2016d703f120d32f9691a025374afbf" + ); + + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "derive_priv_key", + "params": { + "coin": "ETH", + "account_id": 0, + "address_id": 3 + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "!derive_priv_key: {}", rc.1); + let privkey: Json = json::from_str(&rc.1).unwrap(); + assert_eq!(privkey["result"]["coin"], "ETH"); + assert_eq!( + privkey["result"]["address"], + "0x1e8B4aA6a8B8a376E0357504cF2ebC11Bc02288b" + ); + assert_eq!(privkey["result"]["derivation_path"], "m/44'/60'/0'/0/3"); + assert_eq!( + privkey["result"]["priv_key"], + "0x932ec93805200317394d6f63216791cf6052e6bb7412153f799c0b0660086e24" + ); + assert_eq!( + privkey["result"]["pub_key"], + "0x036b521bb1f9e845301f8bcb1f025151784ac2ea54b95fa50b9c491aced4a34c04" + ); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_electrum_and_enable_response() {