Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
6 changes: 6 additions & 0 deletions mm2src/coins/lp_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -2153,6 +2154,11 @@ pub trait MarketCoinOps {
/// Should burn part of dex fee coin
fn should_burn_dex_fee(&self) -> bool;

/// Returns the WIF prefix for the coin.
fn wif_prefix(&self) -> Option<u8> { None }

fn is_utxo(&self) -> bool { false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These shouldn't be defined in MarketCoinOps. I don't think we need to define them anywhere at all; we can simply derive the UTXO coin from MmCoinEnum.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 5641673

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

superceded by 236f9d0


fn is_trezor(&self) -> bool;
}

Expand Down
135 changes: 135 additions & 0 deletions mm2src/coins/priv_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use crate::hd_wallet::{HDAccountOps, HDWalletOps};
use crate::{CoinWithDerivationMethod, CoinWithPrivKeyPolicy, DerivationMethod, MarketCoinOps, MmCoin, PrivKeyPolicy};
use async_trait::async_trait;
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<Bip44Chain>,
pub address_id: u32,
}

#[derive(Debug, Display, Serialize, SerializeErrorType)]
#[serde(tag = "error_type", content = "error_data")]
pub enum DerivePrivKeyError {
#[display(fmt = "No such coin: {}", _0)]
NoSuchCoin(String),
#[display(fmt = "Coin {} doesn't support HD wallet derivation", _0)]
CoinDoesntSupportDerivation(String),
#[display(fmt = "Hardware/remote wallet doesn't allow exporting private keys")]
HwWalletNotAllowed,
#[display(fmt = "Internal error: {}", _0)]
Internal(String),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be more clear to use struct-like errors instead if tuple-like ones, e.g.,:

    NoSuchCoin {
        ticker: String,
    },
    Internal {
        reason: String,
    }

so we can know what are the inner values are about without having to look use-cases.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 221b9fe

}

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,
}
}
}

#[async_trait]
pub trait DerivePrivKeyV2: MmCoin + CoinWithPrivKeyPolicy + CoinWithDerivationMethod + Sized {
async fn derive_priv_key(&self, req: &DerivePrivKeyReq) -> Result<DerivedPrivKey, MmError<DerivePrivKeyError>>;
}

#[async_trait]
impl<Coin> DerivePrivKeyV2 for Coin
where
Coin: MmCoin + CoinWithPrivKeyPolicy + CoinWithDerivationMethod + MarketCoinOps + Sync,
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If derive_priv_key can take MmCoinEnum as a parameter, that would greatly simplify the implementation so we don't have to do all the generic-dances here (also in priv_key::derive_priv_key).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 236f9d0

async fn derive_priv_key(&self, req: &DerivePrivKeyReq) -> Result<DerivedPrivKey, MmError<DerivePrivKeyError>> {
match self.priv_key_policy() {
PrivKeyPolicy::Iguana(_) => MmError::err(DerivePrivKeyError::CoinDoesntSupportDerivation(
self.ticker().to_string(),
)),
PrivKeyPolicy::Trezor | PrivKeyPolicy::WalletConnect { .. } => {
MmError::err(DerivePrivKeyError::HwWalletNotAllowed)
},
PrivKeyPolicy::HDWallet { .. } => {
let hd_wallet = match self.derivation_method() {
DerivationMethod::HDWallet(hd_wallet) => hd_wallet,
_ => {
return MmError::err(DerivePrivKeyError::CoinDoesntSupportDerivation(
self.ticker().to_string(),
))
},
};

let account = hd_wallet
.get_account(req.account_id)
.await
.ok_or_else(|| DerivePrivKeyError::Internal(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 = self
.priv_key_policy()
.hd_wallet_derived_priv_key_or_err(&path_to_address)
.map_err(|e| DerivePrivKeyError::Internal(format!("Error deriving secret key: {}", e)))?;

let private = Private {
prefix: self.wif_prefix().unwrap_or(0),
secret: secret_key.into(),
compressed: true,
checksum_type: Default::default(),
};

let key_pair = KeyPair::from_private(private)
.map_err(|e| DerivePrivKeyError::Internal(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("Error converting pubkey slice to array".to_string()))?;

let address = self
.address_from_pubkey(&pubkey.into())
.map_err(|e| DerivePrivKeyError::Internal(format!("Error getting address from pubkey: {}", e)))?;

let priv_key_wif = key_pair.private().to_string();
let priv_key_hex = format!("0x{}", hex::encode(key_pair.private_bytes()));

let priv_key = if self.is_utxo() { priv_key_wif } else { priv_key_hex };

let response = DerivedPrivKey {
coin: self.ticker().to_string(),
address: address.to_string(),
derivation_path: path_to_address.to_string(),
priv_key,
pub_key: if self.is_utxo() {
hex::encode(pubkey)
} else {
format!("0x{}", hex::encode(pubkey))
},
};
Ok(response)
},
}
}
}
4 changes: 4 additions & 0 deletions mm2src/coins/utxo/utxo_standard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,10 @@ impl MarketCoinOps for UtxoStandardCoin {

fn should_burn_dex_fee(&self) -> bool { utxo_common::should_burn_dex_fee() }

fn wif_prefix(&self) -> Option<u8> { Some(self.as_ref().conf.wif_prefix) }

fn is_utxo(&self) -> bool { true }

fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() }
}

Expand Down
2 changes: 2 additions & 0 deletions mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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};
Expand Down Expand Up @@ -220,6 +221,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult<Re
"enable_tendermint_token" => handle_mmrpc(ctx, request, enable_token::<TendermintToken>).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).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,
Expand Down
1 change: 1 addition & 0 deletions mm2src/mm2_main/src/rpc/lp_commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
39 changes: 39 additions & 0 deletions mm2src/mm2_main/src/rpc/lp_commands/priv_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use coins::lp_coinfind_any;
use coins::priv_key::{DerivePrivKeyError, DerivePrivKeyReq, DerivePrivKeyV2, DerivedPrivKey};
use coins::MmCoinEnum;
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<Bip44Chain>,
}

pub async fn derive_priv_key(ctx: MmArc, req: DerivePrivKeyRequest) -> MmResult<DerivedPrivKey, DerivePrivKeyError> {
let coin_ticker = req.coin.clone();
let coin = lp_coinfind_any(&ctx, &req.coin)
.await
.map_err(|e| DerivePrivKeyError::Internal(e.to_string()))?
.ok_or_else(|| DerivePrivKeyError::NoSuchCoin(req.coin.clone()))?
.inner;

let req = DerivePrivKeyReq {
account_id: req.account_id,
chain: req.chain,
address_id: req.address_id,
};

match coin {
MmCoinEnum::UtxoCoin(c) => c.derive_priv_key(&req).await,
MmCoinEnum::Bch(c) => c.derive_priv_key(&req).await,
MmCoinEnum::QtumCoin(c) => c.derive_priv_key(&req).await,
MmCoinEnum::EthCoin(c) => c.derive_priv_key(&req).await,
_ => MmError::err(DerivePrivKeyError::CoinDoesntSupportDerivation(coin_ticker)),
}
}
103 changes: 98 additions & 5 deletions mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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") .ok(),
"rpcip": env::var ("BOB_TRADE_IP") .ok(),
"canbind": env::var ("BOB_TRADE_PORT") .ok().map (|s| s.parse::<i64>().unwrap()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove white spaces?

We should also unwrap instead of calling ok to fail the test when BOB_TRADE_IP and BOB_TRADE_PORT isn't present.

@smk762 smk762 Jul 22, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 79e632c

FWIW, the rest of the file uses .ok() and similar white spacing throughout. Should I fix up those also while I'm here?

"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() {
Expand Down
Loading