diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 074c496db6..2fcf9eef45 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -120,7 +120,6 @@ use web3::{self, Web3}; cfg_wasm32! { use crypto::MetamaskArc; - use ethereum_types::H520; use mm2_metamask::MetamaskError; use web3::types::TransactionRequest; } @@ -866,9 +865,7 @@ pub enum EthPrivKeyBuildPolicy { Metamask(MetamaskArc), Trezor, WalletConnect { - address: Address, - public_key_uncompressed: H520, - session_topic: String, + session_topic: kdf_walletconnect::WcTopic, }, } @@ -889,11 +886,14 @@ impl EthPrivKeyBuildPolicy { } impl From for EthPrivKeyBuildPolicy { - fn from(policy: PrivKeyBuildPolicy) -> Self { + fn from(policy: PrivKeyBuildPolicy) -> EthPrivKeyBuildPolicy { match policy { PrivKeyBuildPolicy::IguanaPrivKey(iguana) => EthPrivKeyBuildPolicy::IguanaPrivKey(iguana), PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd), PrivKeyBuildPolicy::Trezor => EthPrivKeyBuildPolicy::Trezor, + PrivKeyBuildPolicy::WalletConnect { session_topic } => { + EthPrivKeyBuildPolicy::WalletConnect { session_topic } + }, } } } @@ -2992,8 +2992,7 @@ async fn sign_raw_eth_tx(coin: &EthCoin, args: &SignEthTransactionParams) -> Raw .map_to_mm(|err| RawTransactionError::TransactionError(err.get_plain_text_format())) }, EthPrivKeyPolicy::WalletConnect { .. } => { - // NOTE: doesn't work with wallets that doesn't support `eth_signTransaction`. - // e.g Metamask + // NOTE: doesn't work with wallets that doesn't support `eth_signTransaction`. e.g TrustWallet let wc = { let ctx = MmArc::from_weak(&coin.ctx).expect("No context"); WalletConnectCtx::from_ctx(&ctx) @@ -6564,8 +6563,15 @@ pub async fn eth_coin_from_conf_and_request( } } - // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. - let priv_key_policy = From::from(priv_key_policy); + // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy`. + let priv_key_policy = match priv_key_policy { + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => EthPrivKeyBuildPolicy::IguanaPrivKey(iguana), + PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd), + PrivKeyBuildPolicy::Trezor => EthPrivKeyBuildPolicy::Trezor, + PrivKeyBuildPolicy::WalletConnect { .. } => { + return ERR!("WalletConnect private key policy is not supported for legacy ETH coin activation"); + }, + }; let mut urls: Vec = try_s!(json::from_value(req["urls"].clone())); if urls.is_empty() { @@ -6590,8 +6596,9 @@ pub async fn eth_coin_from_conf_and_request( req["path_to_address"].clone() )) .unwrap_or_default(); - let (key_pair, derivation_method) = - try_s!(build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, &path_to_address, None).await); + let (key_pair, derivation_method) = try_s!( + build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, &path_to_address, None, None).await + ); let mut web3_instances = vec![]; let event_handlers = rpc_event_handlers_for_eth_transport(ctx, ticker.to_string()); @@ -6869,7 +6876,7 @@ pub async fn get_eth_address( .into(); let (_, derivation_method) = - build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, path_to_address, None) + build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, path_to_address, None, None) .await .map_mm_err()?; let my_address = derivation_method.single_addr_or_err().await.map_mm_err()?; diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 96eb00a8a1..7a2172c84b 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -1,5 +1,6 @@ use super::*; use crate::eth::erc20::{get_enabled_erc20_by_platform_and_contract, get_token_decimals}; +use crate::eth::wallet_connect::eth_request_wc_personal_sign; use crate::eth::web3_transport::http_transport::HttpTransport; use crate::hd_wallet::{ load_hd_accounts_from_storage, HDAccountsMutex, HDPathAccountToAddressId, HDWalletCoinStorage, @@ -17,6 +18,7 @@ use crypto::{trezor::TrezorError, Bip32Error, CryptoCtxError, HwError}; use enum_derives::EnumFromTrait; use ethereum_types::H264; use kdf_walletconnect::error::WalletConnectError; +use kdf_walletconnect::WcTopic; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; @@ -201,7 +203,7 @@ pub enum EthPrivKeyActivationPolicy { #[cfg(target_arch = "wasm32")] Metamask, WalletConnect { - session_topic: String, + session_topic: WcTopic, }, } @@ -681,6 +683,7 @@ pub async fn eth_coin_from_conf_and_request_v2( priv_key_build_policy, &req.path_to_address, req.gap_limit, + Some(&chain_spec), ) .await?; @@ -781,6 +784,7 @@ pub(crate) async fn build_address_and_priv_key_policy( priv_key_build_policy: EthPrivKeyBuildPolicy, path_to_address: &HDPathAccountToAddressId, gap_limit: Option, + chain_spec: Option<&ChainSpec>, ) -> MmResult<(EthPrivKeyPolicy, EthDerivationMethod), EthActivationV2Error> { match priv_key_build_policy { EthPrivKeyBuildPolicy::IguanaPrivKey(iguana) => { @@ -875,11 +879,18 @@ pub(crate) async fn build_address_and_priv_key_policy( DerivationMethod::SingleAddress(address), )) }, - EthPrivKeyBuildPolicy::WalletConnect { - address, - public_key_uncompressed, - session_topic, - } => { + EthPrivKeyBuildPolicy::WalletConnect { session_topic } => { + let wc = WalletConnectCtx::from_ctx(ctx).map_err(|e| { + EthActivationV2Error::WalletConnectError(format!("Failed to get WalletConnect context: {e}")) + })?; + let chain_spec = chain_spec.ok_or(EthActivationV2Error::ChainIdNotSet)?; + let chain_id = chain_spec.chain_id().ok_or(EthActivationV2Error::UnsupportedChain { + chain: chain_spec.kind().to_string(), + feature: "WalletConnect".to_string(), + })?; + let (public_key_uncompressed, address) = eth_request_wc_personal_sign(&wc, &session_topic, chain_id) + .await + .mm_err(|err| EthActivationV2Error::WalletConnectError(err.to_string()))?; let public_key = compress_public_key(public_key_uncompressed)?; Ok(( EthPrivKeyPolicy::WalletConnect { diff --git a/mm2src/coins/eth/wallet_connect.rs b/mm2src/coins/eth/wallet_connect.rs index 2ce06b1275..b5d45ec742 100644 --- a/mm2src/coins/eth/wallet_connect.rs +++ b/mm2src/coins/eth/wallet_connect.rs @@ -16,7 +16,7 @@ use ethereum_types::{Address, Public, H160, H520, U256}; use ethkey::{public_to_address, Message, Signature}; use kdf_walletconnect::chain::{WcChainId, WcRequestMethods}; use kdf_walletconnect::error::WalletConnectError; -use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps, WcTopic}; use mm2_err_handle::prelude::*; use secp256k1::recovery::{RecoverableSignature, RecoveryId}; use secp256k1::{PublicKey, Secp256k1}; @@ -180,7 +180,7 @@ impl WalletConnectOps for EthCoin { Ok((signed_tx, tx_hex)) } - fn session_topic(&self) -> Result<&str, Self::Error> { + fn session_topic(&self) -> Result<&WcTopic, Self::Error> { if let EthPrivKeyPolicy::WalletConnect { ref session_topic, .. } = &self.priv_key_policy { return Ok(session_topic); } @@ -194,7 +194,7 @@ impl WalletConnectOps for EthCoin { pub async fn eth_request_wc_personal_sign( wc: &WalletConnectCtx, - session_topic: &str, + session_topic: &WcTopic, chain_id: u64, ) -> MmResult<(H520, Address), EthWalletConnectError> { let chain_id = WcChainId::new_eip155(chain_id.to_string()); @@ -211,7 +211,7 @@ pub async fn eth_request_wc_personal_sign( json!(&[&message_hex, &account_str]) }; let data = wc - .send_session_request_and_wait::(session_topic, &chain_id, WcRequestMethods::PersonalSign, params) + .send_session_request_and_wait::(session_topic, &chain_id, WcRequestMethods::EthPersonalSign, params) .await .map_mm_err()?; diff --git a/mm2src/coins/hd_wallet/pubkey.rs b/mm2src/coins/hd_wallet/pubkey.rs index ca6590338a..84b3c2acd9 100644 --- a/mm2src/coins/hd_wallet/pubkey.rs +++ b/mm2src/coins/hd_wallet/pubkey.rs @@ -136,7 +136,7 @@ where .or_mm_err(|| HDExtractPubkeyError::HwContextNotInitialized)?; let trezor_message_type = match coin_protocol { - CoinProtocol::UTXO => TrezorMessageType::Bitcoin, + CoinProtocol::UTXO { .. } => TrezorMessageType::Bitcoin, CoinProtocol::QTUM => TrezorMessageType::Bitcoin, CoinProtocol::ETH { .. } | CoinProtocol::ERC20 { .. } => TrezorMessageType::Ethereum, _ => return Err(MmError::new(HDExtractPubkeyError::CoinDoesntSupportTrezor)), diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 54fa128a14..5e1ecb47e7 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -4300,11 +4300,14 @@ impl CoinsContext { } /// This enum is used in coin activation requests. -#[derive(Copy, Clone, Debug, Deserialize, Serialize, Default)] +#[derive(Clone, Debug, Deserialize, Serialize, Default)] pub enum PrivKeyActivationPolicy { #[default] ContextPrivKey, Trezor, + WalletConnect { + session_topic: kdf_walletconnect::WcTopic, + }, } impl PrivKeyActivationPolicy { @@ -4362,10 +4365,16 @@ pub enum PrivKeyPolicy { /// - `public_key`: Compressed public key, represented as [H264]. /// - `public_key_uncompressed`: Uncompressed public key, represented as [H520]. /// - `session_topic`: WalletConnect session that was used to activate this coin. + // TODO: We want to have different variants of WalletConnect policy for different coin types: + // - ETH uses the structure found here. + // - Tendermint doesn't use this variant all together. Tendermint generalizes one level on top of PrivKeyPolicy by having a different activation policy + // structure that is either Priv(PrivKeyPolicy) or Pubkey(PublicKey) and when activated via wallet connect it uses the Pubkey(PublicKey) variant. + // - UTXO coins on the otherhand need to keep a list of all the addresses activated in the wallet and not just a single account. + // - Note: We need to have a way to select which account and address are the active ones (WalletConnect just spams us with all the addresses in every account). WalletConnect { public_key: H264, public_key_uncompressed: H520, - session_topic: String, + session_topic: kdf_walletconnect::WcTopic, }, } @@ -4508,6 +4517,7 @@ pub enum PrivKeyBuildPolicy { IguanaPrivKey(IguanaPrivKey), GlobalHDAccount(GlobalHDAccountArc), Trezor, + WalletConnect { session_topic: kdf_walletconnect::WcTopic }, } impl PrivKeyBuildPolicy { @@ -4683,11 +4693,21 @@ pub trait IguanaBalanceOps { async fn iguana_balances(&self) -> BalanceResult; } +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +/// Information about the UTXO protocol used by a coin. +pub struct UtxoProtocolInfo { + /// A CAIP-2 compliant chain ID. Starts with `b122:` + /// This is used to identify the blockchain when using WalletConnect. + /// https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-4.md + chain_id: String, +} + #[allow(clippy::upper_case_acronyms)] #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "protocol_data")] pub enum CoinProtocol { - UTXO, + // TODO: Nest this option deep into the innert struct fields when more fields are added to the UTXO protocol info. + UTXO(Option), QTUM, QRC20 { platform: String, @@ -4764,7 +4784,7 @@ impl CoinProtocol { CoinProtocol::TENDERMINTTOKEN(info) => Some(&info.platform), #[cfg(not(target_arch = "wasm32"))] CoinProtocol::LIGHTNING { platform, .. } => Some(platform), - CoinProtocol::UTXO + CoinProtocol::UTXO { .. } | CoinProtocol::QTUM | CoinProtocol::ETH { .. } | CoinProtocol::TRX { .. } @@ -4783,7 +4803,7 @@ impl CoinProtocol { Some(contract_address) }, CoinProtocol::SLPTOKEN { .. } - | CoinProtocol::UTXO + | CoinProtocol::UTXO { .. } | CoinProtocol::QTUM | CoinProtocol::ETH { .. } | CoinProtocol::TRX { .. } @@ -5075,7 +5095,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result { + CoinProtocol::UTXO { .. } => { let params = try_s!(UtxoActivationParams::from_legacy_req(req)); try_s!(utxo_standard_coin_with_policy(ctx, ticker, &coins_en, ¶ms, priv_key_policy).await).into() }, @@ -5748,7 +5768,7 @@ pub fn address_by_coin_conf_and_pubkey_str( }, // Todo: implement TRX address generation CoinProtocol::TRX { .. } => ERR!("TRX address generation is not implemented yet"), - CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { + CoinProtocol::UTXO { .. } | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format) }, CoinProtocol::SLPTOKEN { platform, .. } => { diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 9bfa78c19b..d5b0a7d110 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -12,8 +12,8 @@ use crate::utxo::rpc_clients::{ #[cfg(not(target_arch = "wasm32"))] use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; use crate::utxo::utxo_builder::{ - UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, - UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder, + build_utxo_fields_with_global_hd, build_utxo_fields_with_iguana_priv_key, UtxoCoinBuildError, UtxoCoinBuildResult, + UtxoCoinBuilder, UtxoCoinBuilderCommonOps, }; use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_utxo_inputs_signed_by_pub, UtxoTxBuilder}; use crate::utxo::{ @@ -35,7 +35,7 @@ use crate::{ WithdrawResult, }; use async_trait::async_trait; -use bitcrypto::{dhash160, sha256}; +use bitcrypto::{dhash160, sha256, sign_message_hash}; use chain::TransactionOutput; use common::executor::{AbortableSystem, AbortedError, Timer}; use common::jsonrpc_client::{JsonRpcClient, JsonRpcRequest, RpcRes}; @@ -299,14 +299,6 @@ impl UtxoCoinBuilderCommonOps for Qrc20CoinBuilder<'_> { } } -impl UtxoFieldsWithIguanaSecretBuilder for Qrc20CoinBuilder<'_> {} - -impl UtxoFieldsWithGlobalHDBuilder for Qrc20CoinBuilder<'_> {} - -/// Although, `Qrc20Coin` doesn't support [`PrivKeyBuildPolicy::Trezor`] yet, -/// `UtxoCoinBuilder` trait requires `UtxoFieldsWithHardwareWalletBuilder` to be implemented. -impl UtxoFieldsWithHardwareWalletBuilder for Qrc20CoinBuilder<'_> {} - #[async_trait] impl UtxoCoinBuilder for Qrc20CoinBuilder<'_> { type ResultCoin = Qrc20Coin; @@ -316,17 +308,27 @@ impl UtxoCoinBuilder for Qrc20CoinBuilder<'_> { self.priv_key_policy.clone() } - async fn build(self) -> MmResult { - let utxo = match self.priv_key_policy() { - PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => self.build_utxo_fields_with_iguana_secret(priv_key).await?, + async fn build_utxo_fields(&self) -> UtxoCoinBuildResult { + match self.priv_key_policy() { + PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => build_utxo_fields_with_iguana_priv_key(self, priv_key).await, PrivKeyBuildPolicy::GlobalHDAccount(global_hd_ctx) => { - self.build_utxo_fields_with_global_hd(global_hd_ctx).await? + build_utxo_fields_with_global_hd(self, global_hd_ctx).await }, PrivKeyBuildPolicy::Trezor => { let priv_key_err = PrivKeyPolicyNotAllowed::HardwareWalletNotSupported; - return MmError::err(UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err)); + MmError::err(UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err)) }, - }; + PrivKeyBuildPolicy::WalletConnect { .. } => { + let priv_key_err = PrivKeyPolicyNotAllowed::UnsupportedMethod( + "WalletConnect is not available for QRC20 coin".to_string(), + ); + MmError::err(UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err)) + }, + } + } + + async fn build(self) -> MmResult { + let utxo = self.build_utxo_fields().await?; let inner = Qrc20CoinFields { utxo, @@ -1104,7 +1106,8 @@ impl MarketCoinOps for Qrc20Coin { } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { - utxo_common::sign_message_hash(self.as_ref(), message) + let prefix = self.as_ref().conf.sign_message_prefix.as_ref()?; + Some(sign_message_hash(prefix, message)) } fn sign_message(&self, message: &str, address: Option) -> SignatureResult { diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index df60d665ad..c77b38344f 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -310,7 +310,8 @@ impl RpcTask for InitCreateAccountTask { self.task_state.clone(), task_handle, utxo.is_trezor(), - CoinProtocol::UTXO, + // Note that the actual UtxoProtocolInfo isn't needed by trezor XPUB extractor. + CoinProtocol::UTXO(Default::default()), ) .await?, )), diff --git a/mm2src/coins/rpc_command/offline_keys.rs b/mm2src/coins/rpc_command/offline_keys.rs index 8ee3ae3f97..3a306503b4 100644 --- a/mm2src/coins/rpc_command/offline_keys.rs +++ b/mm2src/coins/rpc_command/offline_keys.rs @@ -141,7 +141,7 @@ fn extract_prefix_values( match protocol { CoinProtocol::ETH { .. } | CoinProtocol::ERC20 { .. } | CoinProtocol::NFT { .. } => Ok(None), - CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { + CoinProtocol::UTXO { .. } | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { let wif_type = coin_conf["wiftype"] .as_u64() .ok_or_else(|| OfflineKeysError::MissingPrefixValue { diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index ae2868282b..1c39980270 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -399,8 +399,8 @@ impl RpcCommonOps for TendermintCoin { #[derive(PartialEq)] pub enum TendermintWalletConnectionType { - Wc(String), - WcLedger(String), + Wc(kdf_walletconnect::WcTopic), + WcLedger(kdf_walletconnect::WcTopic), KeplrLedger, Keplr, Native, @@ -4301,6 +4301,15 @@ pub fn tendermint_priv_key_policy( kind, }) }, + PrivKeyBuildPolicy::WalletConnect { .. } => { + let kind = TendermintInitErrorKind::PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed::UnsupportedMethod( + "Cannot use WalletConnect to get TendermintPrivKeyPolicy".to_string(), + )); + MmError::err(TendermintInitError { + ticker: ticker.to_string(), + kind, + }) + }, } } diff --git a/mm2src/coins/tendermint/wallet_connect.rs b/mm2src/coins/tendermint/wallet_connect.rs index d3242d4f77..e8df678ce9 100644 --- a/mm2src/coins/tendermint/wallet_connect.rs +++ b/mm2src/coins/tendermint/wallet_connect.rs @@ -5,7 +5,7 @@ use cosmrs::proto::cosmos::tx::v1beta1::TxRaw; use kdf_walletconnect::chain::WcChainId; use kdf_walletconnect::error::WalletConnectError; use kdf_walletconnect::WalletConnectOps; -use kdf_walletconnect::{chain::WcRequestMethods, WalletConnectCtx}; +use kdf_walletconnect::{chain::WcRequestMethods, WalletConnectCtx, WcTopic}; use mm2_err_handle::prelude::*; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -121,7 +121,7 @@ impl WalletConnectOps for TendermintCoin { todo!() } - fn session_topic(&self) -> Result<&str, Self::Error> { + fn session_topic(&self) -> Result<&WcTopic, Self::Error> { match self.wallet_type { TendermintWalletConnectionType::WcLedger(ref session_topic) | TendermintWalletConnectionType::Wc(ref session_topic) => Ok(session_topic), @@ -135,7 +135,7 @@ impl WalletConnectOps for TendermintCoin { pub async fn cosmos_get_accounts_impl( wc: &WalletConnectCtx, - session_topic: &str, + session_topic: &WcTopic, chain_id: &str, ) -> MmResult { let chain_id = WcChainId::new_cosmos(chain_id.to_string()); diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 00810d742e..a7d4774ac8 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -41,6 +41,7 @@ pub mod utxo_hd_wallet; pub mod utxo_standard; pub mod utxo_tx_history_v2; pub mod utxo_withdraw; +pub mod wallet_connect; use async_trait::async_trait; #[cfg(not(target_arch = "wasm32"))] diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index e580b0d8f2..9b03f40b8a 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -29,6 +29,7 @@ use crate::{ VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, }; +use bitcrypto::sign_message_hash; use common::executor::{AbortableSystem, AbortedError}; use common::log::warn; use derive_more::Display; @@ -1185,7 +1186,8 @@ impl MarketCoinOps for BchCoin { } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { - utxo_common::sign_message_hash(self.as_ref(), message) + let prefix = self.as_ref().conf.sign_message_prefix.as_ref()?; + Some(sign_message_hash(prefix, message)) } fn sign_message(&self, message: &str, address: Option) -> SignatureResult { diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 7a24a58353..2a889801df 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -24,10 +24,7 @@ use crate::rpc_command::init_scan_for_new_addresses::{ }; use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandleShared}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; -use crate::utxo::utxo_builder::{ - MergeUtxoArcOps, UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, - UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder, -}; +use crate::utxo::utxo_builder::{MergeUtxoArcOps, UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps}; use crate::utxo::utxo_hd_wallet::{UtxoHDAccount, UtxoHDAddress}; use crate::utxo::utxo_tx_history_v2::{ UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps, @@ -44,6 +41,7 @@ use crate::{ WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, }; +use bitcrypto::sign_message_hash; use common::executor::{AbortableSystem, AbortedError}; use ethereum_types::H160; use futures::{FutureExt, TryFutureExt}; @@ -231,12 +229,6 @@ impl UtxoCoinBuilderCommonOps for QtumCoinBuilder<'_> { } } -impl UtxoFieldsWithIguanaSecretBuilder for QtumCoinBuilder<'_> {} - -impl UtxoFieldsWithGlobalHDBuilder for QtumCoinBuilder<'_> {} - -impl UtxoFieldsWithHardwareWalletBuilder for QtumCoinBuilder<'_> {} - #[async_trait] impl UtxoCoinBuilder for QtumCoinBuilder<'_> { type ResultCoin = QtumCoin; @@ -822,7 +814,8 @@ impl MarketCoinOps for QtumCoin { } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { - utxo_common::sign_message_hash(self.as_ref(), message) + let prefix = self.as_ref().conf.sign_message_prefix.as_ref()?; + Some(sign_message_hash(prefix, message)) } fn sign_message(&self, message: &str, address: Option) -> SignatureResult { diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 0c3c93bb81..40711282fd 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -30,7 +30,7 @@ use crate::{ use async_trait::async_trait; use base64::engine::general_purpose::STANDARD; use base64::Engine; -use bitcrypto::dhash160; +use bitcrypto::{dhash160, sign_message_hash}; use chain::constants::SEQUENCE_FINAL; use chain::{OutPoint, TransactionOutput}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}; @@ -1196,7 +1196,8 @@ impl MarketCoinOps for SlpToken { } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { - utxo_common::sign_message_hash(self.as_ref(), message) + let prefix = self.as_ref().conf.sign_message_prefix.as_ref()?; + Some(sign_message_hash(prefix, message)) } fn sign_message(&self, message: &str, address: Option) -> SignatureResult { diff --git a/mm2src/coins/utxo/utxo_builder/mod.rs b/mm2src/coins/utxo/utxo_builder/mod.rs index 1b66bd0621..16f08737bc 100644 --- a/mm2src/coins/utxo/utxo_builder/mod.rs +++ b/mm2src/coins/utxo/utxo_builder/mod.rs @@ -4,8 +4,8 @@ mod utxo_conf_builder; pub use utxo_arc_builder::{MergeUtxoArcOps, UtxoArcBuilder}; pub use utxo_coin_builder::{ - UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, - UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder, DAY_IN_SECONDS, + build_utxo_fields_with_global_hd, build_utxo_fields_with_iguana_priv_key, UtxoCoinBuildError, UtxoCoinBuildResult, + UtxoCoinBuilder, UtxoCoinBuilderCommonOps, DAY_IN_SECONDS, }; pub use utxo_conf_builder::{UtxoConfBuilder, UtxoConfError, UtxoConfResult}; diff --git a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs index 683af299a8..1ad9796787 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs @@ -1,10 +1,7 @@ use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientImpl, UtxoJsonRpcClientInfo, UtxoRpcClientEnum}; use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; -use crate::utxo::utxo_builder::{ - UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, - UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder, -}; +use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps}; use crate::utxo::{ generate_and_send_tx, FeePolicy, GetUtxoListOps, UtxoArc, UtxoCommonOps, UtxoSyncStatusLoopHandle, UtxoWeak, }; @@ -90,18 +87,6 @@ where } } -impl UtxoFieldsWithIguanaSecretBuilder for UtxoArcBuilder<'_, F, T> where - F: Fn(UtxoArc) -> T + Send + Sync + 'static -{ -} - -impl UtxoFieldsWithGlobalHDBuilder for UtxoArcBuilder<'_, F, T> where F: Fn(UtxoArc) -> T + Send + Sync + 'static {} - -impl UtxoFieldsWithHardwareWalletBuilder for UtxoArcBuilder<'_, F, T> where - F: Fn(UtxoArc) -> T + Send + Sync + 'static -{ -} - #[async_trait] impl UtxoCoinBuilder for UtxoArcBuilder<'_, F, T> where diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index ee671fecf4..fa54623322 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -8,31 +8,41 @@ use crate::utxo::rpc_clients::{ use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::utxo_conf_builder::{UtxoConfBuilder, UtxoConfError, UtxoFeeConfig}; +use crate::utxo::wallet_connect::{get_pubkey_via_wallatconnect_signature, get_walletconnect_address}; use crate::utxo::{ output_script, ElectrumBuilderArgs, FeeRate, RecentlySpentOutPoints, UtxoCoinConf, UtxoCoinFields, UtxoHDWallet, UtxoRpcMode, UtxoSyncStatus, UtxoSyncStatusLoopHandle, UTXO_DUST_AMOUNT, }; use crate::{ - BlockchainNetwork, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, PrivKeyBuildPolicy, - PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, SharableRpcTransportEventHandler, UtxoActivationParams, + BlockchainNetwork, CoinProtocol, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, + PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, SharableRpcTransportEventHandler, + UtxoActivationParams, }; use async_trait::async_trait; use chain::TxHashAlgo; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}; use common::now_sec; -use crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, StandardHDPathError}; +use crypto::{ + Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HDPathToCoin, HwWalletType, StandardHDPath, + StandardHDPathError, +}; use derive_more::Display; use futures::channel::mpsc::{channel, Receiver as AsyncReceiver}; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; -pub use keys::{Address, AddressBuilder, AddressFormat as UtxoAddressFormat, KeyPair, Private}; +use kdf_walletconnect::chain::WcChainId; +use kdf_walletconnect::error::WalletConnectError; +use kdf_walletconnect::{WalletConnectCtx, WcTopic}; +pub use keys::{Address, AddressBuilder, AddressFormat as UtxoAddressFormat, KeyPair, Private, Public}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use primitives::hash::H160; +use secp256k1::PublicKey; use serde_json::{self as json, Value as Json}; use spv_validation::conf::SPVConf; use spv_validation::helpers_validation::SPVError; use spv_validation::storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; +use std::convert::TryFrom; +use std::str::FromStr; use std::sync::Mutex; cfg_native! { @@ -83,6 +93,7 @@ pub enum UtxoCoinBuildError { mode: String, }, InvalidPathToAddress(String), + WalletConnectError(WalletConnectError), } impl From for UtxoCoinBuildError { @@ -134,10 +145,14 @@ impl From for UtxoCoinBuildError { } } +impl From for UtxoCoinBuildError { + fn from(e: WalletConnectError) -> Self { + UtxoCoinBuildError::WalletConnectError(e) + } +} + #[async_trait] -pub trait UtxoCoinBuilder: - UtxoFieldsWithIguanaSecretBuilder + UtxoFieldsWithGlobalHDBuilder + UtxoFieldsWithHardwareWalletBuilder -{ +pub trait UtxoCoinBuilder: UtxoCoinBuilderCommonOps { type ResultCoin; type Error: NotMmError; @@ -147,112 +162,195 @@ pub trait UtxoCoinBuilder: async fn build_utxo_fields(&self) -> UtxoCoinBuildResult { match self.priv_key_policy() { - PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => self.build_utxo_fields_with_iguana_secret(priv_key).await, + PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => build_utxo_fields_with_iguana_priv_key(self, priv_key).await, PrivKeyBuildPolicy::GlobalHDAccount(global_hd_ctx) => { - self.build_utxo_fields_with_global_hd(global_hd_ctx).await + build_utxo_fields_with_global_hd(self, global_hd_ctx).await + }, + PrivKeyBuildPolicy::Trezor => build_utxo_fields_with_trezor(self).await, + PrivKeyBuildPolicy::WalletConnect { session_topic } => { + build_utxo_fields_with_walletconnect(self, &session_topic).await }, - PrivKeyBuildPolicy::Trezor => self.build_utxo_fields_with_trezor().await, } } } -#[async_trait] -pub trait UtxoFieldsWithIguanaSecretBuilder: UtxoCoinBuilderCommonOps { - async fn build_utxo_fields_with_iguana_secret( - &self, - priv_key: IguanaPrivKey, - ) -> UtxoCoinBuildResult { - let conf = UtxoConfBuilder::new(self.conf(), self.activation_params(), self.ticker()) - .build() - .map_mm_err()?; - let private = Private { - prefix: conf.wif_prefix, - secret: priv_key, - compressed: true, - checksum_type: conf.checksum_type, - }; - let key_pair = KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; - let priv_key_policy = PrivKeyPolicy::Iguana(key_pair); - let addr_format = self.address_format()?; - let my_address = AddressBuilder::new( - addr_format, - conf.checksum_type, - conf.address_prefixes.clone(), - conf.bech32_hrp.clone(), - ) - .as_pkh_from_pk(*key_pair.public()) +pub async fn build_utxo_fields_with_iguana_priv_key( + builder: &Builder, + priv_key: IguanaPrivKey, +) -> UtxoCoinBuildResult +where + Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, +{ + let conf = UtxoConfBuilder::new(builder.conf(), builder.activation_params(), builder.ticker()) .build() - .map_to_mm(UtxoCoinBuildError::Internal)?; - let derivation_method = DerivationMethod::SingleAddress(my_address); - build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy, derivation_method).await - } + .map_mm_err()?; + + let private = Private { + prefix: conf.wif_prefix, + secret: priv_key, + compressed: true, + checksum_type: conf.checksum_type, + }; + + let key_pair = KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; + let priv_key_policy = PrivKeyPolicy::Iguana(key_pair); + + let my_address = AddressBuilder::new( + builder.address_format()?, + conf.checksum_type, + conf.address_prefixes.clone(), + conf.bech32_hrp.clone(), + ) + .as_pkh_from_pk(*key_pair.public()) + .build() + .map_to_mm(UtxoCoinBuildError::Internal)?; + + let derivation_method = DerivationMethod::SingleAddress(my_address); + build_utxo_coin_fields_with_conf_and_policy(builder, conf, priv_key_policy, derivation_method).await } -#[async_trait] -pub trait UtxoFieldsWithGlobalHDBuilder: UtxoCoinBuilderCommonOps { - async fn build_utxo_fields_with_global_hd( - &self, - global_hd_ctx: GlobalHDAccountArc, - ) -> UtxoCoinBuildResult { - let conf = UtxoConfBuilder::new(self.conf(), self.activation_params(), self.ticker()) - .build() - .map_mm_err()?; +pub async fn build_utxo_fields_with_global_hd( + builder: &Builder, + global_hd_ctx: GlobalHDAccountArc, +) -> UtxoCoinBuildResult +where + Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, +{ + let conf = UtxoConfBuilder::new(builder.conf(), builder.activation_params(), builder.ticker()) + .build() + .map_mm_err()?; - let path_to_address = self.activation_params().path_to_address; - let path_to_coin = conf - .derivation_path - .as_ref() - .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet) + let path_to_address = builder.activation_params().path_to_address; + + let path_to_coin = conf + .derivation_path + .as_ref() + .ok_or(UtxoConfError::DerivationPathIsNotSet)?; + + let derivation_path = path_to_address + .to_derivation_path(path_to_coin) + .mm_err(|e| UtxoCoinBuildError::InvalidPathToAddress(e.to_string()))?; + + let secret = global_hd_ctx + .derive_secp256k1_secret(&derivation_path) + .mm_err(|e| UtxoCoinBuildError::Internal(e.to_string()))?; + + let private = Private { + prefix: conf.wif_prefix, + secret, + compressed: true, + checksum_type: conf.checksum_type, + }; + + let activated_key_pair = + KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; + let priv_key_policy = PrivKeyPolicy::HDWallet { + path_to_coin: path_to_coin.clone(), + activated_key: activated_key_pair, + bip39_secp_priv_key: global_hd_ctx.root_priv_key().clone(), + }; + + let address_format = builder.address_format()?; + let hd_wallet_rmd160 = *builder.ctx().rmd160(); + let hd_wallet_storage = + HDWalletCoinStorage::init_with_rmd160(builder.ctx(), builder.ticker().to_owned(), hd_wallet_rmd160) + .await .map_mm_err()?; - let secret = global_hd_ctx - .derive_secp256k1_secret( - &path_to_address - .to_derivation_path(path_to_coin) - .mm_err(|e| UtxoCoinBuildError::InvalidPathToAddress(e.to_string()))?, - ) - .mm_err(|e| UtxoCoinBuildError::Internal(e.to_string()))?; - let private = Private { - prefix: conf.wif_prefix, - secret, - compressed: true, - checksum_type: conf.checksum_type, - }; - let activated_key_pair = - KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; - let priv_key_policy = PrivKeyPolicy::HDWallet { - path_to_coin: path_to_coin.clone(), - activated_key: activated_key_pair, - bip39_secp_priv_key: global_hd_ctx.root_priv_key().clone(), - }; - let address_format = self.address_format()?; - let hd_wallet_rmd160 = *self.ctx().rmd160(); - let hd_wallet_storage = - HDWalletCoinStorage::init_with_rmd160(self.ctx(), self.ticker().to_owned(), hd_wallet_rmd160) + let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, path_to_coin) + .await + .mm_err(UtxoCoinBuildError::from)?; + + let gap_limit = builder.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT); + + let hd_wallet = UtxoHDWallet { + inner: HDWallet { + hd_wallet_rmd160, + hd_wallet_storage, + derivation_path: path_to_coin.clone(), + accounts: HDAccountsMutex::new(accounts), + enabled_address: path_to_address, + gap_limit, + }, + address_format, + }; + + let derivation_method = DerivationMethod::HDWallet(hd_wallet); + build_utxo_coin_fields_with_conf_and_policy(builder, conf, priv_key_policy, derivation_method).await +} + +async fn build_utxo_fields_with_walletconnect( + builder: &Builder, + session_topic: &WcTopic, +) -> UtxoCoinBuildResult +where + Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, +{ + let conf = UtxoConfBuilder::new(builder.conf(), builder.activation_params(), builder.ticker()) + .build() + .map_mm_err()?; + + let chain_id = builder.wallet_connect_chain_id()?; + let full_derivation_path = builder.full_derivation_path()?; + + let wc_ctx = WalletConnectCtx::from_ctx(builder.ctx()).map_mm_err()?; + let (address, pubkey) = get_walletconnect_address(&wc_ctx, session_topic, &chain_id, &full_derivation_path) + .await + .map_mm_err()?; + + let pubkey = match pubkey { + Some(pubkey) => pubkey, + // If getAccountAddresses didn't return the public key, we will try to recover it from a signature instead. + None => { + let sign_message_prefix = conf.sign_message_prefix.as_ref().ok_or_else(|| { + UtxoCoinBuildError::Internal("sign_message_prefix is not set in coins config".to_string()) + })?; + get_pubkey_via_wallatconnect_signature(&wc_ctx, session_topic, &chain_id, &address, sign_message_prefix) .await - .map_mm_err()?; - let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, path_to_coin) - .await - .mm_err(UtxoCoinBuildError::from)?; - let gap_limit = self.gap_limit(); - let hd_wallet = UtxoHDWallet { - inner: HDWallet { - hd_wallet_rmd160, - hd_wallet_storage, - derivation_path: path_to_coin.clone(), - accounts: HDAccountsMutex::new(accounts), - enabled_address: path_to_address, - gap_limit, - }, - address_format, - }; - let derivation_method = DerivationMethod::HDWallet(hd_wallet); - build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy, derivation_method).await - } + .map_mm_err()? + }, + }; - fn gap_limit(&self) -> u32 { - self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) - } + // Construct the PrivKeyPolicy (of WalletConnect type). + let pubkey = PublicKey::from_str(&pubkey).map_err(|e| { + WalletConnectError::ClientError(format!("Received a bad pubkey={pubkey} from WalletConnect: {e}")) + })?; + let public_key = pubkey.serialize().into(); + let public_key_uncompressed = pubkey.serialize_uncompressed().into(); + let priv_key_policy = PrivKeyPolicy::WalletConnect { + public_key, + public_key_uncompressed, + session_topic: session_topic.to_owned(), + }; + + // Construct the derivation method (of SingleAddress type). + let my_address = AddressBuilder::new( + builder.address_format()?, + conf.checksum_type, + conf.address_prefixes.clone(), + conf.bech32_hrp.clone(), + ) + .as_pkh_from_pk(Public::Compressed(pubkey.serialize().into())) + .build() + .map_to_mm(UtxoCoinBuildError::Internal)?; + let derivation_method = DerivationMethod::SingleAddress(my_address.clone()); + + // Validate that the address received from WalletConnect matches the one derived from the public key. + // This is a sanity check to ensure that the WalletConnect session is valid and the address + // corresponds to the public key we have. Otherwise, the wallet (or our address builder) is messed up. + let my_address_serialized = my_address + .display_address() + .map_err(|e| UtxoCoinBuildError::Internal(format!("Failed to serialize address: {e}")))?; + if my_address_serialized != address { + return MmError::err( + WalletConnectError::ClientError(format!( + "Received address={my_address_serialized} from WalletConnect doesn't match the expected address={address} derived via the public key" + )) + .into(), + ); + } + + build_utxo_coin_fields_with_conf_and_policy(builder, conf, priv_key_policy, derivation_method).await } async fn build_utxo_coin_fields_with_conf_and_policy( @@ -264,7 +362,12 @@ async fn build_utxo_coin_fields_with_conf_and_policy( where Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, { - let key_pair = priv_key_policy.activated_key_or_err().map_mm_err()?; + let pubkey = { + match priv_key_policy { + PrivKeyPolicy::WalletConnect { public_key, .. } => Public::Compressed(public_key.0.into()), + _ => *priv_key_policy.activated_key_or_err().map_mm_err()?.public(), + } + }; let addr_format = builder.address_format()?; let my_address = AddressBuilder::new( addr_format, @@ -272,7 +375,7 @@ where conf.address_prefixes.clone(), conf.bech32_hrp.clone(), ) - .as_pkh_from_pk(*key_pair.public()) + .as_pkh_from_pk(pubkey) .build() .map_to_mm(UtxoCoinBuildError::Internal)?; @@ -316,111 +419,92 @@ where Ok(coin) } -#[async_trait] -pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { - async fn build_utxo_fields_with_trezor(&self) -> UtxoCoinBuildResult { - let ticker = self.ticker().to_owned(); - let conf = UtxoConfBuilder::new(self.conf(), self.activation_params(), &ticker) - .build() - .map_mm_err()?; - - if !self.supports_trezor(&conf) { - return MmError::err(UtxoCoinBuildError::CoinDoesntSupportTrezor); - } - let hd_wallet_rmd160 = self.trezor_wallet_rmd160()?; - - let address_format = self.address_format()?; - let path_to_coin = conf - .derivation_path - .clone() - .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet) - .map_mm_err()?; - - let hd_wallet_storage = HDWalletCoinStorage::init(self.ctx(), ticker).await.map_mm_err()?; - - let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, &path_to_coin) - .await - .mm_err(UtxoCoinBuildError::from)?; - let gap_limit = self.gap_limit(); - let hd_wallet = UtxoHDWallet { - inner: HDWallet { - hd_wallet_rmd160, - hd_wallet_storage, - derivation_path: path_to_coin, - accounts: HDAccountsMutex::new(accounts), - enabled_address: self.activation_params().path_to_address, - gap_limit, - }, - address_format, - }; - - // TODO: Creating a dummy output script for now. We better set it to the enabled address output script. - let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(Default::default())); - - // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, - // all spawned futures related to this `UTXO` coin will be aborted as well. - let abortable_system: AbortableQueue = self.ctx().abortable_system.create_subsystem()?; - - let rpc_client = self.rpc_client(abortable_system.create_subsystem()?).await?; - let tx_fee = self.tx_fee(&rpc_client).await?; - let decimals = self.decimals(&rpc_client).await?; - let dust_amount = self.dust_amount(); - - let initial_history_state = self.initial_history_state(); - let tx_hash_algo = self.tx_hash_algo(); - let check_utxo_maturity = self.check_utxo_maturity(); - let tx_cache = self.tx_cache(); - let (block_headers_status_notifier, block_headers_status_watcher) = - self.block_header_status_channel(&conf.spv_conf); - - let coin = UtxoCoinFields { - conf, - decimals, - dust_amount, - rpc_client, - priv_key_policy: PrivKeyPolicy::Trezor, - derivation_method: DerivationMethod::HDWallet(hd_wallet), - history_sync_state: Mutex::new(initial_history_state), - tx_cache, - recently_spent_outpoints, - tx_fee, - tx_hash_algo, - check_utxo_maturity, - block_headers_status_notifier, - block_headers_status_watcher, - ctx: self.ctx().clone().weak(), - abortable_system, - }; - Ok(coin) - } - - fn gap_limit(&self) -> u32 { - self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) - } +async fn build_utxo_fields_with_trezor(builder: &Builder) -> UtxoCoinBuildResult +where + Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, +{ + let ticker = builder.ticker().to_owned(); + let conf = UtxoConfBuilder::new(builder.conf(), builder.activation_params(), &ticker) + .build() + .map_mm_err()?; - fn supports_trezor(&self, conf: &UtxoCoinConf) -> bool { - conf.trezor_coin.is_some() + // Make sure this coin supports Trezor. + if conf.trezor_coin.is_none() { + return MmError::err(UtxoCoinBuildError::CoinDoesntSupportTrezor); } - fn trezor_wallet_rmd160(&self) -> UtxoCoinBuildResult { - let crypto_ctx = CryptoCtx::from_ctx(self.ctx()).map_mm_err()?; + let hd_wallet_rmd160 = { + let crypto_ctx = CryptoCtx::from_ctx(builder.ctx()).map_mm_err()?; let hw_ctx = crypto_ctx .hw_ctx() .or_mm_err(|| UtxoCoinBuildError::HwContextNotInitialized)?; match hw_ctx.hw_wallet_type() { - HwWalletType::Trezor => Ok(hw_ctx.rmd160()), + HwWalletType::Trezor => hw_ctx.rmd160(), } - } + }; - fn check_if_trezor_is_initialized(&self) -> UtxoCoinBuildResult<()> { - let crypto_ctx = CryptoCtx::from_ctx(self.ctx()).map_mm_err()?; - let hw_ctx = crypto_ctx - .hw_ctx() - .or_mm_err(|| UtxoCoinBuildError::HwContextNotInitialized)?; - match hw_ctx.hw_wallet_type() { - HwWalletType::Trezor => Ok(()), - } - } + let address_format = builder.address_format()?; + let path_to_coin = conf + .derivation_path + .clone() + .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet) + .map_mm_err()?; + + let hd_wallet_storage = HDWalletCoinStorage::init(builder.ctx(), ticker).await.map_mm_err()?; + + let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, &path_to_coin) + .await + .mm_err(UtxoCoinBuildError::from)?; + let gap_limit = builder.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT); + let hd_wallet = UtxoHDWallet { + inner: HDWallet { + hd_wallet_rmd160, + hd_wallet_storage, + derivation_path: path_to_coin, + accounts: HDAccountsMutex::new(accounts), + enabled_address: builder.activation_params().path_to_address, + gap_limit, + }, + address_format, + }; + + let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(Default::default())); + + // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, + // all spawned futures related to this `UTXO` coin will be aborted as well. + let abortable_system: AbortableQueue = builder.ctx().abortable_system.create_subsystem()?; + + let rpc_client = builder.rpc_client(abortable_system.create_subsystem()?).await?; + let tx_fee = builder.tx_fee(&rpc_client).await?; + let decimals = builder.decimals(&rpc_client).await?; + let dust_amount = builder.dust_amount(); + + let initial_history_state = builder.initial_history_state(); + let tx_hash_algo = builder.tx_hash_algo(); + let check_utxo_maturity = builder.check_utxo_maturity(); + let tx_cache = builder.tx_cache(); + let (block_headers_status_notifier, block_headers_status_watcher) = + builder.block_header_status_channel(&conf.spv_conf); + + let coin = UtxoCoinFields { + conf, + decimals, + dust_amount, + rpc_client, + priv_key_policy: PrivKeyPolicy::Trezor, + derivation_method: DerivationMethod::HDWallet(hd_wallet), + history_sync_state: Mutex::new(initial_history_state), + tx_cache, + recently_spent_outpoints, + tx_fee, + tx_hash_algo, + check_utxo_maturity, + block_headers_status_notifier, + block_headers_status_watcher, + ctx: builder.ctx().clone().weak(), + abortable_system, + }; + Ok(coin) } #[async_trait] @@ -499,6 +583,50 @@ pub trait UtxoCoinBuilderCommonOps { Ok(BlockchainNetwork::Mainnet) } + /// Returns WcChainId for this coin. Parsed from the coin config. + fn wallet_connect_chain_id(&self) -> UtxoCoinBuildResult { + let protocol: CoinProtocol = json::from_value(self.conf()["protocol"].clone()).map_to_mm(|e| { + UtxoCoinBuildError::ConfError(UtxoConfError::InvalidProtocolData(format!( + "Couldn't parse protocol info: {e}" + ))) + })?; + + if let CoinProtocol::UTXO(utxo_info) = protocol { + let utxo_info = utxo_info.ok_or_else(|| { + WalletConnectError::InvalidChainId(format!( + "coin={} doesn't have chain_id (bip122 standard) set in coin config which is required for WalletConnect", + self.ticker() + )) + })?; + let chain_id = WcChainId::try_from_str(&utxo_info.chain_id).map_mm_err()?; + Ok(chain_id) + } else { + MmError::err(UtxoCoinBuildError::ConfError(UtxoConfError::InvalidProtocolData( + format!("Expected UTXO protocol, got: {protocol:?}"), + ))) + } + } + + /// Constructs the full HD derivation path from the coin config and the activation params partial paths. + fn full_derivation_path(&self) -> UtxoCoinBuildResult { + let path_purpose_to_coin = self.conf()["derivation_path"].as_str().ok_or_else(|| { + UtxoCoinBuildError::InvalidPathToAddress("derivation_path is not set in coin config".to_owned()) + })?; + let path_purpose_to_coin = HDPathToCoin::from_str(path_purpose_to_coin).map_err(|e| { + UtxoCoinBuildError::InvalidPathToAddress(format!("Failed to parse derivation_path in coins config: {e:?}")) + })?; + let path_account_to_address = self.activation_params().path_to_address; + let full_derivation_path = path_account_to_address + .to_derivation_path(&path_purpose_to_coin) + .map_err(|e| { + UtxoCoinBuildError::InvalidPathToAddress(format!("Failed to construct full derivation path: {e}")) + })?; + let full_derivation_path = StandardHDPath::try_from(full_derivation_path).map_err(|e| { + UtxoCoinBuildError::InvalidPathToAddress(format!("Failed to parse full derivation path: {e:?}")) + })?; + Ok(full_derivation_path) + } + async fn decimals(&self, _rpc_client: &UtxoRpcClientEnum) -> UtxoCoinBuildResult { Ok(self.conf()["decimals"].as_u64().unwrap_or(8) as u8) } diff --git a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs index 64e0cec321..d803326bcc 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs @@ -35,6 +35,7 @@ pub enum UtxoConfError { InvalidVersionGroupId(String), InvalidAddressFormat(String), InvalidDecimals(String), + InvalidProtocolData(String), } impl From for UtxoConfError { diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index e20cb5510b..c6f9f6a9bb 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -35,7 +35,7 @@ use crate::{MmCoinEnum, WatcherReward, WatcherRewardError}; use base64::engine::general_purpose::STANDARD; use base64::Engine; pub use bitcrypto::{dhash160, sha256, ChecksumType}; -use bitcrypto::{dhash256, ripemd160}; +use bitcrypto::{ripemd160, sign_message_hash}; use chain::constants::SEQUENCE_FINAL; use chain::{OutPoint, TransactionInput, TransactionOutput}; use common::executor::Timer; @@ -64,10 +64,7 @@ use rpc_clients::NativeClientImpl; use script::{Builder, Opcode, Script, ScriptAddress, TransactionInputSigner, UnsignedTransactionInput}; use secp256k1::{PublicKey, Signature as SecpSignature}; use serde_json::{self as json}; -use serialization::{ - deserialize, serialize, serialize_with_flags, CoinVariant, CompactInteger, Serializable, Stream, - SERIALIZE_TRANSACTION_WITNESS, -}; +use serialization::{deserialize, serialize, serialize_with_flags, CoinVariant, SERIALIZE_TRANSACTION_WITNESS}; use std::cmp::Ordering; use std::collections::hash_map::{Entry, HashMap}; use std::convert::TryFrom; @@ -2925,26 +2922,17 @@ pub fn burn_address(coin: &T) -> Result Option<[u8; 32]> { - let message_prefix = coin.conf.sign_message_prefix.clone()?; - let mut stream = Stream::new(); - let prefix_len = CompactInteger::from(message_prefix.len()); - prefix_len.serialize(&mut stream); - stream.append_slice(message_prefix.as_bytes()); - let msg_len = CompactInteger::from(message.len()); - msg_len.serialize(&mut stream); - stream.append_slice(message.as_bytes()); - Some(dhash256(&stream.out()).take()) -} - pub fn sign_message( coin: &UtxoCoinFields, message: &str, account: Option, ) -> SignatureResult { - let message_hash = sign_message_hash(coin, message).ok_or(SignatureError::PrefixNotFound)?; + let sign_message_prefix = coin + .conf + .sign_message_prefix + .as_ref() + .ok_or(SignatureError::PrefixNotFound)?; + let message_hash = sign_message_hash(sign_message_prefix, message); let private = if let Some(account) = account { let path_to_coin = coin.priv_key_policy.path_to_coin_or_err().map_mm_err()?; @@ -2977,7 +2965,13 @@ pub fn verify_message( message: &str, address: &str, ) -> VerificationResult { - let message_hash = sign_message_hash(coin.as_ref(), message).ok_or(VerificationError::PrefixNotFound)?; + let sign_message_prefix = coin + .as_ref() + .conf + .sign_message_prefix + .as_ref() + .ok_or(VerificationError::PrefixNotFound)?; + let message_hash = sign_message_hash(sign_message_prefix, message); let signature = CompactSignature::try_from(STANDARD.decode(signature_base64)?) .map_to_mm(|err| VerificationError::SignatureDecodingError(err.to_string()))?; let recovered_pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index b0fccc370a..447fb7540f 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -47,6 +47,7 @@ use crate::{ WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, }; +use bitcrypto::sign_message_hash; use common::executor::{AbortableSystem, AbortedError}; use futures::{FutureExt, TryFutureExt}; use mm2_metrics::MetricsArc; @@ -884,7 +885,8 @@ impl MarketCoinOps for UtxoStandardCoin { } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { - utxo_common::sign_message_hash(self.as_ref(), message) + let prefix = self.as_ref().conf.sign_message_prefix.as_ref()?; + Some(sign_message_hash(prefix, message)) } fn sign_message(&self, message: &str, address: Option) -> SignatureResult { diff --git a/mm2src/coins/utxo/wallet_connect.rs b/mm2src/coins/utxo/wallet_connect.rs new file mode 100644 index 0000000000..de600694ec --- /dev/null +++ b/mm2src/coins/utxo/wallet_connect.rs @@ -0,0 +1,143 @@ +//! This module provides functionality to interact with WalletConnect for UTXO-based coins. +use std::convert::TryFrom; + +use bitcrypto::sign_message_hash; +use chain::hash::H256; +use crypto::StandardHDPath; +use kdf_walletconnect::{ + chain::{WcChainId, WcRequestMethods}, + error::WalletConnectError, + WalletConnectCtx, WcTopic, +}; +use keys::{CompactSignature, Public}; +use mm2_err_handle::prelude::{MmError, MmResult}; + +use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; +use base64::Engine; + +/// Represents a UTXO address returned by GetAccountAddresses request in WalletConnect. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct GetAccountAddressesItem { + address: String, + public_key: Option, + path: Option, + #[allow(dead_code)] + intention: Option, +} + +/// Get the enabled address (chosen by the user) +pub async fn get_walletconnect_address( + wc: &WalletConnectCtx, + session_topic: &WcTopic, + chain_id: &WcChainId, + derivation_path: &StandardHDPath, +) -> MmResult<(String, Option), WalletConnectError> { + wc.validate_update_active_chain_id(session_topic, chain_id).await?; + let (account_str, _) = wc.get_account_and_properties_for_chain_id(session_topic, chain_id)?; + let params = json!({ + "account": account_str, + }); + let accounts: Vec = wc + .send_session_request_and_wait( + session_topic, + chain_id, + WcRequestMethods::UtxoGetAccountAddresses, + params, + ) + .await?; + + // Find the address that the user is interested in (the enabled address). + let account = accounts.iter().find(|a| a.path.as_ref() == Some(derivation_path)); + + match account { + // If we found an account with the specific derivation path, we pick it. + Some(account) => Ok((account.address.clone(), account.public_key.clone())), + // If we didn't find the account with the specific derivation path, we perform some sane fallback. + None => { + let first_account = accounts.into_iter().next().ok_or_else(|| { + WalletConnectError::NoAccountFound( + "WalletConnect returned no addresses for getAccountAddresses".to_string(), + ) + })?; + // If the response doesn't include derivation path information, just return the first address. + if first_account.path.is_none() { + common::log::warn!("WalletConnect didn't specify derivation paths for getAccountAddresses, picking the first address: {}", first_account.address); + Ok((first_account.address, first_account.public_key)) + } else { + // Otherwise, the response includes a derivation path, which means we didn't find the one that the user was interested in. + MmError::err(WalletConnectError::NoAccountFound(format!( + "No address found for derivation path: {derivation_path}" + ))) + } + }, + } +} + +/// The response from WalletConnect for `signMessage` request. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SignMessageResponse { + address: String, + signature: String, + #[allow(dead_code)] + message_hash: Option, +} + +/// Get the public key associated with some address via WalletConnect signature. +pub async fn get_pubkey_via_wallatconnect_signature( + wc: &WalletConnectCtx, + session_topic: &WcTopic, + chain_id: &WcChainId, + address: &str, + sign_message_prefix: &str, +) -> MmResult { + const AUTH_MSG: &str = "Authenticate with KDF"; + + wc.validate_update_active_chain_id(session_topic, chain_id).await?; + let (account_str, _) = wc.get_account_and_properties_for_chain_id(session_topic, chain_id)?; + let params = json!({ + "account": account_str, + "address": address, + "message": AUTH_MSG, + "protocol": "ecdsa", + }); + let signature_response: SignMessageResponse = wc + .send_session_request_and_wait(session_topic, chain_id, WcRequestMethods::UtxoPersonalSign, params) + .await?; + + // The wallet is required to send back the same address in the response. + // We validate it here even though there shouldn't be a mismatch (otherwise the wallet is broken). + if signature_response.address != address { + return MmError::err(WalletConnectError::InternalError(format!( + "Address mismatch: requested signature from {}, got it from {}", + address, signature_response.address + ))); + } + + let decoded_signature = match hex::decode(&signature_response.signature) { + Ok(decoded) => decoded, + Err(hex_decode_err) => BASE64_ENGINE + .decode(&signature_response.signature) + .map_err(|base64_decode_err| { + WalletConnectError::InternalError(format!( + "Failed to decode signature={} from hex (error={:?}) and from base64 (error={:?})", + signature_response.signature, hex_decode_err, base64_decode_err + )) + })?, + }; + let signature = CompactSignature::try_from(decoded_signature).map_err(|e| { + WalletConnectError::InternalError(format!( + "Failed to parse signature={} into compact signature: {:?}", + signature_response.signature, e + )) + })?; + let message_hash = sign_message_hash(sign_message_prefix, AUTH_MSG); + let pubkey = Public::recover_compact(&H256::from(message_hash), &signature).map_err(|e| { + WalletConnectError::InternalError(format!( + "Failed to recover public key from walletconnect signature={signature:?}: {e:?}" + )) + })?; + + Ok(pubkey.to_string()) +} diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 62f0bd4f82..cf83ab7399 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -19,10 +19,7 @@ use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawInProgressStat use crate::utxo::rpc_clients::{ ElectrumConnectionSettings, UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, UtxoRpcResult, }; -use crate::utxo::utxo_builder::{ - UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, - UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder, -}; +use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps}; use crate::utxo::utxo_common::{ addresses_from_script, big_decimal_from_sat, big_decimal_from_sat_unsigned, payment_script, }; @@ -962,14 +959,6 @@ impl UtxoCoinBuilderCommonOps for ZCoinBuilder<'_> { } } -impl UtxoFieldsWithIguanaSecretBuilder for ZCoinBuilder<'_> {} - -impl UtxoFieldsWithGlobalHDBuilder for ZCoinBuilder<'_> {} - -/// Although, `ZCoin` doesn't support [`PrivKeyBuildPolicy::Trezor`] yet, -/// `UtxoCoinBuilder` trait requires `UtxoFieldsWithHardwareWalletBuilder` to be implemented. -impl UtxoFieldsWithHardwareWalletBuilder for ZCoinBuilder<'_> {} - #[async_trait] impl UtxoCoinBuilder for ZCoinBuilder<'_> { type ResultCoin = ZCoin; @@ -2278,6 +2267,13 @@ fn extended_spending_key_from_protocol_info_and_policy( UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err), )) }, + PrivKeyBuildPolicy::WalletConnect { .. } => { + let priv_key_err = + PrivKeyPolicyNotAllowed::UnsupportedMethod("WalletConnect is not supported for ZCoin".to_string()); + MmError::err(ZCoinBuildError::UtxoBuilderError( + UtxoCoinBuildError::PrivKeyPolicyNotAllowed(priv_key_err), + )) + }, } } diff --git a/mm2src/coins/z_coin/storage/z_locked_notes/sqlite.rs b/mm2src/coins/z_coin/storage/z_locked_notes/sqlite.rs index 334e00bf62..f0caf6e9a0 100644 --- a/mm2src/coins/z_coin/storage/z_locked_notes/sqlite.rs +++ b/mm2src/coins/z_coin/storage/z_locked_notes/sqlite.rs @@ -34,7 +34,7 @@ async fn create_table(conn: Arc>) -> Result<(), AsyncConn impl LockedNotesStorage { #[cfg(not(any(test, feature = "run-docker-tests")))] pub(crate) async fn new(ctx: &MmArc, address: String) -> MmResult { - let path = ctx.wallet_dir().join(format!("{}_locked_notes_cache.db", address)); + let path = ctx.wallet_dir().join(format!("{address}_locked_notes_cache.db")); let db = AsyncConnection::open(path) .await .map_to_mm(|err| LockedNotesStorageError::SqliteError(err.to_string()))?; diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index d3fd52d474..09a5a795a9 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -13,7 +13,6 @@ use coins::eth::v2_activation::{ eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationRequest, EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy, EthTokenActivationError, NftActivationRequest, NftProviderEnum, }; -use coins::eth::wallet_connect::eth_request_wc_personal_sign; use coins::eth::{ChainSpec, Erc20TokenDetails, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; use coins::hd_wallet::{DisplayAddress, RpcTaskXPubExtractor}; use coins::my_tx_history_v2::TxHistoryStorage; @@ -22,7 +21,6 @@ use coins::{ CoinBalance, CoinBalanceMap, CoinProtocol, CoinWithDerivationMethod, DerivationMethod, MarketCoinOps, MmCoin, MmCoinEnum, }; -use kdf_walletconnect::WalletConnectCtx; use crate::platform_coin_with_tokens::InitPlatformCoinWithTokensTask; use common::Future01CompatExt; @@ -298,7 +296,7 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { protocol: Self::PlatformProtocolInfo, ) -> Result> { let priv_key_policy = - eth_priv_key_build_policy(&ctx, &activation_request.platform_request.priv_key_policy, &protocol).await?; + eth_priv_key_build_policy(&ctx, &activation_request.platform_request.priv_key_policy).await?; let platform_coin = eth_coin_from_conf_and_request_v2( &ctx, @@ -484,7 +482,6 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { async fn eth_priv_key_build_policy( ctx: &MmArc, activation_policy: &EthPrivKeyActivationPolicy, - protocol: &ChainSpec, ) -> MmResult { match activation_policy { EthPrivKeyActivationPolicy::ContextPrivKey => { @@ -499,20 +496,8 @@ async fn eth_priv_key_build_policy( Ok(EthPrivKeyBuildPolicy::Metamask(metamask_ctx)) }, EthPrivKeyActivationPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), - EthPrivKeyActivationPolicy::WalletConnect { session_topic } => { - let wc = WalletConnectCtx::from_ctx(ctx) - .expect("TODO: handle error when enable kdf initialization without key."); - let chain_id = protocol.chain_id().ok_or(EthActivationV2Error::ChainIdNotSet)?; - let (public_key_uncompressed, address) = - eth_request_wc_personal_sign(&wc, session_topic, chain_id) - .await - .mm_err(|err| EthActivationV2Error::WalletConnectError(err.to_string()))?; - - Ok(EthPrivKeyBuildPolicy::WalletConnect { - address, - public_key_uncompressed, - session_topic: session_topic.clone(), - }) - }, + EthPrivKeyActivationPolicy::WalletConnect { session_topic } => Ok(EthPrivKeyBuildPolicy::WalletConnect { + session_topic: session_topic.clone(), + }), } } diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 3bdf267acc..ff3390ebf3 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -20,7 +20,7 @@ use coins::tendermint::{ use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum, PrivKeyBuildPolicy}; use common::executor::{AbortSettings, SpawnAbortable}; use common::{true_f, Future01CompatExt}; -use kdf_walletconnect::WalletConnectCtx; +use kdf_walletconnect::{WalletConnectCtx, WcTopic}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; @@ -51,7 +51,7 @@ pub enum TendermintPubkeyActivationParams { is_ledger_connection: bool, }, /// Activate with WalletConnect - WalletConnect { session_topic: String }, + WalletConnect { session_topic: WcTopic }, } #[derive(Clone, Deserialize)] @@ -234,7 +234,7 @@ impl From for EnablePlatformCoinWithTokensError { async fn activate_with_walletconnect( ctx: &MmArc, - session_topic: String, + session_topic: WcTopic, chain_id: &str, ticker: &str, ) -> MmResult<(TendermintActivationPolicy, TendermintWalletConnectionType), TendermintInitError> { diff --git a/mm2src/coins_activation/src/utxo_activation/common_impl.rs b/mm2src/coins_activation/src/utxo_activation/common_impl.rs index d4e670d38f..ed98fe4b3f 100644 --- a/mm2src/coins_activation/src/utxo_activation/common_impl.rs +++ b/mm2src/coins_activation/src/utxo_activation/common_impl.rs @@ -49,7 +49,8 @@ where ctx, task_handle.clone(), xpub_extractor_rpc_statuses(), - coins::CoinProtocol::UTXO, + // Note that the actual UtxoProtocolInfo isn't needed by trezor XPUB extractor. + coins::CoinProtocol::UTXO(Default::default()), ) .mm_err(|_| InitUtxoStandardError::HwError(HwRpcError::NotInitialized))?, ) @@ -93,11 +94,14 @@ fn xpub_extractor_rpc_statuses() -> HwConnectStatuses MmResult { match activation_policy { PrivKeyActivationPolicy::ContextPrivKey => PrivKeyBuildPolicy::detect_priv_key_policy(ctx), PrivKeyActivationPolicy::Trezor => Ok(PrivKeyBuildPolicy::Trezor), + PrivKeyActivationPolicy::WalletConnect { session_topic } => Ok(PrivKeyBuildPolicy::WalletConnect { + session_topic: session_topic.clone(), + }), } } diff --git a/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs index a545ccb9aa..039fbe8315 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs @@ -81,7 +81,7 @@ impl InitStandaloneCoinActivationOps for BchCoin { } })?; let priv_key_policy = - priv_key_build_policy(&ctx, activation_request.utxo_params.priv_key_policy).map_mm_err()?; + priv_key_build_policy(&ctx, &activation_request.utxo_params.priv_key_policy).map_mm_err()?; let bchd_urls = activation_request.bchd_urls.clone(); let constructor = { move |utxo_arc| BchCoin::new(utxo_arc, prefix.clone(), bchd_urls.clone()) }; diff --git a/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs index 49bbf20208..4fafa430c1 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs @@ -65,7 +65,7 @@ impl InitStandaloneCoinActivationOps for QtumCoin { _protocol_info: Self::StandaloneProtocol, _task_handle: QtumRpcTaskHandleShared, ) -> Result> { - let priv_key_policy = priv_key_build_policy(&ctx, activation_request.priv_key_policy).map_mm_err()?; + let priv_key_policy = priv_key_build_policy(&ctx, &activation_request.priv_key_policy).map_mm_err()?; let coin = QtumCoinBuilder::new(&ctx, &ticker, &coin_conf, activation_request, priv_key_policy) .build() diff --git a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs index 13935dd924..abe8e13e78 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs @@ -38,7 +38,7 @@ impl TryFromCoinProtocol for UtxoStandardProtocolInfo { Self: Sized, { match proto { - CoinProtocol::UTXO => Ok(UtxoStandardProtocolInfo), + CoinProtocol::UTXO { .. } => Ok(UtxoStandardProtocolInfo), protocol => MmError::err(protocol), } } @@ -66,7 +66,7 @@ impl InitStandaloneCoinActivationOps for UtxoStandardCoin { _protocol_info: Self::StandaloneProtocol, task_handle: UtxoStandardRpcTaskHandleShared, ) -> MmResult { - let priv_key_policy = priv_key_build_policy(&ctx, activation_request.priv_key_policy).map_mm_err()?; + let priv_key_policy = priv_key_build_policy(&ctx, &activation_request.priv_key_policy).map_mm_err()?; let coin = UtxoArcBuilder::new( &ctx, diff --git a/mm2src/kdf_walletconnect/src/chain.rs b/mm2src/kdf_walletconnect/src/chain.rs index 20e1acd6a8..ebf53c0e26 100644 --- a/mm2src/kdf_walletconnect/src/chain.rs +++ b/mm2src/kdf_walletconnect/src/chain.rs @@ -10,6 +10,7 @@ pub(crate) const SUPPORTED_PROTOCOL: &str = "irn"; pub enum WcChain { Eip155, Cosmos, + Bip122, } impl FromStr for WcChain { @@ -18,6 +19,7 @@ impl FromStr for WcChain { match s { "eip155" => Ok(WcChain::Eip155), "cosmos" => Ok(WcChain::Cosmos), + "bip122" => Ok(WcChain::Bip122), _ => MmError::err(WalletConnectError::InvalidChainId(format!( "chain_id not supported: {s}" ))), @@ -30,6 +32,7 @@ impl AsRef for WcChain { match self { Self::Eip155 => "eip155", Self::Cosmos => "cosmos", + Self::Bip122 => "bip122", } } } @@ -90,7 +93,17 @@ pub enum WcRequestMethods { CosmosGetAccounts, EthSignTransaction, EthSendTransaction, - PersonalSign, + EthPersonalSign, + // TODO: (remove these notes later) + // - This method will return the pubkey of each address :D + // - Wallets will return ALL addresses found in every purpose' derivation (44, 49, 84, 86), you need to filter for the ones the coin enabled with (or enable mixture of legacy and segwits?). + // - You want to listen to `bip122_addressesChanged` event (which has the same format as `getAccountAddresses` response) + // but we can keep this a todo for later since we probably can manage without it for now. + // ref. https://docs.reown.com/advanced/multichain/rpc-reference/bitcoin-rpc + UtxoGetAccountAddresses, + UtxoSendTransfer, + UtxoSignPsbt, + UtxoPersonalSign, } impl AsRef for WcRequestMethods { @@ -101,7 +114,11 @@ impl AsRef for WcRequestMethods { Self::CosmosGetAccounts => "cosmos_getAccounts", Self::EthSignTransaction => "eth_signTransaction", Self::EthSendTransaction => "eth_sendTransaction", - Self::PersonalSign => "personal_sign", + Self::EthPersonalSign => "personal_sign", + Self::UtxoGetAccountAddresses => "getAccountAddresses", + Self::UtxoSendTransfer => "sendTransfer", + Self::UtxoSignPsbt => "signPsbt", + Self::UtxoPersonalSign => "signMessage", } } } diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs index 39b67107db..e6ee89797d 100644 --- a/mm2src/kdf_walletconnect/src/lib.rs +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -9,8 +9,11 @@ mod pairing; pub mod session; mod storage; +// Re-export `Topic` as it is used within KDF to identify which sessions a coin is running on. +pub use relay_rpc::domain::Topic as WcTopic; + use crate::connection_handler::{Handler, MAX_BACKOFF}; -use crate::session::rpc::propose::send_proposal_request; +use crate::session::rpc::propose::send_session_proposal_request; use chain::{WcChainId, WcRequestMethods, SUPPORTED_PROTOCOL}; use common::custom_futures::timeout::FutureTimerExt; use common::executor::abortable_queue::AbortableQueue; @@ -56,9 +59,7 @@ const CONNECTION_TIMEOUT_S: f64 = 30.; /// established pairing by KDF (via [`WalletConnectCtxImpl::new_connection`]). pub struct NewConnection { pub url: String, - // TODO: Convert this to a `Topic` instead (after the merger of - // https://github.com/KomodoPlatform/komodo-defi-framework/pull/2499, which pub-uses/exposes `Topic` to other dependent crates) - pub pairing_topic: String, + pub pairing_topic: Topic, } /// Broadcast by the lifecycle task so every RPC can cheaply await connectivity. @@ -94,7 +95,7 @@ pub trait WalletConnectOps { ) -> Result; /// Session topic used to activate this. - fn session_topic(&self) -> Result<&str, Self::Error>; + fn session_topic(&self) -> Result<&Topic, Self::Error>; } /// Implements the WalletConnect context, providing functionality for @@ -315,12 +316,17 @@ impl WalletConnectCtxImpl { .map_to_mm(|e| e.into())?; info!("[{topic}] Subscribed to topic"); - - send_proposal_request(self, &topic, required_namespaces, optional_namespaces).await?; + // Note that the creation of pairing doesn't have to do anything with the session proposal but we choose + // to do them on one go. + // TODO: We probably want to separate creating the pairing (done above) and then using the pairing + // to propose a session into two separate steps/functions. This aligns more with WalletConnect spec + // here and is easier to follow (have a clear boundary between a pairing and sessions instantiated using it). + // ref. https://specs.walletconnect.com/2.0/specs/clients/sign#context + send_session_proposal_request(self, &topic, required_namespaces, optional_namespaces).await?; Ok(NewConnection { url, - pairing_topic: topic.to_string(), + pairing_topic: topic, }) } @@ -398,11 +404,10 @@ impl WalletConnectCtxImpl { Ok(()) } - pub fn encode>(&self, session_topic: &str, data: T) -> String { - let session_topic = session_topic.into(); + pub fn encode>(&self, session_topic: &Topic, data: T) -> String { let algo = self .session_manager - .get_session(&session_topic) + .get_session(session_topic) .map(|session| session.encoding_algo.unwrap_or(EncodingAlgo::Hex)) .unwrap_or(EncodingAlgo::Hex); @@ -499,12 +504,12 @@ impl WalletConnectCtxImpl { /// Checks if the current session is connected to a Ledger device. /// NOTE: for COSMOS chains only. - pub fn is_ledger_connection(&self, session_topic: &str) -> bool { - let session_topic = session_topic.into(); + pub fn is_ledger_connection(&self, session_topic: &Topic) -> bool { self.session_manager - .get_session(&session_topic) + .get_session(session_topic) .and_then(|session| session.session_properties) .and_then(|props| props.keys.as_ref().cloned()) + // TODO: This is flaky. ref. https://github.com/KomodoPlatform/komodo-defi-framework/pull/2499#discussion_r2174531817 .and_then(|keys| keys.first().cloned()) .map(|key| key.is_nano_ledger) .unwrap_or(false) @@ -512,10 +517,9 @@ impl WalletConnectCtxImpl { /// Checks if the current session is connected via Keplr wallet. /// NOTE: for COSMOS chains only. - pub fn is_keplr_connection(&self, session_topic: &str) -> bool { - let session_topic = session_topic.into(); + pub fn is_keplr_connection(&self, session_topic: &Topic) -> bool { self.session_manager - .get_session(&session_topic) + .get_session(session_topic) .map(|session| session.controller.metadata.name == "Keplr") .unwrap_or_default() } @@ -534,6 +538,8 @@ impl WalletConnectCtxImpl { } }, None => { + // TODO: Please re-check the correctness of this logic. This doesn't seem to be part of the spec. And the link provided + // doesn't have anything to do with sessionProperties. // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#13-chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index if let Some(SessionProperties { keys: Some(keys) }) = &session.session_properties { if keys.iter().any(|k| k.chain_id == chain_id.id) { @@ -555,13 +561,12 @@ impl WalletConnectCtxImpl { /// Validate and send update active chain to WC if needed. pub async fn validate_update_active_chain_id( &self, - session_topic: &str, + session_topic: &Topic, chain_id: &WcChainId, ) -> MmResult<(), WalletConnectError> { - let session_topic = session_topic.into(); let session = self.session_manager - .get_session(&session_topic) + .get_session(session_topic) .ok_or(MmError::new(WalletConnectError::SessionError( "No active WalletConnect session found".to_string(), )))?; @@ -615,13 +620,12 @@ impl WalletConnectCtxImpl { /// Get available account for a given chain ID. pub fn get_account_and_properties_for_chain_id( &self, - session_topic: &str, + session_topic: &Topic, chain_id: &WcChainId, ) -> MmResult<(String, Option), WalletConnectError> { - let session_topic = session_topic.into(); let session = self.session_manager - .get_session(&session_topic) + .get_session(session_topic) .ok_or(MmError::new(WalletConnectError::SessionError( "No active WalletConnect session found".to_string(), )))?; @@ -643,7 +647,7 @@ impl WalletConnectCtxImpl { /// https://specs.walletconnect.com/2.0/specs/clients/sign/session-events#session_request pub async fn send_session_request_and_wait( &self, - session_topic: &str, + session_topic: &Topic, chain_id: &WcChainId, method: WcRequestMethods, params: serde_json::Value, @@ -651,8 +655,7 @@ impl WalletConnectCtxImpl { where R: DeserializeOwned, { - let session_topic = session_topic.into(); - self.session_manager.validate_session_exists(&session_topic)?; + self.session_manager.validate_session_exists(session_topic)?; let request = SessionRequestRequest { chain_id: chain_id.to_string(), @@ -663,7 +666,7 @@ impl WalletConnectCtxImpl { }, }; let (rx, ttl) = self - .publish_request(&session_topic, RequestParams::SessionRequest(request)) + .publish_request(session_topic, RequestParams::SessionRequest(request)) .await?; let response = rx diff --git a/mm2src/kdf_walletconnect/src/session/rpc/propose.rs b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs index 9b61328f9d..c2a067f475 100644 --- a/mm2src/kdf_walletconnect/src/session/rpc/propose.rs +++ b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs @@ -20,7 +20,7 @@ use relay_rpc::{ }; /// Creates a new session proposal from topic and metadata. -pub(crate) async fn send_proposal_request( +pub(crate) async fn send_session_proposal_request( ctx: &WalletConnectCtxImpl, topic: &Topic, required_namespaces: ProposeNamespaces, @@ -72,6 +72,9 @@ pub async fn reply_session_proposal_request( SessionType::Controller, ) }; + // TODO: Note that this will always error since we never populate `propose_namespaces`. + // But this doesn't matter for now as this method (replying to session proposal) is only relevant when KDF is acting as a wallet. + // TODO: If the required namespaces aren't supported, we should ideally return SessionReject response. session .propose_namespaces .supported(&proposal.required_namespaces) diff --git a/mm2src/mm2_bitcoin/crypto/src/lib.rs b/mm2src/mm2_bitcoin/crypto/src/lib.rs index fdc7e24fc8..04f446cef5 100644 --- a/mm2src/mm2_bitcoin/crypto/src/lib.rs +++ b/mm2src/mm2_bitcoin/crypto/src/lib.rs @@ -118,6 +118,20 @@ pub fn checksum(data: &[u8], sum_type: &ChecksumType) -> H32 { result } +/// Hash message for signature using Bitcoin's message signing format. +/// sha256(sha256(PREFIX_LENGTH + PREFIX + MESSAGE_LENGTH + MESSAGE)) +pub fn sign_message_hash(sign_message_prefix: &str, message: &str) -> [u8; 32] { + use serialization::{CompactInteger, Serializable, Stream}; + let mut stream = Stream::new(); + let prefix_len = CompactInteger::from(sign_message_prefix.len()); + prefix_len.serialize(&mut stream); + stream.append_slice(sign_message_prefix.as_bytes()); + let msg_len = CompactInteger::from(message.len()); + msg_len.serialize(&mut stream); + stream.append_slice(message.as_bytes()); + dhash256(&stream.out()).take() +} + #[cfg(test)] mod tests { use super::{checksum, dhash160, dhash256, ripemd160, sha1, sha256, siphash24}; diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index d726209b57..b07f106e59 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -6342,7 +6342,7 @@ fn orderbook_address( }, // Todo: implement TRX address generation CoinProtocol::TRX { .. } => MmError::err(OrderbookAddrErr::CoinIsNotSupported(coin.to_owned())), - CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { + CoinProtocol::UTXO { .. } | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { coins::utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format) .map(OrderbookAddress::Transparent) .map_to_mm(OrderbookAddrErr::AddrFromPubkeyError) diff --git a/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs b/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs index 2886eb73de..db4d58c424 100644 --- a/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs +++ b/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs @@ -1,4 +1,4 @@ -use kdf_walletconnect::{NewConnection, WalletConnectCtx}; +use kdf_walletconnect::{NewConnection, WalletConnectCtx, WcTopic}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use serde::{Deserialize, Serialize}; @@ -8,7 +8,7 @@ use super::WalletConnectRpcError; #[derive(Debug, PartialEq, Serialize)] pub struct CreateConnectionResponse { pub url: String, - pub pairing_topic: String, + pub pairing_topic: WcTopic, } #[derive(Deserialize)]