-
Notifications
You must be signed in to change notification settings - Fork 118
Adds derive_priv_key v2 RPC for HD wallets
#2541
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
Changes from 4 commits
a085b28
d9483cb
5f878a5
3619acd
5641673
221b9fe
236f9d0
79e632c
e640ba0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.,: so we can know what are the inner values are about without having to look use-cases.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| }, | ||
| } | ||
| } | ||
| } | ||
| 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)), | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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") .ok(), | ||
| "rpcip": env::var ("BOB_TRADE_IP") .ok(), | ||
| "canbind": env::var ("BOB_TRADE_PORT") .ok().map (|s| s.parse::<i64>().unwrap()), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you remove white spaces? We should also
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 79e632c FWIW, the rest of the file uses |
||
| "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() { | ||
|
|
||
There was a problem hiding this comment.
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 fromMmCoinEnum.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 5641673
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
superceded by 236f9d0